speedy-entities 2.0.0 → 3.0.1-next.2f4dd34

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) 2021 Savva Mikhalevski
3
+ Copyright (c) 2025 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,77 +1,71 @@
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
+ # Speedy Entities
2
2
 
3
- [The fastest](#performance) XML/HTML entity decoder that supports customizable named character references.
3
+ [The fastest](#performance) XML/HTML entity encoder/decoder in
4
+ [20 kB gzipped](https://bundlephobia.com/package/speedy-entities).
4
5
 
5
6
  ```shell
6
7
  npm install --save-prod speedy-entities
7
8
  ```
8
9
 
9
- # Usage
10
+ # Decode
10
11
 
11
- [API documentation is available here.](https://smikhalevski.github.io/speedy-entities/)
12
-
13
- ## Preconfigured decoders
14
-
15
- There are two preconfigured decoders: `decodeXml` and `decodeHtml`.
12
+ There are two preconfigured decoders: `decodeXML` and `decodeHTML`.
16
13
 
17
14
  ```ts
18
- import {decodeXml, decodeHtml} from 'speedy-entities';
15
+ import { decodeXML, decodeHTML } from 'speedy-entities';
19
16
 
20
- decodeXml('ab<'); // → "ab&lt"
17
+ decodeXML('ab&lt');
18
+ // ⮕ 'ab&lt'
21
19
 
22
- decodeHtml('&ltfoo&AElig'); // → "<foo\u00c6"
20
+ decodeHTML('&ltfoo&AElig');
21
+ // ⮕ '<foo\u00c6'
23
22
 
24
- decodeHtml('&NotNestedGreaterGreater;&CounterClockwiseContourIntegral;');
25
- // → "\u2aa2\u0338\u2233"
23
+ decodeHTML('&NotNestedGreaterGreater;&CounterClockwiseContourIntegral;');
24
+ // ⮕ '\u2aa2\u0338\u2233'
26
25
  ```
27
26
 
28
- You can add custom entities that `decodeXml` and `decodeHtml` would recognize:
29
-
30
- ```ts
31
- import {decodeXml, decodeHtml, xmlEntityManager, htmlEntityManager} from 'speedy-entities';
32
-
33
- xmlEntityManager.set('foo', 'okay');
34
- decodeXml('&foo;'); // → "okay"
27
+ ## Custom decoder
35
28
 
36
- htmlEntityManager.set('bar', 'nope');
37
- decodeHtml('&bar;'); // → "nope"
38
- ```
29
+ You can create a decoder that decodes custom named entities:
39
30
 
40
- ## Custom decoders
31
+ ```ts
32
+ import { createEntityDecoder } from 'speedy-entities';
41
33
 
42
- You can create a custom decoder that would recognize numeric and custom entities.
34
+ // Create a decoder
35
+ const decode = createEntityDecoder({
36
+ entities: {
37
+ 'foo;': 'okay',
38
+ 'qux;': 'yeah',
39
+ },
40
+ isNumericReferenceSemicolonRequired: true,
41
+ });
43
42
 
44
- ```ts
45
- import {createEntityDecoder, EntityManager} from 'speedy-entities';
43
+ // Decode entities
44
+ decode('&foo;');
45
+ // ⮕ 'okay'
46
46
 
47
- // Create an entity manager
48
- const entityManager = new EntityManager();
47
+ decode('&foo');
48
+ // ⮕ '&foo'
49
49
 
50
- // Register a new entity
51
- entityManager.set('foo', 'okay');
50
+ // Decode numeric character references
51
+ decode('&#X61;&#x62;&#x63;');
52
+ // ⮕ 'abc'
53
+ ```
52
54
 
53
- // Register a new legacy entity
54
- entityManager.set('bar', 'nope', true);
55
+ # Encode
55
56
 
56
- // Register a batch of entities
57
- entityManager.set({
58
- foo: 'okay',
59
- qux: 'yeah',
60
- });
57
+ `encodeXML` encodes non-ASCII characters as named XML entities or as numeric references.
61
58
 
62
- // Create a decoder
63
- const decode = createEntityDecoder(entityManager);
59
+ `escapeXML` escapes only `"&'<>` characters.
64
60
 
65
- // Decode non-legacy entities
66
- decode('&foo'); // → "&foo"
67
- decode('&foo;'); // → "okay"
61
+ ```ts
62
+ import { encodeXML, escapeXML } from 'speedy-entities';
68
63
 
69
- // Decode legacy entities
70
- decode('&bar'); // → "nope"
71
- decode('&bar;'); // → "nope"
64
+ encodeXML('&😘❤️');
65
+ // ⮕ '&amp;&#x1f618;&#x2764;&#xfe0f;'
72
66
 
73
- // Decode numeric character references
74
- decode('&#X61;&#x62;&#x63;'); // → "abc"
67
+ escapeXML('&😘❤️');
68
+ // ⮕ '&amp;😘❤'
75
69
  ```
76
70
 
77
71
  # Performance
@@ -80,13 +74,10 @@ Clone this repo and use `npm ci && npm run perf` to run the performance testsuit
80
74
 
81
75
  Results are in millions of operations per second. The higher number is better.
82
76
 
83
- | | speedy-entities <br/>`decodeXml` | [fb55/entities](https://github.com/fb55/entities) <br/>`decodeXML` | speedy-entities <br/>`decodeHtml` | [fb55/entities](https://github.com/fb55/entities) <br/>`decodeHTML` |
84
- |-------------------------------|---------------------------------:| ---: | ---: | ---: |
85
- | `"&#X61;&#x62;&#x63;"` | 3.9 | 2.5 | 4.3 | 2.5 |
86
- | `"&#X61&#x62&#x63"` | 5.1 | 2.7 | 4.3 | 2.4 |
87
- | `"&#97;&#98;&#99;"` | 4.5 | 2.4 | 4.5 | 2.4 |
88
- | `"&#97&#98&#99"` | 5.5 | 2.4 | 4.2 | 2.3 |
89
- | `"&amp;&lt;&gt;"` | 3.8 | 3.3 | 3.7 | 3.1 |
90
- | `"&amp&lt&gt"` | 3.6 | 3.4 | 3.6 | 3.3 |
91
- | `"&NotNestedGreaterGreater;"` | 5.3 | 4.6 | 3.9 | 3.2 |
92
- | `"&NotNestedGreaterGreater"` | 5.3 | 4.6 | 3.9 | 3.3 |
77
+ ### Decode
78
+
79
+ ![Decode HTML performance chart](./images/perf-decode-html.svg)
80
+
81
+ ### Encode
82
+
83
+ ![Encode XML performance chart](./images/perf-encode-xml.svg)
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The options recognized by {@linkcode createEntityDecoder}.
3
+ */
4
+ export interface EntityDecoderOptions {
5
+ /**
6
+ * Mapping from an [character entity reference](https://www.w3.org/TR/html4/charset.html#h-5.3.2) to its decoded value.
7
+ *
8
+ * Keys may optionally start with an ampersand "&" character. If an key ends with a semicolon ";" then
9
+ * a character entity reference must end with a semicolon as well in decoded strings.
10
+ *
11
+ * @example
12
+ * {
13
+ * '&Delta;': '\u0394',
14
+ * 'LongRightArrow;': '\u27F6'
15
+ * }
16
+ */
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;
26
+ }
27
+ /**
28
+ * Creates an entity decoder that rewrites numeric and named entities found in input to their respective values.
29
+ *
30
+ * @param options The decoder options.
31
+ * @returns A function that decodes entities in the string.
32
+ */
33
+ export declare function createEntityDecoder(options?: EntityDecoderOptions): (input: string) => string;
@@ -0,0 +1,170 @@
1
+ const fromCharCode = String.fromCharCode;
2
+ /**
3
+ * Creates an entity decoder that rewrites numeric and named entities found in input to their respective values.
4
+ *
5
+ * @param options The decoder options.
6
+ * @returns A function that decodes entities in the string.
7
+ */
8
+ export function createEntityDecoder(options = {}) {
9
+ const { entities, isNumericReferenceSemicolonRequired = false } = options;
10
+ let entitiesMap;
11
+ let maximumNamedReferenceLength = 0;
12
+ if (entities !== undefined) {
13
+ entitiesMap = new Map();
14
+ for (let key in entities) {
15
+ key = key.charCodeAt(0) === /* & */ 38 ? key.substring(1) : key;
16
+ maximumNamedReferenceLength = Math.max(maximumNamedReferenceLength, key.length);
17
+ entitiesMap.set(getHashCode(key), entities[key]);
18
+ }
19
+ }
20
+ return input => {
21
+ let output = '';
22
+ let textIndex = 0;
23
+ let charIndex = 0;
24
+ const inputLength = input.length;
25
+ while (charIndex < inputLength - 1) {
26
+ let startIndex = input.indexOf('&', charIndex);
27
+ if (startIndex === -1) {
28
+ break;
29
+ }
30
+ charIndex = startIndex++;
31
+ let value;
32
+ let endIndex = startIndex;
33
+ let charCode = 0;
34
+ if (startIndex < inputLength - 2 && input.charCodeAt(startIndex) === /* # */ 35) {
35
+ // Numeric character reference
36
+ let codePoint = 0;
37
+ if ((input.charCodeAt(++startIndex) | 32) === /* x */ 120) {
38
+ endIndex = ++startIndex;
39
+ // parseInt of a hexadecimal number
40
+ while (endIndex - startIndex < 6 && endIndex < inputLength) {
41
+ // Convert alpha to lower case
42
+ charCode = input.charCodeAt(endIndex) | 32;
43
+ if ((charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57) ||
44
+ (charCode >= /* a */ 97 && charCode <= /* f */ 102)) {
45
+ // Convert "0" → 0 and "f" → 15
46
+ codePoint = codePoint * 16 + charCode - (charCode & 112) + (charCode >> 6) * 9;
47
+ ++endIndex;
48
+ }
49
+ else {
50
+ break;
51
+ }
52
+ }
53
+ }
54
+ else {
55
+ endIndex = startIndex;
56
+ // parseInt of a decimal number
57
+ while (endIndex - startIndex < 6 && endIndex < inputLength) {
58
+ charCode = input.charCodeAt(endIndex) | 32;
59
+ if (charCode >= /* 0 */ 48 && charCode <= /* 9 */ 57) {
60
+ codePoint = codePoint * 10 + charCode - (charCode & 112);
61
+ ++endIndex;
62
+ }
63
+ else {
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ if (endIndex !== startIndex) {
69
+ // At least one digit must present
70
+ const isTerminated = endIndex < inputLength && input.charCodeAt(endIndex) === /* ; */ 59;
71
+ if (isTerminated || !isNumericReferenceSemicolonRequired) {
72
+ // Convert a code point to a string
73
+ // https://github.com/mathiasbynens/he/blob/master/src/he.js#L106-L134
74
+ if (codePoint === 0 || (codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {
75
+ // Null char code, or character reference is outside the permissible Unicode range
76
+ value = '\uFFFD';
77
+ }
78
+ else if (codePoint >= 128 &&
79
+ codePoint <= 195 &&
80
+ codePoint !== 129 &&
81
+ codePoint !== 141 &&
82
+ codePoint !== 143 &&
83
+ codePoint !== 144 &&
84
+ codePoint !== 157) {
85
+ // Overridden char code
86
+ value = entityOverrides[codePoint];
87
+ }
88
+ else if (codePoint > 0xffff) {
89
+ // Surrogate pair
90
+ codePoint -= 0x10000;
91
+ value = fromCharCode(((codePoint >>> 10) & 0x3ff) | 0xd800, 0xdc00 | (codePoint & 0x3ff));
92
+ }
93
+ else {
94
+ // Char code
95
+ value = fromCharCode(codePoint);
96
+ }
97
+ }
98
+ if (isTerminated) {
99
+ ++endIndex;
100
+ }
101
+ }
102
+ }
103
+ else if (entitiesMap !== undefined) {
104
+ // Named character reference
105
+ for (let index = startIndex, hashCode = 0, nextValue; index < inputLength; ++index) {
106
+ charCode = input.charCodeAt(index);
107
+ if (charCode !== /* ; */ 59 &&
108
+ (charCode < /* a */ 97 || charCode > /* z */ 122) &&
109
+ (charCode < /* A */ 65 || charCode > /* Z */ 90)) {
110
+ break;
111
+ }
112
+ hashCode = (hashCode << 5) - hashCode + charCode;
113
+ nextValue = entitiesMap.get(hashCode);
114
+ if (nextValue !== undefined) {
115
+ value = nextValue;
116
+ endIndex = index + 1;
117
+ }
118
+ if (charCode === /* ; */ 59 || index - startIndex >= maximumNamedReferenceLength) {
119
+ break;
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 getHashCode(str) {
165
+ let hashCode = 0;
166
+ for (let i = 0; i < str.length; ++i) {
167
+ hashCode = (hashCode << 5) - hashCode + str.charCodeAt(i);
168
+ }
169
+ return hashCode;
170
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Decodes all known HTML entities and numeric character references in the input.
3
+ */
4
+ export declare const decodeHTML: (input: string) => string;
package/decodeHTML.js ADDED
@@ -0,0 +1,9 @@
1
+ import { createEntityDecoder } from './createEntityDecoder.js';
2
+ import htmlEntities from './html-entities.js';
3
+ /**
4
+ * Decodes all known HTML entities and numeric character references in the input.
5
+ */
6
+ export const decodeHTML = createEntityDecoder({
7
+ entities: htmlEntities,
8
+ isNumericReferenceSemicolonRequired: false,
9
+ });
package/decodeXML.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Decodes XML entities and numeric character references in the input.
3
+ */
4
+ export declare const decodeXML: (input: string) => string;
package/decodeXML.js ADDED
@@ -0,0 +1,9 @@
1
+ import { createEntityDecoder } from './createEntityDecoder.js';
2
+ import xmlEntities from './xml-entities.js';
3
+ /**
4
+ * Decodes XML entities and numeric character references in the input.
5
+ */
6
+ export const decodeXML = createEntityDecoder({
7
+ entities: xmlEntities,
8
+ isNumericReferenceSemicolonRequired: true,
9
+ });
package/encodeXML.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function encodeXML(input: string): string;
package/encodeXML.js ADDED
@@ -0,0 +1,38 @@
1
+ const re = /["&'<>\u0080-\uffff]/g;
2
+ export function encodeXML(input) {
3
+ let output = '';
4
+ let textIndex = 0;
5
+ while (re.test(input)) {
6
+ let lastIndex = re.lastIndex;
7
+ const startIndex = lastIndex - 1;
8
+ const charCode = input.charCodeAt(startIndex);
9
+ let entity;
10
+ if (charCode >= 0x80 && charCode <= 0xffff) {
11
+ let codePoint = charCode;
12
+ if ((charCode & 0xfc00) === 0xd800) {
13
+ // Surrogate pair
14
+ codePoint = charCode * 0x400 + input.charCodeAt(lastIndex) - 0x35fdc00;
15
+ re.lastIndex = ++lastIndex;
16
+ }
17
+ entity = '&#x' + codePoint.toString(16) + ';';
18
+ }
19
+ else if (charCode === 34) {
20
+ entity = '&quot;';
21
+ }
22
+ else if (charCode === 38) {
23
+ entity = '&amp;';
24
+ }
25
+ else if (charCode === 39) {
26
+ entity = '&apos;';
27
+ }
28
+ else if (charCode === 60) {
29
+ entity = '&lt;';
30
+ }
31
+ else {
32
+ entity = '&gt;';
33
+ }
34
+ output += textIndex === startIndex ? entity : input.slice(textIndex, startIndex) + entity;
35
+ textIndex = lastIndex;
36
+ }
37
+ return textIndex === 0 ? input : textIndex === input.length ? output : output + input.slice(textIndex);
38
+ }
package/escapeXML.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function escapeXML(input: string): string;
package/escapeXML.js ADDED
@@ -0,0 +1,29 @@
1
+ const re = /["&'<>]/g;
2
+ export function escapeXML(input) {
3
+ let output = '';
4
+ let textIndex = 0;
5
+ while (re.test(input)) {
6
+ const lastIndex = re.lastIndex;
7
+ const startIndex = lastIndex - 1;
8
+ const charCode = input.charCodeAt(startIndex);
9
+ let entity;
10
+ if (charCode === 34) {
11
+ entity = '&quot;';
12
+ }
13
+ else if (charCode === 38) {
14
+ entity = '&amp;';
15
+ }
16
+ else if (charCode === 39) {
17
+ entity = '&apos;';
18
+ }
19
+ else if (charCode === 60) {
20
+ entity = '&lt;';
21
+ }
22
+ else {
23
+ entity = '&gt;';
24
+ }
25
+ output += textIndex === startIndex ? entity : input.slice(textIndex, startIndex) + entity;
26
+ textIndex = lastIndex;
27
+ }
28
+ return textIndex === 0 ? input : textIndex === input.length ? output : output + input.slice(textIndex);
29
+ }