// Minimal syntax highlighter for the docs (shared). // - CSS: converts plain-text CSS inside `.css-view` // - JS: converts plain-text JS inside `.code-view` // - HTML: converts plain-text HTML inside `.html-view` // // It uses the existing demo.css token classes: // .kwd .str .num .punc .com .attr .val .fun .tag (function () { function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>'); } function highlightCSS(input) { // IMPORTANT: // Do not insert `` too early, otherwise later regex passes // will re-match inside those tags (e.g. `class="com"`) and break HTML. // We use placeholders then restore. let s = escapeHtml(input); const comments = []; const strings = []; // Comments (placeholder) s = s.replace(/\/\*[\s\S]*?\*\//g, (m) => { const i = comments.length; comments.push(m); return `@@COM${i}@@`; }); // Strings (placeholder) s = s.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g, (m) => { const i = strings.length; strings.push(m); return `@@STR${i}@@`; }); // @rules s = s.replace(/(^|\s)(@[\w-]+)/g, (m, p1, p2) => `${p1}${p2}`); // Hex colors s = s.replace(/(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8}))/g, (m) => `${m}`); // Numbers with units (very rough, but good enough visually) s = s.replace(/(\b\d+(?:\.\d+)?)(%|px|rem|em|vh|vw|vmin|vmax|deg|turn|s|ms)?\b/g, (m, n, unit) => { return `${n}${unit || ''}`; }); // Property names: "prop: value" s = s.replace(/(^|\n)(\s*)([a-zA-Z_-][\w-]*)(\s*):/g, (m, p1, ws, prop, ws2) => { return `${p1}${ws}${prop}${ws2}:`; }); // Highlight braces / punctuation s = s.replace(/([{}();,])/g, (m) => `${m}`); // Selectors (line before "{"), skip @rules s = s.replace(/(^|\n)([^\n{]+?)(\s*)(\{<\/span>)/g, (m, p1, sel, ws, brace) => { const trimmed = sel.trimStart(); if (trimmed.startsWith('@')) return `${p1}${sel}${ws}${brace}`; return `${p1}${sel}${ws}${brace}`; }); // Values: after ":" until ";" or "}" (best-effort, runs after punctuation) s = s.replace(/(:<\/span>)([^;\n}]+)(?=(;|\}))/g, (m, colon, val) => { // avoid re-highlighting spans if (val.includes('${val}`; }); // Restore placeholders s = s.replace(/@@COM(\d+)@@/g, (m, idx) => `${comments[Number(idx)] || ''}`); s = s.replace(/@@STR(\d+)@@/g, (m, idx) => `${strings[Number(idx)] || ''}`); return s; } function highlightJS(input) { let s = escapeHtml(input); // Protect strings and comments first (placeholders), so later regexes won't touch them. const strings = []; const comments = []; // Strings (", ', `) s = s.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/g, (m) => { const i = strings.length; strings.push(m); return `@@STR${i}@@`; }); // Comments (/* */ and //) s = s.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m) => { const i = comments.length; comments.push(m); return `@@COM${i}@@`; }); // Keywords (small set, enough for docs) const KW = new Set([ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'new', 'class', 'extends', 'super', 'import', 'from', 'export', 'default', 'async', 'await', 'true', 'false', 'null', 'undefined' ]); s = s.replace(/\b([A-Za-z_$][\w$]*)\b/g, (m, id) => { if (KW.has(id)) return `${id}`; return m; }); // Numbers s = s.replace(/\b(\d+(?:\.\d+)?)\b/g, (m, n) => `${n}`); // Function calls: identifier followed by "(" (skip keywords) s = s.replace(/\b([A-Za-z_$][\w$]*)\b(?=\s*\()/g, (m, id) => { if (KW.has(id)) return id; return `${id}`; }); // Basic punctuation (avoid "." / "=" to not conflict with injected HTML) s = s.replace(/([{}()[\];,:])/g, (m) => `${m}`); // Restore comments and strings (escaped) s = s.replace(/@@COM(\d+)@@/g, (m, idx) => `${comments[Number(idx)] || ''}`); s = s.replace(/@@STR(\d+)@@/g, (m, idx) => `${strings[Number(idx)] || ''}`); return s; } function highlightHTML(input) { // Input comes from textContent, so it contains real "<" / ">" characters. let s = escapeHtml(input); // HTML comments s = s.replace(/<!--[\s\S]*?-->/g, (m) => `${m}`); // Tags + attributes (best-effort) s = s.replace(/<(\/?)([A-Za-z][\w:-]*)([\s\S]*?)>/g, (m, slash, name, rest) => { // self-close const selfClose = /\s\/\s*$/.test(rest) || /\/\s*$/.test(rest); const restNoClose = rest.replace(/\/\s*$/, ''); let out = `<${slash || ''}${name}`; // attributes: key="value" / key='value' / key=value const attrRe = /(\s+)([^\s=\/]+)(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g; let lastIdx = 0; let attrs = ''; let match; while ((match = attrRe.exec(restNoClose))) { const [full, ws, key, eq, v1, v2, v3] = match; const start = match.index; // keep any junk between attrs attrs += restNoClose.slice(lastIdx, start); lastIdx = start + full.length; const rawVal = (v1 !== undefined ? `"${v1}"` : (v2 !== undefined ? `'${v2}'` : (v3 !== undefined ? v3 : ''))); attrs += `${ws}${key}${eq}${escapeHtml(rawVal)}`; } attrs += restNoClose.slice(lastIdx); out += attrs; if (selfClose) out += `/>`; else out += `>`; return out; }); return s; } function enhance() { const blocks = document.querySelectorAll('.css-view, .code-view, .html-view'); blocks.forEach((el) => { try { if (el.dataset.highlighted === '1') return; const text = el.textContent || ''; // If already contains spans, assume it's already highlighted. if (el.innerHTML.includes(' { const tab = e.target && e.target.closest ? e.target.closest('.tab') : null; if (!tab) return; const label = (tab.textContent || '').trim().toLowerCase(); if (label === 'css' || label === 'html' || label === 'javascript' || label === 'js') { // Defer so the tab switch can toggle DOM/classes first setTimeout(enhance, 0); } }, true ); // Observe DOM changes to catch dynamically added `.css-view` blocks. try { const mo = new MutationObserver(() => { // Cheap debounce via microtask Promise.resolve().then(enhance); }); mo.observe(document.documentElement, { subtree: true, childList: true, characterData: true }); } catch (_) {} })();