whatwg-url 17.0.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ whatwg-url is a full implementation of the WHATWG [URL Standard](https://url.spe
4
4
 
5
5
  ## Specification conformance
6
6
 
7
- whatwg-url is currently up to date with the URL spec up to commit [3c83874](https://github.com/whatwg/url/commit/3c83874ae1eac84ceb71d8c4344673251c982bbe) and the web platform tests up to commit [2a91c2e](https://github.com/web-platform-tests/wpt/commit/2a91c2e71952bdceef8b2825ce3a0a0dc73aa588).
7
+ whatwg-url is currently up to date with the URL spec up to commit [9dc3827](https://github.com/whatwg/url/commit/9dc3827fc722ac4af3f11061aa3e9adb44a17c8b).
8
8
 
9
9
  For `file:` URLs, whose [origin is left unspecified](https://url.spec.whatwg.org/#concept-url-origin), whatwg-url chooses to use a new opaque origin (which serializes to `"null"`).
10
10
 
@@ -19,6 +19,8 @@ The main API is provided by the [`URL`](https://url.spec.whatwg.org/#url-class)
19
19
  The following methods are exported for use by places like jsdom that need to implement things like [`HTMLHyperlinkElementUtils`](https://html.spec.whatwg.org/#htmlhyperlinkelementutils). They mostly operate on or return an "internal URL" or ["URL record"](https://url.spec.whatwg.org/#concept-url) type.
20
20
 
21
21
  - [URL parser](https://url.spec.whatwg.org/#concept-url-parser): `parseURL(input, { baseURL, encoding = "UTF-8" })`
22
+ - URL parser with [validation errors](https://url.spec.whatwg.org/#validation-error): `parseURLWithValidationErrors(input, { baseURL, encoding = "UTF-8" })`
23
+ - [Valid URL string](https://url.spec.whatwg.org/#valid-url-string) checker: `isValidURLString(input, { baseURL })`
22
24
  - [Basic URL parser](https://url.spec.whatwg.org/#concept-basic-url-parser): `basicURLParse(input, { baseURL, url, stateOverride, encoding = "UTF-8" })`
23
25
  - [URL serializer](https://url.spec.whatwg.org/#concept-url-serializer): `serializeURL(urlRecord, excludeFragment)`
24
26
  - [Host serializer](https://url.spec.whatwg.org/#concept-host-serializer): `serializeHost(hostFromURLRecord)`
@@ -71,6 +73,17 @@ These properties should be treated with care, as in general changing them will c
71
73
 
72
74
  The return value of "failure" in the spec is represented by `null`. That is, functions like `parseURL` and `basicURLParse` can return _either_ a URL record _or_ `null`.
73
75
 
76
+ The `parseURLWithValidationErrors` function returns an object with a `url` property and a `validationErrors` property. The `url` property is a URL record or `null`, and the `validationErrors` property is an array of validation error names reported while parsing.
77
+
78
+ ```js
79
+ const result = parseURLWithValidationErrors("https://example.org/%s");
80
+
81
+ console.log(result.url === null); // false
82
+ console.log(result.validationErrors); // [ "invalid-URL-unit" ]
83
+ ```
84
+
85
+ The `isValidURLString` function implements the grammar-based validity checker from the URL Standard's URL writing section. This is separate from parser validation errors, so it can disagree with `parseURLWithValidationErrors()` in some cases. Pass a `baseURL` URL record to check relative-URL strings.
86
+
74
87
  ### `whatwg-url/webidl2js-wrapper` module
75
88
 
76
89
  This module exports the `URL` and `URLSearchParams` [interface wrappers API](https://github.com/jsdom/webidl2js#for-interfaces) generated by [webidl2js](https://github.com/jsdom/webidl2js).
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { URL, URLSearchParams } = require("./webidl2js-wrapper");
4
4
  const urlStateMachine = require("./lib/url-state-machine");
5
5
  const percentEncoding = require("./lib/percent-encoding");
6
+ const urlStringValidator = require("./lib/url-string-validator");
6
7
 
7
8
  const sharedGlobalObject = { Array, Object, Promise, String, TypeError };
8
9
  URL.install(sharedGlobalObject, ["Window"]);
@@ -12,6 +13,8 @@ exports.URL = sharedGlobalObject.URL;
12
13
  exports.URLSearchParams = sharedGlobalObject.URLSearchParams;
13
14
 
14
15
  exports.parseURL = urlStateMachine.parseURL;
16
+ exports.parseURLWithValidationErrors = urlStateMachine.parseURLWithValidationErrors;
17
+ exports.isValidURLString = urlStringValidator.isValidURLString;
15
18
  exports.basicURLParse = urlStateMachine.basicURLParse;
16
19
  exports.serializeURL = urlStateMachine.serializeURL;
17
20
  exports.serializePath = urlStateMachine.serializePath;
package/lib/infra.js CHANGED
@@ -18,9 +18,19 @@ function isASCIIHex(c) {
18
18
  return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66);
19
19
  }
20
20
 
21
+ function isASCIIString(string) {
22
+ return !/[^\u0000-\u007F]/u.test(string);
23
+ }
24
+
25
+ function isNoncharacter(c) {
26
+ return (c >= 0xFDD0 && c <= 0xFDEF) || (c & 0xFFFE) === 0xFFFE;
27
+ }
28
+
21
29
  module.exports = {
22
30
  isASCIIDigit,
23
31
  isASCIIAlpha,
24
32
  isASCIIAlphanumeric,
25
- isASCIIHex
33
+ isASCIIHex,
34
+ isASCIIString,
35
+ isNoncharacter
26
36
  };
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+
3
+ const tr46 = require("tr46");
4
+
5
+ const infra = require("./infra");
6
+
7
+ function p(char) {
8
+ return char.codePointAt(0);
9
+ }
10
+
11
+ const failure = Symbol("failure");
12
+
13
+ const specialSchemes = {
14
+ ftp: 21,
15
+ file: null,
16
+ http: 80,
17
+ https: 443,
18
+ ws: 80,
19
+ wss: 443
20
+ };
21
+
22
+ const urlCodePoints = new Set([
23
+ p("!"), p("$"), p("&"), p("'"), p("("), p(")"), p("*"), p("+"), p(","), p("-"), p("."), p("/"),
24
+ p(":"), p(";"), p("="), p("?"), p("@"), p("_"), p("~")
25
+ ]);
26
+
27
+ const forbiddenHostCodePoints = new Set([
28
+ 0x00, 0x09, 0x0A, 0x0D, 0x20, p("#"), p("/"), p(":"), p("<"), p(">"), p("?"), p("@"), p("["),
29
+ p("\\"), p("]"), p("^"), p("|")
30
+ ]);
31
+
32
+ function isURLCodePoint(c) {
33
+ return infra.isASCIIAlphanumeric(c) ||
34
+ urlCodePoints.has(c) ||
35
+ (c >= 0xA0 && c <= 0x10FFFD && (c < 0xD800 || c > 0xDFFF) && !infra.isNoncharacter(c));
36
+ }
37
+
38
+ function isInvalidURLCodePoint(c) {
39
+ return !isURLCodePoint(c) && c !== p("%");
40
+ }
41
+
42
+ function isPercentEncodedByteAt(input, index) {
43
+ return index + 2 < input.length &&
44
+ input[index] === "%" &&
45
+ infra.isASCIIHex(input.charCodeAt(index + 1)) &&
46
+ infra.isASCIIHex(input.charCodeAt(index + 2));
47
+ }
48
+
49
+ function containsPercentEncodedByte(input) {
50
+ return /%[0-9A-Fa-f]{2}/u.test(input);
51
+ }
52
+
53
+ function containsForbiddenHostCodePoint(string) {
54
+ return [...string].some(c => forbiddenHostCodePoints.has(c.codePointAt(0)));
55
+ }
56
+
57
+ function containsForbiddenDomainCodePoint(string) {
58
+ return [...string].some(c => {
59
+ const cp = c.codePointAt(0);
60
+ return forbiddenHostCodePoints.has(cp) || (cp >= 0x00 && cp <= 0x1F) || cp === p("%") || cp === 0x7F;
61
+ });
62
+ }
63
+
64
+ function domainParserToASCII(domain, beStrict) {
65
+ return tr46.toASCII(domain, {
66
+ checkHyphens: beStrict,
67
+ checkBidi: true,
68
+ checkJoiners: true,
69
+ useSTD3ASCIIRules: beStrict,
70
+ transitionalProcessing: false,
71
+ verifyDNSLength: beStrict,
72
+ ignoreInvalidPunycode: false
73
+ });
74
+ }
75
+
76
+ function parseIPv4Number(input, validationErrors = null) {
77
+ if (input === "") {
78
+ return failure;
79
+ }
80
+
81
+ let validationErrorSeen = false;
82
+ let R = 10;
83
+
84
+ if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") {
85
+ validationErrorSeen = true;
86
+ input = input.substring(2);
87
+ R = 16;
88
+ } else if (input.length >= 2 && input.charAt(0) === "0") {
89
+ validationErrorSeen = true;
90
+ input = input.substring(1);
91
+ R = 8;
92
+ }
93
+
94
+ if (input === "") {
95
+ validationErrors?.push("IPv4-non-decimal-part");
96
+ return 0;
97
+ }
98
+
99
+ let regex = /[^0-7]/u;
100
+ if (R === 10) {
101
+ regex = /[^0-9]/u;
102
+ }
103
+ if (R === 16) {
104
+ regex = /[^0-9A-Fa-f]/u;
105
+ }
106
+
107
+ if (regex.test(input)) {
108
+ return failure;
109
+ }
110
+
111
+ if (validationErrorSeen) {
112
+ validationErrors?.push("IPv4-non-decimal-part");
113
+ }
114
+
115
+ return parseInt(input, R);
116
+ }
117
+
118
+ function endsInANumber(input) {
119
+ const parts = input.split(".");
120
+ if (parts[parts.length - 1] === "") {
121
+ if (parts.length === 1) {
122
+ return false;
123
+ }
124
+ parts.pop();
125
+ }
126
+
127
+ const last = parts[parts.length - 1];
128
+ if (/^[0-9]+$/u.test(last)) {
129
+ return true;
130
+ }
131
+
132
+ if (parseIPv4Number(last) !== failure) {
133
+ return true;
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ function domainParser(domain, validationErrors = null, beStrict = false) {
140
+ // A domain-to-ASCII validation error is reported whenever the strict Unicode ToASCII (CheckHyphens,
141
+ // UseSTD3ASCIIRules, and VerifyDnsLength all true) fails, even when the relaxed parameters used
142
+ // for non-strict parsing succeed. This step does not itself fail the algorithm.
143
+ //
144
+ // Spec divergence (performance): the URL Standard runs this strict pass unconditionally, but when
145
+ // beStrict is false its only effect is that validation error, so we skip it when we are neither
146
+ // being strict nor collecting validation errors.
147
+ if (beStrict || validationErrors) {
148
+ const strictResult = domainParserToASCII(domain, true);
149
+ if (strictResult === null) {
150
+ validationErrors?.push("domain-to-ASCII");
151
+ }
152
+
153
+ if (beStrict) {
154
+ return strictResult === null ? failure : strictResult;
155
+ }
156
+ }
157
+
158
+ let result;
159
+ if (infra.isASCIIString(domain)) {
160
+ // For web compatibility an ASCII domain is returned lowercased regardless of ToASCII's outcome.
161
+ result = domain.toLowerCase();
162
+ } else {
163
+ result = domainParserToASCII(domain, false);
164
+ if (result === null) {
165
+ return failure;
166
+ }
167
+ }
168
+
169
+ if (result === "") {
170
+ return failure;
171
+ }
172
+ if (containsForbiddenDomainCodePoint(result)) {
173
+ return failure;
174
+ }
175
+
176
+ return result;
177
+ }
178
+
179
+ function isSpecialScheme(scheme) {
180
+ return specialSchemes[scheme.toLowerCase()] !== undefined;
181
+ }
182
+
183
+ function isSpecialSchemeExceptFile(scheme) {
184
+ return isSpecialScheme(scheme) && scheme.toLowerCase() !== "file";
185
+ }
186
+
187
+ function defaultPort(scheme) {
188
+ return specialSchemes[scheme.toLowerCase()];
189
+ }
190
+
191
+ function isWindowsDriveLetterCodePoints(cp1, cp2) {
192
+ return infra.isASCIIAlpha(cp1) && (cp2 === p(":") || cp2 === p("|"));
193
+ }
194
+
195
+ function isWindowsDriveLetterString(string) {
196
+ return string.length === 2 && infra.isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|");
197
+ }
198
+
199
+ function isNormalizedWindowsDriveLetterString(string) {
200
+ return string.length === 2 && infra.isASCIIAlpha(string.codePointAt(0)) && string[1] === ":";
201
+ }
202
+
203
+ module.exports = {
204
+ containsForbiddenHostCodePoint,
205
+ containsPercentEncodedByte,
206
+ defaultPort,
207
+ domainParser,
208
+ endsInANumber,
209
+ failure,
210
+ forbiddenHostCodePoints,
211
+ isInvalidURLCodePoint,
212
+ isNormalizedWindowsDriveLetterString,
213
+ isPercentEncodedByteAt,
214
+ parseIPv4Number,
215
+ isSpecialScheme,
216
+ isSpecialSchemeExceptFile,
217
+ isURLCodePoint,
218
+ isWindowsDriveLetterCodePoints,
219
+ isWindowsDriveLetterString,
220
+ p
221
+ };