/** * This script automatically generates src/core/lib/HTMLEntities.mjs, the shared * HTML entity lookup tables used by the "To HTML Entity" and "From HTML Entity" * operations. * * The data is derived from the vendored WHATWG named character reference set * (src/core/vendor/htmlEntities/entity.json, from * https://html.spec.whatwg.org/entities.json) so the * two operations cannot drift apart and every entity is spec-conformant: * * - HTML_ENTITY_REVERSE_LOOKUP (decode) is every single-code-point spec name. * - HTML_ENTITY_LOOKUP (encode) picks one canonical name per code point via a * deterministic tiebreak, overridden by htmlEntityOverrides.mjs where a * specific historical name is preferred. * * @author roberson-io [michaelroberson@gmail.com] * @copyright Crown Copyright 2026 * @license Apache-2.0 */ /* eslint no-console: ["off"] */ import path from "path"; import fs from "fs"; import process from "process"; import { fileURLToPath } from "url"; import { HTML_ENTITY_CANONICAL_OVERRIDES } from "./htmlEntityOverrides.mjs"; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); if (!fs.existsSync(path.join(process.cwd(), "src/core/lib"))) { console.log("\nCWD: " + process.cwd()); console.log("Error: generateHTMLEntities.mjs should be run from the project root"); console.log("Example> node src/core/config/scripts/generateHTMLEntities.mjs"); process.exit(1); } const SPEC = JSON.parse(fs.readFileSync( path.join(scriptDir, "..", "..", "vendor", "htmlEntities", "entity.json"), "utf8")); // Build code point -> [spec names] for single-code-point, semicolon-terminated // references (the representable subset; multi-code-point entities are skipped). const codePointNames = {}; for (const [key, val] of Object.entries(SPEC)) { if (!key.endsWith(";") || val.codepoints.length !== 1) continue; const name = key.slice(1, -1); // strip leading "&" and trailing ";" (codePointNames[val.codepoints[0]] ??= []).push(name); } // Deterministic canonical-name tiebreak: prefer a lower-case name, then the // shortest, then alphabetical. Overridden per code point where a specific // historical name is preferred. const isAllUpper = s => s === s.toUpperCase() && s !== s.toLowerCase(); /** * Choose the single canonical entity name for a code point. * * @param {number} codePoint - the Unicode code point being encoded * @param {string[]} names - all valid WHATWG names for that code point * @returns {string} the canonical name to emit when encoding */ function canonicalName(codePoint, names) { const override = HTML_ENTITY_CANONICAL_OVERRIDES[codePoint]; if (override !== undefined) { if (!names.includes(override)) throw new Error(`Override &${override}; is not a spec name for code point ${codePoint} (spec: ${names})`); return override; } return [...names].sort((a, b) => (isAllUpper(a) - isAllUpper(b)) || (a.length - b.length) || (a < b ? -1 : a > b ? 1 : 0) )[0]; } const forward = {}; // code point -> canonical name (encode) const reverse = {}; // name -> code point (decode, every spec name) for (const [cpStr, names] of Object.entries(codePointNames)) { const cp = Number(cpStr); forward[cp] = canonicalName(cp, names); for (const name of names) reverse[name] = cp; } // Decode-only aliases = spec names that are not the canonical encode name. const aliases = {}; for (const [name, cp] of Object.entries(reverse)) { if (forward[cp] !== name) aliases[name] = cp; } // --- emit ----------------------------------------------------------------- const fwdEntries = Object.keys(forward).map(Number).sort((a, b) => a - b) .map(cp => ` ${cp}: "${forward[cp]}",`).join("\n").replace(/,$/, ""); const aliasEntries = Object.keys(aliases).sort() .map(n => ` ${JSON.stringify(n)}: ${aliases[n]},`).join("\n").replace(/,$/, ""); const code = `/** * THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateHTMLEntities.mjs * * HTML entity lookup tables shared by the "To HTML Entity" and "From HTML Entity" * operations, derived from the WHATWG named character reference set * (https://html.spec.whatwg.org/entities.json). Do not edit by hand — change the * vendored src/core/vendor/htmlEntities/entity.json or htmlEntityOverrides.mjs * and regenerate. * * @author roberson-io [michaelroberson@gmail.com] * @copyright Crown Copyright ${new Date().getUTCFullYear()} * @license Apache-2.0 */ /** * Canonical lookup: Unicode code point -> entity name (without "&" and ";"), * used for ENCODING. One canonical name per code point. */ export const HTML_ENTITY_LOOKUP = { ${fwdEntries} }; /** * Legacy / alias names that only DECODE (spelling variants, deprecated names, * box-drawing aliases, etc.); not used for encoding. */ export const HTML_ENTITY_DECODE_ALIASES = { ${aliasEntries} }; /** * Derived reverse lookup: entity name -> code point, used for DECODING. Built * from the canonical lookup plus the legacy aliases. */ export const HTML_ENTITY_REVERSE_LOOKUP = {...HTML_ENTITY_DECODE_ALIASES}; for (const codePoint in HTML_ENTITY_LOOKUP) { HTML_ENTITY_REVERSE_LOOKUP[HTML_ENTITY_LOOKUP[codePoint]] = Number(codePoint); } `; fs.writeFileSync(path.join(process.cwd(), "src/core/lib/HTMLEntities.mjs"), code); console.log(`generateHTMLEntities: ${Object.keys(forward).length} encode names, ` + `${Object.keys(reverse).length} decode names, ${Object.keys(aliases).length} aliases, ` + `${Object.keys(HTML_ENTITY_CANONICAL_OVERRIDES).length} overrides applied.`);