/** * CaTML (Cat Altered Text Markup Language) * A feline-powered HTML compiler & runtime interpreter. * * Repository: https://git.p20.dev/christian/CaTML.git */ (function (global) { 'use strict'; // Tag translations: CaTML Tag -> HTML Tag const TAG_MAP = { 'catworld': 'html', 'catml': 'html', 'whiskers': 'head', 'collar': 'title', 'fur': 'body', 'box': 'div', 'cardboard': 'div', 'feather': 'span', 'mouse': 'span', 'meow1': 'h1', 'meow2': 'h2', 'meow3': 'h3', 'meow4': 'h4', 'meow5': 'h5', 'meow6': 'h6', 'purr': 'p', 'scratch': 'a', 'stare': 'img', 'bap': 'button', 'paw': 'button', 'sniff': 'input', 'yowl': 'textarea', 'bowl': 'ul', 'numbered-bowl': 'ol', 'treat': 'li', 'claw': 'strong', 'hiss': 'em', 'catnip': 'script', 'groom': 'style', 'litterbox': 'footer', 'nap': 'hr', 'door': 'form', 'shelf': 'nav', 'hammock': 'main', 'tree': 'article', 'perch': 'section', 'curtain': 'header' }; // Attribute translations: CaTML Attribute -> HTML Attribute const ATTR_MAP = { 'catch': 'href', 'prey': 'src', 'scent': 'alt', 'tag': 'id', 'breed': 'class', 'fluff': 'style', 'on-bap': 'onclick', 'on-sniff': 'oninput', 'on-change': 'onchange', 'on-stalk': 'onmouseover', 'on-pounce': 'onmousedown', 'treat-type': 'type', 'kibble': 'value', 'curiosity': 'placeholder', 'collar-name': 'name', 'target': 'target', 'width': 'width', 'height': 'height' }; /** * Translates a single CaTML tag name to HTML tag name */ function translateTag(tag) { const lower = tag.toLowerCase(); return TAG_MAP[lower] || tag; } /** * Translates a single CaTML attribute name to HTML attribute name */ function translateAttr(attr) { const lower = attr.toLowerCase(); return ATTR_MAP[lower] || attr; } /** * Translates an attribute string of a tag * e.g. `breed="tabby" on-bap="alert(1)"` -> `class="tabby" onclick="alert(1)"` */ function compileAttributes(attrString) { if (!attrString) return ''; // Regex matching attribute key="value", key='value', or boolean key const attrRegex = /([a-zA-Z0-9_-]+)(?:=("[^"]*"|'[^']*'|[^\s"'>]+))?/g; return attrString.replace(attrRegex, (match, attrName, attrVal) => { const mappedName = translateAttr(attrName); if (attrVal !== undefined) { return `${mappedName}=${attrVal}`; } return mappedName; }); } /** * Compiles CaTML markup into standard HTML string * Handles opening tags, closing tags, self-closing tags, and comments. */ function compile(catmlSource) { if (typeof catmlSource !== 'string') return ''; // Regex for HTML/CaTML tags: // Captures: // 1: '/' if closing tag // 2: tag name // 3: attributes // 4: '/' if self-closing const tagRegex = /<\s*(\/?)\s*([a-zA-Z0-9_-]+)([\s\S]*?)(\/?)\s*>/g; return catmlSource.replace(tagRegex, (fullMatch, isClosing, tagName, attrString, isSelfClosing) => { // Don't modify standard DOCTYPE or XML headers if (tagName.toLowerCase() === '!doctype') { return fullMatch; } const mappedTag = translateTag(tagName); if (isClosing) { return ``; } const compiledAttrs = compileAttributes(attrString); const trailingSlash = isSelfClosing ? ' /' : ''; return `<${mappedTag}${compiledAttrs ? compiledAttrs : ''}${trailingSlash}>`; }); } /** * Renders compiled CaTML directly into a DOM element or document */ function render(catmlSource, targetElement) { const html = compile(catmlSource); if (!targetElement) { return html; } if (typeof targetElement === 'string') { targetElement = document.querySelector(targetElement); } if (targetElement && targetElement.innerHTML !== undefined) { targetElement.innerHTML = html; // Execute any script tags generated from or