Spaces:
Paused
Paused
| /** | |
| * 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) | |
| */ | |
| export type EncodeTrieNode = | |
| | string | |
| | { | |
| /** | |
| * Entity value for the current code point sequence (wrapped: `&...;`). | |
| * Present when the path to this node itself is a valid named entity. | |
| */ | |
| value: string | undefined; | |
| /** If a number, the next code unit of the only next character. */ | |
| next: number | Map<number, EncodeTrieNode>; | |
| /** If next is a number, `nextValue` contains the entity value. */ | |
| nextValue?: string; | |
| }; | |
| /** | |
| * Parse a compact encode trie string into a Map structure used for encoding. | |
| * | |
| * Format per entry (ascending code points using delta encoding): | |
| * <diffBase36>[&name;][{<children>}] -- diff omitted when 0 | |
| * Where diff = currentKey - previousKey - 1 (first entry stores absolute key). | |
| * `&name;` is the entity value (already wrapped); a following `{` denotes children. | |
| * @param serialized Serialized text fragment to encode. | |
| */ | |
| export function parseEncodeTrie( | |
| serialized: string, | |
| ): Map<number, EncodeTrieNode> { | |
| const top = new Map<number, EncodeTrieNode>(); | |
| const totalLength = serialized.length; | |
| let cursor = 0; | |
| let lastTopKey = -1; | |
| function readDiff(): number { | |
| const start = cursor; | |
| while (cursor < totalLength) { | |
| const char = serialized.charAt(cursor); | |
| if ((char < "0" || char > "9") && (char < "a" || char > "z")) { | |
| break; | |
| } | |
| cursor++; | |
| } | |
| if (cursor === start) return 0; | |
| return Number.parseInt(serialized.slice(start, cursor), 36); | |
| } | |
| function readEntity(): string { | |
| if (serialized[cursor] !== "&") { | |
| throw new Error(`Child entry missing value near index ${cursor}`); | |
| } | |
| // Cursor currently points at '&' | |
| const start = cursor; | |
| const end = serialized.indexOf(";", cursor + 1); | |
| if (end === -1) { | |
| throw new Error(`Unterminated entity starting at index ${start}`); | |
| } | |
| cursor = end + 1; // Move past ';' | |
| return serialized.slice(start, cursor); // Includes & ... ; | |
| } | |
| while (cursor < totalLength) { | |
| const keyDiff = readDiff(); | |
| const key = lastTopKey === -1 ? keyDiff : lastTopKey + keyDiff + 1; | |
| let value: string | undefined; | |
| if (serialized[cursor] === "&") value = readEntity(); | |
| if (serialized[cursor] === "{") { | |
| cursor++; // Skip '{' | |
| // Parse first child | |
| let diff = readDiff(); | |
| let childKey = diff; // First key (lastChildKey = -1) | |
| const firstValue = readEntity(); | |
| if (serialized[cursor] === "{") { | |
| throw new Error("Unexpected nested '{' beyond depth 2"); | |
| } | |
| // If end of block -> single child optimization | |
| if (serialized[cursor] === "}") { | |
| top.set(key, { value, next: childKey, nextValue: firstValue }); | |
| cursor++; // Skip '}' | |
| } else { | |
| const childMap = new Map<number, EncodeTrieNode>([ | |
| [childKey, firstValue], | |
| ]); | |
| let lastChildKey = childKey; | |
| while (cursor < totalLength && serialized[cursor] !== "}") { | |
| diff = readDiff(); | |
| childKey = lastChildKey + diff + 1; | |
| const childValue = readEntity(); | |
| if (serialized[cursor] === "{") { | |
| throw new Error("Unexpected nested '{' beyond depth 2"); | |
| } | |
| childMap.set(childKey, childValue); | |
| lastChildKey = childKey; | |
| } | |
| if (serialized[cursor] !== "}") { | |
| throw new Error("Unterminated child block"); | |
| } | |
| cursor++; // Skip '}' | |
| top.set(key, { value, next: childMap }); | |
| } | |
| } else if (value === undefined) { | |
| throw new Error( | |
| `Malformed encode trie: missing value at index ${cursor}`, | |
| ); | |
| } else { | |
| top.set(key, value); | |
| } | |
| lastTopKey = key; | |
| } | |
| return top; | |
| } | |