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 +1 -1
- package/README.md +141 -26
- package/createDoubleArrayEntityTrie.d.ts +49 -0
- package/createDoubleArrayEntityTrie.js +159 -0
- package/createEntityDecoder.d.ts +18 -0
- package/createEntityDecoder.js +9 -0
- package/createEntityTrie.d.ts +9 -0
- package/createEntityTrie.js +69 -0
- package/createEntityTrieDecoder.d.ts +33 -0
- package/createEntityTrieDecoder.js +159 -0
- package/decodeHTML.d.ts +9 -0
- package/decodeHTML.js +20 -0
- package/decodeXML.d.ts +4 -0
- package/decodeXML.js +130 -0
- package/encodeXML.d.ts +5 -0
- package/encodeXML.js +132 -0
- package/entities.d.ts +5 -0
- package/entities.js +2237 -0
- package/escapeXML.d.ts +5 -0
- package/escapeXML.js +38 -0
- package/index.d.ts +8 -0
- package/index.js +7 -0
- package/package.json +18 -39
- package/lib/index.d.ts +0 -42
- package/lib/index.js +0 -249
- package/lib/index.mjs +0 -243
package/LICENSE.txt
CHANGED
package/README.md
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
|
|
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
|
-
[
|
|
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
|
|
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('ab<');
|
|
18
28
|
// ⮕ 'ab<'
|
|
@@ -22,30 +32,26 @@ decodeHTML('<fooÆ');
|
|
|
22
32
|
|
|
23
33
|
decodeHTML('⪢̸∳');
|
|
24
34
|
// ⮕ '\u2aa2\u0338\u2233'
|
|
35
|
+
|
|
36
|
+
// Like browsers, preserves unterminated entities followed by "=" or an alphanumeric char
|
|
37
|
+
decodeHTMLAttribute('/search?a=1©=2®');
|
|
38
|
+
// ⮕ '/search?a=1©=2\u00ae'
|
|
25
39
|
```
|
|
26
40
|
|
|
27
41
|
## Custom decoder
|
|
28
42
|
|
|
29
|
-
You can create a decoder that decodes custom
|
|
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
|
-
|
|
48
|
-
|
|
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('abc');
|
|
|
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`, `€` to `Ÿ` are decoded as Windows-1252 characters, as HTML does. For example, `€` 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`
|
|
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 `�`.
|
|
66
141
|
|
|
67
142
|
`escapeXML` escapes only `"&'<>` characters.
|
|
68
143
|
|
|
@@ -73,19 +148,59 @@ encodeXML('&😘❤️');
|
|
|
73
148
|
// ⮕ '&😘❤️'
|
|
74
149
|
|
|
75
150
|
escapeXML('&😘❤️');
|
|
76
|
-
// ⮕ '&
|
|
151
|
+
// ⮕ '&😘❤️'
|
|
77
152
|
```
|
|
78
153
|
|
|
79
154
|
# Performance
|
|
80
155
|
|
|
81
|
-
|
|
156
|
+
Results are in thousands of operations per second (kHz). The higher number is better.
|
|
82
157
|
|
|
83
|
-
|
|
158
|
+
| Library | Library Size | Decode HTML | Decode XML | Encode XML | Escape XML |
|
|
159
|
+
| ----------------------------------------------------------------------: | ----------------------------------------------------------------: | ------------: | ------------: | -----------: | ------------: |
|
|
160
|
+
| speedy-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-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
|
-
|
|
165
|
+
Tests were conducted using [TooFast](https://github.com/smikhalevski/toofast#readme) on Apple M1 with Node.js v26.11.1.
|
|
86
166
|
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `{ '&': '&', 'lt;': '<' }`, or a list of references and
|
|
8
|
+
* values in turn, for example `['&', '&', '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 "<" 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, "€" 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©=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;
|