CaTML/catml.js

271 lines
7.4 KiB
JavaScript

/**
* 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 `</${mappedTag}>`;
}
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 <catnip> or <script>
const scripts = targetElement.querySelectorAll('script');
scripts.forEach(oldScript => {
const newScript = document.createElement('script');
Array.from(oldScript.attributes).forEach(attr => {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
}
return html;
}
/**
* In-place DOM Transformer:
* Scans document DOM for CaTML custom tags and converts them into real HTML elements.
*/
function transformDOM(rootNode) {
const root = rootNode || document.body;
if (!root) return;
// List of custom tags to replace
const catmlTags = Object.keys(TAG_MAP).filter(t =>
!['html', 'head', 'title', 'body', 'script', 'style'].includes(TAG_MAP[t])
);
let foundElements = true;
while (foundElements) {
foundElements = false;
for (const catTag of catmlTags) {
const elements = root.getElementsByTagName(catTag);
if (elements.length > 0) {
foundElements = true;
const el = elements[0];
const newTagName = TAG_MAP[catTag];
const replacement = document.createElement(newTagName);
// Copy and translate attributes
Array.from(el.attributes).forEach(attr => {
const mappedName = translateAttr(attr.name);
replacement.setAttribute(mappedName, attr.value);
});
// Move child nodes
while (el.firstChild) {
replacement.appendChild(el.firstChild);
}
el.parentNode.replaceChild(replacement, el);
break; // restart query selector because DOM mutated
}
}
}
}
/**
* Automatic Bootstrapper:
* Finds all <script type="text/catml"> and renders them or replaces them.
*/
function autoBoot() {
if (typeof document === 'undefined') return;
// 1. Process <script type="text/catml">
const catScripts = document.querySelectorAll('script[type="text/catml"]');
catScripts.forEach(script => {
const targetId = script.getAttribute('target') || script.getAttribute('data-target');
const compiledHTML = compile(script.textContent);
if (targetId) {
const target = document.querySelector(targetId);
if (target) {
render(script.textContent, target);
}
} else {
// Replace the script tag itself with a container or direct elements
const container = document.createElement('div');
container.className = 'catml-container';
render(script.textContent, container);
script.parentNode.insertBefore(container, script);
}
});
// 2. Transform any custom elements in the body
transformDOM(document.body);
}
// Public API
const CatML = {
compile: compile,
parse: compile,
render: render,
transformDOM: transformDOM,
autoBoot: autoBoot,
TAG_MAP: TAG_MAP,
ATTR_MAP: ATTR_MAP
};
// Auto-run when DOM is ready in browser
if (typeof window !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoBoot);
} else {
setTimeout(autoBoot, 0);
}
window.CatML = CatML;
}
// CommonJS / Node export support
if (typeof module !== 'undefined' && module.exports) {
module.exports = CatML;
}
})(typeof globalThis !== 'undefined' ? globalThis : this);