140 lines
5.5 KiB
JavaScript
140 lines
5.5 KiB
JavaScript
/**
|
|
* 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.`);
|