|
|
@@ -1,6 +1,10 @@
|
|
|
-// Minimal CSS syntax highlighter for the docs (shared).
|
|
|
-// It converts plain-text CSS inside `.css-view` into highlighted HTML using
|
|
|
-// the existing demo.css token classes: .kwd .str .num .punc .com .attr .val .fun
|
|
|
+// 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) {
|
|
|
@@ -11,13 +15,28 @@
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
- // Comments
|
|
|
- s = s.replace(/\/\*[\s\S]*?\*\//g, (m) => `<span class="com">${m}</span>`);
|
|
|
+ const comments = [];
|
|
|
+ const strings = [];
|
|
|
|
|
|
- // Strings
|
|
|
- s = s.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g, (m) => `<span class="str">${m}</span>`);
|
|
|
+ // 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>`);
|
|
|
@@ -52,11 +71,112 @@
|
|
|
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');
|
|
|
+ const blocks = document.querySelectorAll('.css-view, .code-view, .html-view');
|
|
|
blocks.forEach((el) => {
|
|
|
try {
|
|
|
if (el.dataset.highlighted === '1') return;
|
|
|
@@ -66,7 +186,14 @@
|
|
|
el.dataset.highlighted = '1';
|
|
|
return;
|
|
|
}
|
|
|
- el.innerHTML = highlightCSS(text);
|
|
|
+ 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.
|
|
|
@@ -91,7 +218,7 @@
|
|
|
const tab = e.target && e.target.closest ? e.target.closest('.tab') : null;
|
|
|
if (!tab) return;
|
|
|
const label = (tab.textContent || '').trim().toLowerCase();
|
|
|
- if (label === 'css') {
|
|
|
+ if (label === 'css' || label === 'html' || label === 'javascript' || label === 'js') {
|
|
|
// Defer so the tab switch can toggle DOM/classes first
|
|
|
setTimeout(enhance, 0);
|
|
|
}
|