speedy-entities 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Savva Mikhalevski
3
+ Copyright (c) 2026 Savva Mikhalevski
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,7 +1,17 @@
1
- # speedy-entities 🏎💨
1
+ <p align="center">
2
+ <a href="#readme"><img alt="Speedy Entities" src="./assets/logo.png" width="350" /></a>
3
+ </p>
2
4
 
3
5
  [The fastest](#performance) XML/HTML entity encoder/decoder in
4
- [13 kB gzipped](https://bundlephobia.com/package/speedy-entities).
6
+ [15 kB gzipped](https://bundlephobia.com/package/speedy-entities).
7
+
8
+ - Keeps all HTML entities in about 120 kB of RAM.
9
+ - [15 kB gzipped](https://bundlephobia.com/package/speedy-entities), including all HTML entities.
10
+ - Decodes HTML exactly like browsers do.
11
+ - Decodes your own entities with [custom decoders](#custom-decoder).
12
+ - Zero dependencies.
13
+
14
+ <br>
5
15
 
6
16
  ```shell
7
17
  npm install --save-prod speedy-entities
@@ -9,10 +19,10 @@ npm install --save-prod speedy-entities
9
19
 
10
20
  # Decode
11
21
 
12
- There are two preconfigured decoders: `decodeXML` and `decodeHTML`.
22
+ There are three preconfigured decoders: `decodeXML`, `decodeHTML` and `decodeHTMLAttribute`.
13
23
 
14
24
  ```ts
15
- import { decodeXML, decodeHTML } from 'speedy-entities';
25
+ import { decodeXML, decodeHTML, decodeHTMLAttribute } from 'speedy-entities';
16
26
 
17
27
  decodeXML('&#X61;&#98;&lt');
18
28
  // ⮕ 'ab&lt'
@@ -22,6 +32,10 @@ decodeHTML('&ltfoo&AElig');
22
32
 
23
33
  decodeHTML('&NotNestedGreaterGreater;&CounterClockwiseContourIntegral;');
24
34
  // ⮕ '\u2aa2\u0338\u2233'
35
+
36
+ // Like browsers, preserves unterminated entities followed by "=" or an alphanumeric char
37
+ decodeHTMLAttribute('/search?a=1&copy=2&reg');
38
+ // ⮕ '/search?a=1&copy=2\u00ae'
25
39
  ```
26
40
 
27
41
  ## Custom decoder
@@ -52,9 +66,78 @@ decode('&#X61;&#x62;&#x63;');
52
66
  // ⮕ 'abc'
53
67
  ```
54
68
 
69
+ `createEntityDecoder` accepts these options:
70
+
71
+ <dl>
72
+ <dt><code>entities</code></dt>
73
+ <dd>
74
+
75
+ Entity references mapped to their values, or a list of references and values in turn, like `['&foo;', 'okay']`. A
76
+ reference is an optional `&`, a name of ASCII letters, digits and `-.:_`, and an optional `;`. If a reference has the
77
+ `;`, the input needs it too.
78
+
79
+ `createEntityDecoder` throws an error if a reference doesn't match this format.
80
+
81
+ Defaults to `{}`.
82
+
83
+ </dd>
84
+ <dt><code>isNumericReferenceSemicolonRequired</code></dt>
85
+ <dd>
86
+
87
+ If `true`, numeric references are decoded only if they end with `;`, as XML requires.
88
+
89
+ Defaults to `false`.
90
+
91
+ </dd>
92
+ <dt><code>areC1ControlsRemapped</code></dt>
93
+ <dd>
94
+
95
+ If `true`, `&#128;` to `&#159;` are decoded as Windows-1252 characters, as HTML does. For example, `&#128;` is decoded
96
+ as `€`.
97
+
98
+ Defaults to `true`.
99
+
100
+ </dd>
101
+ <dt><code>isAttributeValue</code></dt>
102
+ <dd>
103
+
104
+ If `true`, a named reference without `;` that is followed by `=` or an ASCII letter or digit isn't decoded, as HTML does
105
+ in attribute values.
106
+
107
+ Defaults to `false`.
108
+
109
+ </dd>
110
+ </dl>
111
+
112
+ ## Sharing a trie
113
+
114
+ `createEntityDecoder` builds a trie of entity names on every call. To create several decoders for the same entities,
115
+ build the trie once with `createEntityTrie` and pass it to `createEntityTrieDecoder`, which accepts the same options
116
+ except `entities`. This is how `decodeHTML` and `decodeHTMLAttribute` share one trie of HTML entities.
117
+
118
+ ```ts
119
+ import { createEntityTrie, createEntityTrieDecoder } from 'speedy-entities';
120
+
121
+ const entityTrie = createEntityTrie({
122
+ '&foo;': 'okay',
123
+ '&qux': 'yeah',
124
+ });
125
+
126
+ const decodeText = createEntityTrieDecoder(entityTrie);
127
+
128
+ const decodeAttribute = createEntityTrieDecoder(entityTrie, { isAttributeValue: true });
129
+
130
+ decodeText('&foo;&qux=1');
131
+ // ⮕ 'okayyeah=1'
132
+
133
+ decodeAttribute('&foo;&qux=1');
134
+ // ⮕ 'okay&qux=1'
135
+ ```
136
+
55
137
  # Encode
56
138
 
57
- `encodeXML` encodes non-ASCII characters as named XML entities or as numeric references.
139
+ `encodeXML` replaces `"&'<>` with XML entities and non-ASCII characters with numeric character references, so the output
140
+ is ASCII-only. A lone surrogate is encoded as `&#xfffd;`.
58
141
 
59
142
  `escapeXML` escapes only `"&'<>` characters.
60
143
 
@@ -65,26 +148,59 @@ encodeXML('&😘❤️');
65
148
  // ⮕ '&amp;&#x1f618;&#x2764;&#xfe0f;'
66
149
 
67
150
  escapeXML('&😘❤️');
68
- // ⮕ '&amp;😘❤'
151
+ // ⮕ '&amp;😘❤️'
69
152
  ```
70
153
 
71
154
  # Performance
72
155
 
73
- Results are in millions of operations per second (MHz). The higher number is better.
156
+ Results are in thousands of operations per second (kHz). The higher number is better.
74
157
 
75
- | Library | Decode HTML | Decode XML | Encode XML | Escape XML |
76
- | --------------------------------: | ----------: | ---------: | ---------: | ---------: |
77
- | **speedy-entities**&#x200B;@3.1.0 | **4.9** | **5.2** | **5.3** | **7.2** |
78
- | **entities**&#x200B;@6.0.1 | 3.4 | 3.6 | 4.5 | 6.1 |
79
- | **html-entities**&#x200B;@2.6.0 | 2.2 | 2.3 | 3.3 | 5.1 |
80
- | **he**&#x200B;@1.2.0 | 1.9 | 1.8 | 2.2 | 5.2 |
158
+ | Library | Library Size | Decode HTML | Decode XML | Encode XML | Escape XML |
159
+ | ----------------------------------------------------------------------: | ----------------------------------------------------------------: | ------------: | ------------: | -----------: | ------------: |
160
+ | speedy-&#xFEFF;entities@4.0.0 | [14.9 kB](https://bundlephobia.com/package/speedy-entities@3.1.0) | 🥇 **161.74** | 🥇 **224.97** | 🥇 **85.43** | 🥇 **216.82** |
161
+ | [entities](https://github.com/fb55/entities)@9.0.0 | [21.7 kB](https://bundlephobia.com/package/entities@6.0.1) | 150.36 | 202.38 | 66.02 | 198.83 |
162
+ | [html-&#xFEFF;entities](https://github.com/mdevils/html-entities)@2.6.0 | [13.3 kB](https://bundlephobia.com/package/html-entities@2.6.0) | 39.64 | 49.36 | 33.76 | 127.65 |
163
+ | [he](https://github.com/mathiasbynens/he)@2.0.0 | [29.9 kB](https://bundlephobia.com/package/he@1.2.0) | 30.52 | 30.92 | 21.78 | 129.98 |
81
164
 
82
- Tests were conducted using [TooFast](https://github.com/smikhalevski/toofast#readme) on Apple M1 with Node.js v23.11.1.
165
+ Tests were conducted using [TooFast](https://github.com/smikhalevski/toofast#readme) on Apple M1 with Node.js v26.11.1.
83
166
 
84
- To reproduce [the performance test suite](./src/test/perf/overall.perf.js) results, clone this repo and run:
167
+ To reproduce [the performance test suite](src/test/perf.ts) results, clone this repo and run:
85
168
 
86
169
  ```shell
87
170
  npm ci
88
171
  npm run build
89
- npm run perf
172
+ GROUP_MEASURE=2 npm run perf
173
+ ```
174
+
175
+ The inputs of each function are split into groups, such as short and long inputs. `GROUP_MEASURE` sets what a test
176
+ covers:
177
+
178
+ <dl>
179
+ <dt>Unset</dt>
180
+ <dd>
181
+
182
+ Each input is one test per library.
183
+
184
+ </dd>
185
+ <dt><code>GROUP_MEASURE=1</code></dt>
186
+ <dd>
187
+
188
+ Each group of inputs is one test per library.
189
+
190
+ </dd>
191
+ <dt><code>GROUP_MEASURE=2</code></dt>
192
+ <dd>
193
+
194
+ Each function is one test per library.
195
+
196
+ </dd>
197
+ </dl>
198
+
199
+ A grouped test measures each of its inputs separately and combines the results, so its margin of error includes the
200
+ spread between inputs.
201
+
202
+ Pass a name pattern to run some of the tests:
203
+
204
+ ```shell
205
+ GROUP_MEASURE=1 npm run perf -- 'decodeHTML*'
90
206
  ```
@@ -0,0 +1,49 @@
1
+ declare const OPAQUE_BRAND: unique symbol;
2
+ /**
3
+ * A trie of entity names, see {@linkcode createEntityTrie}. Its contents are internal, so they can change in any
4
+ * release.
5
+ */
6
+ export interface EntityTrie {
7
+ /**
8
+ * Marks objects that {@linkcode createEntityTrie} returns. {@linkcode createEntityTrieDecoder} checks it at runtime,
9
+ * so it doesn't read a trie of an unknown layout.
10
+ *
11
+ * @internal
12
+ */
13
+ readonly [OPAQUE_BRAND]: never;
14
+ }
15
+ /**
16
+ * The contents of an {@linkcode EntityTrie}, see {@linkcode createEntityTrie}. The package doesn't export this module,
17
+ * so the layout can change in any release.
18
+ */
19
+ export interface DoubleArrayEntityTrie extends EntityTrie {
20
+ /**
21
+ * The child of a node for a char is at `nodeBases[node] + charCode`.
22
+ */
23
+ nodeBases: Int32Array;
24
+ /**
25
+ * `nodeChecks[node] >> 1` is the parent of a node, and the low bit is set if an entity name ends at the node. -1
26
+ * marks an unused index.
27
+ */
28
+ nodeChecks: Int32Array;
29
+ /**
30
+ * The index in `values` of the value of the entity name that ends at a node. Most nodes have no value, so values
31
+ * aren't stored per node.
32
+ */
33
+ nodeValueIndexes: Uint16Array | Int32Array;
34
+ /**
35
+ * The values of entity names, see `nodeValueIndexes`.
36
+ */
37
+ values: string[];
38
+ }
39
+ /**
40
+ * Builds a trie from entity names and their values in turn, like `['amp', '&', 'amp;', '&']`. Names must be sorted,
41
+ * valid, and have no leading "&", see {@linkcode createEntityTrie}. If a name is listed several times, its last value
42
+ * is used.
43
+ *
44
+ * Nodes are visited depth first, and the children of a node are placed when it's visited. In sorted order, the names
45
+ * that go through a child of a node are next to each other, so they are found without lookups. The rest of a name that
46
+ * no other name goes through is a chain of single children, which is placed in one go.
47
+ */
48
+ export declare function createDoubleArrayEntityTrie(entities: readonly string[]): DoubleArrayEntityTrie;
49
+ export {};
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Builds a trie from entity names and their values in turn, like `['amp', '&', 'amp;', '&']`. Names must be sorted,
3
+ * valid, and have no leading "&", see {@linkcode createEntityTrie}. If a name is listed several times, its last value
4
+ * is used.
5
+ *
6
+ * Nodes are visited depth first, and the children of a node are placed when it's visited. In sorted order, the names
7
+ * that go through a child of a node are next to each other, so they are found without lookups. The rest of a name that
8
+ * no other name goes through is a chain of single children, which is placed in one go.
9
+ */
10
+ export function createDoubleArrayEntityTrie(entities) {
11
+ let charCount = 0;
12
+ for (let index = 0; index < entities.length; index += 2) {
13
+ charCount += entities[index].length;
14
+ }
15
+ // Each char adds at most one node, and arrays grow if nodes don't pack tightly
16
+ let size = charCount + 512;
17
+ let nodeBases = new Int32Array(size);
18
+ let nodeChecks = new Int32Array(size).fill(-1);
19
+ // The index in values of the value of the entity name that ends at a node
20
+ let valueIndexes = new Int32Array(size);
21
+ const values = [];
22
+ // Indexes below 128 stay unused, so a base is never negative and base + charCode is in bounds for any ASCII char
23
+ let firstUnusedIndex = 128;
24
+ // The largest index placed by a node with several children, other children are placed before firstUnusedIndex
25
+ let maxIndex = 128;
26
+ // The index where the search for a base of a node with several children starts, see below
27
+ let searchStartIndex = 128;
28
+ // The chars of the children of a node, and the names that go through each of them
29
+ const childChars = new Int32Array(128);
30
+ const childNameStarts = new Int32Array(128);
31
+ const childNameEnds = new Int32Array(128);
32
+ // Nodes to visit, four numbers each: the index of a node, the start and the end of the names in entityList that go
33
+ // through it, and its depth
34
+ const stack = [0, 0, entities.length, 0];
35
+ let stackLength = stack.length;
36
+ while (stackLength !== 0) {
37
+ stackLength -= 4;
38
+ const nodeIndex = stack[stackLength];
39
+ const nameEnd = stack[stackLength + 2];
40
+ const depth = stack[stackLength + 3];
41
+ let nameStart = stack[stackLength + 1];
42
+ if (nameStart !== nameEnd && entities[nameStart].length === depth) {
43
+ // A name ends at the node, it sorts before longer names, and copies of it follow it
44
+ do {
45
+ nameStart += 2;
46
+ } while (nameStart !== nameEnd && entities[nameStart].length === depth);
47
+ valueIndexes[nodeIndex] = values.length;
48
+ values.push(entities[nameStart - 1]);
49
+ }
50
+ if (nameStart === nameEnd) {
51
+ continue;
52
+ }
53
+ if (nameStart + 2 === nameEnd) {
54
+ // No other name goes through the rest of this name, so each of its chars is a single child
55
+ const name = entities[nameStart];
56
+ let parentIndex = nodeIndex;
57
+ for (let charIndex = depth; charIndex < name.length; ++charIndex) {
58
+ if (firstUnusedIndex + 128 > size) {
59
+ size <<= 1;
60
+ nodeBases = growInt32Array(nodeBases, size, 0);
61
+ nodeChecks = growInt32Array(nodeChecks, size, -1);
62
+ valueIndexes = growInt32Array(valueIndexes, size, 0);
63
+ }
64
+ nodeBases[parentIndex] = firstUnusedIndex - name.charCodeAt(charIndex);
65
+ nodeChecks[firstUnusedIndex] = (parentIndex << 1) | (charIndex === name.length - 1 ? 1 : 0);
66
+ parentIndex = firstUnusedIndex;
67
+ while (firstUnusedIndex < size && nodeChecks[firstUnusedIndex] !== -1) {
68
+ ++firstUnusedIndex;
69
+ }
70
+ }
71
+ valueIndexes[parentIndex] = values.length;
72
+ values.push(entities[nameStart + 1]);
73
+ continue;
74
+ }
75
+ // The names that go through a child have the same char at this depth
76
+ let childCount = 0;
77
+ for (let index = nameStart; index !== nameEnd; ++childCount) {
78
+ const charCode = entities[index].charCodeAt(depth);
79
+ childChars[childCount] = charCode;
80
+ childNameStarts[childCount] = index;
81
+ do {
82
+ index += 2;
83
+ } while (index !== nameEnd && entities[index].charCodeAt(depth) === charCode);
84
+ childNameEnds[childCount] = index;
85
+ }
86
+ const isOnlyChild = childCount === 1;
87
+ // Children are sorted by char, so the first child lands on the first unused index, or on searchStartIndex if
88
+ // several children can't be placed before it
89
+ const startBase = (!isOnlyChild && searchStartIndex > firstUnusedIndex ? searchStartIndex : firstUnusedIndex) - childChars[0];
90
+ let base = startBase;
91
+ // Find a base that puts all children on unused indexes
92
+ while (true) {
93
+ if (base + 128 > size) {
94
+ size <<= 1;
95
+ nodeBases = growInt32Array(nodeBases, size, 0);
96
+ nodeChecks = growInt32Array(nodeChecks, size, -1);
97
+ valueIndexes = growInt32Array(valueIndexes, size, 0);
98
+ }
99
+ if (isOnlyChild) {
100
+ break;
101
+ }
102
+ let isUnused = true;
103
+ for (let childIndex = 0; childIndex < childCount; ++childIndex) {
104
+ if (nodeChecks[base + childChars[childIndex]] !== -1) {
105
+ isUnused = false;
106
+ break;
107
+ }
108
+ }
109
+ if (isUnused) {
110
+ break;
111
+ }
112
+ ++base;
113
+ }
114
+ if (base - startBase > 256) {
115
+ // The holes that a long search passed rarely fit several children. Later searches skip them, since passing the
116
+ // same holes for each node would make building a table with many such nodes take quadratic time.
117
+ searchStartIndex = base + childChars[0];
118
+ }
119
+ nodeBases[nodeIndex] = base;
120
+ for (let childIndex = 0; childIndex < childCount; ++childIndex) {
121
+ const index = base + childChars[childIndex];
122
+ // An entity name ends at a child if the first name that goes through it ends there
123
+ nodeChecks[index] = (nodeIndex << 1) | (entities[childNameStarts[childIndex]].length === depth + 1 ? 1 : 0);
124
+ if (!isOnlyChild && index > maxIndex) {
125
+ maxIndex = index;
126
+ }
127
+ }
128
+ while (firstUnusedIndex < size && nodeChecks[firstUnusedIndex] !== -1) {
129
+ ++firstUnusedIndex;
130
+ }
131
+ // Pushed in reverse, so children are visited in char order, and nodes are placed in preorder
132
+ for (let childIndex = childCount - 1; childIndex >= 0; --childIndex) {
133
+ stack[stackLength] = base + childChars[childIndex];
134
+ stack[stackLength + 1] = childNameStarts[childIndex];
135
+ stack[stackLength + 2] = childNameEnds[childIndex];
136
+ stack[stackLength + 3] = depth + 1;
137
+ stackLength += 4;
138
+ }
139
+ }
140
+ if (maxIndex < firstUnusedIndex) {
141
+ maxIndex = firstUnusedIndex;
142
+ }
143
+ // Keep base + charCode in bounds for any ASCII char
144
+ const finalSize = maxIndex + 128;
145
+ // 2-byte indexes take half the memory and are enough for most tables
146
+ const nodeValueIndexes = values.length <= 0x10000 ? new Uint16Array(finalSize) : new Int32Array(finalSize);
147
+ nodeValueIndexes.set(valueIndexes.subarray(0, finalSize));
148
+ return {
149
+ nodeBases: nodeBases.slice(0, finalSize),
150
+ nodeChecks: nodeChecks.slice(0, finalSize),
151
+ nodeValueIndexes,
152
+ values,
153
+ };
154
+ }
155
+ function growInt32Array(array, size, fillValue) {
156
+ const grownArray = new Int32Array(size).fill(fillValue, array.length);
157
+ grownArray.set(array);
158
+ return grownArray;
159
+ }
@@ -1,33 +1,18 @@
1
+ import { type EntityTrieDecoderOptions } from './createEntityTrieDecoder.ts';
1
2
  /**
2
- * The options recognized by {@linkcode createEntityDecoder}.
3
+ * Options of {@linkcode createEntityDecoder}.
3
4
  */
4
- export interface EntityDecoderOptions {
5
+ export interface EntityDecoderOptions extends EntityTrieDecoderOptions {
5
6
  /**
6
- * Mapping from an [character entity reference](https://www.w3.org/TR/html4/charset.html#h-5.3.2) to its decoded value.
7
- * Entity references should contain only [a-zA-Z] and may optionally start with an ampersand "&" and end with
8
- * a semicolon ";".
7
+ * Entity references mapped to their values, for example `{ '&amp;': '&', 'lt;': '<' }`, or a list of references and
8
+ * values in turn, for example `['&amp;', '&', 'lt;', '<']`. A reference is an optional "&", a name of ASCII letters,
9
+ * digits and "-.:_", and an optional ";". If the reference has the ";", the input needs it too.
9
10
  *
10
- * ```json
11
- * {
12
- * "&Delta;": "\u0394",
13
- * "LongRightArrow;": "\u27F6"
14
- * }
15
- * ```
11
+ * @default {}
16
12
  */
17
- entities?: Record<string, string>;
18
- /**
19
- * If `true` then [numeric character references](https://www.w3.org/TR/html4/charset.html#h-5.3.1) must be terminated
20
- * with a semicolon to be decoded. Otherwise, numeric character references are recognized even if they aren't
21
- * terminated with a semicolon.
22
- *
23
- * @default false
24
- */
25
- isNumericReferenceSemicolonRequired?: boolean;
13
+ entities?: Record<string, string> | readonly string[];
26
14
  }
27
15
  /**
28
- * Creates an entity decoder that rewrites numeric and named entities found in input to their respective values.
29
- *
30
- * @param options The decoder options.
31
- * @returns A function that decodes entities in the string.
16
+ * Creates a decoder for numeric references and the given named entities.
32
17
  */
33
18
  export declare function createEntityDecoder(options?: EntityDecoderOptions): (input: string) => string;
@@ -1,182 +1,9 @@
1
+ import { createEntityTrie } from "./createEntityTrie.js";
2
+ import { createEntityTrieDecoder } from "./createEntityTrieDecoder.js";
1
3
  /**
2
- * Creates an entity decoder that rewrites numeric and named entities found in input to their respective values.
3
- *
4
- * @param options The decoder options.
5
- * @returns A function that decodes entities in the string.
4
+ * Creates a decoder for numeric references and the given named entities.
6
5
  */
7
6
  export function createEntityDecoder(options = {}) {
8
- const { entities, isNumericReferenceSemicolonRequired = false } = options;
9
- const fromCharCode = String.fromCharCode;
10
- let entityDereferenceMap;
11
- let maximumEntityReferenceLength = 32;
12
- if (entities !== undefined) {
13
- entityDereferenceMap = new Map();
14
- for (const entity in entities) {
15
- const hashCode = getEntityReferenceHashCode(entity);
16
- if (hashCode !== 0) {
17
- maximumEntityReferenceLength = Math.max(maximumEntityReferenceLength, entity.length);
18
- entityDereferenceMap.set(hashCode, entities[entity]);
19
- }
20
- }
21
- }
22
- return input => {
23
- let output = '';
24
- let textIndex = 0;
25
- let charIndex = 0;
26
- const inputLength = input.length;
27
- while (charIndex < inputLength - 1) {
28
- let startIndex = input.indexOf('&', charIndex);
29
- if (startIndex === -1) {
30
- break;
31
- }
32
- charIndex = startIndex++;
33
- let value;
34
- let endIndex = startIndex;
35
- let charCode = 0;
36
- if (startIndex < inputLength - 2 && input.charCodeAt(startIndex) === /* # */ 35) {
37
- // Numeric character reference
38
- let codePoint = 0;
39
- if ((input.charCodeAt(++startIndex) | 32) === /* x */ 120) {
40
- endIndex = ++startIndex;
41
- // parseInt of a hexadecimal number
42
- while (endIndex - startIndex < 6 && endIndex < inputLength) {
43
- // Convert alpha to lower case
44
- charCode = input.charCodeAt(endIndex) | 32;
45
- if ((charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57) ||
46
- (charCode >= /* a */ 97 && charCode <= /* f */ 102)) {
47
- // Convert "0" → 0 and "f" → 15
48
- codePoint = codePoint * 16 + charCode - (charCode & 112) + (charCode >> 6) * 9;
49
- ++endIndex;
50
- }
51
- else {
52
- break;
53
- }
54
- }
55
- }
56
- else {
57
- endIndex = startIndex;
58
- // parseInt of a decimal number
59
- while (endIndex - startIndex < 6 && endIndex < inputLength) {
60
- charCode = input.charCodeAt(endIndex) | 32;
61
- if (charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57) {
62
- codePoint = codePoint * 10 + charCode - (charCode & 112);
63
- ++endIndex;
64
- }
65
- else {
66
- break;
67
- }
68
- }
69
- }
70
- if (endIndex !== startIndex) {
71
- // At least one digit must present
72
- const isTerminated = endIndex < inputLength && input.charCodeAt(endIndex) === /* ; */ 59;
73
- if (isTerminated || !isNumericReferenceSemicolonRequired) {
74
- // Convert a code point to a string
75
- // https://github.com/mathiasbynens/he/blob/master/src/he.js#L106-L134
76
- if (codePoint === 0 || (codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
77
- // Null char code, or character reference is outside the permissible Unicode range
78
- value = '\uFFFD';
79
- }
80
- else if (codePoint >= 128 &&
81
- codePoint <= 195 &&
82
- codePoint !== 129 &&
83
- codePoint !== 141 &&
84
- codePoint !== 143 &&
85
- codePoint !== 144 &&
86
- codePoint !== 157) {
87
- // Overridden char code
88
- value = entityOverrides[codePoint];
89
- }
90
- else if (codePoint > 0xffff) {
91
- // Surrogate pair
92
- codePoint -= 0x10000;
93
- value = fromCharCode(((codePoint >>> 10) & 0x3ff) | 0xd800, 0xdc00 | (codePoint & 0x3ff));
94
- }
95
- else {
96
- // Char code
97
- value = fromCharCode(codePoint);
98
- }
99
- }
100
- if (isTerminated) {
101
- ++endIndex;
102
- }
103
- }
104
- }
105
- else if (entityDereferenceMap !== undefined) {
106
- // Character entity reference
107
- let index = startIndex;
108
- let hashCode = 0;
109
- let referencedValue;
110
- while (charCode !== /* ; */ 59 &&
111
- index < inputLength &&
112
- index - startIndex < maximumEntityReferenceLength &&
113
- ((charCode = input.charCodeAt(index) | 0), isEntityReferenceChar(charCode))) {
114
- ++index;
115
- hashCode = (hashCode << 5) - hashCode + charCode;
116
- referencedValue = entityDereferenceMap.get(hashCode);
117
- if (referencedValue !== undefined) {
118
- value = referencedValue;
119
- endIndex = index;
120
- }
121
- }
122
- }
123
- // Concat decoded entity and preceding substring
124
- if (value !== undefined) {
125
- output += textIndex === charIndex ? value : input.slice(textIndex, charIndex) + value;
126
- textIndex = endIndex;
127
- }
128
- charIndex = endIndex;
129
- }
130
- return textIndex === 0 ? input : textIndex === inputLength ? output : output + input.slice(textIndex);
131
- };
132
- }
133
- // Standing on the shoulders of giants
134
- // https://github.com/mathiasbynens/he/blob/master/data/decode-map-overrides.json
135
- const entityOverrides = {
136
- 128: '\u20AC',
137
- 130: '\u201A',
138
- 131: '\u0192',
139
- 132: '\u201E',
140
- 133: '\u2026',
141
- 134: '\u2020',
142
- 135: '\u2021',
143
- 136: '\u02C6',
144
- 137: '\u2030',
145
- 138: '\u0160',
146
- 139: '\u2039',
147
- 140: '\u0152',
148
- 142: '\u017D',
149
- 145: '\u2018',
150
- 146: '\u2019',
151
- 147: '\u201C',
152
- 148: '\u201D',
153
- 149: '\u2022',
154
- 150: '\u2013',
155
- 151: '\u2014',
156
- 152: '\u02DC',
157
- 153: '\u2122',
158
- 154: '\u0161',
159
- 155: '\u203A',
160
- 156: '\u0153',
161
- 158: '\u017E',
162
- 159: '\u0178',
163
- };
164
- function getEntityReferenceHashCode(entity) {
165
- if (entity.length === 0) {
166
- return 0;
167
- }
168
- let hashCode = 0;
169
- for (let index = entity.charCodeAt(0) === /* & */ 38 ? 1 : 0; index < entity.length; ++index) {
170
- const charCode = entity.charCodeAt(index);
171
- if (!isEntityReferenceChar(charCode)) {
172
- return 0;
173
- }
174
- hashCode = (hashCode << 5) - hashCode + entity.charCodeAt(index);
175
- }
176
- return hashCode;
177
- }
178
- function isEntityReferenceChar(charCode) {
179
- return ((charCode >= /* a */ 97 && charCode <= /* z */ 122) ||
180
- (charCode >= /* A */ 65 && charCode <= /* Z */ 90) ||
181
- charCode === /* ; */ 59);
7
+ const { entities = {} } = options;
8
+ return createEntityTrieDecoder(createEntityTrie(entities), options);
182
9
  }
@@ -0,0 +1,9 @@
1
+ import { type EntityTrie } from './createDoubleArrayEntityTrie.ts';
2
+ /**
3
+ * Builds a trie of entity names. Pass it to {@linkcode createEntityTrieDecoder} to create decoders that share it,
4
+ * since building a trie takes time and memory.
5
+ *
6
+ * @param entities Entity references mapped to their values, or a list of references and values in turn, see
7
+ * {@linkcode EntityDecoderOptions.entities}.
8
+ */
9
+ export declare function createEntityTrie(entities: Record<string, string> | readonly string[]): EntityTrie;