Initial release of CaTML (Cat Altered Text Markup Language) compiler and runtime

This commit is contained in:
Christian Johannesen 2026-10-12 08:56:18 +02:00
commit 2adbc60fe7
5 changed files with 1135 additions and 0 deletions

151
README.md Normal file
View file

@ -0,0 +1,151 @@
# CaTML (Cat Altered Text Markup Language) 🐾
> Standard HTML is for humans. **CaTML** is for fine felines.
CaTML is a lightweight, zero-dependency HTML compiler and runtime written in JavaScript that translates feline markup tags and attributes directly into standard semantic HTML.
Repository: [https://git.p20.dev/christian/CaTML.git](https://git.p20.dev/christian/CaTML.git)
---
## 🚀 Quick Start
### 1. In HTML with `<script type="text/catml">`
Simply load `catml.js` and write your CaTML code directly:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My Cat Page</title>
<script src="catml.js"></script>
</head>
<body>
<script type="text/catml">
<box breed="container">
<meow1>🐾 Welcome to My Website</meow1>
<purr>This page is written in pure <claw>CaTML</claw>!</purr>
<bap on-bap="alert('PURRR!')">Bap Me</bap>
</box>
</script>
</body>
</html>
```
### 2. Standalone Page (`<catworld>`)
You can even write the entire document root in CaTML:
```html
<catworld>
<whiskers>
<collar>Kitty Portal</collar>
<script src="catml.js"></script>
</whiskers>
<fur>
<box>
<meow1>Welcome!</meow1>
<purr>The world is feline now.</purr>
</box>
</fur>
</catworld>
```
### 3. JavaScript API
```javascript
// Compile to HTML string
const html = CatML.compile('<box><purr>Meow!</purr></box>');
console.log(html); // <div><p>Meow!</p></div>
// Render directly into a DOM element
CatML.render('<box><purr>Meow!</purr></box>', document.getElementById('app'));
```
---
## 📖 Feline Dictionary
### Core Tags
| CaTML Tag | HTML Tag | Feline Concept / Description |
|---|---|---|
| `<catworld>` / `<catml>` | `<html>` | The whole cat universe |
| `<whiskers>` | `<head>` | Metadata & sensory inputs |
| `<collar>` | `<title>` | Identification |
| `<fur>` | `<body>` | The visible body |
| `<box>` / `<cardboard>` | `<div>` | If it fits, I sits (container) |
| `<feather>` / `<mouse>` | `<span>` | Small things to chase |
| `<meow1>` ... `<meow6>` | `<h1>` ... `<h6>` | Headings (from roar to tiny squeak) |
| `<purr>` | `<p>` | Steady purring paragraph |
| `<scratch>` | `<a>` | Hyperlink (scratch and jump) |
| `<stare>` | `<img>` | Image (what the cat stares at) |
| `<bap>` / `<paw>` | `<button>` | Action button (bap items off tables) |
| `<sniff>` | `<input>` | Input field |
| `<yowl>` | `<textarea>` | Multi-line text field |
| `<bowl>` | `<ul>` | Food bowl / list |
| `<numbered-bowl>` | `<ol>` | Ordered list |
| `<treat>` | `<li>` | Single treat / list item |
| `<claw>` | `<strong>` / `<b>` | Sharp emphasis |
| `<hiss>` | `<em>` / `<i>` | Italic hissed tone |
| `<catnip>` | `<script>` | JavaScript (gives zoomies!) |
| `<groom>` | `<style>` | CSS styling |
| `<litterbox>` | `<footer>` | Bottom footer |
| `<nap>` | `<hr>` | Rest / horizontal rule |
| `<door>` | `<form>` | Form container |
| `<curtain>` | `<header>` | Page header |
| `<shelf>` | `<nav>` | Navigation bar |
| `<hammock>` | `<main>` | Main content |
| `<perch>` | `<section>` | Section |
### Attributes
| CaTML Attribute | HTML Attribute | Description |
|---|---|---|
| `catch="..."` | `href="..."` | Destination URL to chase |
| `prey="..."` | `src="..."` | Source image URL |
| `scent="..."` | `alt="..."` | Alt text / scent profile |
| `tag="..."` | `id="..."` | ID attribute |
| `breed="..."` | `class="..."` | CSS class attribute |
| `fluff="..."` | `style="..."` | Inline CSS styles |
| `on-bap="..."` | `onclick="..."` | Click event listener |
| `on-sniff="..."` | `oninput="..."` | Input event listener |
| `on-change="..."` | `onchange="..."` | Change event listener |
| `on-stalk="..."` | `onmouseover="..."` | Mouseover event listener |
| `on-pounce="..."` | `onmousedown="..."` | Mousedown event listener |
| `treat-type="..."` | `type="..."` | Input type (`text`, `number`, etc.) |
| `kibble="..."` | `value="..."` | Input value |
| `curiosity="..."` | `placeholder="..."` | Placeholder text |
| `collar-name="..."` | `name="..."` | Name attribute |
---
## 🎮 Interactive Playground
Open `index.html` in any browser to experience the **CaTML Playground**:
- Real-time syntax compiler
- Split view: Live preview & compiled raw HTML
- Pre-built templates:
- *Welcome to CaTML*
- *Interactive Bap Counter*
- *Kitten Adoption Form*
- *Sir Pounce-a-Lot Portfolio*
- Built-in interactive feline vocabulary modal
---
## 🧪 Testing
Run the automated test suite with Node.js:
```bash
node test/test.js
```
---
## 📄 License
MIT. Built with 🐾 purrs and catnip.

271
catml.js Normal file
View file

@ -0,0 +1,271 @@
/**
* 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);

67
demo.html Normal file
View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CaTML Standalone Demo</title>
<!-- Load the CaTML runtime interpreter -->
<script src="catml.js"></script>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #fff8f0; color: #333; padding: 20px; }
.cardboard-box { background: #ffe4c4; border: 2px dashed #cd853f; border-radius: 12px; padding: 25px; max-width: 650px; margin: 0 auto; box-shadow: 0 4px 15px rgba(0,0,0,0.06); }
.btn-bap { background: #ff6b6b; color: white; border: none; padding: 10px 18px; border-radius: 20px; cursor: pointer; font-size: 16px; font-weight: bold; transition: transform 0.1s; }
.btn-bap:hover { background: #fa5252; transform: scale(1.05); }
.sniff-box { padding: 8px 14px; border: 2px solid #deb887; border-radius: 8px; font-size: 14px; width: 60%; margin-right: 10px; }
.treat-list { background: #fff; border-radius: 8px; padding: 15px 30px; margin: 15px 0; border: 1px solid #fed; }
.footer-box { margin-top: 25px; border-top: 1px solid #ebd3be; padding-top: 15px; font-size: 13px; color: #8c7b75; text-align: center; }
</style>
</head>
<body>
<!-- Writing pure CaTML inside <script type="text/catml"> -->
<script type="text/catml">
<box breed="cardboard-box">
<curtain>
<meow1>🐾 Welcome to CaTML!</meow1>
<purr><claw>CaTML</claw> stands for <hiss>Cat Altered Text Markup Language</hiss>.</purr>
</curtain>
<nap />
<perch>
<meow2>Why CaTML?</meow2>
<purr>
Standard HTML is for humans. Cats require proper semantics: boxes for divs, purrs for paragraphs, and baps for buttons!
</purr>
<meow3>Favorite Treats (<feather breed="fluffy">ranked by tastiness</feather>)</meow3>
<bowl breed="treat-list">
<treat>🐟 Fresh Atlantic Salmon</treat>
<treat>🍗 Warm Roast Chicken Bits</treat>
<treat>🌿 Organic Catnip Sprinkles</treat>
</bowl>
</perch>
<perch>
<meow3>Interactive Bap Station</meow3>
<purr>Try bapping this button to test inline event handling:</purr>
<bap breed="btn-bap" on-bap="alert('🐾 BAP! You pushed the glass off the counter! 🥛💥')">
🐾 Bap the Glass!
</bap>
</perch>
<nap />
<perch>
<meow3>Sniff Test (Input field)</meow3>
<sniff breed="sniff-box" treat-type="text" curiosity="Sniff something here..." on-sniff="document.getElementById('sniff-output').textContent = 'Cat smells: ' + this.value;" />
<purr tag="sniff-output" fluff="color: #d9480f; font-weight: bold; margin-top: 8px;"></purr>
</perch>
<litterbox breed="footer-box">
<purr>Built with ❤️ and fur • Powered by <scratch catch="https://git.p20.dev/christian/CaTML.git" target="_blank">CaTML Runtime</scratch></purr>
</litterbox>
</box>
</script>
</body>
</html>

571
index.html Normal file
View file

@ -0,0 +1,571 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CaTML - Cat Altered Text Markup Language Playground</title>
<script src="catml.js"></script>
<style>
:root {
--bg-warm: #fcf9f5;
--cat-orange: #f76707;
--cat-amber: #ff922b;
--cat-cream: #ffe8cc;
--cat-dark: #2b2622;
--card-bg: #ffffff;
--border-color: #e9ecef;
--code-bg: #1e1e24;
--code-text: #f8f9fa;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg-warm);
color: var(--cat-dark);
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
header {
background: white;
border-bottom: 2px solid var(--cat-cream);
padding: 12px 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand h1 {
font-size: 20px;
font-weight: 800;
color: var(--cat-orange);
display: flex;
align-items: center;
gap: 8px;
}
.brand .subtitle {
font-size: 13px;
color: #868e96;
font-weight: 500;
}
.controls {
display: flex;
align-items: center;
gap: 10px;
}
select, button {
font-family: inherit;
font-size: 13px;
padding: 8px 14px;
border-radius: 8px;
border: 1px solid #ced4da;
background: white;
cursor: pointer;
transition: all 0.15s ease;
}
select:hover, button:hover {
border-color: var(--cat-amber);
}
.btn-action {
background: var(--cat-orange);
color: white;
font-weight: 600;
border: none;
}
.btn-action:hover {
background: #d9480f;
}
.btn-docs {
background: #f1f3f5;
color: #495057;
border: none;
font-weight: 600;
}
.main-container {
display: flex;
flex: 1;
height: calc(100vh - 65px);
overflow: hidden;
}
.panel {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
border-right: 1px solid var(--border-color);
background: white;
}
.panel:last-child {
border-right: none;
}
.panel-header {
padding: 10px 16px;
background: #f8f9fa;
border-bottom: 1px solid var(--border-color);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #6c757d;
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-wrapper {
flex: 1;
position: relative;
}
#catml-input {
width: 100%;
height: 100%;
border: none;
padding: 16px;
font-family: "Fira Code", Consolas, "Courier New", monospace;
font-size: 14px;
line-height: 1.5;
background: var(--code-bg);
color: var(--code-text);
resize: none;
outline: none;
tab-size: 2;
}
.tabs {
display: flex;
gap: 4px;
}
.tab-btn {
padding: 4px 10px;
font-size: 11px;
border-radius: 4px;
background: transparent;
border: none;
color: #868e96;
}
.tab-btn.active {
background: white;
color: var(--cat-orange);
font-weight: bold;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.preview-frame-container {
flex: 1;
position: relative;
background: white;
overflow: auto;
}
#preview-target {
padding: 24px;
min-height: 100%;
}
#html-output-raw {
display: none;
padding: 16px;
font-family: Consolas, monospace;
font-size: 13px;
white-space: pre-wrap;
word-break: break-all;
background: #f8f9fa;
height: 100%;
overflow: auto;
color: #212529;
}
/* Modal for Dictionary */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal-overlay.open { display: flex; }
.modal-card {
background: white;
width: 700px;
max-width: 90vw;
max-height: 85vh;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.modal-header {
padding: 16px 20px;
background: var(--cat-cream);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 { font-size: 18px; color: var(--cat-orange); }
.modal-body {
padding: 20px;
overflow-y: auto;
font-size: 14px;
line-height: 1.6;
}
.dict-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
margin-bottom: 20px;
}
.dict-table th, .dict-table td {
border: 1px solid #dee2e6;
padding: 8px 12px;
text-align: left;
}
.dict-table th { background: #f8f9fa; }
.badge {
display: inline-block;
padding: 2px 6px;
background: #e9ecef;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<header>
<div class="brand">
<h1>🐾 CaTML Playground</h1>
<span class="subtitle">Cat Altered Text Markup Language</span>
</div>
<div class="controls">
<select id="template-select">
<option value="welcome">Template: Welcome to CaTML</option>
<option value="bapGame">Template: Interactive Bap Game</option>
<option value="adoptionForm">Template: Feline Adoption Form</option>
<option value="portfolio">Template: Whiskers Portfolio</option>
</select>
<button class="btn-docs" onclick="toggleDictionary()">📖 Dictionary</button>
<button class="btn-action" onclick="runCatML()">▶ Run / Compile</button>
</div>
</header>
<div class="main-container">
<!-- Left Panel: Editor -->
<div class="panel">
<div class="panel-header">
<span>CaTML Code Input</span>
<span style="font-size: 10px; text-transform: none; color: #adb5bd;">Type in CaTML - updates automatically</span>
</div>
<div class="editor-wrapper">
<textarea id="catml-input" spellcheck="false" placeholder="Write your CaTML code here..."></textarea>
</div>
</div>
<!-- Right Panel: Output & Live Preview -->
<div class="panel">
<div class="panel-header">
<span>Output</span>
<div class="tabs">
<button class="tab-btn active" id="tab-btn-preview" onclick="showTab('preview')">Live Browser Preview</button>
<button class="tab-btn" id="tab-btn-html" onclick="showTab('html')">Generated HTML</button>
</div>
</div>
<div class="preview-frame-container">
<div id="preview-target"></div>
<pre id="html-output-raw"></pre>
</div>
</div>
</div>
<!-- Dictionary Modal -->
<div class="modal-overlay" id="dict-modal" onclick="if(event.target===this) toggleDictionary()">
<div class="modal-card">
<div class="modal-header">
<h2>🐾 CaTML Feline Vocabulary Guide</h2>
<button onclick="toggleDictionary()" style="border:none;background:none;font-size:20px;cursor:pointer;">&times;</button>
</div>
<div class="modal-body">
<h3>Core Tags</h3>
<table class="dict-table">
<tr><th>CaTML Tag</th><th>HTML Equivalent</th><th>Meaning</th></tr>
<tr><td><span class="badge">&lt;box&gt;</span></td><td><code>&lt;div&gt;</code></td><td>Container box ("If it fits, I sits")</td></tr>
<tr><td><span class="badge">&lt;purr&gt;</span></td><td><code>&lt;p&gt;</code></td><td>Paragraph text / steady purr</td></tr>
<tr><td><span class="badge">&lt;meow1&gt;..&lt;meow6&gt;</span></td><td><code>&lt;h1&gt;..&lt;h6&gt;</code></td><td>Headings (roaring to tiny squeak)</td></tr>
<tr><td><span class="badge">&lt;scratch&gt;</span></td><td><code>&lt;a&gt;</code></td><td>Hyperlink to scratch and chase</td></tr>
<tr><td><span class="badge">&lt;stare&gt;</span></td><td><code>&lt;img&gt;</code></td><td>Visual image</td></tr>
<tr><td><span class="badge">&lt;bap&gt;</span></td><td><code>&lt;button&gt;</code></td><td>Action button</td></tr>
<tr><td><span class="badge">&lt;sniff&gt;</span></td><td><code>&lt;input&gt;</code></td><td>Input field</td></tr>
<tr><td><span class="badge">&lt;bowl&gt;</span></td><td><code>&lt;ul&gt;</code></td><td>List of food</td></tr>
<tr><td><span class="badge">&lt;treat&gt;</span></td><td><code>&lt;li&gt;</code></td><td>Individual treat / item</td></tr>
<tr><td><span class="badge">&lt;claw&gt;</span> / <span class="badge">&lt;hiss&gt;</span></td><td><code>&lt;strong&gt;</code> / <code>&lt;em&gt;</code></td><td>Bold and italic styling</td></tr>
<tr><td><span class="badge">&lt;groom&gt;</span></td><td><code>&lt;style&gt;</code></td><td>CSS Stylesheet</td></tr>
<tr><td><span class="badge">&lt;catnip&gt;</span></td><td><code>&lt;script&gt;</code></td><td>JavaScript code (zoomies!)</td></tr>
<tr><td><span class="badge">&lt;litterbox&gt;</span></td><td><code>&lt;footer&gt;</code></td><td>Footer container</td></tr>
</table>
<h3>Attributes</h3>
<table class="dict-table">
<tr><th>CaTML Attribute</th><th>HTML Equivalent</th><th>Purpose</th></tr>
<tr><td><span class="badge">catch="..."</span></td><td><code>href="..."</code></td><td>Link URL</td></tr>
<tr><td><span class="badge">prey="..."</span></td><td><code>src="..."</code></td><td>Image Source</td></tr>
<tr><td><span class="badge">scent="..."</span></td><td><code>alt="..."</code></td><td>Alternative text</td></tr>
<tr><td><span class="badge">tag="..."</span></td><td><code>id="..."</code></td><td>Element ID</td></tr>
<tr><td><span class="badge">breed="..."</span></td><td><code>class="..."</code></td><td>CSS Class</td></tr>
<tr><td><span class="badge">fluff="..."</span></td><td><code>style="..."</code></td><td>Inline Styles</td></tr>
<tr><td><span class="badge">on-bap="..."</span></td><td><code>onclick="..."</code></td><td>Click handler</td></tr>
<tr><td><span class="badge">on-sniff="..."</span></td><td><code>oninput="..."</code></td><td>Input listener</td></tr>
<tr><td><span class="badge">treat-type="..."</span></td><td><code>type="..."</code></td><td>Input type</td></tr>
<tr><td><span class="badge">curiosity="..."</span></td><td><code>placeholder="..."</code></td><td>Placeholder prompt</td></tr>
</table>
</div>
</div>
</div>
<script>
const TEMPLATES = {
welcome: `<groom>
.cat-hero {
background: linear-gradient(135deg, #ffe8cc, #ffd8a8);
border-radius: 12px;
padding: 24px;
border: 2px dashed #f76707;
}
.highlight { color: #d9480f; font-weight: bold; }
.bap-btn {
background: #f76707;
color: white;
border: none;
padding: 10px 18px;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
}
</groom>
<box breed="cat-hero">
<meow1>🐾 Welcome to CaTML!</meow1>
<purr>
You are writing <claw>Cat Altered Text Markup Language</claw>.
Standard web markup translated for fine felines.
</purr>
<meow2>Why CaTML?</meow2>
<bowl>
<treat>🐱 <claw>&lt;box&gt;</claw> instead of divs (if I fits, I sits)</treat>
<treat>🐟 <claw>&lt;bowl&gt;</claw> and <claw>&lt;treat&gt;</claw> for lists</treat>
<treat>🐾 <claw>&lt;bap&gt;</claw> buttons with <hiss>on-bap</hiss> events</treat>
</bowl>
<bap breed="bap-btn" on-bap="alert('🐾 MEOW! You clicked with your furry paw!')">
Bap Me Now!
</bap>
</box>`,
bapGame: `<groom>
.counter-box {
text-align: center;
padding: 30px;
background: #f8f9fa;
border-radius: 16px;
border: 3px solid #ffa94d;
}
.score-display {
font-size: 48px;
color: #e8590c;
font-weight: 900;
margin: 15px 0;
}
.btn-counter {
background: #e8590c;
color: white;
font-size: 18px;
padding: 12px 28px;
border: none;
border-radius: 24px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(232, 89, 12, 0.3);
}
.btn-counter:active {
transform: scale(0.95);
}
</groom>
<box breed="counter-box">
<meow1>🐾 The Infinite Counter-Bapper</meow1>
<purr>How many cups can your paws knock off the edge?</purr>
<box tag="score" breed="score-display">0</box>
<bap breed="btn-counter" on-bap="let el=document.getElementById('score'); el.innerText=parseInt(el.innerText)+1;">
🥛 Bap The Cup!
</bap>
<purr fluff="margin-top: 15px; color: #868e96; font-size: 13px;">
Each bap increases your feline chaos level.
</purr>
</box>`,
adoptionForm: `<groom>
.form-card {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 12px;
padding: 24px;
max-width: 500px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.input-group { margin-bottom: 16px; }
.input-group label { display: block; font-weight: bold; margin-bottom: 6px; font-size: 14px; }
.field {
width: 100%;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
}
.field:focus { border-color: #f76707; outline: none; }
.submit-paw {
width: 100%;
background: #2b8a3e;
color: white;
padding: 12px;
border-radius: 8px;
border: none;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
</groom>
<box breed="form-card">
<meow2>🐾 Royal Kitten Adoption Application</meow2>
<purr>Tell us about your home and treat budget.</purr>
<door>
<box breed="input-group">
<purr><claw>Human Name:</claw></purr>
<sniff breed="field" treat-type="text" curiosity="e.g. Servant John" />
</box>
<box breed="input-group">
<purr><claw>Daily Salmon Guarantee:</claw></purr>
<sniff breed="field" treat-type="number" kibble="3" curiosity="Cans per day" />
</box>
<box breed="input-group">
<purr><claw>Special Sleeping Spots:</claw></purr>
<yowl breed="field" curiosity="Describe all pillows, laps, and warm laptops available..."></yowl>
</box>
<bap breed="submit-paw" on-bap="alert('Application received! A council of cats will inspect your lap.')">
Pledge Loyalty (Submit)
</bap>
</door>
</box>`,
portfolio: `<groom>
.profile-wrap { display: flex; gap: 20px; align-items: center; margin-bottom: 20px; }
.avatar { font-size: 60px; background: #ffe8cc; border-radius: 50%; padding: 15px; }
.badge { background: #ffe3e3; color: #e03131; padding: 4px 8px; border-radius: 4px; font-size: 12px; }
</groom>
<box>
<curtain>
<box breed="profile-wrap">
<feather breed="avatar">🐱</feather>
<box>
<meow1>Sir Pounce-a-Lot</meow1>
<purr><hiss>Senior Software Mouse Catcher & Nap Architect</hiss></purr>
<feather breed="badge">Open to Treats</feather>
</box>
</box>
</curtain>
<perch>
<meow3>Core Proficiencies</meow3>
<bowl>
<treat><claw>Zoomies at 3 AM</claw> - 99.8% Efficiency</treat>
<treat><claw>Keyboard Warming</claw> - Full-stack coverage</treat>
<treat><claw>Laser Chasing</claw> - High-frequency agile response</treat>
</bowl>
</perch>
<litterbox fluff="margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px;">
<purr>Contact: <scratch catch="mailto:pounce@catml.org">pounce@catml.org</scratch></purr>
</litterbox>
</box>`
};
const editor = document.getElementById('catml-input');
const previewTarget = document.getElementById('preview-target');
const htmlOutputRaw = document.getElementById('html-output-raw');
const templateSelect = document.getElementById('template-select');
const modal = document.getElementById('dict-modal');
function runCatML() {
const source = editor.value;
const compiledHTML = CatML.compile(source);
// Update Preview
CatML.render(source, previewTarget);
// Update Raw HTML
htmlOutputRaw.textContent = compiledHTML;
}
let debounceTimer;
editor.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(runCatML, 250);
});
templateSelect.addEventListener('change', (e) => {
const selected = e.target.value;
if (TEMPLATES[selected]) {
editor.value = TEMPLATES[selected];
runCatML();
}
});
function showTab(tab) {
if (tab === 'preview') {
previewTarget.style.display = 'block';
htmlOutputRaw.style.display = 'none';
document.getElementById('tab-btn-preview').classList.add('active');
document.getElementById('tab-btn-html').classList.remove('active');
} else {
previewTarget.style.display = 'none';
htmlOutputRaw.style.display = 'block';
document.getElementById('tab-btn-preview').classList.remove('active');
document.getElementById('tab-btn-html').classList.add('active');
}
}
function toggleDictionary() {
modal.classList.toggle('open');
}
// Initialize with default template
editor.value = TEMPLATES.welcome;
runCatML();
</script>
</body>
</html>

75
test/test.js Normal file
View file

@ -0,0 +1,75 @@
const assert = require('assert');
const CatML = require('../catml.js');
console.log('🐾 Running CaTML Test Suite...\n');
// Test 1: Tag Translations
console.log('Test 1: Basic tag translations');
const source1 = '<box><purr>Hello world</purr></box>';
const expected1 = '<div><p>Hello world</p></div>';
assert.strictEqual(CatML.compile(source1), expected1);
console.log('✓ Passed: Basic tags');
// Test 2: Headings and lists
console.log('Test 2: Headings and food bowls');
const source2 = '<meow1>Grand Meow</meow1><bowl><treat>Tuna</treat><treat>Salmon</treat></bowl>';
const expected2 = '<h1>Grand Meow</h1><ul><li>Tuna</li><li>Salmon</li></ul>';
assert.strictEqual(CatML.compile(source2), expected2);
console.log('✓ Passed: Headings and lists');
// Test 3: Attributes (catch, prey, scent, breed, tag, fluff)
console.log('Test 3: Attribute translation');
const source3 = '<stare prey="cat.jpg" scent="Cute kitty" tag="avatar" breed="round-cat" fluff="margin: 10px;" />';
const compiled3 = CatML.compile(source3);
assert(compiled3.includes('<img'));
assert(compiled3.includes('src="cat.jpg"'));
assert(compiled3.includes('alt="Cute kitty"'));
assert(compiled3.includes('id="avatar"'));
assert(compiled3.includes('class="round-cat"'));
assert(compiled3.includes('style="margin: 10px;"'));
console.log('✓ Passed: Attributes');
// Test 4: Interactive elements & Events (bap, sniff, on-bap)
console.log('Test 4: Interactive buttons & events');
const source4 = '<bap on-bap="pounce()" breed="red-laser">Catch!</bap><sniff treat-type="text" curiosity="Search prey..." on-sniff="search()" />';
const compiled4 = CatML.compile(source4);
assert(compiled4.includes('<button onclick="pounce()" class="red-laser">Catch!</button>'));
assert(compiled4.includes('<input type="text" placeholder="Search prey..." oninput="search()"'));
console.log('✓ Passed: Interactive & events');
// Test 5: Full document wrapper (<catworld>, <whiskers>, <collar>, <fur>, <litterbox>)
console.log('Test 5: Full document translation');
const source5 = `
<catworld>
<whiskers>
<collar>My Cat Page</collar>
</whiskers>
<fur>
<curtain><meow1>Header</meow1></curtain>
<hammock>
<purr>Content</purr>
</hammock>
<litterbox><purr>Footer</purr></litterbox>
</fur>
</catworld>
`.trim();
const compiled5 = CatML.compile(source5);
assert(compiled5.includes('<html>'));
assert(compiled5.includes('<head>'));
assert(compiled5.includes('<title>My Cat Page</title>'));
assert(compiled5.includes('<body>'));
assert(compiled5.includes('<header><h1>Header</h1></header>'));
assert(compiled5.includes('<main>'));
assert(compiled5.includes('<footer><p>Footer</p></footer>'));
assert(compiled5.includes('</html>'));
console.log('✓ Passed: Full document structure');
// Test 6: Script / Style elements (<catnip>, <groom>)
console.log('Test 6: Script & styles');
const source6 = '<groom>box { color: orange; }</groom><catnip>console.log("purr");</catnip>';
const compiled6 = CatML.compile(source6);
assert.strictEqual(compiled6, '<style>box { color: orange; }</style><script>console.log("purr");</script>');
console.log('✓ Passed: Styles and scripts');
console.log('\n🎉 ALL CaTML TESTS PASSED! Purrfection achieved! 🐱');