@jarenjs/core 0.9.2 → 0.34.2
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/ARCHITECTURE.md +229 -26
- package/README.md +64 -10
- package/dist/types/array.d.ts +13 -0
- package/dist/types/cache.d.ts +166 -0
- package/dist/types/chunk.d.ts +102 -0
- package/dist/types/color.d.ts +64 -0
- package/dist/types/convert/convert.d.ts +33 -0
- package/dist/types/convert/currency.d.ts +47 -0
- package/dist/types/convert/index.d.ts +9 -0
- package/dist/types/convert/registry.d.ts +55 -0
- package/dist/types/dates/civil.d.ts +146 -0
- package/dist/types/dates/duration.d.ts +83 -0
- package/dist/types/dates/format.d.ts +59 -0
- package/dist/types/dates/index.d.ts +4 -0
- package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
- package/dist/types/errors.d.ts +77 -0
- package/dist/types/finance/amortization.d.ts +51 -0
- package/dist/types/finance/bond.d.ts +45 -0
- package/dist/types/finance/cashflow.d.ts +44 -0
- package/dist/types/finance/depreciation.d.ts +26 -0
- package/dist/types/finance/index.d.ts +16 -0
- package/dist/types/finance/indicators.d.ts +76 -0
- package/dist/types/finance/interest.d.ts +44 -0
- package/dist/types/finance/returns.d.ts +43 -0
- package/dist/types/finance/tvm.d.ts +50 -0
- package/dist/types/geo/angle.d.ts +4 -0
- package/dist/types/geo/bbox.d.ts +32 -0
- package/dist/types/geo/distance.d.ts +67 -0
- package/dist/types/geo/geohash.d.ts +63 -0
- package/dist/types/geo/geojson.d.ts +108 -0
- package/dist/types/geo/index-tree.d.ts +52 -0
- package/dist/types/geo/index.d.ts +11 -0
- package/dist/types/geo/mercator.d.ts +57 -0
- package/dist/types/geo/predicates.d.ts +44 -0
- package/dist/types/geo/ring.d.ts +80 -0
- package/dist/types/geo/simplify.d.ts +31 -0
- package/dist/types/geo/valid.d.ts +24 -0
- package/dist/types/geo/wkt.d.ts +16 -0
- package/dist/types/math/float64.d.ts +126 -3
- package/dist/types/math/format.d.ts +58 -0
- package/dist/types/math/index.d.ts +5 -0
- package/dist/types/math/mat4.d.ts +60 -0
- package/dist/types/math/project.d.ts +54 -0
- package/dist/types/math/solve.d.ts +67 -0
- package/dist/types/math/word.d.ts +82 -0
- package/dist/types/message.d.ts +52 -0
- package/dist/types/object.d.ts +151 -0
- package/dist/types/scan.d.ts +30 -0
- package/dist/types/schema.d.ts +24 -0
- package/dist/types/string.d.ts +96 -0
- package/dist/types/text/base64.d.ts +6 -4
- package/dist/types/text/email.d.ts +0 -1
- package/dist/types/text/host.d.ts +39 -8
- package/dist/types/text/i18n.d.ts +13 -1
- package/dist/types/text/index.d.ts +1 -0
- package/dist/types/text/misc.d.ts +15 -1
- package/dist/types/text/punycode.d.ts +45 -85
- package/docs/CONVERT.md +45 -0
- package/docs/DATES.md +78 -0
- package/docs/FINANCE.md +59 -0
- package/docs/GEO.md +114 -0
- package/docs/MATH.md +75 -0
- package/package.json +57 -4
- package/src/array.js +16 -0
- package/src/cache.js +206 -0
- package/src/chunk.js +159 -0
- package/src/color.js +125 -0
- package/src/convert/convert.js +59 -0
- package/src/convert/currency.js +74 -0
- package/src/convert/index.js +11 -0
- package/src/convert/registry.js +213 -0
- package/src/dates/civil.js +360 -0
- package/src/dates/duration.js +225 -0
- package/src/dates/format.js +238 -0
- package/src/dates/index.js +30 -0
- package/src/dates/rfc3339.js +621 -0
- package/src/errors.js +102 -0
- package/src/finance/amortization.js +50 -0
- package/src/finance/bond.js +100 -0
- package/src/finance/cashflow.js +122 -0
- package/src/finance/depreciation.js +54 -0
- package/src/finance/index.js +27 -0
- package/src/finance/indicators.js +207 -0
- package/src/finance/interest.js +63 -0
- package/src/finance/returns.js +90 -0
- package/src/finance/tvm.js +94 -0
- package/src/function.js +0 -2
- package/src/geo/angle.js +12 -0
- package/src/geo/bbox.js +80 -0
- package/src/geo/distance.js +155 -0
- package/src/geo/geohash.js +224 -0
- package/src/geo/geojson.js +361 -0
- package/src/geo/index-tree.js +294 -0
- package/src/geo/index.js +52 -0
- package/src/geo/mercator.js +124 -0
- package/src/geo/predicates.js +313 -0
- package/src/geo/ring.js +182 -0
- package/src/geo/simplify.js +130 -0
- package/src/geo/valid.js +142 -0
- package/src/geo/wkt.js +262 -0
- package/src/math/float64.js +228 -11
- package/src/math/format.js +157 -0
- package/src/math/index.js +5 -0
- package/src/math/mat4.js +131 -0
- package/src/math/project.js +49 -0
- package/src/math/solve.js +112 -0
- package/src/math/word.js +177 -0
- package/src/message.js +119 -0
- package/src/object.js +329 -0
- package/src/scan.js +42 -0
- package/src/schema.js +37 -0
- package/src/string.js +189 -0
- package/src/text/base64.js +15 -52
- package/src/text/email.js +0 -5
- package/src/text/host.js +515 -140
- package/src/text/i18n.js +13 -6
- package/src/text/identifiers.js +1 -1
- package/src/text/index.js +1 -0
- package/src/text/misc.js +81 -5
- package/src/text/punycode.js +235 -323
- package/src/dates.js +0 -371
package/src/text/host.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
domainToASCII,
|
|
4
|
+
punycodeEncode,
|
|
5
|
+
punycodeDecode,
|
|
5
6
|
} from './punycode.js';
|
|
6
7
|
|
|
8
|
+
import {
|
|
9
|
+
toCodePoints,
|
|
10
|
+
} from '../string.js';
|
|
11
|
+
|
|
7
12
|
import {
|
|
8
13
|
checkContextualRules,
|
|
9
14
|
checkDigitMixing,
|
|
@@ -38,39 +43,52 @@ export function isValidHostname(str) {
|
|
|
38
43
|
return str.length <= 255 && CONST_REGEXP_HOSTNAME.test(str);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// If decode succeeds but returns the same string, it's invalid
|
|
66
|
-
// (e.g., "xn--X" can't be properly decoded)
|
|
67
|
-
if (decoded === label && punycodePart.length < 2) {
|
|
68
|
-
return false;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the ACE (Punycode) form of a hostname is well formed: one or
|
|
48
|
+
* more labels of 1 to 63 letters, digits and hyphens, with no hyphen at
|
|
49
|
+
* either end of a label. The `xn--` prefix needs no special case - it is
|
|
50
|
+
* letters and hyphens like any other label body.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} ace - A hostname already in its ACE form
|
|
53
|
+
* @returns {boolean} True when every label is well formed
|
|
54
|
+
*/
|
|
55
|
+
function isValidAceForm(ace) {
|
|
56
|
+
const end = ace.length;
|
|
57
|
+
if (end === 0) return false;
|
|
58
|
+
let start = 0;
|
|
59
|
+
for (let i = 0; i <= end; i++) {
|
|
60
|
+
if (i < end && ace.charCodeAt(i) !== 0x2E) continue; // '.'
|
|
61
|
+
const len = i - start;
|
|
62
|
+
if (len === 0 || len > 63) return false;
|
|
63
|
+
if (ace.charCodeAt(start) === 0x2D || ace.charCodeAt(i - 1) === 0x2D) return false; // '-'
|
|
64
|
+
for (let k = start; k < i; k++) {
|
|
65
|
+
const c = ace.charCodeAt(k);
|
|
66
|
+
if ((c < 0x61 || c > 0x7A) // a-z
|
|
67
|
+
&& (c < 0x41 || c > 0x5A) // A-Z
|
|
68
|
+
&& (c < 0x30 || c > 0x39) // 0-9
|
|
69
|
+
&& c !== 0x2D) return false;
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
-
} catch (e) {
|
|
72
|
-
return false;
|
|
71
|
+
start = i + 1;
|
|
73
72
|
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The encoded body of an ACE (xn--) label: alphanumerics and hyphens only
|
|
77
|
+
const CONST_REGEXP_ACE_BODY = /^[a-zA-Z0-9-]+$/;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether the label carries the ACE prefix, in any case. Compared by
|
|
81
|
+
* character code because this runs per label and a `toLowerCase()` would
|
|
82
|
+
* allocate a copy of every one of them just to read four characters.
|
|
83
|
+
* @param {string} label - A hostname label
|
|
84
|
+
* @returns {boolean} True when the label starts with `xn--`
|
|
85
|
+
*/
|
|
86
|
+
function startsWithAcePrefix(label) {
|
|
87
|
+
return label.length >= 4
|
|
88
|
+
&& (label.charCodeAt(0) | 0x20) === 0x78 // 'x'
|
|
89
|
+
&& (label.charCodeAt(1) | 0x20) === 0x6E // 'n'
|
|
90
|
+
&& label.charCodeAt(2) === 0x2D // '-'
|
|
91
|
+
&& label.charCodeAt(3) === 0x2D;
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
export function isValidIdnHostname(str) {
|
|
@@ -106,13 +124,10 @@ export function isValidIdnHostname(str) {
|
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
//
|
|
127
|
+
// An ASCII-only hostname without an ACE prefix has no U-label to
|
|
128
|
+
// decode and no label to encode, so it is already its own ACE form.
|
|
110
129
|
if (isAsciiOnly && !hasAcePrefix) {
|
|
111
|
-
|
|
112
|
-
if (!str.includes('.')) {
|
|
113
|
-
return CONST_REGEXP_ACEHOSTNAME_SINGLE.test(str);
|
|
114
|
-
}
|
|
115
|
-
return CONST_REGEXP_ACEHOSTNAME.test(str);
|
|
130
|
+
return isValidAceForm(str);
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
// Split into labels
|
|
@@ -123,7 +138,27 @@ export function isValidIdnHostname(str) {
|
|
|
123
138
|
return false;
|
|
124
139
|
}
|
|
125
140
|
|
|
126
|
-
|
|
141
|
+
// U+3002, U+FF0E and U+FF61 are IDNA label separators, and "@" splits
|
|
142
|
+
// off a local part: a domain-wide conversion cuts labels at all four
|
|
143
|
+
// where the split on "." above does not. Absent them, the labels
|
|
144
|
+
// walked here ARE the labels that conversion would see, so the ACE
|
|
145
|
+
// form comes out of this same walk - one encode per U-label, and none
|
|
146
|
+
// of the second split, per-label regex test and intermediate array
|
|
147
|
+
// that converting the whole string again costs. The lookups are worth
|
|
148
|
+
// their own pass only on this branch, so they stay off the ASCII one.
|
|
149
|
+
const buildAce = !isAsciiOnly
|
|
150
|
+
&& str.indexOf('。') < 0
|
|
151
|
+
&& str.indexOf('.') < 0
|
|
152
|
+
&& str.indexOf('。') < 0
|
|
153
|
+
&& str.indexOf('@') < 0;
|
|
154
|
+
let punycode = isAsciiOnly ? str : '';
|
|
155
|
+
|
|
156
|
+
for (let li = 0; li < labels.length; li++) {
|
|
157
|
+
const label = labels[li];
|
|
158
|
+
if (buildAce && li > 0) {
|
|
159
|
+
punycode += '.';
|
|
160
|
+
}
|
|
161
|
+
|
|
127
162
|
// Empty label check (trailing dot)
|
|
128
163
|
if (label.length === 0) {
|
|
129
164
|
continue;
|
|
@@ -135,28 +170,33 @@ export function isValidIdnHostname(str) {
|
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
// Check for ACE prefix (xn--)
|
|
138
|
-
const isAce = label
|
|
173
|
+
const isAce = startsWithAcePrefix(label);
|
|
139
174
|
|
|
140
175
|
// For ACE labels, decode first then validate the decoded form
|
|
141
176
|
let decodedLabel = label;
|
|
142
177
|
if (isAce) {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
const rest = label.slice(4);
|
|
146
|
-
if (rest.length < 1) {
|
|
178
|
+
// ACE labels must have at least one character after xn--
|
|
179
|
+
if (label.length < 5) {
|
|
147
180
|
return false; // xn-- with nothing after
|
|
148
181
|
}
|
|
149
182
|
|
|
150
|
-
//
|
|
151
|
-
if (!
|
|
183
|
+
// The encoded part allows only alphanumerics and hyphens
|
|
184
|
+
if (!CONST_REGEXP_ACE_BODY.test(label.slice(4))) {
|
|
152
185
|
return false;
|
|
153
186
|
}
|
|
154
187
|
|
|
155
|
-
// Decode the
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
// Decode once; the decoded form drives the remaining label rules.
|
|
189
|
+
// The body is pure LDH (checked above), so decoding the label
|
|
190
|
+
// directly equals toUnicode's domain-wise mapping without its
|
|
191
|
+
// split/join passes. Punycode only recognizes an all-lowercase
|
|
192
|
+
// 'xn--' prefix; other casings keep the label undecoded and the
|
|
193
|
+
// '--' rule below rejects them.
|
|
194
|
+
if (label.startsWith('xn--')) {
|
|
195
|
+
try {
|
|
196
|
+
decodedLabel = punycodeDecode(label.slice(4).toLowerCase());
|
|
197
|
+
} catch (_e) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
160
200
|
}
|
|
161
201
|
}
|
|
162
202
|
|
|
@@ -168,7 +208,7 @@ export function isValidIdnHostname(str) {
|
|
|
168
208
|
// Raw ACE labels (xn--) legitimately have "--" at 2-3
|
|
169
209
|
// But if an ACE label decodes to something with "--" at 2-3, it's invalid
|
|
170
210
|
// because the decoded form shouldn't look like an ACE label
|
|
171
|
-
const decodedStartsWithACE = decodedLabel
|
|
211
|
+
const decodedStartsWithACE = startsWithAcePrefix(decodedLabel);
|
|
172
212
|
if (!decodedStartsWithACE) {
|
|
173
213
|
// Non-ACE form with "--" at 2-3 is invalid
|
|
174
214
|
return false;
|
|
@@ -181,7 +221,14 @@ export function isValidIdnHostname(str) {
|
|
|
181
221
|
}
|
|
182
222
|
|
|
183
223
|
// Get the Unicode code points from the decoded label
|
|
184
|
-
const codes =
|
|
224
|
+
const codes = toCodePoints(decodedLabel);
|
|
225
|
+
let labelNonAscii = false;
|
|
226
|
+
for (let i = 0; i < codes.length; i++) {
|
|
227
|
+
if (codes[i] > 127) {
|
|
228
|
+
labelNonAscii = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
185
232
|
|
|
186
233
|
// Check first character doesn't start with combining mark
|
|
187
234
|
if (isCombiningMark(codes[0])) {
|
|
@@ -206,7 +253,7 @@ export function isValidIdnHostname(str) {
|
|
|
206
253
|
}
|
|
207
254
|
|
|
208
255
|
// Check contextual rules
|
|
209
|
-
if (!checkContextualRules(
|
|
256
|
+
if (!checkContextualRules(codes)) {
|
|
210
257
|
return false;
|
|
211
258
|
}
|
|
212
259
|
|
|
@@ -214,122 +261,450 @@ export function isValidIdnHostname(str) {
|
|
|
214
261
|
if (!checkDigitMixing(codes)) {
|
|
215
262
|
return false;
|
|
216
263
|
}
|
|
217
|
-
}
|
|
218
264
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
265
|
+
if (buildAce) {
|
|
266
|
+
// An ACE label is already ASCII and stays as written; a U-label
|
|
267
|
+
// becomes its Punycode. An overflowing label cannot be encoded, so
|
|
268
|
+
// it is not a hostname.
|
|
269
|
+
if (isAce || !labelNonAscii) {
|
|
270
|
+
punycode += label;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
try {
|
|
274
|
+
punycode += 'xn--' + punycodeEncode(label);
|
|
275
|
+
}
|
|
276
|
+
catch (_e) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
226
281
|
}
|
|
227
282
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
283
|
+
if (!isAsciiOnly && !buildAce) {
|
|
284
|
+
// A separator the label split did not see gives the ACE form a
|
|
285
|
+
// different label structure than the walk above, so only the
|
|
286
|
+
// domain-wide conversion gets it right.
|
|
287
|
+
try {
|
|
288
|
+
punycode = domainToASCII(str);
|
|
289
|
+
}
|
|
290
|
+
catch (_e) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
231
293
|
}
|
|
232
294
|
|
|
233
|
-
return
|
|
295
|
+
return isValidAceForm(punycode);
|
|
234
296
|
}
|
|
235
297
|
//#endregion
|
|
236
298
|
|
|
237
|
-
//#region
|
|
238
|
-
//
|
|
239
|
-
const CONST_REGEXP_URL = /^https?:\/\/[^\s]+$/i;
|
|
240
|
-
export function isValidUrl(str) {
|
|
241
|
-
return CONST_REGEXP_URL.test(str);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// For the source: https://gist.github.com/dperini/729294
|
|
245
|
-
// For test cases: https://mathiasbynens.be/demo/url-regex
|
|
246
|
-
// @todo Delete current URL in favour of the commented out URL rule when this issue is fixed https://github.com/eslint/eslint/issues/7983.
|
|
247
|
-
// URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu;
|
|
299
|
+
//#region URI Template Tests
|
|
300
|
+
// uri-template: https://tools.ietf.org/html/rfc6570
|
|
248
301
|
// eslint-disable-next-line no-control-regex
|
|
249
|
-
const
|
|
250
|
-
export function
|
|
251
|
-
return
|
|
302
|
+
const CONST_REGEXP_URITEMPLATE = /^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i;
|
|
303
|
+
export function isValidUriTemplate(str) {
|
|
304
|
+
return CONST_REGEXP_URITEMPLATE.test(str);
|
|
252
305
|
}
|
|
253
306
|
//#endregion
|
|
254
307
|
|
|
255
|
-
//#region URI Tests
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
308
|
+
//#region URI and IRI Tests
|
|
309
|
+
// RFC 3986 URIs and RFC 3987 IRIs, scanned by character code. They are
|
|
310
|
+
// one grammar: an IRI is a URI with `iunreserved` widening `unreserved`
|
|
311
|
+
// by the `ucschar` ranges and `iprivate` admitted in the query. So the
|
|
312
|
+
// scanners below are shared, each shaped like the ABNF production it is
|
|
313
|
+
// named after, and each returns the index it stopped at - never a slice,
|
|
314
|
+
// so a whole identifier is decided without allocating.
|
|
315
|
+
|
|
316
|
+
/** Which non-ASCII characters a production admits. */
|
|
317
|
+
const UCS_NONE = 0; // RFC 3986: none, the grammar is ASCII
|
|
318
|
+
const UCS_CHAR = 1; // RFC 3987 iunreserved: ucschar
|
|
319
|
+
const UCS_PRIVATE = 2; // RFC 3987 iquery: ucschar and iprivate
|
|
320
|
+
|
|
321
|
+
// ASCII character classes of the RFC 3986/3987 core rules, one bit each.
|
|
322
|
+
const CH_UNRESERVED = 1; // ALPHA / DIGIT / "-" / "." / "_" / "~"
|
|
323
|
+
const CH_SUBDELIM = 2; // "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "="
|
|
324
|
+
const CH_HEXDIG = 4;
|
|
325
|
+
const CH_ALPHA = 8;
|
|
326
|
+
const CH_DIGIT = 16;
|
|
327
|
+
const CH_SCHEME = 32; // ALPHA / DIGIT / "+" / "-" / "."
|
|
328
|
+
// The four run productions differ only in whether ":" and "@" are in the
|
|
329
|
+
// set. Giving each its own bit costs a byte per character and takes the
|
|
330
|
+
// difference out of the scanning loop, where it would otherwise be two
|
|
331
|
+
// comparisons on every character of every IRI.
|
|
332
|
+
const CH_PCHAR = 64; // iunreserved / sub-delims / ":" / "@"
|
|
333
|
+
const CH_USERINFO = 128; // ipchar without "@"
|
|
334
|
+
const CH_REGNAME = 256; // ipchar without ":" or "@"
|
|
335
|
+
const CH_SEGNC = 512; // ipchar without ":" - isegment-nz-nc
|
|
336
|
+
const CH_QUERY = 1024; // ipchar / "/" / "?" - also the ifragment set
|
|
337
|
+
|
|
338
|
+
const IRI_ASCII = new Uint16Array(128);
|
|
339
|
+
for (let c = 0; c < 128; c++) {
|
|
340
|
+
const alpha = (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A);
|
|
341
|
+
const digit = c >= 0x30 && c <= 0x39;
|
|
342
|
+
let f = 0;
|
|
343
|
+
if (alpha) f |= CH_ALPHA;
|
|
344
|
+
if (digit) f |= CH_DIGIT;
|
|
345
|
+
// - . _ ~
|
|
346
|
+
if (alpha || digit || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E) f |= CH_UNRESERVED;
|
|
347
|
+
if (digit || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66)) f |= CH_HEXDIG;
|
|
348
|
+
// + - .
|
|
349
|
+
if (alpha || digit || c === 0x2B || c === 0x2D || c === 0x2E) f |= CH_SCHEME;
|
|
350
|
+
// ! $ then the contiguous & ' ( ) * + , then ; =
|
|
351
|
+
if (c === 0x21 || c === 0x24 || (c >= 0x26 && c <= 0x2C) || c === 0x3B || c === 0x3D) f |= CH_SUBDELIM;
|
|
352
|
+
const shared = (f & (CH_UNRESERVED | CH_SUBDELIM)) !== 0;
|
|
353
|
+
if (shared) f |= CH_REGNAME;
|
|
354
|
+
if (shared || c === 0x3A) f |= CH_USERINFO; // ':'
|
|
355
|
+
if (shared || c === 0x40) f |= CH_SEGNC; // '@'
|
|
356
|
+
if (shared || c === 0x3A || c === 0x40) f |= CH_PCHAR;
|
|
357
|
+
if ((f & CH_PCHAR) || c === 0x2F || c === 0x3F) f |= CH_QUERY; // '/' '?'
|
|
358
|
+
IRI_ASCII[c] = f;
|
|
265
359
|
}
|
|
266
360
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Whether the code point is an RFC 3987 `ucschar` - the non-ASCII half of
|
|
363
|
+
* `iunreserved`. Private-use areas are deliberately absent: they are
|
|
364
|
+
* `iprivate`, which only the query admits.
|
|
365
|
+
* @param {number} cp - A Unicode code point
|
|
366
|
+
* @returns {boolean} True for a ucschar
|
|
367
|
+
*/
|
|
368
|
+
function isUcsChar(cp) {
|
|
369
|
+
if (cp < 0xA0) return false;
|
|
370
|
+
if (cp <= 0xD7FF) return true;
|
|
371
|
+
if (cp < 0xF900) return false;
|
|
372
|
+
if (cp <= 0xFDCF) return true;
|
|
373
|
+
if (cp < 0xFDF0) return false;
|
|
374
|
+
if (cp <= 0xFFFD) return true;
|
|
375
|
+
if (cp < 0x10000) return false;
|
|
376
|
+
const plane = cp >>> 16;
|
|
377
|
+
if (plane > 0x0E) return false;
|
|
378
|
+
const low = cp & 0xFFFF;
|
|
379
|
+
// The supplementary ranges run to xFFFD in every plane, and plane 14
|
|
380
|
+
// starts at xE1000 rather than at its plane boundary.
|
|
381
|
+
return low <= 0xFFFD && (plane !== 0x0E || low >= 0x1000);
|
|
273
382
|
}
|
|
274
383
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Whether the code point is an RFC 3987 `iprivate`.
|
|
386
|
+
* @param {number} cp - A Unicode code point
|
|
387
|
+
* @returns {boolean} True for an iprivate code point
|
|
388
|
+
*/
|
|
389
|
+
function isIPrivate(cp) {
|
|
390
|
+
return (cp >= 0xE000 && cp <= 0xF8FF)
|
|
391
|
+
|| (cp >= 0xF0000 && cp <= 0xFFFFD)
|
|
392
|
+
|| (cp >= 0x100000 && cp <= 0x10FFFD);
|
|
278
393
|
}
|
|
279
394
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
395
|
+
/**
|
|
396
|
+
* The code point at `i`, or -1 for an unpaired surrogate. An unpaired
|
|
397
|
+
* surrogate is not a character, so no production can accept it.
|
|
398
|
+
* @param {string} str - The string being scanned
|
|
399
|
+
* @param {number} i - The index to read
|
|
400
|
+
* @param {number} end - One past the last readable index
|
|
401
|
+
* @returns {number} The code point, or -1
|
|
402
|
+
*/
|
|
403
|
+
function codePointAtStrict(str, i, end) {
|
|
404
|
+
const c = str.charCodeAt(i);
|
|
405
|
+
if (c < 0xD800 || c > 0xDFFF) return c;
|
|
406
|
+
if (c > 0xDBFF || i + 1 >= end) return -1;
|
|
407
|
+
const lo = str.charCodeAt(i + 1);
|
|
408
|
+
if (lo < 0xDC00 || lo > 0xDFFF) return -1;
|
|
409
|
+
return ((c - 0xD800) << 10) + (lo - 0xDC00) + 0x10000;
|
|
283
410
|
}
|
|
284
411
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Consume the longest run of characters admitted by an IRI production,
|
|
414
|
+
* plus percent-encoded triplets and the non-ASCII ranges. Every run
|
|
415
|
+
* production is a CH_* class, so one scanner covers them all.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} str - The string being scanned
|
|
418
|
+
* @param {number} i - Where to start
|
|
419
|
+
* @param {number} end - One past the last readable index
|
|
420
|
+
* @param {number} mask - The CH_* class the production admits
|
|
421
|
+
* @param {number} unicode - Which UCS_* set of non-ASCII characters the
|
|
422
|
+
* production admits
|
|
423
|
+
* @returns {number} The index one past the run
|
|
424
|
+
*/
|
|
425
|
+
function scanRun(str, i, end, mask, unicode) {
|
|
426
|
+
while (i < end) {
|
|
427
|
+
const c = str.charCodeAt(i);
|
|
428
|
+
if (c < 0x80) {
|
|
429
|
+
if (IRI_ASCII[c] & mask) {
|
|
430
|
+
i++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (c !== 0x25) return i; // '%' - pct-encoded = "%" HEXDIG HEXDIG
|
|
434
|
+
if (i + 2 >= end) return i;
|
|
435
|
+
const h1 = str.charCodeAt(i + 1);
|
|
436
|
+
const h2 = str.charCodeAt(i + 2);
|
|
437
|
+
if (h1 > 0x7F || h2 > 0x7F) return i;
|
|
438
|
+
if (!(IRI_ASCII[h1] & CH_HEXDIG) || !(IRI_ASCII[h2] & CH_HEXDIG)) return i;
|
|
439
|
+
i += 3;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (unicode === UCS_NONE) return i; // RFC 3986 is ASCII throughout
|
|
443
|
+
// One contiguous range holds the bulk of ucschar; only the tail of
|
|
444
|
+
// the BMP and the supplementary planes need a decoded code point.
|
|
445
|
+
if (c >= 0xA0 && c <= 0xD7FF) {
|
|
446
|
+
i++;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const cp = codePointAtStrict(str, i, end);
|
|
450
|
+
if (cp < 0) return i;
|
|
451
|
+
if (!isUcsChar(cp) && !(unicode === UCS_PRIVATE && isIPrivate(cp))) return i;
|
|
452
|
+
i += cp > 0xFFFF ? 2 : 1;
|
|
453
|
+
}
|
|
454
|
+
return i;
|
|
290
455
|
}
|
|
291
|
-
//#endregion
|
|
292
456
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
457
|
+
/**
|
|
458
|
+
* scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"
|
|
459
|
+
* @param {string} str - The string being scanned
|
|
460
|
+
* @param {number} end - One past the last readable index
|
|
461
|
+
* @returns {number} The index one past the ":", or -1 when absent
|
|
462
|
+
*/
|
|
463
|
+
function scanScheme(str, end) {
|
|
464
|
+
if (end === 0) return -1;
|
|
465
|
+
const c = str.charCodeAt(0);
|
|
466
|
+
if (c > 0x7F || !(IRI_ASCII[c] & CH_ALPHA)) return -1;
|
|
467
|
+
let i = 1;
|
|
468
|
+
while (i < end) {
|
|
469
|
+
const d = str.charCodeAt(i);
|
|
470
|
+
if (d > 0x7F || !(IRI_ASCII[d] & CH_SCHEME)) break;
|
|
471
|
+
i++;
|
|
472
|
+
}
|
|
473
|
+
return i < end && str.charCodeAt(i) === 0x3A ? i + 1 : -1; // ':'
|
|
474
|
+
}
|
|
298
475
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
476
|
+
/**
|
|
477
|
+
* IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
|
|
478
|
+
* @param {string} inner - The text between the IP-literal brackets
|
|
479
|
+
* @returns {boolean} True for a well-formed IPvFuture
|
|
480
|
+
*/
|
|
481
|
+
function isIPvFuture(inner) {
|
|
482
|
+
const n = inner.length;
|
|
483
|
+
let i = 1; // the caller matched the leading "v"
|
|
484
|
+
const start = i;
|
|
485
|
+
while (i < n) {
|
|
486
|
+
const c = inner.charCodeAt(i);
|
|
487
|
+
if (c > 0x7F || !(IRI_ASCII[c] & CH_HEXDIG)) break;
|
|
488
|
+
i++;
|
|
489
|
+
}
|
|
490
|
+
if (i === start || i >= n || inner.charCodeAt(i) !== 0x2E) return false; // '.'
|
|
491
|
+
if (++i >= n) return false;
|
|
492
|
+
for (; i < n; i++) {
|
|
493
|
+
const c = inner.charCodeAt(i);
|
|
494
|
+
if (c > 0x7F) return false;
|
|
495
|
+
if (!(IRI_ASCII[c] & (CH_UNRESERVED | CH_SUBDELIM)) && c !== 0x3A) return false;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
305
499
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
500
|
+
/**
|
|
501
|
+
* IP-literal = "[" ( IPv6address / IPvFuture ) "]"
|
|
502
|
+
* @param {string} str - The string being scanned
|
|
503
|
+
* @param {number} i - The index of the "["
|
|
504
|
+
* @param {number} end - One past the last readable index
|
|
505
|
+
* @returns {number} The index one past the "]", or -1
|
|
506
|
+
*/
|
|
507
|
+
function scanIPLiteral(str, i, end) {
|
|
508
|
+
let j = i + 1;
|
|
509
|
+
while (j < end && str.charCodeAt(j) !== 0x5D) j++; // ']'
|
|
510
|
+
if (j >= end || j === i + 1) return -1;
|
|
511
|
+
// The bracketed forms are rare enough that reusing the IPv6 tester -
|
|
512
|
+
// the one the ipv6 format already answers with - beats a second
|
|
513
|
+
// address grammar that would have to be kept in step with it.
|
|
514
|
+
const inner = str.slice(i + 1, j);
|
|
515
|
+
const c = inner.charCodeAt(0);
|
|
516
|
+
const ok = (c === 0x76 || c === 0x56) ? isIPvFuture(inner) : isValidIPv6(inner); // 'v' 'V'
|
|
517
|
+
return ok ? j + 1 : -1;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* ihost = IP-literal / IPv4address / ireg-name. Every IPv4address is
|
|
522
|
+
* also an ireg-name, so the alternation needs no separate dotted-quad
|
|
523
|
+
* pass: the grammar admits "999.999.999.999" as a registered name.
|
|
524
|
+
* @param {string} str - The string being scanned
|
|
525
|
+
* @param {number} i - Where the host starts
|
|
526
|
+
* @param {number} end - One past the last readable index
|
|
527
|
+
* @returns {number} The index one past the host, or -1
|
|
528
|
+
*/
|
|
529
|
+
function scanHost(str, i, end, unicode) {
|
|
530
|
+
if (i < end && str.charCodeAt(i) === 0x5B) return scanIPLiteral(str, i, end); // '['
|
|
531
|
+
return scanRun(str, i, end, CH_REGNAME, unicode);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* iauthority = [ iuserinfo "@" ] ihost [ ":" port ]
|
|
536
|
+
* @param {string} str - The string being scanned
|
|
537
|
+
* @param {number} i - Where the authority starts (just past "//")
|
|
538
|
+
* @param {number} end - One past the last readable index
|
|
539
|
+
* @returns {number} The index one past the authority, or -1
|
|
540
|
+
*/
|
|
541
|
+
function scanAuthority(str, i, end, unicode) {
|
|
542
|
+
// An IP-literal host cannot be preceded by a bracket-free userinfo run,
|
|
543
|
+
// so take it before anything else.
|
|
544
|
+
if (i < end && str.charCodeAt(i) === 0x5B) { // '['
|
|
545
|
+
const afterHost = scanIPLiteral(str, i, end);
|
|
546
|
+
return afterHost < 0 ? -1 : scanPort(str, afterHost, end);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// iuserinfo admits ":" but not "@", so one run of the wider set either
|
|
550
|
+
// stops at the "@" that ends a userinfo, or has already covered the
|
|
551
|
+
// host and its port. Deciding afterwards is what keeps the common
|
|
552
|
+
// authority - which has no userinfo - down to a single pass.
|
|
553
|
+
const stop = scanRun(str, i, end, CH_USERINFO, unicode);
|
|
554
|
+
if (stop < end && str.charCodeAt(stop) === 0x40) {
|
|
555
|
+
const afterHost = scanHost(str, stop + 1, end, unicode);
|
|
556
|
+
return afterHost < 0 ? -1 : scanPort(str, afterHost, end);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// No userinfo: an ireg-name admits no colon, so the first colon in the
|
|
560
|
+
// run opens the port and everything after it must be a digit.
|
|
561
|
+
for (let k = i; k < stop; k++) {
|
|
562
|
+
if (str.charCodeAt(k) === 0x3A) {
|
|
563
|
+
return scanPort(str, k, end) === stop ? stop : -1;
|
|
317
564
|
}
|
|
318
565
|
}
|
|
566
|
+
return stop;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* [ ":" port ], where port = *DIGIT.
|
|
571
|
+
* @param {string} str - The string being scanned
|
|
572
|
+
* @param {number} i - The index just past the host
|
|
573
|
+
* @param {number} end - One past the last readable index
|
|
574
|
+
* @returns {number} The index one past the port
|
|
575
|
+
*/
|
|
576
|
+
function scanPort(str, i, end) {
|
|
577
|
+
if (i >= end || str.charCodeAt(i) !== 0x3A) return i; // ':'
|
|
578
|
+
i++;
|
|
579
|
+
while (i < end) {
|
|
580
|
+
const c = str.charCodeAt(i);
|
|
581
|
+
if (c > 0x7F || !(IRI_ASCII[c] & CH_DIGIT)) break;
|
|
582
|
+
i++;
|
|
583
|
+
}
|
|
584
|
+
return i;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* ipath-abempty = *( "/" isegment ). Also covers ipath-absolute, whose
|
|
589
|
+
* only extra restriction - no leading "//" - the caller has already
|
|
590
|
+
* settled by taking that case as an authority.
|
|
591
|
+
* @param {string} str - The string being scanned
|
|
592
|
+
* @param {number} i - Where the path starts
|
|
593
|
+
* @param {number} end - One past the last readable index
|
|
594
|
+
* @returns {number} The index one past the path
|
|
595
|
+
*/
|
|
596
|
+
function scanPathAbempty(str, i, end, unicode) {
|
|
597
|
+
while (i < end && str.charCodeAt(i) === 0x2F) { // '/'
|
|
598
|
+
i = scanRun(str, i + 1, end, CH_PCHAR, unicode);
|
|
599
|
+
}
|
|
600
|
+
return i;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Scan a whole URI, IRI or reference of either.
|
|
605
|
+
* @param {string} str - The candidate
|
|
606
|
+
* @param {boolean} allowRelative - Whether a missing scheme is allowed
|
|
607
|
+
* @param {number} unicode - UCS_NONE for RFC 3986, UCS_CHAR for RFC 3987
|
|
608
|
+
* @returns {boolean} True when the whole string is the production
|
|
609
|
+
*/
|
|
610
|
+
function scanIdentifier(str, allowRelative, unicode) {
|
|
611
|
+
const end = str.length;
|
|
612
|
+
let i = 0;
|
|
613
|
+
|
|
614
|
+
const afterScheme = scanScheme(str, end);
|
|
615
|
+
const hasScheme = afterScheme >= 0;
|
|
616
|
+
if (hasScheme) i = afterScheme;
|
|
617
|
+
else if (!allowRelative) return false;
|
|
618
|
+
|
|
619
|
+
if (i + 1 < end && str.charCodeAt(i) === 0x2F && str.charCodeAt(i + 1) === 0x2F) {
|
|
620
|
+
const a = scanAuthority(str, i + 2, end, unicode);
|
|
621
|
+
if (a < 0) return false;
|
|
622
|
+
i = scanPathAbempty(str, a, end, unicode);
|
|
623
|
+
}
|
|
624
|
+
else if (i < end && str.charCodeAt(i) === 0x2F) {
|
|
625
|
+
i = scanPathAbempty(str, i, end, unicode); // ipath-absolute
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
// ipath-rootless with a scheme, ipath-noscheme without: a scheme-less
|
|
629
|
+
// reference may not put a colon in its first segment, or the segment
|
|
630
|
+
// would read as a scheme.
|
|
631
|
+
const seg = scanRun(str, i, end, hasScheme ? CH_PCHAR : CH_SEGNC, unicode);
|
|
632
|
+
i = seg === i ? i : scanPathAbempty(str, seg, end, unicode);
|
|
633
|
+
}
|
|
319
634
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
635
|
+
if (i < end && str.charCodeAt(i) === 0x3F) { // '?'
|
|
636
|
+
// Only the query admits iprivate, and only in an IRI.
|
|
637
|
+
i = scanRun(str, i + 1, end, CH_QUERY, unicode === UCS_CHAR ? UCS_PRIVATE : unicode);
|
|
638
|
+
}
|
|
639
|
+
if (i < end && str.charCodeAt(i) === 0x23) { // '#'
|
|
640
|
+
i = scanRun(str, i + 1, end, CH_QUERY, unicode);
|
|
326
641
|
}
|
|
642
|
+
return i === end;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Validate an absolute URI (RFC 3986 section 3) - a scheme followed by a
|
|
647
|
+
* hierarchical part, with an optional query and fragment.
|
|
648
|
+
* @param {string} str - The candidate URI
|
|
649
|
+
* @returns {boolean} True for a well-formed URI
|
|
650
|
+
*/
|
|
651
|
+
export function isValidUri(str) {
|
|
652
|
+
return scanIdentifier(str, false, UCS_NONE);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Validate a URI reference (RFC 3986 section 4.1) - a URI or a relative
|
|
657
|
+
* reference resolved against a base.
|
|
658
|
+
* @param {string} str - The candidate URI reference
|
|
659
|
+
* @returns {boolean} True for a well-formed URI reference
|
|
660
|
+
*/
|
|
661
|
+
export function isValidUriRef(str) {
|
|
662
|
+
return scanIdentifier(str, true, UCS_NONE);
|
|
663
|
+
}
|
|
327
664
|
|
|
328
|
-
|
|
329
|
-
|
|
665
|
+
/**
|
|
666
|
+
* Validate an absolute IRI (RFC 3987 section 2.2) - a URI whose
|
|
667
|
+
* unreserved characters extend into the `ucschar` ranges.
|
|
668
|
+
* @param {string} str - The candidate IRI
|
|
669
|
+
* @returns {boolean} True for a well-formed IRI
|
|
670
|
+
*/
|
|
671
|
+
export function isValidIRI(str) {
|
|
672
|
+
return scanIdentifier(str, false, UCS_CHAR);
|
|
330
673
|
}
|
|
331
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Validate an IRI reference (RFC 3987 section 2.2) - an IRI or a
|
|
677
|
+
* relative reference resolved against a base.
|
|
678
|
+
* @param {string} str - The candidate IRI reference
|
|
679
|
+
* @returns {boolean} True for a well-formed IRI reference
|
|
680
|
+
*/
|
|
332
681
|
export function isValidIRIRef(str) {
|
|
333
|
-
return
|
|
682
|
+
return scanIdentifier(str, true, UCS_CHAR);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Validate a URL: a URI carrying one of the web schemes and an
|
|
687
|
+
* authority. The grammar is RFC 3986's - a URL is a narrower thing than
|
|
688
|
+
* a URI, not a looser one - so `http://x/a|b` is no more a URL than it
|
|
689
|
+
* is a URI, while `http://localhost:8080` and `http://127.0.0.1/` are
|
|
690
|
+
* both perfectly good ones.
|
|
691
|
+
*
|
|
692
|
+
* @param {string} str - The candidate URL
|
|
693
|
+
* @returns {boolean} True for a well-formed http or https URL
|
|
694
|
+
*/
|
|
695
|
+
export function isValidUrl(str) {
|
|
696
|
+
if ((str.charCodeAt(0) | 0x20) !== 0x68 // 'h'
|
|
697
|
+
|| (str.charCodeAt(1) | 0x20) !== 0x74 // 't'
|
|
698
|
+
|| (str.charCodeAt(2) | 0x20) !== 0x74 // 't'
|
|
699
|
+
|| (str.charCodeAt(3) | 0x20) !== 0x70) return false; // 'p'
|
|
700
|
+
let i = 4;
|
|
701
|
+
if ((str.charCodeAt(i) | 0x20) === 0x73) i++; // 's'
|
|
702
|
+
if (str.charCodeAt(i) !== 0x3A // ':'
|
|
703
|
+
|| str.charCodeAt(i + 1) !== 0x2F // '/'
|
|
704
|
+
|| str.charCodeAt(i + 2) !== 0x2F) return false;
|
|
705
|
+
// "http://" alone names no resource.
|
|
706
|
+
if (str.length <= i + 3) return false;
|
|
707
|
+
return scanIdentifier(str, false, UCS_NONE);
|
|
334
708
|
}
|
|
709
|
+
|
|
335
710
|
//#endregion
|