1/**2 * The one JS syntax highlighter (Create's source viewer + Browse's shared3 * source). Scanner-based — strings, templates, and comments are real lexer4 * states, so a `//` inside a string never eats the line. Token classes are5 * styled centrally in style.css (.tok-*).6 *7 * Domain-aware: the tribe-script trinity (state, memory, api) gets its own8 * color — a reader's eye finds the game surface instantly.9 */1011const KEYWORDS = new Set([12 'break', 'case', 'catch', 'class', 'const', 'continue', 'default', 'delete',13 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', 'let',14 'new', 'of', 'return', 'switch', 'throw', 'try', 'typeof', 'var', 'while',15 'yield', 'await', 'async',16]);17const LITERALS = new Set(['true', 'false', 'null', 'undefined', 'NaN', 'Infinity']);18const BUILTINS = new Set(['state', 'memory', 'api', 'Math', 'JSON']);1920const isWordChar = (c: string): boolean => /[A-Za-z0-9_$]/.test(c);21const isDigit = (c: string): boolean => c >= '0' && c <= '9';2223/** Render source into a target element as colored spans (no innerHTML). */24export function renderHighlighted(target: HTMLElement, source: string): void {25 target.textContent = '';26 const frag = document.createDocumentFragment();27 const emit = (text: string, cls?: string): void => {28 if (text === '') return;29 if (cls === undefined) {30 frag.append(document.createTextNode(text));31 } else {32 const span = document.createElement('span');33 span.className = cls;34 span.textContent = text;35 frag.append(span);36 }37 };3839 const n = source.length;40 let i = 0;41 let plain = '';42 const flush = (): void => {43 emit(plain);44 plain = '';45 };4647 while (i < n) {48 const c = source[i]!;4950 // Comments.51 if (c === '/' && source[i + 1] === '/') {52 flush();53 let j = i;54 while (j < n && source[j] !== '\n') j++;55 emit(source.slice(i, j), 'tok-comment');56 i = j;57 continue;58 }59 if (c === '/' && source[i + 1] === '*') {60 flush();61 let j = i + 2;62 while (j + 1 < n && !(source[j] === '*' && source[j + 1] === '/')) j++;63 j = Math.min(n, j + 2);64 emit(source.slice(i, j), 'tok-comment');65 i = j;66 continue;67 }6869 // Strings (quotes and templates; templates kept whole — good enough70 // for display, and interpolations stay legible inside the color).71 if (c === '"' || c === "'" || c === '`') {72 flush();73 let j = i + 1;74 while (j < n) {75 if (source[j] === '\\') {76 j += 2;77 continue;78 }79 if (source[j] === c) {80 j++;81 break;82 }83 if (c !== '`' && source[j] === '\n') break; // unterminated line string84 j++;85 }86 emit(source.slice(i, j), 'tok-string');87 i = j;88 continue;89 }9091 // Numbers (int, float, hex).92 if (isDigit(c) || (c === '.' && isDigit(source[i + 1] ?? ''))) {93 flush();94 let j = i;95 if (c === '0' && (source[i + 1] === 'x' || source[i + 1] === 'X')) {96 j = i + 2;97 while (j < n && /[0-9a-fA-F]/.test(source[j]!)) j++;98 } else {99 while (j < n && /[0-9._eE+-]/.test(source[j]!)) {100 // Stop signs that end a numeric literal in practice.101 if ((source[j] === '+' || source[j] === '-') && !/[eE]/.test(source[j - 1]!)) break;102 j++;103 }104 }105 emit(source.slice(i, j), 'tok-number');106 i = j;107 continue;108 }109110 // Words: keywords, literals, builtins, function names, properties.111 if (isWordChar(c) && !isDigit(c)) {112 let j = i;113 while (j < n && isWordChar(source[j]!)) j++;114 const word = source.slice(i, j);115 const prevChar = ((): string => {116 let k = i - 1;117 while (k >= 0 && (source[k] === ' ' || source[k] === '\t')) k--;118 return k >= 0 ? source[k]! : '';119 })();120 let k = j;121 while (k < n && (source[k] === ' ' || source[k] === '\t')) k++;122 const nextChar = k < n ? source[k]! : '';123124 flush();125 if (KEYWORDS.has(word)) emit(word, 'tok-keyword');126 else if (LITERALS.has(word)) emit(word, 'tok-literal');127 else if (BUILTINS.has(word) && prevChar !== '.') emit(word, 'tok-builtin');128 else if (nextChar === '(') emit(word, 'tok-fn');129 else if (prevChar === '.') emit(word, 'tok-prop');130 else emit(word);131 i = j;132 continue;133 }134135 plain += c;136 i++;137 }138 flush();139 target.append(frag);140}