robert 1 день тому
батько
коміт
2140d6bbe8
1 змінених файлів з 137 додано та 10 видалено
  1. 137 10
      doc/highlight_css.js

+ 137 - 10
doc/highlight_css.js

@@ -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(/&lt;!--[\s\S]*?--&gt;/g, (m) => `<span class="com">${m}</span>`);
+
+    // Tags + attributes (best-effort)
+    s = s.replace(/&lt;(\/?)([A-Za-z][\w:-]*)([\s\S]*?)&gt;/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">&lt;${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">/&gt;</span>`;
+      else out += `<span class="tag">&gt;</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);
       }