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
|
@@ -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;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {} from "./createDoubleArrayEntityTrie.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a decoder that uses a trie from {@linkcode createEntityTrie}, so decoders that differ only in options can
|
|
4
|
+
* share a trie.
|
|
5
|
+
*/
|
|
6
|
+
export function createEntityTrieDecoder(entityTrie, options = {}) {
|
|
7
|
+
const { isNumericReferenceSemicolonRequired = false, areC1ControlsRemapped = true, isAttributeValue = false, } = options;
|
|
8
|
+
const { nodeBases, nodeChecks, nodeValueIndexes, values } = entityTrie;
|
|
9
|
+
return input => {
|
|
10
|
+
let output = '';
|
|
11
|
+
let textIndex = 0;
|
|
12
|
+
const inputLength = input.length;
|
|
13
|
+
// A reference is "&" followed by at least one char, so it can't start at the last char
|
|
14
|
+
let charIndex = inputLength > 1 ? input.indexOf('&') : -1;
|
|
15
|
+
while (charIndex !== -1 && charIndex < inputLength - 1) {
|
|
16
|
+
let startIndex = charIndex + 1;
|
|
17
|
+
let value;
|
|
18
|
+
let endIndex = startIndex;
|
|
19
|
+
// Both reads below must stay in bounds: reading past the end of the input makes V8 deoptimize this function,
|
|
20
|
+
// and it stays about twice as slow on short inputs afterwards
|
|
21
|
+
if (startIndex < inputLength - 1 && input.charCodeAt(startIndex) === /* # */ 35) {
|
|
22
|
+
// Numeric character reference
|
|
23
|
+
// All digits are consumed, out-of-range code points (Infinity included) are replaced with U+FFFD below
|
|
24
|
+
let codePoint = 0;
|
|
25
|
+
if ((input.charCodeAt(++startIndex) | 32) === /* x */ 120) {
|
|
26
|
+
endIndex = ++startIndex;
|
|
27
|
+
// parseInt of a hexadecimal number
|
|
28
|
+
while (endIndex < inputLength) {
|
|
29
|
+
const charCode = input.charCodeAt(endIndex);
|
|
30
|
+
// Convert alpha to lower case
|
|
31
|
+
const lowerCharCode = charCode | 32;
|
|
32
|
+
if (charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57) {
|
|
33
|
+
codePoint = codePoint * 16 + charCode - 48;
|
|
34
|
+
}
|
|
35
|
+
else if (lowerCharCode >= /* a */ 97 && lowerCharCode <= /* f */ 102) {
|
|
36
|
+
codePoint = codePoint * 16 + lowerCharCode - 87;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
++endIndex;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
endIndex = startIndex;
|
|
46
|
+
// parseInt of a decimal number
|
|
47
|
+
while (endIndex < inputLength) {
|
|
48
|
+
const charCode = input.charCodeAt(endIndex);
|
|
49
|
+
if (charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57) {
|
|
50
|
+
codePoint = codePoint * 10 + charCode - 48;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
++endIndex;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (endIndex !== startIndex) {
|
|
59
|
+
// At least one digit must be present
|
|
60
|
+
const isTerminated = endIndex < inputLength && input.charCodeAt(endIndex) === /* ; */ 59;
|
|
61
|
+
if (isTerminated || !isNumericReferenceSemicolonRequired) {
|
|
62
|
+
// Convert a code point to a string
|
|
63
|
+
if (codePoint === 0 || (codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
|
|
64
|
+
// Null char code, or character reference is outside the permissible Unicode range
|
|
65
|
+
value = '\uFFFD';
|
|
66
|
+
}
|
|
67
|
+
else if (areC1ControlsRemapped && codePoint >= 0x80 && codePoint <= 0x9f) {
|
|
68
|
+
value = c1ControlValues[codePoint - 0x80];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
value = String.fromCodePoint(codePoint);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (isTerminated) {
|
|
75
|
+
++endIndex;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Character entity reference
|
|
81
|
+
let index = startIndex;
|
|
82
|
+
let node = 0;
|
|
83
|
+
// The node where the longest entity name read so far ends, or 0. Its value is read once, after the loop, since
|
|
84
|
+
// reading the value at each node where a name ends makes decoding a few percent slower.
|
|
85
|
+
let valueNode = 0;
|
|
86
|
+
while (index < inputLength) {
|
|
87
|
+
const charCode = input.charCodeAt(index);
|
|
88
|
+
// Only ASCII chars have nodes, and bases keep base + charCode in bounds for them
|
|
89
|
+
if (charCode > 127) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
const childNode = nodeBases[node] + charCode;
|
|
93
|
+
const childCheck = nodeChecks[childNode];
|
|
94
|
+
if (childCheck >> 1 !== node) {
|
|
95
|
+
// No entity name starts with chars read so far
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
node = childNode;
|
|
99
|
+
++index;
|
|
100
|
+
if ((childCheck & 1) === 1) {
|
|
101
|
+
// An entity name ends at the node
|
|
102
|
+
valueNode = node;
|
|
103
|
+
endIndex = index;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (valueNode !== 0) {
|
|
107
|
+
value = values[nodeValueIndexes[valueNode]];
|
|
108
|
+
}
|
|
109
|
+
if (isAttributeValue &&
|
|
110
|
+
value !== undefined &&
|
|
111
|
+
endIndex < inputLength &&
|
|
112
|
+
input.charCodeAt(endIndex - 1) !== /* ; */ 59) {
|
|
113
|
+
const charCode = input.charCodeAt(endIndex);
|
|
114
|
+
// An unterminated reference followed by "=" or an alphanumeric char is likely a URL query parameter
|
|
115
|
+
if (charCode === /* = */ 61 || isAlphanumericCharCode(charCode)) {
|
|
116
|
+
value = undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (value !== undefined) {
|
|
121
|
+
// Appending the preceding text and the value separately avoids joining them into a new string first. Once
|
|
122
|
+
// the output is long enough to be a rope, each append only adds a node, even if the encodings differ.
|
|
123
|
+
if (textIndex !== charIndex) {
|
|
124
|
+
output += input.slice(textIndex, charIndex);
|
|
125
|
+
}
|
|
126
|
+
output += value;
|
|
127
|
+
textIndex = endIndex;
|
|
128
|
+
}
|
|
129
|
+
if (endIndex >= inputLength - 1) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
// Entities often follow each other, and checking the next char is cheaper than calling indexOf
|
|
133
|
+
if (value === undefined || input.charCodeAt(endIndex) !== /* & */ 38) {
|
|
134
|
+
charIndex = input.indexOf('&', endIndex);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
charIndex = endIndex;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return textIndex === 0 ? input : textIndex === inputLength ? output : output + input.slice(textIndex);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Windows-1252 chars for U+0080 to U+009F. Code points that Windows-1252 leaves undefined map to themselves.
|
|
145
|
+
*
|
|
146
|
+
* @see https://github.com/mathiasbynens/he/blob/master/data/decode-map-overrides.json
|
|
147
|
+
*/
|
|
148
|
+
// prettier-ignore
|
|
149
|
+
const c1ControlValues = [
|
|
150
|
+
'\u20AC', '\u0081', '\u201A', '\u0192', '\u201E', '\u2026', '\u2020', '\u2021',
|
|
151
|
+
'\u02C6', '\u2030', '\u0160', '\u2039', '\u0152', '\u008D', '\u017D', '\u008F',
|
|
152
|
+
'\u0090', '\u2018', '\u2019', '\u201C', '\u201D', '\u2022', '\u2013', '\u2014',
|
|
153
|
+
'\u02DC', '\u2122', '\u0161', '\u203A', '\u0153', '\u009D', '\u017E', '\u0178',
|
|
154
|
+
];
|
|
155
|
+
function isAlphanumericCharCode(charCode) {
|
|
156
|
+
return ((charCode >= /* a */ 97 && charCode <= /* z */ 122) ||
|
|
157
|
+
(charCode >= /* A */ 65 && charCode <= /* Z */ 90) ||
|
|
158
|
+
(charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57));
|
|
159
|
+
}
|
package/decodeHTML.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decodes
|
|
2
|
+
* Decodes HTML entities and numeric references in text.
|
|
3
3
|
*/
|
|
4
4
|
export declare const decodeHTML: (input: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Same as {@linkcode decodeHTML}, but for attribute values: like browsers, it doesn't decode a named reference without
|
|
7
|
+
* ";" if "=" or an ASCII letter or digit follows it, so "?a=1©=2" stays intact.
|
|
8
|
+
*/
|
|
9
|
+
export declare const decodeHTMLAttribute: (input: string) => string;
|
package/decodeHTML.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { createDoubleArrayEntityTrie } from "./createDoubleArrayEntityTrie.js";
|
|
2
|
+
import { createEntityTrieDecoder } from "./createEntityTrieDecoder.js";
|
|
3
|
+
import entities from "./entities.js";
|
|
4
|
+
const entityTrie = createDoubleArrayEntityTrie(entities);
|
|
3
5
|
/**
|
|
4
|
-
* Decodes
|
|
6
|
+
* Decodes HTML entities and numeric references in text.
|
|
5
7
|
*/
|
|
6
|
-
export const decodeHTML =
|
|
7
|
-
entities: htmlEntities,
|
|
8
|
+
export const decodeHTML = createEntityTrieDecoder(entityTrie, {
|
|
8
9
|
isNumericReferenceSemicolonRequired: false,
|
|
10
|
+
areC1ControlsRemapped: true,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Same as {@linkcode decodeHTML}, but for attribute values: like browsers, it doesn't decode a named reference without
|
|
14
|
+
* ";" if "=" or an ASCII letter or digit follows it, so "?a=1©=2" stays intact.
|
|
15
|
+
*/
|
|
16
|
+
export const decodeHTMLAttribute = createEntityTrieDecoder(entityTrie, {
|
|
17
|
+
isNumericReferenceSemicolonRequired: false,
|
|
18
|
+
areC1ControlsRemapped: true,
|
|
19
|
+
isAttributeValue: true,
|
|
9
20
|
});
|
package/decodeXML.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Decodes XML entities
|
|
2
|
+
* Decodes numeric references and the five predefined XML entities. References must end with ";", as XML requires.
|
|
3
3
|
*/
|
|
4
|
-
export declare
|
|
4
|
+
export declare function decodeXML(input: string): string;
|
package/decodeXML.js
CHANGED
|
@@ -1,9 +1,130 @@
|
|
|
1
|
-
import { createEntityDecoder } from './createEntityDecoder.js';
|
|
2
|
-
import xmlEntities from './xml-entities.js';
|
|
3
1
|
/**
|
|
4
|
-
* Decodes XML entities
|
|
2
|
+
* Decodes numeric references and the five predefined XML entities. References must end with ";", as XML requires.
|
|
5
3
|
*/
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
export function decodeXML(input) {
|
|
5
|
+
let charIndex = input.indexOf('&');
|
|
6
|
+
if (charIndex === -1) {
|
|
7
|
+
// Most text has no references
|
|
8
|
+
return input;
|
|
9
|
+
}
|
|
10
|
+
let output = '';
|
|
11
|
+
let textIndex = 0;
|
|
12
|
+
const inputLength = input.length;
|
|
13
|
+
while (charIndex !== -1 && charIndex < inputLength - 1) {
|
|
14
|
+
let startIndex = charIndex + 1;
|
|
15
|
+
let value;
|
|
16
|
+
let endIndex = startIndex;
|
|
17
|
+
const charCode = input.charCodeAt(startIndex);
|
|
18
|
+
if (charCode === /* # */ 35) {
|
|
19
|
+
// Numeric character reference
|
|
20
|
+
if (startIndex < inputLength - 1) {
|
|
21
|
+
// All digits are consumed, out-of-range code points (Infinity included) are replaced with U+FFFD below
|
|
22
|
+
let codePoint = 0;
|
|
23
|
+
// The char after the digits, or -1 at the end of the input
|
|
24
|
+
let terminatorCharCode = -1;
|
|
25
|
+
if ((input.charCodeAt(++startIndex) | 32) === /* x */ 120) {
|
|
26
|
+
endIndex = ++startIndex;
|
|
27
|
+
// parseInt of a hexadecimal number
|
|
28
|
+
while (endIndex < inputLength) {
|
|
29
|
+
const digitCharCode = input.charCodeAt(endIndex);
|
|
30
|
+
// Convert alpha to lower case
|
|
31
|
+
const lowerCharCode = digitCharCode | 32;
|
|
32
|
+
if (digitCharCode >= /* 0 */ 48 && digitCharCode <= /* 9 */ 57) {
|
|
33
|
+
codePoint = codePoint * 16 + digitCharCode - 48;
|
|
34
|
+
}
|
|
35
|
+
else if (lowerCharCode >= /* a */ 97 && lowerCharCode <= /* f */ 102) {
|
|
36
|
+
codePoint = codePoint * 16 + lowerCharCode - 87;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
terminatorCharCode = digitCharCode;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
++endIndex;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
endIndex = startIndex;
|
|
47
|
+
// parseInt of a decimal number
|
|
48
|
+
while (endIndex < inputLength) {
|
|
49
|
+
const digitCharCode = input.charCodeAt(endIndex);
|
|
50
|
+
if (digitCharCode >= /* 0 */ 48 && digitCharCode <= /* 9 */ 57) {
|
|
51
|
+
codePoint = codePoint * 10 + digitCharCode - 48;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
terminatorCharCode = digitCharCode;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
++endIndex;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// At least one digit and ";" must be present
|
|
61
|
+
if (endIndex !== startIndex && terminatorCharCode === /* ; */ 59) {
|
|
62
|
+
++endIndex;
|
|
63
|
+
if (codePoint === 0 || (codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
|
|
64
|
+
// Null char code, or character reference is outside the permissible Unicode range
|
|
65
|
+
value = '\uFFFD';
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
value = String.fromCodePoint(codePoint);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (charCode === /* l */ 108 || charCode === /* g */ 103) {
|
|
74
|
+
// "<" or ">"
|
|
75
|
+
if (startIndex + 2 < inputLength &&
|
|
76
|
+
input.charCodeAt(startIndex + 1) === /* t */ 116 &&
|
|
77
|
+
input.charCodeAt(startIndex + 2) === /* ; */ 59) {
|
|
78
|
+
value = charCode === /* l */ 108 ? '<' : '>';
|
|
79
|
+
endIndex = startIndex + 3;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (charCode === /* a */ 97) {
|
|
83
|
+
// "&" or "'"
|
|
84
|
+
if (startIndex + 3 < inputLength) {
|
|
85
|
+
const nextCharCode = input.charCodeAt(startIndex + 1);
|
|
86
|
+
if (nextCharCode === /* m */ 109 &&
|
|
87
|
+
input.charCodeAt(startIndex + 2) === /* p */ 112 &&
|
|
88
|
+
input.charCodeAt(startIndex + 3) === /* ; */ 59) {
|
|
89
|
+
value = '&';
|
|
90
|
+
endIndex = startIndex + 4;
|
|
91
|
+
}
|
|
92
|
+
else if (nextCharCode === /* p */ 112 &&
|
|
93
|
+
startIndex + 4 < inputLength &&
|
|
94
|
+
input.charCodeAt(startIndex + 2) === /* o */ 111 &&
|
|
95
|
+
input.charCodeAt(startIndex + 3) === /* s */ 115 &&
|
|
96
|
+
input.charCodeAt(startIndex + 4) === /* ; */ 59) {
|
|
97
|
+
value = "'";
|
|
98
|
+
endIndex = startIndex + 5;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (charCode === /* q */ 113) {
|
|
103
|
+
// """
|
|
104
|
+
if (startIndex + 4 < inputLength &&
|
|
105
|
+
input.charCodeAt(startIndex + 1) === /* u */ 117 &&
|
|
106
|
+
input.charCodeAt(startIndex + 2) === /* o */ 111 &&
|
|
107
|
+
input.charCodeAt(startIndex + 3) === /* t */ 116 &&
|
|
108
|
+
input.charCodeAt(startIndex + 4) === /* ; */ 59) {
|
|
109
|
+
value = '"';
|
|
110
|
+
endIndex = startIndex + 5;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (value === undefined) {
|
|
114
|
+
charIndex = endIndex < inputLength - 1 ? input.indexOf('&', endIndex) : -1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Appending the preceding text and the value separately avoids joining them into a new string first
|
|
118
|
+
if (textIndex !== charIndex) {
|
|
119
|
+
output += input.slice(textIndex, charIndex);
|
|
120
|
+
}
|
|
121
|
+
output += value;
|
|
122
|
+
textIndex = endIndex;
|
|
123
|
+
if (endIndex >= inputLength - 1) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
// References often follow each other, and checking the next char is cheaper than calling indexOf
|
|
127
|
+
charIndex = input.charCodeAt(endIndex) === /* & */ 38 ? endIndex : input.indexOf('&', endIndex);
|
|
128
|
+
}
|
|
129
|
+
return textIndex === 0 ? input : textIndex === inputLength ? output : output + input.slice(textIndex);
|
|
130
|
+
}
|
package/encodeXML.d.ts
CHANGED
package/encodeXML.js
CHANGED
|
@@ -1,38 +1,132 @@
|
|
|
1
|
-
const
|
|
1
|
+
const encodedCharRegExp = /["&'<>\u0080-\uFFFF]/g;
|
|
2
|
+
/**
|
|
3
|
+
* 1 for ASCII chars that are encoded.
|
|
4
|
+
*/
|
|
5
|
+
const encodedAsciiFlags = new Uint8Array(128);
|
|
6
|
+
for (const char of '"&\'<>') {
|
|
7
|
+
encodedAsciiFlags[char.charCodeAt(0)] = 1;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* References for U+0080 to U+07FF, formatted in advance, since text in Latin, Greek, Cyrillic, Hebrew or Arabic script
|
|
11
|
+
* reuses a few dozen of them.
|
|
12
|
+
*/
|
|
13
|
+
const numericReferences = [];
|
|
14
|
+
for (let codePoint = 0x80; codePoint < 0x800; ++codePoint) {
|
|
15
|
+
numericReferences.push('&#x' + codePoint.toString(16) + ';');
|
|
16
|
+
}
|
|
17
|
+
const hexDigitCharCodes = Uint8Array.from('0123456789abcdef', char => char.charCodeAt(0));
|
|
18
|
+
/**
|
|
19
|
+
* Same as {@linkcode escapeXML}, but also encodes non-ASCII chars as numeric references, so the output is ASCII and
|
|
20
|
+
* safe for any document encoding. A lone surrogate is encoded as `�`.
|
|
21
|
+
*/
|
|
2
22
|
export function encodeXML(input) {
|
|
23
|
+
const inputLength = input.length;
|
|
3
24
|
let output = '';
|
|
4
25
|
let textIndex = 0;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
26
|
+
let index = 0;
|
|
27
|
+
while (index < inputLength) {
|
|
28
|
+
const charCode = input.charCodeAt(index);
|
|
29
|
+
if (!isEncodedCharCode(charCode)) {
|
|
30
|
+
// Encoded chars are often close to each other, so a short run of plain chars is read inline
|
|
31
|
+
const scanEndIndex = Math.min(index + 24, inputLength);
|
|
32
|
+
do {
|
|
33
|
+
++index;
|
|
34
|
+
} while (index < scanEndIndex && !isEncodedCharCode(input.charCodeAt(index)));
|
|
35
|
+
if (index < scanEndIndex) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (index === inputLength) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
// In a longer run, the regexp finds the next encoded char faster than reading chars one by one
|
|
42
|
+
encodedCharRegExp.lastIndex = index;
|
|
43
|
+
if (!encodedCharRegExp.test(input)) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
index = encodedCharRegExp.lastIndex - 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
9
49
|
let entity;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
50
|
+
let nextIndex = index + 1;
|
|
51
|
+
if (charCode >= 0xd800 && charCode <= 0xdfff) {
|
|
52
|
+
const codePoint = input.codePointAt(index);
|
|
53
|
+
if (codePoint > 0xffff) {
|
|
54
|
+
entity = getNumericReference(codePoint);
|
|
55
|
+
nextIndex = index + 2;
|
|
16
56
|
}
|
|
17
|
-
|
|
57
|
+
else {
|
|
58
|
+
// A lone surrogate isn't a Unicode scalar value, so HTML parsers, decodeHTML and decodeXML decode a reference
|
|
59
|
+
// to it as U+FFFD anyway
|
|
60
|
+
entity = '�';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (charCode >= 0x80) {
|
|
64
|
+
entity = getNumericReference(charCode);
|
|
18
65
|
}
|
|
19
|
-
else if (charCode === 34) {
|
|
66
|
+
else if (charCode === /* " */ 34) {
|
|
20
67
|
entity = '"';
|
|
21
68
|
}
|
|
22
|
-
else if (charCode === 38) {
|
|
69
|
+
else if (charCode === /* & */ 38) {
|
|
23
70
|
entity = '&';
|
|
24
71
|
}
|
|
25
|
-
else if (charCode === 39) {
|
|
72
|
+
else if (charCode === /* ' */ 39) {
|
|
26
73
|
entity = ''';
|
|
27
74
|
}
|
|
28
|
-
else if (charCode === 60) {
|
|
75
|
+
else if (charCode === /* < */ 60) {
|
|
29
76
|
entity = '<';
|
|
30
77
|
}
|
|
31
78
|
else {
|
|
32
79
|
entity = '>';
|
|
33
80
|
}
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
if (textIndex !== index) {
|
|
82
|
+
output += input.slice(textIndex, index);
|
|
83
|
+
}
|
|
84
|
+
output += entity;
|
|
85
|
+
textIndex = nextIndex;
|
|
86
|
+
index = nextIndex;
|
|
87
|
+
}
|
|
88
|
+
return textIndex === 0 ? input : textIndex === inputLength ? output : output + input.slice(textIndex);
|
|
89
|
+
}
|
|
90
|
+
function isEncodedCharCode(charCode) {
|
|
91
|
+
return charCode >= 0x80 || encodedAsciiFlags[charCode] === 1;
|
|
92
|
+
}
|
|
93
|
+
// The code point must be at least 0x80
|
|
94
|
+
function getNumericReference(codePoint) {
|
|
95
|
+
return codePoint < 0x800 ? numericReferences[codePoint - 0x80] : formatNumericReference(codePoint);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Formats a reference like "😀". Building it from char codes in one call is about twice as fast as toString(16)
|
|
99
|
+
* and concatenation. The code point must be at least 0x100.
|
|
100
|
+
*/
|
|
101
|
+
function formatNumericReference(codePoint) {
|
|
102
|
+
if (codePoint < 0x1000) {
|
|
103
|
+
return String.fromCharCode(
|
|
104
|
+
/* & */ 38,
|
|
105
|
+
/* # */ 35,
|
|
106
|
+
/* x */ 120, getHexDigitCharCode(codePoint, 8), getHexDigitCharCode(codePoint, 4), getHexDigitCharCode(codePoint, 0),
|
|
107
|
+
/* ; */ 59);
|
|
108
|
+
}
|
|
109
|
+
if (codePoint < 0x10000) {
|
|
110
|
+
return String.fromCharCode(
|
|
111
|
+
/* & */ 38,
|
|
112
|
+
/* # */ 35,
|
|
113
|
+
/* x */ 120, getHexDigitCharCode(codePoint, 12), getHexDigitCharCode(codePoint, 8), getHexDigitCharCode(codePoint, 4), getHexDigitCharCode(codePoint, 0),
|
|
114
|
+
/* ; */ 59);
|
|
36
115
|
}
|
|
37
|
-
|
|
116
|
+
if (codePoint < 0x100000) {
|
|
117
|
+
return String.fromCharCode(
|
|
118
|
+
/* & */ 38,
|
|
119
|
+
/* # */ 35,
|
|
120
|
+
/* x */ 120, getHexDigitCharCode(codePoint, 16), getHexDigitCharCode(codePoint, 12), getHexDigitCharCode(codePoint, 8), getHexDigitCharCode(codePoint, 4), getHexDigitCharCode(codePoint, 0),
|
|
121
|
+
/* ; */ 59);
|
|
122
|
+
}
|
|
123
|
+
return String.fromCharCode(
|
|
124
|
+
/* & */ 38,
|
|
125
|
+
/* # */ 35,
|
|
126
|
+
/* x */ 120, getHexDigitCharCode(codePoint, 20), getHexDigitCharCode(codePoint, 16), getHexDigitCharCode(codePoint, 12), getHexDigitCharCode(codePoint, 8), getHexDigitCharCode(codePoint, 4), getHexDigitCharCode(codePoint, 0),
|
|
127
|
+
/* ; */ 59);
|
|
128
|
+
}
|
|
129
|
+
// The char code of the hex digit at the given bit offset
|
|
130
|
+
function getHexDigitCharCode(codePoint, offset) {
|
|
131
|
+
return hexDigitCharCodes[(codePoint >> offset) & 15];
|
|
38
132
|
}
|