highlight_css.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. // Minimal syntax highlighter for the docs (shared).
  2. // - CSS: converts plain-text CSS inside `.css-view`
  3. // - JS: converts plain-text JS inside `.code-view`
  4. // - HTML: converts plain-text HTML inside `.html-view`
  5. //
  6. // It uses the existing demo.css token classes:
  7. // .kwd .str .num .punc .com .attr .val .fun .tag
  8. (function () {
  9. function escapeHtml(s) {
  10. return String(s)
  11. .replace(/&/g, '&')
  12. .replace(/</g, '&lt;')
  13. .replace(/>/g, '&gt;');
  14. }
  15. function highlightCSS(input) {
  16. // IMPORTANT:
  17. // Do not insert `<span ...>` too early, otherwise later regex passes
  18. // will re-match inside those tags (e.g. `class="com"`) and break HTML.
  19. // We use placeholders then restore.
  20. let s = escapeHtml(input);
  21. const comments = [];
  22. const strings = [];
  23. // Comments (placeholder)
  24. s = s.replace(/\/\*[\s\S]*?\*\//g, (m) => {
  25. const i = comments.length;
  26. comments.push(m);
  27. return `@@COM${i}@@`;
  28. });
  29. // Strings (placeholder)
  30. s = s.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/g, (m) => {
  31. const i = strings.length;
  32. strings.push(m);
  33. return `@@STR${i}@@`;
  34. });
  35. // @rules
  36. s = s.replace(/(^|\s)(@[\w-]+)/g, (m, p1, p2) => `${p1}<span class="kwd">${p2}</span>`);
  37. // Hex colors
  38. 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>`);
  39. // Numbers with units (very rough, but good enough visually)
  40. s = s.replace(/(\b\d+(?:\.\d+)?)(%|px|rem|em|vh|vw|vmin|vmax|deg|turn|s|ms)?\b/g, (m, n, unit) => {
  41. return `<span class="num">${n}</span>${unit || ''}`;
  42. });
  43. // Property names: "prop: value"
  44. s = s.replace(/(^|\n)(\s*)([a-zA-Z_-][\w-]*)(\s*):/g, (m, p1, ws, prop, ws2) => {
  45. return `${p1}${ws}<span class="attr">${prop}</span>${ws2}<span class="punc">:</span>`;
  46. });
  47. // Highlight braces / punctuation
  48. s = s.replace(/([{}();,])/g, (m) => `<span class="punc">${m}</span>`);
  49. // Selectors (line before "{"), skip @rules
  50. s = s.replace(/(^|\n)([^\n{]+?)(\s*)(<span class="punc">\{<\/span>)/g, (m, p1, sel, ws, brace) => {
  51. const trimmed = sel.trimStart();
  52. if (trimmed.startsWith('@')) return `${p1}${sel}${ws}${brace}`;
  53. return `${p1}<span class="fun">${sel}</span>${ws}${brace}`;
  54. });
  55. // Values: after ":" until ";" or "}" (best-effort, runs after punctuation)
  56. s = s.replace(/(<span class="punc">:<\/span>)([^;\n}]+)(?=(<span class="punc">;|<span class="punc">\}))/g, (m, colon, val) => {
  57. // avoid re-highlighting spans
  58. if (val.includes('<span')) return `${colon}${val}`;
  59. return `${colon}<span class="val">${val}</span>`;
  60. });
  61. // Restore placeholders
  62. s = s.replace(/@@COM(\d+)@@/g, (m, idx) => `<span class="com">${comments[Number(idx)] || ''}</span>`);
  63. s = s.replace(/@@STR(\d+)@@/g, (m, idx) => `<span class="str">${strings[Number(idx)] || ''}</span>`);
  64. return s;
  65. }
  66. function highlightJS(input) {
  67. let s = escapeHtml(input);
  68. // Protect strings and comments first (placeholders), so later regexes won't touch them.
  69. const strings = [];
  70. const comments = [];
  71. // Strings (", ', `)
  72. s = s.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)/g, (m) => {
  73. const i = strings.length;
  74. strings.push(m);
  75. return `@@STR${i}@@`;
  76. });
  77. // Comments (/* */ and //)
  78. s = s.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (m) => {
  79. const i = comments.length;
  80. comments.push(m);
  81. return `@@COM${i}@@`;
  82. });
  83. // Keywords (small set, enough for docs)
  84. const KW = new Set([
  85. 'const', 'let', 'var', 'function', 'return',
  86. 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue',
  87. 'try', 'catch', 'finally', 'throw',
  88. 'new', 'class', 'extends', 'super',
  89. 'import', 'from', 'export', 'default',
  90. 'async', 'await',
  91. 'true', 'false', 'null', 'undefined'
  92. ]);
  93. s = s.replace(/\b([A-Za-z_$][\w$]*)\b/g, (m, id) => {
  94. if (KW.has(id)) return `<span class="kwd">${id}</span>`;
  95. return m;
  96. });
  97. // Numbers
  98. s = s.replace(/\b(\d+(?:\.\d+)?)\b/g, (m, n) => `<span class="num">${n}</span>`);
  99. // Function calls: identifier followed by "(" (skip keywords)
  100. s = s.replace(/\b([A-Za-z_$][\w$]*)\b(?=\s*\()/g, (m, id) => {
  101. if (KW.has(id)) return id;
  102. return `<span class="fun">${id}</span>`;
  103. });
  104. // Basic punctuation (avoid "." / "=" to not conflict with injected HTML)
  105. s = s.replace(/([{}()[\];,:])/g, (m) => `<span class="punc">${m}</span>`);
  106. // Restore comments and strings (escaped)
  107. s = s.replace(/@@COM(\d+)@@/g, (m, idx) => `<span class="com">${comments[Number(idx)] || ''}</span>`);
  108. s = s.replace(/@@STR(\d+)@@/g, (m, idx) => `<span class="str">${strings[Number(idx)] || ''}</span>`);
  109. return s;
  110. }
  111. function highlightHTML(input) {
  112. // Input comes from textContent, so it contains real "<" / ">" characters.
  113. let s = escapeHtml(input);
  114. // HTML comments
  115. s = s.replace(/&lt;!--[\s\S]*?--&gt;/g, (m) => `<span class="com">${m}</span>`);
  116. // Tags + attributes (best-effort)
  117. s = s.replace(/&lt;(\/?)([A-Za-z][\w:-]*)([\s\S]*?)&gt;/g, (m, slash, name, rest) => {
  118. // self-close
  119. const selfClose = /\s\/\s*$/.test(rest) || /\/\s*$/.test(rest);
  120. const restNoClose = rest.replace(/\/\s*$/, '');
  121. let out = `<span class="tag">&lt;${slash || ''}${name}</span>`;
  122. // attributes: key="value" / key='value' / key=value
  123. const attrRe = /(\s+)([^\s=\/]+)(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/g;
  124. let lastIdx = 0;
  125. let attrs = '';
  126. let match;
  127. while ((match = attrRe.exec(restNoClose))) {
  128. const [full, ws, key, eq, v1, v2, v3] = match;
  129. const start = match.index;
  130. // keep any junk between attrs
  131. attrs += restNoClose.slice(lastIdx, start);
  132. lastIdx = start + full.length;
  133. const rawVal = (v1 !== undefined ? `"${v1}"` : (v2 !== undefined ? `'${v2}'` : (v3 !== undefined ? v3 : '')));
  134. attrs += `${ws}<span class="attr">${key}</span>${eq}<span class="val">${escapeHtml(rawVal)}</span>`;
  135. }
  136. attrs += restNoClose.slice(lastIdx);
  137. out += attrs;
  138. if (selfClose) out += `<span class="tag">/&gt;</span>`;
  139. else out += `<span class="tag">&gt;</span>`;
  140. return out;
  141. });
  142. return s;
  143. }
  144. function enhance() {
  145. const blocks = document.querySelectorAll('.css-view, .code-view, .html-view');
  146. blocks.forEach((el) => {
  147. try {
  148. if (el.dataset.highlighted === '1') return;
  149. const text = el.textContent || '';
  150. // If already contains spans, assume it's already highlighted.
  151. if (el.innerHTML.includes('<span')) {
  152. el.dataset.highlighted = '1';
  153. return;
  154. }
  155. if (el.classList.contains('css-view')) {
  156. el.innerHTML = highlightCSS(text);
  157. } else if (el.classList.contains('html-view')) {
  158. el.innerHTML = highlightHTML(text);
  159. } else {
  160. // `.code-view`
  161. el.innerHTML = highlightJS(text);
  162. }
  163. el.dataset.highlighted = '1';
  164. } catch (_) {
  165. // Never break the page; just skip highlighting if anything goes wrong.
  166. }
  167. });
  168. }
  169. // Expose for pages that want to re-run manually
  170. window.__highlightCssViews = enhance;
  171. if (document.readyState === 'loading') {
  172. document.addEventListener('DOMContentLoaded', enhance, { once: true });
  173. } else {
  174. enhance();
  175. }
  176. // Re-run when switching to the CSS tab (covers cases where the page
  177. // injects/replaces code blocks after load or during navigation).
  178. document.addEventListener(
  179. 'click',
  180. (e) => {
  181. const tab = e.target && e.target.closest ? e.target.closest('.tab') : null;
  182. if (!tab) return;
  183. const label = (tab.textContent || '').trim().toLowerCase();
  184. if (label === 'css' || label === 'html' || label === 'javascript' || label === 'js') {
  185. // Defer so the tab switch can toggle DOM/classes first
  186. setTimeout(enhance, 0);
  187. }
  188. },
  189. true
  190. );
  191. // Observe DOM changes to catch dynamically added `.css-view` blocks.
  192. try {
  193. const mo = new MutationObserver(() => {
  194. // Cheap debounce via microtask
  195. Promise.resolve().then(enhance);
  196. });
  197. mo.observe(document.documentElement, {
  198. subtree: true,
  199. childList: true,
  200. characterData: true
  201. });
  202. } catch (_) {}
  203. })();