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 +1 -1
- package/README.md +49 -58
- package/createEntityDecoder.d.ts +33 -0
- package/createEntityDecoder.js +170 -0
- package/decodeHTML.d.ts +4 -0
- package/decodeHTML.js +9 -0
- package/decodeXML.d.ts +4 -0
- package/decodeXML.js +9 -0
- package/encodeXML.d.ts +1 -0
- package/encodeXML.js +38 -0
- package/escapeXML.d.ts +1 -0
- package/escapeXML.js +29 -0
- package/html-entities.d.ts +2234 -0
- package/html-entities.js +2233 -0
- package/index.d.ts +5 -0
- package/index.js +5 -0
- package/package.json +11 -37
- package/xml-entities.d.ts +8 -0
- package/xml-entities.js +7 -0
- package/lib/EntityManager.d.ts +0 -39
- package/lib/EntityManager.js +0 -49
- package/lib/createEntityDecoder.d.ts +0 -29
- package/lib/createEntityDecoder.js +0 -104
- package/lib/data/illegal-code-points.json +0 -129
- package/lib/decodeHtml.d.ts +0 -8
- package/lib/decodeHtml.js +0 -14
- package/lib/decodeXml.d.ts +0 -10
- package/lib/decodeXml.js +0 -18
- package/lib/fromCodePoint.d.ts +0 -5
- package/lib/fromCodePoint.js +0 -34
- package/lib/gen/entities.json +0 -1
- package/lib/gen/legacy-entities.json +0 -1
- package/lib/gen/overrides.json +0 -1
- package/lib/index-cjs.js +0 -388
- package/lib/index.d.ts +0 -4
- package/lib/index.js +0 -4
- package/lib/misc.d.ts +0 -2
- package/lib/misc.js +0 -4
- package/lib/unpackMap.d.ts +0 -4
- package/lib/unpackMap.js +0 -22
package/LICENSE.txt
CHANGED
package/README.md
CHANGED
|
@@ -1,77 +1,71 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Speedy Entities
|
|
2
2
|
|
|
3
|
-
[The fastest](#performance) XML/HTML entity decoder
|
|
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
|
-
#
|
|
10
|
+
# Decode
|
|
10
11
|
|
|
11
|
-
|
|
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 {
|
|
15
|
+
import { decodeXML, decodeHTML } from 'speedy-entities';
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
decodeXML('ab<');
|
|
18
|
+
// ⮕ 'ab<'
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
decodeHTML('<fooÆ');
|
|
21
|
+
// ⮕ '<foo\u00c6'
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
//
|
|
23
|
+
decodeHTML('⪢̸∳');
|
|
24
|
+
// ⮕ '\u2aa2\u0338\u2233'
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
decodeHtml('&bar;'); // → "nope"
|
|
38
|
-
```
|
|
29
|
+
You can create a decoder that decodes custom named entities:
|
|
39
30
|
|
|
40
|
-
|
|
31
|
+
```ts
|
|
32
|
+
import { createEntityDecoder } from 'speedy-entities';
|
|
41
33
|
|
|
42
|
-
|
|
34
|
+
// Create a decoder
|
|
35
|
+
const decode = createEntityDecoder({
|
|
36
|
+
entities: {
|
|
37
|
+
'foo;': 'okay',
|
|
38
|
+
'qux;': 'yeah',
|
|
39
|
+
},
|
|
40
|
+
isNumericReferenceSemicolonRequired: true,
|
|
41
|
+
});
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// Decode entities
|
|
44
|
+
decode('&foo;');
|
|
45
|
+
// ⮕ 'okay'
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
decode('&foo');
|
|
48
|
+
// ⮕ '&foo'
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// Decode numeric character references
|
|
51
|
+
decode('abc');
|
|
52
|
+
// ⮕ 'abc'
|
|
53
|
+
```
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
entityManager.set('bar', 'nope', true);
|
|
55
|
+
# Encode
|
|
55
56
|
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
const decode = createEntityDecoder(entityManager);
|
|
59
|
+
`escapeXML` escapes only `"&'<>` characters.
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
decode('&foo;'); // → "okay"
|
|
61
|
+
```ts
|
|
62
|
+
import { encodeXML, escapeXML } from 'speedy-entities';
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
decode('&bar;'); // → "nope"
|
|
64
|
+
encodeXML('&😘❤️');
|
|
65
|
+
// ⮕ '&😘❤️'
|
|
72
66
|
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
escapeXML('&😘❤️');
|
|
68
|
+
// ⮕ '&😘❤'
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
| `"&<>"` | 3.6 | 3.4 | 3.6 | 3.3 |
|
|
91
|
-
| `"⪢̸"` | 5.3 | 4.6 | 3.9 | 3.2 |
|
|
92
|
-
| `"&NotNestedGreaterGreater"` | 5.3 | 4.6 | 3.9 | 3.3 |
|
|
77
|
+
### Decode
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
### Encode
|
|
82
|
+
|
|
83
|
+

|
|
@@ -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
|
+
* 'Δ': '\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
|
+
}
|
package/decodeHTML.d.ts
ADDED
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
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 = '"';
|
|
21
|
+
}
|
|
22
|
+
else if (charCode === 38) {
|
|
23
|
+
entity = '&';
|
|
24
|
+
}
|
|
25
|
+
else if (charCode === 39) {
|
|
26
|
+
entity = ''';
|
|
27
|
+
}
|
|
28
|
+
else if (charCode === 60) {
|
|
29
|
+
entity = '<';
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
entity = '>';
|
|
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 = '"';
|
|
12
|
+
}
|
|
13
|
+
else if (charCode === 38) {
|
|
14
|
+
entity = '&';
|
|
15
|
+
}
|
|
16
|
+
else if (charCode === 39) {
|
|
17
|
+
entity = ''';
|
|
18
|
+
}
|
|
19
|
+
else if (charCode === 60) {
|
|
20
|
+
entity = '<';
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
entity = '>';
|
|
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
|
+
}
|