/* HTML entity escaping. No regex on the output — fixed-time char scan. */ const AMP = '&'; const LT = '<'; const GT = '>'; const QUOT = '"'; const APOS = '''; /** Escape a string to be safe inside text content (between tags). */ export function escapeText(s: string): string { let out = ''; let lastFlush = 0; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); let repl: string | null = null; if (c === 38 /* & */) repl = AMP; else if (c === 60 /* < */) repl = LT; else if (c === 62 /* > */) repl = GT; if (repl !== null) { if (i !== lastFlush) out += s.slice(lastFlush, i); out += repl; lastFlush = i + 1; } } return lastFlush === 0 ? s : out + s.slice(lastFlush); } /** Escape a string to be safe inside a double-quoted attribute value. Also escapes `<` since * it is occasionally consumed by tools that don't strictly follow HTML5 attribute parsing. */ export function escapeAttr(s: string): string { let out = ''; let lastFlush = 0; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); let repl: string | null = null; if (c === 38 /* & */) repl = AMP; else if (c === 34 /* " */) repl = QUOT; else if (c === 39 /* ' */) repl = APOS; else if (c === 60 /* < */) repl = LT; if (repl !== null) { if (i !== lastFlush) out += s.slice(lastFlush, i); out += repl; lastFlush = i + 1; } } return lastFlush === 0 ? s : out + s.slice(lastFlush); } /** True if the string has no characters that would break inside a script JSON template. */ export function isJsonSafe(s: string): boolean { for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if (c === 60 /* < */ || c === 62 /* > */ || c === 38 /* & */) return false; } return true; } /** Make a JSON string safe to embed inside ``. */ export function safeJsonForTemplate(s: string): string { // The browser parses ` close the tag early; // - `&` starts an HTML entity, so any `"`/`&`/`'` sequence already present in the // serialized *data* (e.g. HTML-escaped doc content) would be DECODED by the parser when the // runtime reads the template's textContent, corrupting the JSON (`"` → `"`). // Both characters only ever appear inside JSON string literals (the structural JSON chars are // `{}[]":,`, whitespace, and number/keyword literals — never `<` or `&`), so rewriting each to a // `\uXXXX` escape is a lossless round-trip: JSON.parse decodes them back to `<` / `&`. let out = ''; let lastFlush = 0; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); let repl: string | null = null; if (c === 60 /* < */) repl = '\\u003c'; else if (c === 38 /* & */) repl = '\\u0026'; if (repl !== null) { if (i !== lastFlush) out += s.slice(lastFlush, i); out += repl; lastFlush = i + 1; } } return lastFlush === 0 ? s : out + s.slice(lastFlush); }