@jarenjs/core 0.9.2 → 0.34.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.
Files changed (121) hide show
  1. package/ARCHITECTURE.md +229 -26
  2. package/README.md +64 -10
  3. package/dist/types/array.d.ts +13 -0
  4. package/dist/types/cache.d.ts +166 -0
  5. package/dist/types/chunk.d.ts +102 -0
  6. package/dist/types/color.d.ts +64 -0
  7. package/dist/types/convert/convert.d.ts +33 -0
  8. package/dist/types/convert/currency.d.ts +47 -0
  9. package/dist/types/convert/index.d.ts +9 -0
  10. package/dist/types/convert/registry.d.ts +55 -0
  11. package/dist/types/dates/civil.d.ts +146 -0
  12. package/dist/types/dates/duration.d.ts +83 -0
  13. package/dist/types/dates/format.d.ts +59 -0
  14. package/dist/types/dates/index.d.ts +4 -0
  15. package/dist/types/{dates.d.ts → dates/rfc3339.d.ts} +57 -3
  16. package/dist/types/errors.d.ts +77 -0
  17. package/dist/types/finance/amortization.d.ts +51 -0
  18. package/dist/types/finance/bond.d.ts +45 -0
  19. package/dist/types/finance/cashflow.d.ts +44 -0
  20. package/dist/types/finance/depreciation.d.ts +26 -0
  21. package/dist/types/finance/index.d.ts +16 -0
  22. package/dist/types/finance/indicators.d.ts +76 -0
  23. package/dist/types/finance/interest.d.ts +44 -0
  24. package/dist/types/finance/returns.d.ts +43 -0
  25. package/dist/types/finance/tvm.d.ts +50 -0
  26. package/dist/types/geo/angle.d.ts +4 -0
  27. package/dist/types/geo/bbox.d.ts +32 -0
  28. package/dist/types/geo/distance.d.ts +67 -0
  29. package/dist/types/geo/geohash.d.ts +63 -0
  30. package/dist/types/geo/geojson.d.ts +108 -0
  31. package/dist/types/geo/index-tree.d.ts +52 -0
  32. package/dist/types/geo/index.d.ts +11 -0
  33. package/dist/types/geo/mercator.d.ts +57 -0
  34. package/dist/types/geo/predicates.d.ts +44 -0
  35. package/dist/types/geo/ring.d.ts +80 -0
  36. package/dist/types/geo/simplify.d.ts +31 -0
  37. package/dist/types/geo/valid.d.ts +24 -0
  38. package/dist/types/geo/wkt.d.ts +16 -0
  39. package/dist/types/math/float64.d.ts +126 -3
  40. package/dist/types/math/format.d.ts +58 -0
  41. package/dist/types/math/index.d.ts +5 -0
  42. package/dist/types/math/mat4.d.ts +60 -0
  43. package/dist/types/math/project.d.ts +54 -0
  44. package/dist/types/math/solve.d.ts +67 -0
  45. package/dist/types/math/word.d.ts +82 -0
  46. package/dist/types/message.d.ts +52 -0
  47. package/dist/types/object.d.ts +151 -0
  48. package/dist/types/scan.d.ts +30 -0
  49. package/dist/types/schema.d.ts +24 -0
  50. package/dist/types/string.d.ts +96 -0
  51. package/dist/types/text/base64.d.ts +6 -4
  52. package/dist/types/text/email.d.ts +0 -1
  53. package/dist/types/text/host.d.ts +39 -8
  54. package/dist/types/text/i18n.d.ts +13 -1
  55. package/dist/types/text/index.d.ts +1 -0
  56. package/dist/types/text/misc.d.ts +15 -1
  57. package/dist/types/text/punycode.d.ts +45 -85
  58. package/docs/CONVERT.md +45 -0
  59. package/docs/DATES.md +78 -0
  60. package/docs/FINANCE.md +59 -0
  61. package/docs/GEO.md +114 -0
  62. package/docs/MATH.md +75 -0
  63. package/package.json +57 -4
  64. package/src/array.js +16 -0
  65. package/src/cache.js +206 -0
  66. package/src/chunk.js +159 -0
  67. package/src/color.js +125 -0
  68. package/src/convert/convert.js +59 -0
  69. package/src/convert/currency.js +74 -0
  70. package/src/convert/index.js +11 -0
  71. package/src/convert/registry.js +213 -0
  72. package/src/dates/civil.js +360 -0
  73. package/src/dates/duration.js +225 -0
  74. package/src/dates/format.js +238 -0
  75. package/src/dates/index.js +30 -0
  76. package/src/dates/rfc3339.js +621 -0
  77. package/src/errors.js +102 -0
  78. package/src/finance/amortization.js +50 -0
  79. package/src/finance/bond.js +100 -0
  80. package/src/finance/cashflow.js +122 -0
  81. package/src/finance/depreciation.js +54 -0
  82. package/src/finance/index.js +27 -0
  83. package/src/finance/indicators.js +207 -0
  84. package/src/finance/interest.js +63 -0
  85. package/src/finance/returns.js +90 -0
  86. package/src/finance/tvm.js +94 -0
  87. package/src/function.js +0 -2
  88. package/src/geo/angle.js +12 -0
  89. package/src/geo/bbox.js +80 -0
  90. package/src/geo/distance.js +155 -0
  91. package/src/geo/geohash.js +224 -0
  92. package/src/geo/geojson.js +361 -0
  93. package/src/geo/index-tree.js +294 -0
  94. package/src/geo/index.js +52 -0
  95. package/src/geo/mercator.js +124 -0
  96. package/src/geo/predicates.js +313 -0
  97. package/src/geo/ring.js +182 -0
  98. package/src/geo/simplify.js +130 -0
  99. package/src/geo/valid.js +142 -0
  100. package/src/geo/wkt.js +262 -0
  101. package/src/math/float64.js +228 -11
  102. package/src/math/format.js +157 -0
  103. package/src/math/index.js +5 -0
  104. package/src/math/mat4.js +131 -0
  105. package/src/math/project.js +49 -0
  106. package/src/math/solve.js +112 -0
  107. package/src/math/word.js +177 -0
  108. package/src/message.js +119 -0
  109. package/src/object.js +329 -0
  110. package/src/scan.js +42 -0
  111. package/src/schema.js +37 -0
  112. package/src/string.js +189 -0
  113. package/src/text/base64.js +15 -52
  114. package/src/text/email.js +0 -5
  115. package/src/text/host.js +515 -140
  116. package/src/text/i18n.js +13 -6
  117. package/src/text/identifiers.js +1 -1
  118. package/src/text/index.js +1 -0
  119. package/src/text/misc.js +81 -5
  120. package/src/text/punycode.js +235 -323
  121. package/src/dates.js +0 -371
