Initial commit
This commit is contained in:
commit
89053153cc
8152 files changed
+2086461
No files matched your search
+69
@@ -0,0 +1,69 @@
|
||||
// Adapted from https://github.com/mathiasbynens/he/blob/36afe179392226cf1b6ccdb16ebbb7a5a844d93a/src/he.js#L106-L134
|
||||
|
||||
/**
|
||||
* C1 Unicode control character reference replacements (code points 128–159).
|
||||
* Index i gives the replacement for code point 128+i; 0 means "no replacement".
|
||||
*/
|
||||
const c1: number[] = [
|
||||
8364, 0, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 0,
|
||||
381, 0, 0, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250,
|
||||
339, 0, 382, 376,
|
||||
];
|
||||
|
||||
/**
|
||||
* True for NUL, UTF-16 surrogates, and values past U+10FFFF.
|
||||
* @param codePoint Unicode code point to check.
|
||||
*/
|
||||
function isInvalidCodePoint(codePoint: number): boolean {
|
||||
return (
|
||||
codePoint === 0 ||
|
||||
(codePoint >= 0xd8_00 && codePoint <= 0xdf_ff) ||
|
||||
codePoint > 0x10_ff_ff
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the given code point with U+FFFD if it is NUL (0), a surrogate, or
|
||||
* outside the valid Unicode range. Code points in the C1 controls range
|
||||
* (128–159) are remapped to their Windows-1252 equivalents, following the
|
||||
* HTML spec. All other code points are returned unchanged.
|
||||
* @param codePoint Unicode code point to convert.
|
||||
*/
|
||||
export function replaceCodePoint(codePoint: number): number {
|
||||
if (isInvalidCodePoint(codePoint)) {
|
||||
return 0xff_fd;
|
||||
}
|
||||
|
||||
if (codePoint >= 128 && codePoint <= 159) {
|
||||
return c1[codePoint - 128] || codePoint;
|
||||
}
|
||||
|
||||
return codePoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* XML numeric character references are the referenced Unicode code point.
|
||||
* Invalid values still become U+FFFD; the HTML Windows-1252 C1 remap is not
|
||||
* applied.
|
||||
* @see https://www.w3.org/TR/xml/#NT-CharRef
|
||||
* @param codePoint Unicode code point to convert.
|
||||
*/
|
||||
export function replaceCodePointXML(codePoint: number): number {
|
||||
return isInvalidCodePoint(codePoint) ? 0xff_fd : codePoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the code point of a decoded numeric entity to a string, replacing
|
||||
* invalid values.
|
||||
*
|
||||
* Fast path for plain BMP code points: [1..0x7F] and [0xA0..0xD7FF] pass
|
||||
* `replaceCodePoint` unchanged (no NUL, C1 remap, surrogate, or out-of-range
|
||||
* handling) and fit a single charCode. 0xd760 = 0xD800 (the first surrogate)
|
||||
* - 0xA0.
|
||||
* @param codePoint Unicode code point to convert.
|
||||
*/
|
||||
export function codePointToString(codePoint: number): string {
|
||||
return (codePoint - 1) >>> 0 < 0x7f || (codePoint - 0xa0) >>> 0 < 0xd7_60
|
||||
? String.fromCharCode(codePoint)
|
||||
: String.fromCodePoint(replaceCodePoint(codePoint));
|
||||
}
|
||||
+1368
File diff suppressed because it is too large.
Load diff
+337
@@ -0,0 +1,337 @@
|
||||
import { XML_BITSET_VALUE, xmlEncodeRegex } from "./escape.js";
|
||||
import htmlTrieData from "./generated/encode-html.js";
|
||||
|
||||
/**
|
||||
* A node inside the encoding trie used by `encode.ts`.
|
||||
*
|
||||
* There are two physical shapes to minimize allocations and lookup cost:
|
||||
*
|
||||
* 1. Leaf node (string)
|
||||
* - A plain string (already in the form `"&name;"`).
|
||||
* - Represents a terminal match with no children.
|
||||
*
|
||||
* 2. Branch / value node (object)
|
||||
*/
|
||||
type EncodeTrieNode =
|
||||
| string
|
||||
| { value: string | null; next: Map<number, EncodeTrieNode> };
|
||||
|
||||
/**
|
||||
* Flat lookup table for ASCII entity values (code points 0–127).
|
||||
*
|
||||
* Built once at startup from the full trie. For ASCII characters that need
|
||||
* encoding, a direct `asciiEntities[charCode]` array index is much faster
|
||||
* than `Map.get(charCode)` because it avoids hashing and bucket lookup.
|
||||
*
|
||||
* A few ASCII characters also start multi-code-point entities
|
||||
* (e.g. `<` + U+20D2 → `<⃒`). For those, the table holds the branch
|
||||
* node itself (children plus the single-char value), so the ASCII path
|
||||
* never consults `htmlTrie`.
|
||||
*/
|
||||
const asciiEntities: (EncodeTrieNode | null)[] = /* #__PURE__ */ Array.from(
|
||||
{ length: 128 },
|
||||
() => null,
|
||||
);
|
||||
|
||||
const htmlTrie: Map<number, EncodeTrieNode> = (() => {
|
||||
/**
|
||||
* Parse a compact encode trie string into a Map keyed by code point.
|
||||
*
|
||||
* The serialized format (produced by `scripts/write-encode-map.ts`) stores
|
||||
* entries in ascending code-point order with delta encoding:
|
||||
*
|
||||
* [gap]name;[{children}]
|
||||
*
|
||||
* - `gap` is a base-10 integer giving `currentKey - previousKey - 1`.
|
||||
* The very first entry stores the absolute key. A gap of 0 is omitted.
|
||||
* - `name;` is the entity name (without `&` prefix) terminated by `;`.
|
||||
* Because gaps use only digits `[0-9]` and entity names always start with
|
||||
* a letter `[A-Za-z]`, no additional delimiter is needed.
|
||||
* - `{…}` is an optional children block using the same scheme recursively.
|
||||
* Children represent the second code unit of multi-character entities
|
||||
* (e.g. `<` + U+20D2 → `<⃒`).
|
||||
*/
|
||||
const trie = new Map<number, EncodeTrieNode>();
|
||||
const data = htmlTrieData;
|
||||
let cursor = 0;
|
||||
let lastKey = -1;
|
||||
|
||||
function readGap(): number {
|
||||
let value = 0;
|
||||
let ch: number;
|
||||
while (
|
||||
(ch = data.charCodeAt(cursor)) >= 48 /* '0' */ &&
|
||||
ch <= 57 /* '9' */
|
||||
) {
|
||||
value = value * 10 + ch - 48;
|
||||
cursor++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function readEntity(): string {
|
||||
const semi = data.indexOf(";", cursor);
|
||||
const entity = `&${data.substring(cursor, semi)};`;
|
||||
cursor = semi + 1;
|
||||
return entity;
|
||||
}
|
||||
|
||||
const astralEntries: [number, string][] = [];
|
||||
|
||||
while (cursor < data.length) {
|
||||
lastKey += readGap() + 1;
|
||||
|
||||
const entityValue =
|
||||
data.charCodeAt(cursor) === 123 /* '{' */ ? null : readEntity();
|
||||
|
||||
if (data.charCodeAt(cursor) === 123 /* '{' */) {
|
||||
cursor++; // Skip '{'
|
||||
const next = new Map<number, EncodeTrieNode>();
|
||||
let childKey = -1;
|
||||
while (data.charCodeAt(cursor) !== 125 /* '}' */) {
|
||||
childKey += readGap() + 1;
|
||||
next.set(childKey, readEntity());
|
||||
}
|
||||
const branch = { value: entityValue, next };
|
||||
trie.set(lastKey, branch);
|
||||
cursor++; // Skip '}'
|
||||
// ASCII fast path holds the branch node itself (children + value).
|
||||
if (lastKey < 0x80) {
|
||||
asciiEntities[lastKey] = branch;
|
||||
}
|
||||
} else if (lastKey < 0x80) {
|
||||
asciiEntities[lastKey] = entityValue;
|
||||
} else if (lastKey > 0xff_ff) {
|
||||
astralEntries.push([lastKey, entityValue!]);
|
||||
} else {
|
||||
trie.set(lastKey, entityValue!);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Batch-insert astral entries as surrogate-pair trie nodes.
|
||||
* Entries are sorted by code point, so same-high-surrogate groups
|
||||
* are contiguous — no intermediate grouping Map needed.
|
||||
*/
|
||||
let astralIndex = 0;
|
||||
while (astralIndex < astralEntries.length) {
|
||||
const hi =
|
||||
0xd8_00 | ((astralEntries[astralIndex][0] - 0x1_00_00) >> 10);
|
||||
const children: [number, string][] = [];
|
||||
while (
|
||||
astralIndex < astralEntries.length &&
|
||||
(0xd8_00 | ((astralEntries[astralIndex][0] - 0x1_00_00) >> 10)) ===
|
||||
hi
|
||||
) {
|
||||
const lo =
|
||||
0xdc_00 |
|
||||
((astralEntries[astralIndex][0] - 0x1_00_00) & 0x3_ff);
|
||||
children.push([lo, astralEntries[astralIndex][1]]);
|
||||
astralIndex++;
|
||||
}
|
||||
trie.set(hi, { value: null, next: new Map(children) });
|
||||
}
|
||||
|
||||
return trie;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Bitset covering ASCII code points 0–127. Each of the four 32-bit words
|
||||
* covers a 32-code-point range. A set bit means "this character needs
|
||||
* encoding" when used with `encodeHTML`.
|
||||
*/
|
||||
const HTML_BITSET = /* #__PURE__ */ new Uint32Array([
|
||||
0x16_00, // 09 (\t), 0A (\n), 0C (\f)
|
||||
0xfc_00_ff_fe, // 21-2D (!-.), 2E (.), 2F (/), 3A-3F (:;<=>?)
|
||||
0xf8_00_00_01, // 40 (@), 5B-5F ([\]^_)
|
||||
0x38_00_00_01, // 60 (`), 7B-7D ({|})
|
||||
]);
|
||||
|
||||
const XML_BITSET = /* #__PURE__ */ new Uint32Array([0, XML_BITSET_VALUE, 0, 0]);
|
||||
|
||||
/*
|
||||
* Regex equivalent of `HTML_BITSET` (plus all non-ASCII code units, lone
|
||||
* surrogates included — no `u` flag). Must stay in sync with the bitset:
|
||||
* the scan uses the regex to find candidates and the bitset to re-check
|
||||
* adjacent characters. The XML equivalent is `xmlEncodeRegex`, shared with
|
||||
* `escape.ts`.
|
||||
*/
|
||||
// eslint-disable-next-line unicorn/prefer-unicode-code-point-escapes -- the `\u{...}` form requires the `u` flag, which we deliberately omit so lone surrogates match by code unit
|
||||
const HTML_ENCODE_RE = /[\t\n\f!-/:-@[-`{-}\u0080-\uFFFF]/g;
|
||||
|
||||
const numericReference = (cp: number) => `&#x${cp.toString(16)};`;
|
||||
|
||||
/**
|
||||
* Encodes all characters in the input using HTML entities. This includes
|
||||
* characters that are valid ASCII characters in HTML documents, such as `#`.
|
||||
*
|
||||
* To get a more compact output, consider using the `encodeNonAsciiHTML`
|
||||
* function, which will only encode characters that are not valid in HTML
|
||||
* documents, as well as non-ASCII characters.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode.
|
||||
*/
|
||||
export function encodeHTML(input: string): string {
|
||||
return encodeHTMLTrieRe(HTML_BITSET, HTML_ENCODE_RE, input);
|
||||
}
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in HTML
|
||||
* documents using HTML entities. This function will not encode characters that
|
||||
* are valid in HTML documents, such as `#`.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode.
|
||||
*/
|
||||
export function encodeNonAsciiHTML(input: string): string {
|
||||
return encodeHTMLTrieRe(XML_BITSET, xmlEncodeRegex, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `code` (a UTF-16 code unit) must be encoded: any non-ASCII unit, or
|
||||
* an ASCII unit flagged in `bitset`.
|
||||
* @param bitset Bitset of ASCII characters to encode.
|
||||
* @param code Code unit to test.
|
||||
*/
|
||||
function isEncodable(bitset: Uint32Array, code: number): boolean {
|
||||
return code >= 0x80 || ((bitset[code >>> 5] >>> code) & 1) === 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* The inline scan beats the regex jump only for short gaps (measured
|
||||
* break-even is ~7 characters; below it the per-call regex overhead
|
||||
* dominates, above it the regex skips clean spans faster in native code).
|
||||
* So we scan at most `INLINE_SCAN_WINDOW` characters inline before deferring
|
||||
* to the regex. Any gap long enough to exhaust the window switches to
|
||||
* long-gap mode, which skips the inline scan entirely; we stay in that mode
|
||||
* while total gaps are at least `LONG_GAP_THRESHOLD` characters long and
|
||||
* drop back to the inline scan as soon as a shorter gap appears. Gaps are
|
||||
* always measured from where they start, so every window-exhausting gap
|
||||
* enters long-gap mode and no gap length pays both scans repeatedly.
|
||||
*/
|
||||
/** @internal Exported for tests; not re-exported from the package entry. */
|
||||
export const INLINE_SCAN_WINDOW = 16;
|
||||
const LONG_GAP_THRESHOLD = 8;
|
||||
|
||||
function encodeHTMLTrieRe(
|
||||
bitset: Uint32Array,
|
||||
re: RegExp,
|
||||
input: string,
|
||||
): string {
|
||||
const { length } = input;
|
||||
let out: string | undefined;
|
||||
let last = 0; // Start of the next untouched slice.
|
||||
let index = 0;
|
||||
let wasLongGap = false; // The previous gap was long; skip the inline scan.
|
||||
|
||||
while (index < length) {
|
||||
let char = input.charCodeAt(index);
|
||||
|
||||
/*
|
||||
* Find the next encodable character (one matching `bitset`, or any
|
||||
* non-ASCII unit). Gaps between entities in dense text are only a
|
||||
* few characters, so scan a short window inline first; the regex —
|
||||
* which matches exactly the same characters and skips clean spans
|
||||
* in native code — runs for longer gaps. Gap lengths cluster, so
|
||||
* after a window-exhausting gap the window is skipped until a gap
|
||||
* shorter than LONG_GAP_THRESHOLD reappears. Once located, the
|
||||
* character is captured in `char` and control falls through to the
|
||||
* encode logic below instead of re-testing it on the next loop
|
||||
* iteration.
|
||||
*/
|
||||
if (!isEncodable(bitset, char)) {
|
||||
const gapStart = index;
|
||||
let next = index + 1;
|
||||
let wasFound = false;
|
||||
if (!wasLongGap) {
|
||||
const bound = Math.min(index + INLINE_SCAN_WINDOW, length);
|
||||
while (
|
||||
next < bound &&
|
||||
!isEncodable(bitset, (char = input.charCodeAt(next)))
|
||||
) {
|
||||
next++;
|
||||
}
|
||||
if (next < bound) {
|
||||
// `char` already holds the encodable unit at `next`.
|
||||
index = next;
|
||||
wasFound = true;
|
||||
} else if (next >= length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!wasFound) {
|
||||
/*
|
||||
* Every match is a single code unit, so `test` pins it at
|
||||
* `lastIndex - 1` without allocating a match object.
|
||||
*/
|
||||
re.lastIndex = next;
|
||||
if (!re.test(input)) break;
|
||||
index = re.lastIndex - 1;
|
||||
/*
|
||||
* Reaching the regex from the inline scan means the gap
|
||||
* exhausted the window, so it is long by definition; once
|
||||
* in long-gap mode, stay until a gap shorter than the
|
||||
* threshold appears.
|
||||
*/
|
||||
wasLongGap =
|
||||
!wasLongGap || index - gapStart >= LONG_GAP_THRESHOLD;
|
||||
char = input.charCodeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy-init: copy the prefix before the first character that needs encoding.
|
||||
if (out == null) out = input.substring(0, index);
|
||||
else if (last !== index) out += input.substring(last, index);
|
||||
|
||||
if (char < 0x80) {
|
||||
const node = asciiEntities[char];
|
||||
if (typeof node === "object" && node !== null) {
|
||||
// Multi-code-point entity first (e.g. < + U+20D2 → <⃒).
|
||||
if (index + 1 < length) {
|
||||
const value = node.next.get(input.charCodeAt(index + 1));
|
||||
if (value != null) {
|
||||
out += value;
|
||||
last = index += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out += node.value ?? numericReference(char);
|
||||
} else {
|
||||
out += node ?? numericReference(char);
|
||||
}
|
||||
} else {
|
||||
// Non-ASCII: full trie lookup with multi-char entity support.
|
||||
let node: EncodeTrieNode | undefined | null = htmlTrie.get(char);
|
||||
|
||||
if (typeof node === "object") {
|
||||
if (index + 1 < length) {
|
||||
const value = node.next.get(input.charCodeAt(index + 1));
|
||||
if (value != null) {
|
||||
out += value;
|
||||
last = index += 2;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
node = node.value;
|
||||
}
|
||||
|
||||
if (node == null) {
|
||||
// No named entity exists; emit a hexadecimal numeric reference.
|
||||
const cp = input.codePointAt(index)!;
|
||||
out += numericReference(cp);
|
||||
// Astral code points consume two UTF-16 code units.
|
||||
if (cp !== char) index++;
|
||||
} else {
|
||||
out += node;
|
||||
}
|
||||
}
|
||||
last = index += 1;
|
||||
}
|
||||
|
||||
// If nothing needed encoding, return the original string (avoids allocation).
|
||||
if (out == null) return input;
|
||||
if (last < length) out += input.substr(last);
|
||||
return out;
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Get the named reference for an XML special character or U+00A0.
|
||||
* @param char Code unit matched by one of the escape regexes.
|
||||
*/
|
||||
function getEscape(char: number): string {
|
||||
return char === 34
|
||||
? """
|
||||
: char === 38
|
||||
? "&"
|
||||
: char === 39
|
||||
? "'"
|
||||
: char === 60
|
||||
? "<"
|
||||
: char === 62
|
||||
? ">"
|
||||
: " ";
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a code point at a given index.
|
||||
* @param input String to read the code point from.
|
||||
* @param index Current read position in the input string.
|
||||
* @returns The code point at `index`, or `undefined` if `index` is out of range.
|
||||
* @deprecated Use `String.prototype.codePointAt` directly instead; this export
|
||||
* will be removed in the next major.
|
||||
*/
|
||||
export const getCodePoint: (input: string, index: number) => number = (
|
||||
input: string,
|
||||
index: number,
|
||||
): number => input.codePointAt(index)!;
|
||||
|
||||
/**
|
||||
* Bitset for ASCII characters that need to be escaped in XML.
|
||||
*/
|
||||
export const XML_BITSET_VALUE = 0x50_00_00_c4; // 32..63 -> 34 ("),38 (&),39 ('),60 (<),62 (>)
|
||||
|
||||
/**
|
||||
* Matches exactly the characters `encodeXML` escapes: the five XML special
|
||||
* characters plus every non-ASCII code unit (lone surrogates included — no
|
||||
* `u` flag). Kept in sync with `XML_BITSET_VALUE`.
|
||||
*
|
||||
* Shared with `encodeNonAsciiHTML` in `encode.ts`. Because the regex is
|
||||
* stateful (`g` flag), every call site must set `lastIndex` before use.
|
||||
*/
|
||||
// eslint-disable-next-line unicorn/prefer-unicode-code-point-escapes -- the `\u{...}` form requires the `u` flag, which we deliberately omit so lone surrogates match by code unit
|
||||
export const xmlEncodeRegex: RegExp = /["&'<>\u0080-\uFFFF]/g;
|
||||
|
||||
/**
|
||||
* Whether `code` (a UTF-16 code unit) is escaped by {@link encodeXML}: a
|
||||
* non-ASCII unit, or one of the five XML specials flagged in
|
||||
* `XML_BITSET_VALUE` (which is only meaningful for code units 32-63).
|
||||
* @param code Code unit to test.
|
||||
*/
|
||||
function isXmlEscapable(code: number): boolean {
|
||||
return (
|
||||
code >= 0x80 ||
|
||||
(code >= 32 && code < 64 && ((XML_BITSET_VALUE >>> code) & 1) === 1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using XML entities.
|
||||
*
|
||||
* If a character has no equivalent entity, a numeric hexadecimal reference
|
||||
* (eg. `ü`) will be used.
|
||||
* @param input Input string to encode.
|
||||
*/
|
||||
export function encodeXML(input: string): string {
|
||||
const { length } = input;
|
||||
let out: string | undefined;
|
||||
let last = 0;
|
||||
let index = 0;
|
||||
|
||||
while (index < length) {
|
||||
const char = input.charCodeAt(index);
|
||||
|
||||
/*
|
||||
* Find the next character to escape: scan a short window inline
|
||||
* (escapable characters cluster in markup-heavy input), then fall
|
||||
* back to the regex, which skips clean spans in native code.
|
||||
*/
|
||||
if (!isXmlEscapable(char)) {
|
||||
const bound = Math.min(index + 32, length);
|
||||
let next = index + 1;
|
||||
while (next < bound && !isXmlEscapable(input.charCodeAt(next))) {
|
||||
next++;
|
||||
}
|
||||
if (next < bound) {
|
||||
index = next;
|
||||
continue;
|
||||
}
|
||||
if (next >= length) break;
|
||||
xmlEncodeRegex.lastIndex = next;
|
||||
if (!xmlEncodeRegex.test(input)) break;
|
||||
index = xmlEncodeRegex.lastIndex - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (out === undefined) out = input.substring(0, index);
|
||||
else if (last !== index) out += input.substring(last, index);
|
||||
|
||||
if (char < 64) {
|
||||
// Known replacement
|
||||
out += getEscape(char);
|
||||
last = index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-ASCII: encode as numeric entity (handle surrogate pair)
|
||||
const cp = input.codePointAt(index)!;
|
||||
out += `&#x${cp.toString(16)};`;
|
||||
if (cp !== char) index++; // Skip trailing surrogate
|
||||
last = index += 1;
|
||||
}
|
||||
|
||||
if (out === undefined) return input;
|
||||
if (last < length) out += input.substr(last);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes all non-ASCII characters, as well as characters not valid in XML
|
||||
* documents using numeric hexadecimal reference (eg. `ü`).
|
||||
*
|
||||
* Have a look at `escapeUTF8` if you want a more concise output at the expense
|
||||
* of reduced transportability.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export const escape: typeof encodeXML = encodeXML;
|
||||
|
||||
/**
|
||||
* Escape `data` using `re`, mapping each matched character to its entity.
|
||||
* Every match is one UTF-16 code unit, so its index is `lastIndex - 1`.
|
||||
* @param re Global regex matching exactly the characters to escape
|
||||
* (`"`, `&`, `'`, `<`, `>`, `\u00A0` at most).
|
||||
* @param data String to escape.
|
||||
*/
|
||||
function escapeWithRegex(re: RegExp, data: string): string {
|
||||
re.lastIndex = 0;
|
||||
if (!re.test(data)) return data;
|
||||
|
||||
let out = "";
|
||||
let last = 0;
|
||||
do {
|
||||
const index = re.lastIndex - 1;
|
||||
if (last !== index) out += data.substring(last, index);
|
||||
const char = data.charCodeAt(index);
|
||||
out += getEscape(char);
|
||||
last = index + 1;
|
||||
} while (re.test(data));
|
||||
|
||||
return out + data.substring(last);
|
||||
}
|
||||
|
||||
const xmlEscapeRegex = /["&'<>]/g;
|
||||
/**
|
||||
* Encodes all characters not valid in XML documents using XML entities.
|
||||
*
|
||||
* Note that the output will be character-set dependent.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export function escapeUTF8(data: string): string {
|
||||
return escapeWithRegex(xmlEscapeRegex, data);
|
||||
}
|
||||
|
||||
const attributeEscapeRegex = /["&\u{A0}]/gu;
|
||||
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export function escapeAttribute(data: string): string {
|
||||
return escapeWithRegex(attributeEscapeRegex, data);
|
||||
}
|
||||
|
||||
const textEscapeRegex = /[&<>\u{A0}]/gu;
|
||||
|
||||
/**
|
||||
* Encodes all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
* @param data String to escape.
|
||||
*/
|
||||
export function escapeText(data: string): string {
|
||||
return escapeWithRegex(textEscapeRegex, data);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Generated using scripts/write-decode-map.ts
|
||||
|
||||
import { decodeTrieDict } from "../internal/decode-shared.js";
|
||||
/** Packed HTML decode trie data. */
|
||||
export const htmlDecodeTree: Uint16Array = /* #__PURE__ */ decodeTrieDict(
|
||||
"!}.&u%}'&}*'~!6*)%&,~!J~!J~%L~y<~!R,~~%Lu~~#GD~~#|)1#%}^%}2%+#.##%##%}&%##%'#%##&%#%#'%#&#%#&#'#%%#&#%##%#)%''%&%#%#'%#%%#%%}%%%#%#&(23#%%#&-%0%('1#(##%#'##+%'*.:1}#%#6-+(%'%%#%%%}#L'2351&('%}&/N'(0(/*-%(%%}#'+&T%7.2}#&%&#%#36/5##%&%%#&#%%#))2%%##%&&'0~!#*+&'%1~!%).'3q?&%'1~!.##%6(~!+%%%(Gw'rT~!E#<nA%#jZ~!H%(~!42##~!*31&~!G%U~#)5~#`3~!J~!Z~%]~%Y~%C~!q~!u~#kz~%#~!6'~!D~!U~!?~#T~!c%~!G#'~%7|~!G~!J~!G&~#pb~(Df}#%}*&}#%##%##%##&#-}&'#'&%#.++}%mI,#,@&(}*%}*'%&##&#%##%}&0}#.},U},%}+%}&%}#%##&}B%(}(%}+%)})%##%#&}&%##%&}<%}>%#%&}*%}(%}9%}/%})%}*%}*%}?&}&%}3%}&*#%})%#%#)}#&#-#+*%E%%'%'#%}#*V##&##I}#&&##%&%#&&Qf%%))w/0+&%#(#.%-''''++++7}>%4'',##1,#%#&%##&#'##&#*#9)%&%}#*}%,#+P(%A&%#'&##wSD',9E00#y#@}(+}&%&>~!#~!X}#*}(&&}(&}(,%}%&#+&}#&}I%#%}%)#(},'%#*}4%%#%}(''}#/##(##),%-##%%)#&}(.}&%#&}%%}*&#%},&&}&%}#%*'#%})%}D&}&%}-&}6&#&}-,%}#%})-(~+`~,=?~I9'9%~!,#%})%})%}@%}?%}(~!?~#<~#pP~#BG~#=1#%K+~#?#~%;)~#A~#mF1~#A'~'X%'~#lR~#N~'N~#r~#m#-~#i'?%#'%~#B%##%,%#~#_%#0%~#]732~,w~2+#:&#%&'0%&>%}#>##F+)#%&&#(+_}4&}-%}(&}@&}O7Fdf0@+/v4}&WU##&/0#&'('B#%}.%}'+#%}#%%&#&%#%##+#&#)#6#'#.},%}c%},%#%##%&#&%#&~#>'*-.%##%##%}#%%}%'~#)D1}#%*&~#_%%'(~#S2%'.}#~#=##*'*-%}&'%'##&&~'E%.#&~#M4}%%##&'%#~#O1##%&#'+~#<B%##%%'%+~#;#@%}#&%#&&%#(~#H1}'%'##&&~#?A}&'~#D#%32}'&&&&~#[}'(#%}'~#;C})&}%%#%~#=&%,3}%'(#%%~#^'#&&)#%'~#Y%-~#d-%'~#^%%&#&&&}#~#b~2t*&'~&(~&@~0%~e~3}%*''0})&}+~!9##-}#%-hD*)1fC#%/&/fB#40~!+#)*4~!+~!K'&:~!/*7~!.#~!H~!L':~%x&~!H#~!*~%1~!I#~!+A~#p'~!F~~#-#~,,(~.Z~!V~%;'B'mq-W~!N~%I%#&&#&}#%},%%}'%}+X#%}#&}(%}'%}<%}#%}%%'}'%}:~![)9@~%>~#UA%-%##&~!C%~!-.9:~!1~!-^2/:a~!y,D*J#-5)/4~%23,~#G~!L1~!0X3`~!2+~!!0-~&E~!W~!o,>Y&]~%cZx_&~#O*9#A#'#+I'%#)~!0B*-5A+-((F&*M#)(-7-5+'-3a5Vi~!Y~!?+[)%3),ERHm~!+:D,VG.+)?fB%%*(%)'(#&80%1'8`K8?`+'Z#&O&'H5#*9)A%%5&3))0%39+.*7#()&&*=4@**L)<'_&*+..;(#*+)./&0#3)%')-8(4ixD(&.}%,('aI:,)%,k2231T)I'#/-W7,/'Q#.'Y24+h')37</31&83##&0#),H(?'&?/1##%#&&#%''-%&&&#(&''&#.-'%#%%(,')*'&#&#'##%(%(#%('#&##%%%%('%#%#%%#%#&%##h>w+v<ayvyvcg.uuhKr}g/v|g>u9i[~>g5uI~=RvdwEg;v/g;uk!!TTSx]@RT!U!#!@VBRUU!'UTe-d0c`e&gSdicedFcrdTaqb.kYcAohdYd@a3e+d}dMdtd.aJ#bqcK`dle/e.e'dwdPdodddjbEb}ogd^ofdpduc6j?l%d{drdqc)d7bacOdQ%T#Y)X.sR[yH>6Vyv3[xwLu>vo'!*.[yBacahoj>6Rew3[xqdZa#!a&#^(X-[yG>6Vyu3[xvg3sEr|g.u/Ri9db0T#^(Xa)!-[y;>6Vylg4wKs{JwNZt3@3r=c4Z([xlg;wKt!cpq's@v7A'*a(a+!-a#[y<3Dt?3Dt'>6Vym3[xmg9rxsNJwLZt4~?r?db1T#`-!(Xa,!0[yS>6Vz%NuQs.g4wKtnJwNZtS@3r>c4Z([y%g;wKtrdga8!a(!#&T*Y-Xa#!a0<or[yc3Dtq>6Vz43[y3JwNZtf@3s!Ju}!%Dti:pm3c_%X#tjB5pkd6q!r]u?voC'*-a.a2!0a&a+[yI3DtI3Ds~3DtH>6Vyw3[xx;:s#~<5pKJwNZtE@3r~d`a)!a2T#a.(!+U.X1[yT3Dt`3Dtv>6Vz&3[y&g9rxwzcxstPu.<rAJwLZtT~?r@dZa%!a.&^*Za(/Reu[ya>6Vz23[y1g3sEr}wkg{NuQRg{ci(U#5@b`~,cg#U(2WnH5wugcRh7dX#T(Y,a'Ta!!a,[yZ<]mj>6Vz,3[y+Pv#5ReZKu+=,%!H}7ABwkaS?Rh:BcW(X#<]mrj:ubv/ARekdg%!(!a.*Ta(Y.X1!#sP>Rl*Dt6[y>>6Vyo3Wf*jOvuumvuRgRJuq*!:9<B@bX~3jVv&v@s@5Re[d/rQt{uAvo&a&a*)a2!,0Wf!3Dt0=Bs'>6Re}3[xy~<5s%JwJZt1~Gs)c;&!#2sJkNuXvzq7rxu,Re8dka4!a8(aEZ+a@Y.X1Xa)[yd=Bs(3DtP>6Vz53[y4cX#X&Re:avRe9~<5s&JwJZtQ~Gs*i^rzvdRg+Jv{%!2sbB@bX}kdga,!Za?&^*T1/!a'Dt+[y6>6Vyf3Wf%g/u;s4hGu6?Rh-JvZ,!c%#&RoX54Rivj7uyvf8RgTKvZB%*!2sGh<vu5Rgq<=C::9bb~#dZ#T&Ta6Y.X*Dt>[y93Wf)coZ(T,6VyifluvRgC@95@B@bX~/hFu34cC#T,k/unq8w8Q5RkUklwQuzunq8w8Q5Rk8d/rJu?v8w9)-&!a0a;a&aIWejg3sEr/h1s<DtDJvyZqY5aws3Jvy!&Wei~Hr1:au5@Bag>23E~5c:Z&bX};kKv?w&unuVu5Rjc;>bs)#~@:Rh.=ay<a]C;b`}Vd6s/t{uAvoaxa()!a,a7%-a#a2Dt,[yF2Wo[>6Vyt3[xuNuPRi&NuPwpi#RoWh?vf8Ri%Jv]!%Ri:KvxD!.'2WeAjZu`q9rxu,Re7woeAg-unLq(qA_/*2Wg_g3u5q^9:4E}/jTrxrzv=Wkkd~0UX#^^Xa-a1a5T&a=U1a'*aEa]!a*aPaA-adok[y54Rn>;:p3~Dp5g9rpsFNvZqjg3uJp4~<5p0Pw;5qlJwNZt*@3p1Pw:5p/Ou!5p2JvG'!6Vye=<qnJvh_[xhg3v,Rh3kOwOw-sDuev/Re^dha[a%!%!a+#Ta7)-5TaCaO!aka!a)sf[yb2>Rl!9ARiq5E}Qg=ucRkBE|oJrJ_@Wk~@Wk{JrJ_@Wk|@WkyJrJ_@Wk}@WkzJvO_[y2g-vMRmiKuYC!)&>Ri;>Ri<@3RkNc](X#@9Rk=g5vuRmhKvDB!+'=]meg3u4Rmgd)#Y'Vz3CARmfd`a+!%T'!+#Ta1Ta6TaM-sTDt9[yA9sYd'%Y#s[[xpj:ueunaXRgEjRq,v-vuqdd2'`#6Rev<32@5>:2<E}5xIo9a*X#Y(;5RePJvD_g>vyRgNj8w)v8<wggs:RgXiZt|vjx,hSq3ah!-(~@:Ro/Ou!5RhWj^v(pyw8unRhUdx-UY#^Ua.a3a70!)%UX1TaDa)'omRiRRhE[y:3Dsz=Br,>6Vyj3[xkg6ruwjcqsrPw;5r*Ku]D'Zt-@3r(~?r.i[vwv]dU1a--U#`a4(g/vsRhPOu!5RhLj:rmu9Wo!~@:wdh@g/vsRiTjXuvvNr}:RhBj^v(pyw8unRn]dz1UYa'a+^Y(!aETZalaRY.Ta?a4[yDJw1!#qLsW>6Vyrfzq-pLflpwRe|Js>%!Dt@3Dt&Jvy_[xs~HrnjMuwpsw'RecKu+D#'!t<~Grl~?rjg5u-x,gwp{ah!-(~@:Rg~Ou!5Rh'jXuvvNr}:Rh#cW#X/c;&!#2sLi[v7u7RgpJv)(!iLrxu,Re6j7v@s@5Se[e7d`aW!Za(a`T.a#!a3!&aDa-!9)Dt_=6s+3[x~~DR|h~DS6avhGun5RkZj3w)v-]mkKunB!&*]kb97R|i<ARk<c:Z(6Vy}Juh'!wziMRoS:F|vkLuauJv5vtvQRh1d='T+Y#VyO~DR|jcF#T'7R|g97R|kJv3'!ay<Rj,Jvh&!:ReXcsa6*a+#a#_aIRf9aLRf?c,Z&Rf5Rf7c.Z&Rf;Rf>cQ#%T'p-Rf8Rf=ct#%'(*!,p,Rf4p+Rf6Rf:Rf<d~'Ua%U*^UYa(!a,-!#a4YaTalaEX0a8a<Weo3Dt/3Dsx=Br93Wen~Dr;~<5p<JwNZt2@3p=Pw:5p;Ou!5r3c7&!#:p>3Ds}KvGB)_6Vyk2sM=<r7x'eovA(!hFu1ARf}cV#X&@r5j6rvwQa^Rf3c=Za'wkghJv__g;unRggA53B9=b^}%j6uduo5Jq;!(hIv%2Re`Ou4ARe_e%a#^^^Xa&!a*a2!&a6YaP!*ad!#a:aE/5Rn?[y@>6Vyp;:pE~DrY~<5pBJwNZt8@3pCh=rt3rWPw:5pAJup_[xoNuPpF9c!#'45pD5ARn)d8#X'X*3@rU72s]h>v<<sSjJpqvewOJq/(!hNw'5ReBk0s2u3w/w'5ReE5@Jq.!a+JQ!&WeU23d(#Y&RjG5]jBk!u7w&u0udARjEe#+^^^Ub#!a2/a`Z(agT1!a-a;|@TaG!aS[yV=Re~fow'RguNuPRe?bLine truncated
|
||||
13_494,
|
||||
2713,
|
||||
49,
|
||||
25,
|
||||
61,
|
||||
);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Generated using scripts/write-decode-map.ts
|
||||
|
||||
/** Packed XML decode trie data. */
|
||||
export const xmlDecodeTree: Uint16Array = /* #__PURE__ */ new Uint16Array([
|
||||
512, 26_465, 29_036, 7, 0, 2, 4, 116, 24_638, 116, 24_636, 8693, 29_807,
|
||||
24_610, 621, 1, 0, 0, 3, 112, 24_614, 111, 115, 24_615,
|
||||
]);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Generated using scripts/write-encode-map.ts
|
||||
|
||||
export default "9Tab;NewLine;22excl;quot;num;dollar;percnt;amp;apos;lpar;rpar;ast;plus;comma;1period;sol;10colon;semi;lt;{8402nvlt;}equals;{8421bne;}gt;{8402nvgt;}quest;commat;26lbrack;bsol;rbrack;Hat;lowbar;DiacriticalGrave;5{106fjlig;}20lbrace;verbar;rbrace;34nbsp;iexcl;cent;pound;curren;yen;brvbar;sect;die;copy;ordf;laquo;not;shy;circledR;macr;deg;PlusMinus;sup2;sup3;acute;micro;para;centerdot;cedil;sup1;ordm;raquo;frac14;frac12;frac34;iquest;Agrave;Aacute;Acirc;Atilde;Auml;angst;AElig;Ccedil;Egrave;Eacute;Ecirc;Euml;Igrave;Iacute;Icirc;Iuml;ETH;Ntilde;Ograve;Oacute;Ocirc;Otilde;Ouml;times;Oslash;Ugrave;Uacute;Ucirc;Uuml;Yacute;THORN;szlig;agrave;aacute;acirc;atilde;auml;aring;aelig;ccedil;egrave;eacute;ecirc;euml;igrave;iacute;icirc;iuml;eth;ntilde;ograve;oacute;ocirc;otilde;ouml;div;oslash;ugrave;uacute;ucirc;uuml;yacute;thorn;yuml;Amacr;amacr;Abreve;abreve;Aogon;aogon;Cacute;cacute;Ccirc;ccirc;Cdot;cdot;Ccaron;ccaron;Dcaron;dcaron;Dstrok;dstrok;Emacr;emacr;2Edot;edot;Eogon;eogon;Ecaron;ecaron;Gcirc;gcirc;Gbreve;gbreve;Gdot;gdot;Gcedil;1Hcirc;hcirc;Hstrok;hstrok;Itilde;itilde;Imacr;imacr;2Iogon;iogon;Idot;imath;IJlig;ijlig;Jcirc;jcirc;Kcedil;kcedil;kgreen;Lacute;lacute;Lcedil;lcedil;Lcaron;lcaron;Lmidot;lmidot;Lstrok;lstrok;Nacute;nacute;Ncedil;ncedil;Ncaron;ncaron;napos;ENG;eng;Omacr;omacr;2Odblac;odblac;OElig;oelig;Racute;racute;Rcedil;rcedil;Rcaron;rcaron;Sacute;sacute;Scirc;scirc;Scedil;scedil;Scaron;scaron;Tcedil;tcedil;Tcaron;tcaron;Tstrok;tstrok;Utilde;utilde;Umacr;umacr;Ubreve;ubreve;Uring;uring;Udblac;udblac;Uogon;uogon;Wcirc;wcirc;Ycirc;ycirc;Yuml;Zacute;zacute;Zdot;zdot;Zcaron;zcaron;19fnof;34imped;63gacute;65jmath;142circ;caron;16breve;DiacriticalDot;ring;ogon;DiacriticalTilde;dblac;51DownBreve;127Alpha;Beta;Gamma;Delta;Epsilon;Zeta;Eta;Theta;Iota;Kappa;Lambda;Mu;Nu;Xi;Omicron;Pi;Rho;1Sigma;Tau;Upsilon;Phi;Chi;Psi;ohm;7alpha;beta;gamma;delta;epsi;zeta;eta;theta;iota;kappa;lambda;mu;nu;xi;omicron;pi;rho;sigmaf;sigma;tau;upsi;phi;chi;psi;omega;7thetasym;Upsi;2phiv;piv;5Gammad;digamma;18kappav;rhov;3epsiv;backepsilon;10IOcy;DJcy;GJcy;Jukcy;DScy;Iukcy;YIcy;Jsercy;LJcy;NJcy;TSHcy;KJcy;1Ubrcy;DZcy;Acy;Bcy;Vcy;Gcy;Dcy;IEcy;ZHcy;Zcy;Icy;Jcy;Kcy;Lcy;Mcy;Ncy;Ocy;Pcy;Rcy;Scy;Tcy;Ucy;Fcy;KHcy;TScy;CHcy;SHcy;SHCHcy;HARDcy;Ycy;SOFTcy;Ecy;YUcy;YAcy;acy;bcy;vcy;gcy;dcy;iecy;zhcy;zcy;icy;jcy;kcy;lcy;mcy;ncy;ocy;pcy;rcy;scy;tcy;ucy;fcy;khcy;tscy;chcy;shcy;shchcy;hardcy;ycy;softcy;ecy;yucy;yacy;1iocy;djcy;gjcy;jukcy;dscy;iukcy;yicy;jsercy;ljcy;njcy;tshcy;kjcy;1ubrcy;dzcy;7074ensp;emsp;emsp13;emsp14;1numsp;puncsp;ThinSpace;hairsp;NegativeMediumSpace;zwnj;zwj;lrm;rlm;dash;2ndash;mdash;horbar;Verbar;1lsquo;CloseCurlyQuote;lsquor;1ldquo;CloseCurlyDoubleQuote;bdquo;1dagger;Dagger;bull;2nldr;hellip;9permil;pertenk;prime;Prime;tprime;backprime;3lsaquo;rsaquo;3oline;2caret;1hybull;frasl;10bsemi;7qprime;7MediumSpace;{8202ThickSpace;}NoBreak;af;InvisibleTimes;ic;72euro;46tdot;DotDot;37complexes;2incare;4gscr;hamilt;Hfr;Hopf;planckh;hbar;imagline;Ifr;lagran;ell;1naturals;numero;copysr;weierp;Popf;Qopf;realine;real;reals;rx;3trade;1integers;2mho;zeetrf;iiota;2bernou;Cayleys;1escr;Escr;Fouriertrf;1Mellintrf;order;alefsym;beth;gimel;daleth;12CapitalDifferentialD;dd;ee;ii;10frac13;frac23;frac15;frac25;frac35;frac45;frac16;frac56;frac18;frac38;frac58;frac78;49larr;ShortUpArrow;rarr;darr;harr;updownarrow;nwarr;nearr;LowerRightArrow;LowerLeftArrow;nlarr;nrarr;1rarrw;{824nrarrw;}Larr;Uarr;Rarr;Darr;larrtl;rarrtl;LeftTeeArrow;mapstoup;map;DownTeeArrow;1hookleftarrow;hookrightarrow;larrlp;looparrowright;harrw;nharr;1lsh;rsh;ldsh;rdsh;1crarr;cularr;curarr;2circlearrowleft;circlearrowright;leftharpoonup;DownLeftVector;RightUpVector;LeftUpVector;rharu;DownRightVector;dharr;dharl;RightArrowLeftArrow;udarr;LeftArrowRightArrow;leftleftarrows;upuparrows;rightrightarrows;ddarr;leftrightharpoons;Equilibrium;nlArr;nhArr;nrArr;DoubleLeftArrow;DoubleUpArrow;DoubleRightArrow;dArr;DoubleLeftRightArrow;DoubleUpDownArrow;nwArr;neArr;seArr;swArr;lAarr;rAarr;1zigrarr;6larrb;rarrb;15DownArrowUpArrow;7loarr;roarr;hoarr;forall;comp;part;{824npart;}exist;nexist;empty;1Del;Element;NotElement;1ni;notni;2prod;coprod;sum;minus;MinusPlus;dotplus;1Backslash;lowast;compfn;1radic;2prop;infin;angrt;ang;{8402nang;}angmsd;angsph;mid;nmid;DoubleVerticalBar;NotDoubleVerticalBar;and;or;cap;{65024caps;}cup;{65024cups;}int;Int;iiint;conint;Conint;Cconint;cwint;ClockwiseContourIntegral;awconint;there4;becaus;ratio;Colon;dotminus;1mDDot;homtht;sim;{8402nvsim;}backsim;{817race;}ac;{819acE;}acd;VerticalTilde;NotTilde;eqsim;{824nesim;}sime;NotTildeEqual;cong;simne;ncong;ap;nap;ape;apid;{824napid;}backcong;asympeq;{8402nvap;}bump;{824nbump;}bumpe;{824nbumpe;}doteq;{824nedot;}doteqdot;efDot;erDot;Assign;ecolon;ecir;circeq;1wedgeq;veeeq;1triangleq;2equest;ne;Congruent;{8421bnequiv;}nequiv;1le;{8402nvle;}ge;{8402nvge;}lE;{824nlE;}gE;{824ngE;}lnE;{65024lvertneqq;}gnE;{65024gvertneqq;}ll;{824nLtv;7577nLt;}gg;{824nGtv;7577nGt;}between;NotCupCap;nless;ngt;nle;nge;lesssim;GreaterLine truncated
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { type DecodingMode, decodeHTML, decodeXML } from "./decode.js";
|
||||
import { encodeHTML, encodeNonAsciiHTML } from "./encode.js";
|
||||
import {
|
||||
encodeXML,
|
||||
escapeAttribute,
|
||||
escapeText,
|
||||
escapeUTF8,
|
||||
} from "./escape.js";
|
||||
|
||||
/** The level of entities to support. */
|
||||
export enum EntityLevel {
|
||||
/** Support only XML entities. */
|
||||
XML = 0,
|
||||
/** Support HTML entities, which are a superset of XML entities. */
|
||||
HTML = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoding strategy used by `encode`.
|
||||
*/
|
||||
export enum EncodingMode {
|
||||
/**
|
||||
* The output is UTF-8 encoded. Only characters that need escaping within
|
||||
* XML will be escaped.
|
||||
*/
|
||||
UTF8,
|
||||
/**
|
||||
* The output consists only of ASCII characters. Characters that need
|
||||
* escaping within HTML, and characters that aren't ASCII characters will
|
||||
* be escaped.
|
||||
*/
|
||||
ASCII,
|
||||
/**
|
||||
* Encode all characters that have an equivalent entity, as well as all
|
||||
* characters that are not ASCII characters.
|
||||
*/
|
||||
Extensive,
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML attributes,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
Attribute,
|
||||
/**
|
||||
* Encode all characters that have to be escaped in HTML text,
|
||||
* following {@link https://html.spec.whatwg.org/multipage/parsing.html#escapingString}.
|
||||
*/
|
||||
Text,
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `decode`.
|
||||
*/
|
||||
export interface DecodingOptions {
|
||||
/**
|
||||
* The level of entities to support.
|
||||
* @default {@link EntityLevel.XML}
|
||||
*/
|
||||
level?: EntityLevel;
|
||||
/**
|
||||
* Decoding mode. If `Legacy`, will support legacy entities not terminated
|
||||
* with a semicolon (`;`).
|
||||
*
|
||||
* Always `Strict` for XML. For HTML, set this to
|
||||
* {@link DecodingMode.Attribute} if you are parsing an attribute value.
|
||||
* @default {@link DecodingMode.Legacy}
|
||||
*/
|
||||
mode?: DecodingMode | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string with entities.
|
||||
* @param input String to decode.
|
||||
* @param options Decoding options.
|
||||
*/
|
||||
export function decode(
|
||||
input: string,
|
||||
options: DecodingOptions | EntityLevel = EntityLevel.XML,
|
||||
): string {
|
||||
const level = typeof options === "number" ? options : options.level;
|
||||
|
||||
if (level === EntityLevel.HTML) {
|
||||
const mode = typeof options === "object" ? options.mode : undefined;
|
||||
return decodeHTML(input, mode);
|
||||
}
|
||||
|
||||
return decodeXML(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for `encode`.
|
||||
*/
|
||||
export interface EncodingOptions {
|
||||
/**
|
||||
* The level of entities to support.
|
||||
* @default {@link EntityLevel.XML}
|
||||
*/
|
||||
level?: EntityLevel;
|
||||
/**
|
||||
* Output format.
|
||||
* @default {@link EncodingMode.Extensive}
|
||||
*/
|
||||
mode?: EncodingMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a string with entities.
|
||||
* @param input String to encode.
|
||||
* @param options Encoding options.
|
||||
*/
|
||||
export function encode(
|
||||
input: string,
|
||||
options: EncodingOptions | EntityLevel = EntityLevel.XML,
|
||||
): string {
|
||||
const { mode = EncodingMode.Extensive, level = EntityLevel.XML } =
|
||||
typeof options === "number" ? { level: options } : options;
|
||||
|
||||
switch (mode) {
|
||||
case EncodingMode.UTF8: {
|
||||
return escapeUTF8(input);
|
||||
}
|
||||
case EncodingMode.Attribute: {
|
||||
return escapeAttribute(input);
|
||||
}
|
||||
case EncodingMode.Text: {
|
||||
return escapeText(input);
|
||||
}
|
||||
case EncodingMode.ASCII: {
|
||||
return (
|
||||
level === EntityLevel.HTML ? encodeNonAsciiHTML : encodeXML
|
||||
)(input);
|
||||
}
|
||||
// biome-ignore lint/complexity/noUselessSwitchCase: we get an error for the switch not being exhaustive
|
||||
case EncodingMode.Extensive:
|
||||
default: {
|
||||
return (level === EntityLevel.HTML ? encodeHTML : encodeXML)(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
DecodingMode,
|
||||
decodeHTML,
|
||||
decodeHTMLAttribute,
|
||||
decodeHTMLStrict,
|
||||
decodeXML,
|
||||
decodeXML as decodeXMLStrict,
|
||||
EntityDecoder,
|
||||
} from "./decode.js";
|
||||
|
||||
export {
|
||||
encodeHTML,
|
||||
encodeNonAsciiHTML,
|
||||
} from "./encode.js";
|
||||
export {
|
||||
encodeXML,
|
||||
escape,
|
||||
escapeAttribute,
|
||||
escapeText,
|
||||
escapeUTF8,
|
||||
} from "./escape.js";
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Bit flags & masks for the binary trie encoding used for entity decoding.
|
||||
*
|
||||
* The trie is a flat `Uint16Array`. Every node starts with one header word:
|
||||
*
|
||||
* 15..14 VALUE_LENGTH Number of words the value occupies, +1.
|
||||
* 0 = no value; 1 = value inline in bits 12..0;
|
||||
* 2/3 = value in the 1/2 words after the header.
|
||||
* 13 FLAG13 If VALUE_LENGTH > 0: semicolon required ("strict"
|
||||
* entity; `;` is never stored as a branch).
|
||||
* If VALUE_LENGTH == 0: this node is a compact run.
|
||||
* 12..7 BRANCH_LENGTH Number of branches (or run length for runs).
|
||||
* 6..0 JUMP_TABLE Jump-table offset / single-branch char / first
|
||||
* run char (see below).
|
||||
*
|
||||
* Branch data follows the header and any value words. Its shape is selected
|
||||
* by (JUMP_TABLE, BRANCH_LENGTH) in the header:
|
||||
*
|
||||
* Single branch JUMP_TABLE = the only child's char, BRANCH_LENGTH = 0.
|
||||
* No branch words; the child node follows immediately.
|
||||
* Jump table JUMP_TABLE = first covered char (> 0), BRANCH_LENGTH =
|
||||
* table length. One word per covered char: 0 = no branch,
|
||||
* otherwise the child's offset from the END of the table,
|
||||
* +1 (so 0 stays the no-branch sentinel).
|
||||
* Dictionary JUMP_TABLE = 0, BRANCH_LENGTH = number of branches.
|
||||
* ceil(n/2) words of sorted keys packed two per word
|
||||
* (low byte first), then n pointer words storing the
|
||||
* child's offset from the END of the branch data.
|
||||
* Compact run VALUE_LENGTH = 0, FLAG13 set. BRANCH_LENGTH = run
|
||||
* length (3..63), JUMP_TABLE = first char; remaining run
|
||||
* chars packed two per word after the header. The target
|
||||
* node follows the packed words immediately.
|
||||
*
|
||||
* Pointers are end-relative (rather than relative to the pointer's own
|
||||
* position) because that makes the common "child encoded right after the
|
||||
* branch data" case a small constant, which compresses far better. Offsets
|
||||
* to already-encoded (shared) nodes wrap via uint16 modulo arithmetic; the
|
||||
* decoder masks navigation results with `& 0xff_ff` to match.
|
||||
*/
|
||||
export const enum BinTrieFlags {
|
||||
VALUE_LENGTH = 0b1100_0000_0000_0000,
|
||||
FLAG13 = 0b0010_0000_0000_0000,
|
||||
BRANCH_LENGTH = 0b0001_1111_1000_0000,
|
||||
JUMP_TABLE = 0b0000_0000_0111_1111,
|
||||
/** Bits 12..0: the inline value of a VALUE_LENGTH = 1 header word. */
|
||||
VALUE_MASK = 0b0001_1111_1111_1111,
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Inverse of the encoder's SAFE alphabet (0x21..0x7E minus 0x22, 0x24, 0x5C),
|
||||
* precomputed once at module load. Entries for excluded chars stay 0 but
|
||||
* are never read.
|
||||
*/
|
||||
const BASE91_INVERSE = /* #__PURE__ */ (() => {
|
||||
const table = new Uint8Array(127);
|
||||
let code = 0;
|
||||
for (let char = 0x21; char <= 0x7e; char++) {
|
||||
if (char !== 0x22 && char !== 0x24 && char !== 0x5c) {
|
||||
table[char] = code++;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Decode a dictionary-encoded trie string back into its Uint16Array.
|
||||
*
|
||||
* Stream layout (consumed in this order):
|
||||
* 1. dict1 atoms — `dict1AtomCount` uint16 values, delta+RLE encoded.
|
||||
* 2. dict2 atoms — `atomCount - dict1AtomCount` values, delta+RLE.
|
||||
* 3. dict2 ngrams — `ngramCount - (dictSize - dict1AtomCount)` entries,
|
||||
* each a pair of slot codes that resolve to earlier slots.
|
||||
* 4. dict1 ngrams — `dictSize - dict1AtomCount` entries, same shape.
|
||||
* 5. data — slot codes, each expanding to one or more uint16 values.
|
||||
*
|
||||
* Codes use a 91-char base (printable ASCII minus `"`, `$`, `\`):
|
||||
* - char1 < dictSize → 1-char code, slot = char1
|
||||
* - char1 ≥ dictSize → 2-char code, slot = dictSize + (char1 - dictSize)*91 + char2
|
||||
*
|
||||
* Slot index → token kind:
|
||||
* [0, A) dict1 atoms (1-char codes)
|
||||
* [A, dictSize) dict1 ngrams (1-char codes)
|
||||
* [dictSize, dictSize+D) dict2 atoms (2-char codes)
|
||||
* [dictSize+D, end) dict2 ngrams (2-char codes)
|
||||
*
|
||||
* Both atom dicts decode before any ngram, and dict2 ngrams decode before
|
||||
* dict1 ngrams. So every ngram entry references slots whose contents are
|
||||
* already filled — no forward references to handle.
|
||||
*
|
||||
* This runs on library import. Flat typed arrays store each slot as either
|
||||
* a plain value (`single`, covering every atom) or a range in a shared
|
||||
* `pool` (ngrams).
|
||||
* @param input Packed trie string.
|
||||
* @param resultLength Expected number of uint16 values in the output.
|
||||
* @param atomCount Total number of distinct uint16 values in the trie.
|
||||
* @param dict1AtomCount Atoms in the 1-char range (`A` above).
|
||||
* @param ngramCount Total number of ngram entries (dict1 + dict2).
|
||||
* @param dictSize Number of 1-char code slots; the rest of `BASE - dictSize`
|
||||
* first-byte values are 2-char codes.
|
||||
*/
|
||||
export function decodeTrieDict(
|
||||
input: string,
|
||||
resultLength: number,
|
||||
atomCount: number,
|
||||
dict1AtomCount: number,
|
||||
ngramCount: number,
|
||||
dictSize: number,
|
||||
): Uint16Array {
|
||||
const base = 91;
|
||||
const inputLength = input.length;
|
||||
// For 2-char codes, slot = char1 * base - twoCharBias + char2.
|
||||
const twoCharBias = dictSize * (base - 1);
|
||||
|
||||
let pos = 0;
|
||||
|
||||
/** Read one slot code at `pos` and return its slot index, advancing pos. */
|
||||
const readSlotCode = (): number => {
|
||||
const c1 = BASE91_INVERSE[input.charCodeAt(pos++)];
|
||||
return c1 < dictSize
|
||||
? c1
|
||||
: c1 * base - twoCharBias + BASE91_INVERSE[input.charCodeAt(pos++)];
|
||||
};
|
||||
|
||||
const dict2AtomCount = atomCount - dict1AtomCount;
|
||||
const slotCount = atomCount + ngramCount;
|
||||
|
||||
/*
|
||||
* Per-slot contents: atoms (always a single value) live directly in
|
||||
* `single`; ngram slots hold -1 there and expand to
|
||||
* `pool[start[slot] .. start[slot] + length[slot])`.
|
||||
*/
|
||||
const single = new Int32Array(slotCount);
|
||||
single.fill(-1, dict1AtomCount, dictSize);
|
||||
single.fill(-1, dictSize + dict2AtomCount, slotCount);
|
||||
const start = new Int32Array(slotCount);
|
||||
const length = new Int32Array(slotCount);
|
||||
|
||||
/**
|
||||
* Decode `count` ascending uint16 values from a delta+RLE stream into
|
||||
* `single[off..off+count)`.
|
||||
*
|
||||
* code < 89 → delta = code
|
||||
* code == 89 → run-length: next char encodes runLength-2; emit `runLength` consecutive +1 values
|
||||
* code == 90, next < 90 → escape: delta = 89 + next * BASE + after-next
|
||||
* code == 90, next == 90 → double-escape: extra char for very large deltas
|
||||
* @param count
|
||||
* @param off
|
||||
*/
|
||||
function decodeDelta(count: number, off: number): void {
|
||||
let previous = 0;
|
||||
let slot = off;
|
||||
const end = off + count;
|
||||
while (slot < end) {
|
||||
const code = BASE91_INVERSE[input.charCodeAt(pos++)];
|
||||
if (code < 89) {
|
||||
previous += code;
|
||||
single[slot++] = previous;
|
||||
} else if (code === 89) {
|
||||
let runLength = BASE91_INVERSE[input.charCodeAt(pos++)] + 2;
|
||||
while (runLength--) single[slot++] = ++previous;
|
||||
} else {
|
||||
const next = BASE91_INVERSE[input.charCodeAt(pos++)];
|
||||
previous +=
|
||||
89 +
|
||||
// eslint-disable-next-line unicorn/prefer-minimal-ternary -- branches read a different number of side-effecting input bytes
|
||||
(next < 90
|
||||
? next * base + BASE91_INVERSE[input.charCodeAt(pos++)]
|
||||
: BASE91_INVERSE[input.charCodeAt(pos++)] * 8281 +
|
||||
BASE91_INVERSE[input.charCodeAt(pos++)] * base +
|
||||
BASE91_INVERSE[input.charCodeAt(pos++)]);
|
||||
single[slot++] = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streams 1 & 2: atoms decoded into their slot ranges.
|
||||
decodeDelta(dict1AtomCount, 0);
|
||||
decodeDelta(dict2AtomCount, dictSize);
|
||||
|
||||
/*
|
||||
* Streams 3 & 4 are read in two passes: first collect every ngram's two
|
||||
* references and derive its expanded length (each ref resolves to an earlier
|
||||
* slot, so lengths are already known), which sizes the shared pool.
|
||||
* Pool ranges are handed out in decode order, so the second pass fills
|
||||
* the pool contiguously with a single write cursor.
|
||||
*/
|
||||
const references = new Int32Array(ngramCount * 2);
|
||||
let poolSize = 0;
|
||||
let ngramIndex = 0;
|
||||
|
||||
/**
|
||||
* Read `count` ngram entries (each = 2 slot-code references) for the slots
|
||||
* starting at `startSlot`, recording references and assigning pool ranges.
|
||||
* @param count
|
||||
* @param startSlot
|
||||
*/
|
||||
function readNgramReferences(count: number, startSlot: number): void {
|
||||
for (let index = 0; index < count; index++) {
|
||||
const slot = startSlot + index;
|
||||
const a = readSlotCode();
|
||||
const b = readSlotCode();
|
||||
references[ngramIndex * 2] = a;
|
||||
references[ngramIndex * 2 + 1] = b;
|
||||
ngramIndex += 1;
|
||||
start[slot] = poolSize;
|
||||
const entryLength =
|
||||
(single[a] < 0 ? length[a] : 1) +
|
||||
(single[b] < 0 ? length[b] : 1);
|
||||
length[slot] = entryLength;
|
||||
poolSize += entryLength;
|
||||
}
|
||||
}
|
||||
readNgramReferences(
|
||||
ngramCount - dictSize + dict1AtomCount,
|
||||
dictSize + dict2AtomCount,
|
||||
);
|
||||
readNgramReferences(dictSize - dict1AtomCount, dict1AtomCount);
|
||||
|
||||
// Second pass: concatenate each ngram's two halves into the pool.
|
||||
const pool = new Uint16Array(poolSize);
|
||||
let write = 0;
|
||||
for (let index = 0; index < ngramIndex; index++) {
|
||||
for (let half = 0; half < 2; half++) {
|
||||
const source = references[index * 2 + half];
|
||||
const value = single[source];
|
||||
if (value < 0) {
|
||||
let read = start[source];
|
||||
const readEnd = read + length[source];
|
||||
while (read < readEnd) pool[write++] = pool[read++];
|
||||
} else {
|
||||
pool[write++] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream 5: data. Each code expands to its slot's stored values.
|
||||
const out = new Uint16Array(resultLength);
|
||||
let outIndex = 0;
|
||||
while (pos < inputLength) {
|
||||
let slot = BASE91_INVERSE[input.charCodeAt(pos++)];
|
||||
if (slot >= dictSize) {
|
||||
slot =
|
||||
slot * base -
|
||||
twoCharBias +
|
||||
BASE91_INVERSE[input.charCodeAt(pos++)];
|
||||
}
|
||||
const value = single[slot];
|
||||
if (value < 0) {
|
||||
let read = start[slot];
|
||||
const readEnd = read + length[slot];
|
||||
while (read < readEnd) out[outIndex++] = pool[read++];
|
||||
} else {
|
||||
out[outIndex++] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user