271 lines
7.4 KiB
JavaScript
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);
|