package/src/text/host.js CHANGED
@@ -1,9 +1,14 @@
1
1
  //@ts-check
2
2
  import {
3
- toASCII,
4
- toUnicode,
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
- const CONST_REGEXP_ACEHOSTNAME = /^(?!-)(xn--)?[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.(?!-)(xn--)?([a-zA-Z0-9\-]{1,50}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,})$/;
42
- const CONST_REGEXP_ACEHOSTNAME_SINGLE = /^(?!-)(xn--)?[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
43
-
44
- // Check if label is valid ACE (Punycode)
45
- function isValidACE(label) {
46
- if (!label.toLowerCase().startsWith('xn--')) {
47
- return false;
48
- }
49
-
50
- // Check for valid Punycode format
51
- // ACE labels must have at least one character after xn--
52
- const punycodePart = label.slice(4);
53
- if (punycodePart.length === 0) {
54
- return false;
55
- }
56
-
57
- // ACE labels must contain only alphanumeric and hyphen
58
- if (!/^[a-zA-Z0-9-]+$/.test(punycodePart)) {
59
- return false;
60
- }
61
-
62
- // Try to decode - if it fails, it's invalid Punycode
63
- try {
64
- const decoded = toUnicode(label);
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
- return true;
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
- // For ASCII-only hostnames without ACE prefix, use simple hostname validation
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
- // Single label hostname
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
- for (const label of labels) {
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.toLowerCase().startsWith('xn--');
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
- // Check for "--" in 3rd and 4th position is always invalid in ACE
144
- // (xn-- is at positions 0-3, so check after that)
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
- // Validate the Punycode is decodable
151
- if (!isValidACE(label)) {
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 label for further validation
156
- try {
157
- decodedLabel = toUnicode(label);
158
- } catch (e) {
159
- return false;
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.toLowerCase().startsWith('xn--');
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 = Array.from(decodedLabel).map(c => c.codePointAt(0));
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(decodedLabel)) {
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
- // For ASCII-only hostnames, also validate with ACE regex
220
- let punycode;
221
- try {
222
- punycode = toASCII(str);
223
- } catch (e) {
224
- // If toASCII fails, the input is invalid
225
- return false;
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
- // Single label hostname
229
- if (labels.length === 1 || (labels.length === 2 && labels[1] === '')) {
230
- return CONST_REGEXP_ACEHOSTNAME_SINGLE.test(punycode);
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 CONST_REGEXP_ACEHOSTNAME.test(punycode);
295
+ return isValidAceForm(punycode);
234
296
  }
235
297
  //#endregion
236
298
 
237
- //#region URL Tests
238
- // Simple URL regex that requires a protocol (http:// or https://)
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 CONST_REGEXP_URL_FULL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i;
250
- export function isValidUrlFull(str) {
251
- return CONST_REGEXP_URL_FULL.test(str);
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
- const CONST_REGEXP_NOT_URI_FRAGMENT = /\/|:/;
257
- // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
258
- // RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
259
- // Note: No comma allowed in scheme
260
- const CONST_REGEXP_URI_FAST = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i;
261
- export function isValidUri(str) {
262
- // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
263
- return CONST_REGEXP_NOT_URI_FRAGMENT.test(str)
264
- && CONST_REGEXP_URI_FAST.test(str);
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
- // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js
268
- const CONST_REGEXP_URI_FULL = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
269
- export function isValidUriFull(str) {
270
- // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "."
271
- return CONST_REGEXP_NOT_URI_FRAGMENT.test(str)
272
- && CONST_REGEXP_URI_FULL.test(str);
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
- const CONST_REGEXP_URIREF_FAST = /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i;
276
- export function isValidUriRef(str) {
277
- return CONST_REGEXP_URIREF_FAST.test(str);
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
- const CONST_REGEXP_URIREF_FULL = /^(?:[a-z][a-z0-9+\-.]*:)?(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'"()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'"()*+,;=:@]|%[0-9a-f]{2})*)*)?(?:\?(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'"()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
281
- export function isValidUriRefFull(str) {
282
- return CONST_REGEXP_URIREF_FULL.test(str);
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
- // uri-template: https://tools.ietf.org/html/rfc6570
286
- // eslint-disable-next-line no-control-regex
287
- 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;
288
- export function isValidUriTemplate(str) {
289
- return CONST_REGEXP_URITEMPLATE.test(str);
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
- //#region IRI Tests
294
- const CONST_REGEXP_ABSOLUTE_IRI = /^([a-z]([a-z]|\d|\+|-|\.)*):(\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?((\[(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))\])|((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=])*)(:\d*)?)(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*|(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)|((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)|((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)){0})(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
295
- // Fast path regex for simple ASCII-only IRIs
296
- // Only matches common, unambiguous cases to avoid false positives
297
- const CONST_REGEXP_ASCII_IRI = /^(?:[a-z][a-z0-9+\-.]*:)\/\/[^\s:]+(:\d+)?(\/[^\s]*)?$/i;
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
- export function isValidIRI(str) {
300
- // Fast path: check if string is ASCII-only and doesn't contain complex patterns
301
- // If so, use a much simpler regex that's ~10x faster
302
- let isAsciiOnly = true;
303
- let hasSquareBracket = false;
304
- let colonCount = 0;
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
- for (let i = 0; i < str.length; i++) {
307
- const code = str.charCodeAt(i);
308
- if (code > 127) {
309
- isAsciiOnly = false;
310
- break;
311
- }
312
- if (code === 91 || code === 93) { // '[' or ']'
313
- hasSquareBracket = true;
314
- }
315
- if (code === 58) { // ':'
316
- colonCount++;
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
- // Use fast path only for simple ASCII-only IRIs:
321
- // - No Unicode characters
322
- // - No square brackets (IPv6 literals)
323
- // - At most 2 colons (for scheme and optional port, not IPv6)
324
- if (isAsciiOnly && !hasSquareBracket && colonCount <= 2) {
325
- return CONST_REGEXP_ASCII_IRI.test(str);
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
- // Fall back to full IRI regex for complex strings
329
- return CONST_REGEXP_ABSOLUTE_IRI.test(str);
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 isValidUriRef(str);
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