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 +1 -1
- package/README.md +132 -16
- package/createDoubleArrayEntityTrie.d.ts +49 -0
- package/createDoubleArrayEntityTrie.js +159 -0
- package/createEntityDecoder.d.ts +9 -24
- package/createEntityDecoder.js +5 -178
- 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 +6 -1
- package/decodeHTML.js +16 -5
- package/decodeXML.d.ts +2 -2
- package/decodeXML.js +128 -7
- package/encodeXML.d.ts +4 -0
- package/encodeXML.js +113 -19
- package/entities.d.ts +5 -0
- package/entities.js +2237 -0
- package/escapeXML.d.ts +4 -0
- package/escapeXML.js +17 -8
- package/index.d.ts +8 -5
- package/index.js +7 -5
- package/package.json +10 -3
- package/html-entities.d.ts +0 -2234
- package/html-entities.js +0 -2233
- package/xml-entities.d.ts +0 -8
- package/xml-entities.js +0 -7
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,6 +32,10 @@ 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
|
|
@@ -52,9 +66,78 @@ decode('abc');
|
|
|
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`, `€` 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
|
+
|
|
55
137
|
# Encode
|
|
56
138
|
|
|
57
|
-
`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 `�`.
|
|
58
141
|
|
|
59
142
|
`escapeXML` escapes only `"&'<>` characters.
|
|
60
143
|
|
|
@@ -65,26 +148,59 @@ encodeXML('&😘❤️');
|
|
|
65
148
|
// ⮕ '&😘❤️'
|
|
66
149
|
|
|
67
150
|
escapeXML('&😘❤️');
|
|
68
|
-
// ⮕ '&
|
|
151
|
+
// ⮕ '&😘❤️'
|
|
69
152
|
```
|
|
70
153
|
|
|
71
154
|
# Performance
|
|
72
155
|
|
|
73
|
-
Results are in
|
|
156
|
+
Results are in thousands of operations per second (kHz). The higher number is better.
|
|
74
157
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
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 |
|
|
81
164
|
|
|
82
|
-
Tests were conducted using [TooFast](https://github.com/smikhalevski/toofast#readme) on Apple M1 with Node.js
|
|
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](
|
|
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
|
+
}
|
package/createEntityDecoder.d.ts
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
|
+
import { type EntityTrieDecoderOptions } from './createEntityTrieDecoder.ts';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Options of {@linkcode createEntityDecoder}.
|
|
3
4
|
*/
|
|
4
|
-
export interface EntityDecoderOptions {
|
|
5
|
+
export interface EntityDecoderOptions extends EntityTrieDecoderOptions {
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
* {
|
|
12
|
-
* "Δ": "\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
|
|
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;
|
package/createEntityDecoder.js
CHANGED
|
@@ -1,182 +1,9 @@
|
|
|
1
|
+
import { createEntityTrie } from "./createEntityTrie.js";
|
|
2
|
+
import { createEntityTrieDecoder } from "./createEntityTrieDecoder.js";
|
|
1
3
|
/**
|
|
2
|
-
* Creates
|
|
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
|
|
9
|
-
|
|
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;
|