| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- // 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, '<')
- .replace(/>/g, '>');
- }
- function highlightCSS(input) {
- // IMPORTANT:
- // Do not insert `<span ...>` 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}<span class="kwd">${p2}</span>`);
- // Hex colors
- s = s.replace(/(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8}))/g, (m) => `<span class="num">${m}</span>`);
- // 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 `<span class="num">${n}</span>${unit || ''}`;
- });
- // Property names: "prop: value"
- s = s.replace(/(^|\n)(\s*)([a-zA-Z_-][\w-]*)(\s*):/g, (m, p1, ws, prop, ws2) => {
- return `${p1}${ws}<span class="attr">${prop}</span>${ws2}<span class="punc">:</span>`;
- });
- // Highlight braces / punctuation
- s = s.replace(/([{}();,])/g, (m) => `<span class="punc">${m}</span>`);
- // Selectors (line before "{"), skip @rules
- s = s.replace(/(^|\n)([^\n{]+?)(\s*)(<span class="punc">\{<\/span>)/g, (m, p1, sel, ws, brace) => {
- const trimmed = sel.trimStart();
- if (trimmed.startsWith('@')) return `${p1}${sel}${ws}${brace}`;
- return `${p1}<span class="fun">${sel}</span>${ws}${brace}`;
- });
- // Values: after ":" until ";" or "}" (best-effort, runs after punctuation)
- s = s.replace(/(<span class="punc">:<\/span>)([^;\n}]+)(?=(<span class="punc">;|<span class="punc">\}))/g, (m, colon, val) => {
- // avoid re-highlighting spans
- if (val.includes('<span')) return `${colon}${val}`;
- return `${colon}<span class="val">${val}</span>`;
- });
- // Restore placeholders
- s = s.replace(/@@COM(\d+)@@/g, (m, idx) => `<span class="com">${comments[Number(idx)] || ''}</span>`);
- s = s.replace(/@@STR(\d+)@@/g, (m, idx) => `<span class="str">${strings[Number(idx)] || ''}</span>`);
- 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 `<span class="kwd">${id}</span>`;
- return m;
- });
- // Numbers
- s = s.replace(/\b(\d+(?:\.\d+)?)\b/g, (m, n) => `<span class="num">${n}</span>`);
- // 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 `<span class="fun">${id}</span>`;
- });
- // Basic punctuation (avoid "." / "=" to not conflict with injected HTML)
- s = s.replace(/([{}()[\];,:])/g, (m) => `<span class="punc">${m}</span>`);
- // Restore comments and strings (escaped)
- s = s.replace(/@@COM(\d+)@@/g, (m, idx) => `<span class="com">${comments[Number(idx)] || ''}</span>`);
- s = s.replace(/@@STR(\d+)@@/g, (m, idx) => `<span class="str">${strings[Number(idx)] || ''}</span>`);
- 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) => `<span class="com">${m}</span>`);
- // 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 = `<span class="tag"><${slash || ''}${name}</span>`;
- // 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}<span class="attr">${key}</span>${eq}<span class="val">${escapeHtml(rawVal)}</span>`;
- }
- attrs += restNoClose.slice(lastIdx);
- out += attrs;
- if (selfClose) out += `<span class="tag">/></span>`;
- else out += `<span class="tag">></span>`;
- 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('<span')) {
- el.dataset.highlighted = '1';
- return;
- }
- if (el.classList.contains('css-view')) {
- el.innerHTML = highlightCSS(text);
- } else if (el.classList.contains('html-view')) {
- el.innerHTML = highlightHTML(text);
- } else {
- // `.code-view`
- el.innerHTML = highlightJS(text);
- }
- el.dataset.highlighted = '1';
- } catch (_) {
- // Never break the page; just skip highlighting if anything goes wrong.
- }
- });
- }
- // Expose for pages that want to re-run manually
- window.__highlightCssViews = enhance;
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', enhance, { once: true });
- } else {
- enhance();
- }
- // Re-run when switching to the CSS tab (covers cases where the page
- // injects/replaces code blocks after load or during navigation).
- document.addEventListener(
- 'click',
- (e) => {
- 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 (_) {}
- })();
|