UNPKG

entities

Version:

Encode & decode XML and HTML entities with ease & speed

162 lines • 6.52 kB
/** * Constants and helpers shared between the decode-data generator * (`scripts/write-decode-map.ts`) and the runtime (`src/decode.ts`). * * The format and the algorithms (front-coded names, exact 32-bit keys in a * two-choice cuckoo table, the length-probe front end, and the measured * engineering notes) are documented with references in * `scripts/README.md`. */ /** Bias added to every meta char so none needs escaping in JS source. */ export const META_BIAS = 0x23; /** Bias for header and choice-bit chars. */ export const HEADER_BIAS = 0x30; /** Number of header chars: n (2), suffixes length (2), bucket count (2). */ export const HEADER_LENGTH = 6; /** Choice bits packed per char. */ export const CHOICE_BITS_PER_CHAR = 6; /** Multiplier deriving the first candidate bucket from a key. */ export const BUCKET_HASH_1 = 0x9e_37_79_b1; /** Multiplier deriving the second candidate bucket from a key. */ export const BUCKET_HASH_2 = 0x85_eb_ca_6b; /** Size of the (c0,c1) class table (10-bit index). */ export const PAIR_TABLE_SIZE = 1024; /** * Map an alphanumeric char code to 6 bits (1..62); 0 = not alphanumeric. * Used for the key's last-character field. */ export const CHAR_REMAP = (() => { const remap = new Uint8Array(128); for (let code = 0x30; code <= 0x39; code++) remap[code] = code - 0x2f; for (let code = 0x41; code <= 0x5a; code++) remap[code] = code - 0x36; for (let code = 0x61; code <= 0x7a; code++) remap[code] = code - 0x3c; return remap; })(); /** * The exact 32-bit key for a name occurring at `nameStart` with `length` * chars. Covers the first two and last two characters plus the length; * middle characters are verified separately against the names blob. * * The hot decoder inlines this computation; tests assert both stay in sync * by looking every entity up through the public API. * @param text Text containing the name. * @param nameStart Index of the name's first character. * @param length Length of the name. */ export function exactKey(text, nameStart, length) { return ((text.charCodeAt(nameStart) << 25) | (text.charCodeAt(nameStart + 1) << 18) | (text.charCodeAt(nameStart + length - 2) << 11) | (CHAR_REMAP[text.charCodeAt(nameStart + length - 1)] << 5) | length); } /** * The 10-bit (c0,c1) class index used by the probe front end. The function * was chosen by exhaustive search over load-free mixers for minimal * candidate-set collisions on the real entity set. * @param c0 First char code of the candidate name. * @param c1 Second char code. */ export function pairIndex(c0, c1) { return (((c0 * 3) << 3) ^ c1) & (PAIR_TABLE_SIZE - 1); } /** * First candidate bucket for a key in a table of `buckets` buckets. * @param key Exact key. * @param buckets Bucket count. */ export function bucketOne(key, buckets) { return ((Math.imul(key, BUCKET_HASH_1) >>> 16) * buckets) >>> 16; } /** * Second candidate bucket for a key. * @param key Exact key. * @param buckets Bucket count. */ export function bucketTwo(key, buckets) { return ((Math.imul(key, BUCKET_HASH_2) >>> 16) * buckets) >>> 16; } /** * Build the lookup structures from a serialized dataset. * @param packed Serialized decode data, see `decode-data-format.ts`. */ export function initDecodeData(packed) { const [data, values] = packed; const nameCount = ((data.charCodeAt(0) - HEADER_BIAS) << 6) | (data.charCodeAt(1) - HEADER_BIAS); const suffixesLength = ((data.charCodeAt(2) - HEADER_BIAS) << 6) | (data.charCodeAt(3) - HEADER_BIAS); const buckets = ((data.charCodeAt(4) - HEADER_BIAS) << 6) | (data.charCodeAt(5) - HEADER_BIAS); const metaStart = HEADER_LENGTH + suffixesLength; const choicesStart = metaStart + 2 * nameCount; const slotCount = 2 * buckets; const keys = new Int32Array(slotCount); const slotMidOff = new Uint16Array(slotCount); const slotValue = new Uint16Array(slotCount); const lengthBits = new Uint32Array(PAIR_TABLE_SIZE); const middleOffsets = new Map(); const middles = []; let name = ""; let suffixOffset = HEADER_LENGTH; let valueOffset = 0; for (let index = 0; index < nameCount; index++) { const meta0 = data.charCodeAt(metaStart + 2 * index) - META_BIAS; const meta1 = data.charCodeAt(metaStart + 2 * index + 1) - META_BIAS; const prefixLength = meta0 & 31; const length = prefixLength + (meta1 & 31); const valueLength = (meta1 >> 5) + 1; name = name.slice(0, prefixLength) + data.slice(suffixOffset, suffixOffset + length - prefixLength); suffixOffset += length - prefixLength; const key = exactKey(name, 0, length); const choice = (data.charCodeAt(choicesStart + Math.floor(index / CHOICE_BITS_PER_CHAR)) - HEADER_BIAS) & (1 << (index % CHOICE_BITS_PER_CHAR)); let slot = 2 * (choice === 0 ? bucketOne : bucketTwo)(key, buckets); if (keys[slot] !== 0) slot += 1; keys[slot] = key; if (length > 4) { const middle = name.slice(2, length - 2); const existing = middleOffsets.get(middle); if (existing === undefined) { middleOffsets.set(middle, middles.length); slotMidOff[slot] = middles.length; for (let middleIndex = 0; middleIndex < middle.length; middleIndex += 2) { middles.push(middle.charCodeAt(middleIndex) | (middle.charCodeAt(middleIndex + 1) << 16)); } } else { slotMidOff[slot] = existing; } } const pair = pairIndex(name.charCodeAt(0), name.charCodeAt(1)); lengthBits[pair] |= length <= 16 ? 1 << (length - 2) : 0x80_00; if ((meta0 & 0x20) !== 0) { lengthBits[pair] |= 1 << (length - 2 + 16); } /* * Values have one or two units, leaving bit 1 for the legacy flag. * The generator checks the 14-bit offset and two-unit value limits. */ slotValue[slot] = (valueOffset << 2) | (valueLength - 1) | ((meta0 & 0x20) >> 4); valueOffset += valueLength; } return { keys, buckets, slotMidOff, slotValue, values, lengthBits, middles: new Uint32Array(middles), }; } //# sourceMappingURL=decode-data-format.js.map