From 2d00cb4e6303559b65c442cbbea4f4064317afb6 Mon Sep 17 00:00:00 2001 From: artheru Date: Wed, 10 Jun 2026 08:31:47 +0800 Subject: [PATCH] docs: safe syntax highlighting (0.61.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - minimal dep-free highlighter for fenced code blocks: strings (green), numbers (amber), keywords (purple) - deliberately NO comment rule, so '//' inside URLs (https://, wss://) is never mis-colored or mangled — the common micro-highlighter pitfall - applied in the DocsApp post-render pass alongside the copy buttons; the copy button still yields the exact original text - verified live: 22 tokens colored, both https:// and wss:// URLs preserved byte-for-byte, code intact --- index.html | 2 +- package.json | 2 +- src/DocsApp.jsx | 18 ++++++++++++++++++ src/Scene2D.jsx | 2 +- src/styles.css | 5 +++++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 0fd6071..fcb9ea2 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - + diff --git a/package.json b/package.json index 43e16ab..dd2036d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.60.0-canvasperf", + "version": "0.61.0-syntax", "private": true, "type": "module", "scripts": { diff --git a/src/DocsApp.jsx b/src/DocsApp.jsx index 545154e..0d4e6ef 100644 --- a/src/DocsApp.jsx +++ b/src/DocsApp.jsx @@ -6,6 +6,22 @@ import { copyToast } from './toast.js'; marked.setOptions({ breaks: true }); +// Minimal, safe syntax highlighter: strings, numbers, keywords only. +// No comment rule on purpose — '//' inside URLs (https://) must never be eaten. +const esc = (s) => s.replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c])); +const HL_RE = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?\b)|\b(const|let|var|function|return|while|do|done|if|then|else|fi|for|true|false|null|async|await|new|export|import|setInterval|setTimeout|fetch|sleep|curl|JSON|Math|console)\b/g; +function highlightCode(code) { + let out = '', last = 0, m; + HL_RE.lastIndex = 0; + while ((m = HL_RE.exec(code))) { + out += esc(code.slice(last, m.index)); + const cls = m[1] ? 'hl-str' : m[2] ? 'hl-num' : 'hl-kw'; + out += `${esc(m[0])}`; + last = m.index + m[0].length; + } + return out + esc(code.slice(last)); +} + function DocsWorkspace({ canEdit }) { const [docs, setDocs] = useState([]); const [sel, setSel] = useState(null); @@ -30,6 +46,8 @@ function DocsWorkspace({ canEdit }) { if (!el || editing) return; el.querySelectorAll('pre').forEach((pre) => { if (pre.parentElement?.classList.contains('docs-code')) return; + const codeEl = pre.querySelector('code') || pre; + codeEl.innerHTML = highlightCode(codeEl.textContent); const wrap = document.createElement('div'); wrap.className = 'docs-code'; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'docs-copy'; btn.textContent = 'copy'; btn.addEventListener('click', () => copyToast(pre.innerText, 'Copied')); diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index dfbf13d..b077bd7 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -7,7 +7,7 @@ import Account from './Account.jsx'; import TavernGame from './TavernGame.jsx'; import { api } from './api.js'; -const RELEASE = '0.60.0-canvasperf'; +const RELEASE = '0.61.0-syntax'; const NAV = [ { id: 'home', label: 'Tavern' }, diff --git a/src/styles.css b/src/styles.css index 4fae3d1..be1e562 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1426,3 +1426,8 @@ button { /* keyboard focus indicators — visible for keyboard users only (a11y, 0.59) */ :focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; } .gm-cta-primary:focus-visible, .px-action:focus-visible { outline-color: #f3d27a; } + +/* docs syntax highlight tokens (0.61) */ +.docs-render pre code .hl-str { color: #9ce5a4; } +.docs-render pre code .hl-num { color: #f0b86a; } +.docs-render pre code .hl-kw { color: #c79aef; }