speedy-entities 3.0.1 → 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) 2023 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 🏎💨 [![build](https://github.com/smikhalevski/speedy-entities/actions/workflows/master.yml/badge.svg?branch=master&event=push)](https://github.com/smikhalevski/speedy-entities/actions/workflows/master.yml)
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
- [20 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,30 +32,26 @@ 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
28
42
 
29
- You can create a decoder that decodes custom named entities:
43
+ You can create a decoder that decodes your custom character entity references:
30
44
 
31
45
  ```ts
32
- import { arrayTrieEncode, trieCreate, trieSet } from '@smikhalevski/trie';
33
46
  import { createEntityDecoder } from 'speedy-entities';
34
47
 
35
- // Create a trie that would hold entities
36
- const trie = trieCreate<string>();
37
-
38
- // Register named entities
39
- trieSet('foo;', 'okay');
40
- trieSet('qux;', 'yeah');
41
-
42
- // Encode a trie
43
- const entitiesTrie = arrayTrieEncode(trie);
44
-
45
48
  // Create a decoder
46
49
  const decode = createEntityDecoder({
47
- entitiesTrie,
48
- numericReferenceSemicolonRequired: true,
50
+ entities: {
51
+ '&foo;': 'okay',
52
+ '&qux;': 'yeah',
53
+ },
54
+ isNumericReferenceSemicolonRequired: true,
49
55
  });
50
56
 
51
57
  // Decode entities
@@ -60,9 +66,78 @@ decode('&#X61;&#x62;&#x63;');
60
66
  // ⮕ 'abc'
61
67
  ```
62
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
+
63
137
  # Encode
64
138
 
65
- `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;`.
66
141
 
67
142
  `escapeXML` escapes only `"&'<>` characters.
68
143
 
@@ -73,19 +148,59 @@ encodeXML('&😘❤️');
73
148
  // ⮕ '&amp;&#x1f618;&#x2764;&#xfe0f;'
74
149
 
75
150
  escapeXML('&😘❤️');
76
- // ⮕ '&amp;😘❤'
151
+ // ⮕ '&amp;😘❤️'
77
152
  ```
78
153
 
79
154
  # Performance
80
155
 
81
- Clone this repo and use `npm ci && npm run perf` to run the performance testsuite.
156
+ Results are in thousands of operations per second (kHz). The higher number is better.
82
157
 
83
- Results are in millions of operations per second. The higher number is better.
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 |
84
164
 
85
- ### Decode
165
+ Tests were conducted using [TooFast](https://github.com/smikhalevski/toofast#readme) on Apple M1 with Node.js v26.11.1.
86
166
 
87
- ![Decode HTML performance chart](./images/perf-decode-html.svg)
167
+ To reproduce [the performance test suite](src/test/perf.ts) results, clone this repo and run:
168
+
169
+ ```shell
170
+ npm ci
171
+ npm run build
172
+ GROUP_MEASURE=2 npm run perf
173
+ ```
88
174
 
89
- ### Encode
175
+ The inputs of each function are split into groups, such as short and long inputs. `GROUP_MEASURE` sets what a test
176
+ covers:
90
177
 
91
- ![Encode XML performance chart](./images/perf-encode-xml.svg)
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*'
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
+ }
@@ -0,0 +1,18 @@
1
+ import { type EntityTrieDecoderOptions } from './createEntityTrieDecoder.ts';
2
+ /**
3
+ * Options of {@linkcode createEntityDecoder}.
4
+ */
5
+ export interface EntityDecoderOptions extends EntityTrieDecoderOptions {
6
+ /**
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.
10
+ *
11
+ * @default {}
12
+ */
13
+ entities?: Record<string, string> | readonly string[];
14
+ }
15
+ /**
16
+ * Creates a decoder for numeric references and the given named entities.
17
+ */
18
+ export declare function createEntityDecoder(options?: EntityDecoderOptions): (input: string) => string;
@@ -0,0 +1,9 @@
1
+ import { createEntityTrie } from "./createEntityTrie.js";
2
+ import { createEntityTrieDecoder } from "./createEntityTrieDecoder.js";
3
+ /**
4
+ * Creates a decoder for numeric references and the given named entities.
5
+ */
6
+ export function createEntityDecoder(options = {}) {
7
+ const { entities = {} } = options;
8
+ return createEntityTrieDecoder(createEntityTrie(entities), options);
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;
@@ -0,0 +1,69 @@
1
+ import { createDoubleArrayEntityTrie } from "./createDoubleArrayEntityTrie.js";
2
+ /**
3
+ * An optional "&", a name of ASCII letters, digits and "-.:_", and an optional ";".
4
+ */
5
+ const entityRegExp = /^&?[a-zA-Z0-9.:_-]+;?$/;
6
+ /**
7
+ * Builds a trie of entity names. Pass it to {@linkcode createEntityTrieDecoder} to create decoders that share it,
8
+ * since building a trie takes time and memory.
9
+ *
10
+ * @param entities Entity references mapped to their values, or a list of references and values in turn, see
11
+ * {@linkcode EntityDecoderOptions.entities}.
12
+ */
13
+ export function createEntityTrie(entities) {
14
+ if (Array.isArray(entities)) {
15
+ if (entities.length % 2 !== 0) {
16
+ throw new Error('entities must list references and values in turn');
17
+ }
18
+ return createEntityTrieFromArray(entities);
19
+ }
20
+ const uncheckedEntities = [];
21
+ for (const entity of Object.keys(entities)) {
22
+ uncheckedEntities.push(entity, entities[entity]);
23
+ }
24
+ return createEntityTrieFromArray(uncheckedEntities);
25
+ }
26
+ /**
27
+ * Builds a trie from references and values in turn. Lists are usually sorted and have no "&", so a list is copied only
28
+ * if a name has "&" or names aren't sorted.
29
+ */
30
+ function createEntityTrieFromArray(uncheckedEntities) {
31
+ let checkedEntities = uncheckedEntities;
32
+ let isSorted = true;
33
+ let previousEntity = '';
34
+ for (let index = 0; index < uncheckedEntities.length; index += 2) {
35
+ let entity = uncheckedEntities[index];
36
+ if (typeof entity !== 'string' || !entityRegExp.test(entity)) {
37
+ throw new Error('Invalid entity reference: ' + JSON.stringify(entity));
38
+ }
39
+ if (typeof uncheckedEntities[index + 1] !== 'string') {
40
+ throw new Error('Value of entity reference must be a string: ' + JSON.stringify(entity));
41
+ }
42
+ if (entity.charCodeAt(0) === /* & */ 38) {
43
+ entity = entity.slice(1);
44
+ if (checkedEntities === uncheckedEntities) {
45
+ checkedEntities = uncheckedEntities.slice(0);
46
+ }
47
+ checkedEntities[index] = entity;
48
+ }
49
+ // Stripping "&" can change the order, so entities are compared without it
50
+ if (entity < previousEntity) {
51
+ isSorted = false;
52
+ }
53
+ previousEntity = entity;
54
+ }
55
+ return createDoubleArrayEntityTrie(isSorted ? checkedEntities : sortByEntityReference(checkedEntities));
56
+ }
57
+ // Sort is stable, so "&lt;" and "lt;" keep their order, and the value of the latter is used
58
+ function sortByEntityReference(entities) {
59
+ const indexes = new Uint32Array(entities.length / 2);
60
+ for (let index = 0; index < indexes.length; ++index) {
61
+ indexes[index] = index * 2;
62
+ }
63
+ indexes.sort((a, b) => (entities[a] < entities[b] ? -1 : entities[a] > entities[b] ? 1 : 0));
64
+ const sortedEntities = [];
65
+ for (const index of indexes) {
66
+ sortedEntities.push(entities[index], entities[index + 1]);
67
+ }
68
+ return sortedEntities;
69
+ }
@@ -0,0 +1,33 @@
1
+ import { type EntityTrie } from './createDoubleArrayEntityTrie.ts';
2
+ /**
3
+ * Options of {@linkcode createEntityTrieDecoder}.
4
+ */
5
+ export interface EntityTrieDecoderOptions {
6
+ /**
7
+ * If `true`, numeric references are decoded only if they end with ";", as XML requires.
8
+ *
9
+ * @default false
10
+ */
11
+ isNumericReferenceSemicolonRequired?: boolean;
12
+ /**
13
+ * If `true`, numeric references to C1 controls (U+0080 to U+009F) are decoded as Windows-1252 chars,
14
+ * [as HTML does](https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state). For
15
+ * example, "&#128;" is decoded as the euro sign instead of U+0080.
16
+ *
17
+ * @default true
18
+ */
19
+ areC1ControlsRemapped?: boolean;
20
+ /**
21
+ * If `true`, a named reference without ";" isn't decoded if "=" or an ASCII letter or digit follows it,
22
+ * [as HTML does in attribute values](https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state).
23
+ * This keeps URL query strings like "?a=1&copy=2" intact.
24
+ *
25
+ * @default false
26
+ */
27
+ isAttributeValue?: boolean;
28
+ }
29
+ /**
30
+ * Creates a decoder that uses a trie from {@linkcode createEntityTrie}, so decoders that differ only in options can
31
+ * share a trie.
32
+ */
33
+ export declare function createEntityTrieDecoder(entityTrie: EntityTrie, options?: EntityTrieDecoderOptions): (input: string) => string;