docs: safe syntax highlighting (0.61.0)

- 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
This commit is contained in:
artheru 2026-06-10 08:31:47 +08:00
parent cf67af7131
commit 2d00cb4e63
5 changed files with 26 additions and 3 deletions

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" /> <meta name="theme-color" content="#0e0a14" />
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." /> <meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.60.0-canvasperf" /> <meta name="version" content="0.61.0-syntax" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" /> <link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />

View File

@ -1,6 +1,6 @@
{ {
"name": "amerc-site", "name": "amerc-site",
"version": "0.60.0-canvasperf", "version": "0.61.0-syntax",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -6,6 +6,22 @@ import { copyToast } from './toast.js';
marked.setOptions({ breaks: true }); 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[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 += `<span class="${cls}">${esc(m[0])}</span>`;
last = m.index + m[0].length;
}
return out + esc(code.slice(last));
}
function DocsWorkspace({ canEdit }) { function DocsWorkspace({ canEdit }) {
const [docs, setDocs] = useState([]); const [docs, setDocs] = useState([]);
const [sel, setSel] = useState(null); const [sel, setSel] = useState(null);
@ -30,6 +46,8 @@ function DocsWorkspace({ canEdit }) {
if (!el || editing) return; if (!el || editing) return;
el.querySelectorAll('pre').forEach((pre) => { el.querySelectorAll('pre').forEach((pre) => {
if (pre.parentElement?.classList.contains('docs-code')) return; 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 wrap = document.createElement('div'); wrap.className = 'docs-code';
const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'docs-copy'; btn.textContent = 'copy'; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'docs-copy'; btn.textContent = 'copy';
btn.addEventListener('click', () => copyToast(pre.innerText, 'Copied')); btn.addEventListener('click', () => copyToast(pre.innerText, 'Copied'));

View File

@ -7,7 +7,7 @@ import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx'; import TavernGame from './TavernGame.jsx';
import { api } from './api.js'; import { api } from './api.js';
const RELEASE = '0.60.0-canvasperf'; const RELEASE = '0.61.0-syntax';
const NAV = [ const NAV = [
{ id: 'home', label: 'Tavern' }, { id: 'home', label: 'Tavern' },

View File

@ -1426,3 +1426,8 @@ button {
/* keyboard focus indicators — visible for keyboard users only (a11y, 0.59) */ /* keyboard focus indicators — visible for keyboard users only (a11y, 0.59) */
:focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; } :focus-visible { outline: 2px solid var(--cyan); outline-offset: 2px; }
.gm-cta-primary:focus-visible, .px-action:focus-visible { outline-color: #f3d27a; } .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; }