@speclynx/apidom-reference 6.1.1 → 6.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@speclynx/apidom-reference",
3
- "version": "6.1.1",
3
+ "version": "6.2.0",
4
4
  "description": "Advanced algorithms for semantic ApiDOM manipulations like dereferencing or resolution.",
5
5
  "keywords": [
6
6
  "apidom",
@@ -299,35 +299,35 @@
299
299
  "author": "Vladimir Gorej",
300
300
  "license": "Apache-2.0",
301
301
  "dependencies": {
302
- "@babel/runtime-corejs3": "^8.0.8",
303
- "@speclynx/apidom-core": "6.1.1",
304
- "@speclynx/apidom-datamodel": "6.1.1",
305
- "@speclynx/apidom-error": "6.1.1",
306
- "@speclynx/apidom-json-pointer": "6.1.1",
307
- "@speclynx/apidom-ns-arazzo-1": "6.1.1",
308
- "@speclynx/apidom-ns-asyncapi-2": "6.1.1",
309
- "@speclynx/apidom-ns-json-schema-2020-12": "6.1.1",
310
- "@speclynx/apidom-ns-openapi-2": "6.1.1",
311
- "@speclynx/apidom-ns-openapi-3-0": "6.1.1",
312
- "@speclynx/apidom-ns-openapi-3-1": "6.1.1",
313
- "@speclynx/apidom-ns-overlay-1": "6.1.1",
314
- "@speclynx/apidom-parser-adapter-arazzo-json-1": "6.1.1",
315
- "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "6.1.1",
316
- "@speclynx/apidom-parser-adapter-asyncapi-json-2": "6.1.1",
317
- "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "6.1.1",
318
- "@speclynx/apidom-parser-adapter-json": "6.1.1",
319
- "@speclynx/apidom-parser-adapter-openapi-json-2": "6.1.1",
320
- "@speclynx/apidom-parser-adapter-openapi-json-3-0": "6.1.1",
321
- "@speclynx/apidom-parser-adapter-openapi-json-3-1": "6.1.1",
322
- "@speclynx/apidom-parser-adapter-openapi-yaml-2": "6.1.1",
323
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "6.1.1",
324
- "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "6.1.1",
325
- "@speclynx/apidom-parser-adapter-overlay-json-1": "6.1.1",
326
- "@speclynx/apidom-parser-adapter-overlay-yaml-1": "6.1.1",
327
- "@speclynx/apidom-parser-adapter-yaml-1-2": "6.1.1",
328
- "@speclynx/apidom-traverse": "6.1.1",
302
+ "@babel/runtime-corejs3": "^8.1.0",
303
+ "@speclynx/apidom-core": "6.2.0",
304
+ "@speclynx/apidom-datamodel": "6.2.0",
305
+ "@speclynx/apidom-error": "6.2.0",
306
+ "@speclynx/apidom-json-pointer": "6.2.0",
307
+ "@speclynx/apidom-ns-arazzo-1": "6.2.0",
308
+ "@speclynx/apidom-ns-asyncapi-2": "6.2.0",
309
+ "@speclynx/apidom-ns-json-schema-2020-12": "6.2.0",
310
+ "@speclynx/apidom-ns-openapi-2": "6.2.0",
311
+ "@speclynx/apidom-ns-openapi-3-0": "6.2.0",
312
+ "@speclynx/apidom-ns-openapi-3-1": "6.2.0",
313
+ "@speclynx/apidom-ns-overlay-1": "6.2.0",
314
+ "@speclynx/apidom-parser-adapter-arazzo-json-1": "6.2.0",
315
+ "@speclynx/apidom-parser-adapter-arazzo-yaml-1": "6.2.0",
316
+ "@speclynx/apidom-parser-adapter-asyncapi-json-2": "6.2.0",
317
+ "@speclynx/apidom-parser-adapter-asyncapi-yaml-2": "6.2.0",
318
+ "@speclynx/apidom-parser-adapter-json": "6.2.0",
319
+ "@speclynx/apidom-parser-adapter-openapi-json-2": "6.2.0",
320
+ "@speclynx/apidom-parser-adapter-openapi-json-3-0": "6.2.0",
321
+ "@speclynx/apidom-parser-adapter-openapi-json-3-1": "6.2.0",
322
+ "@speclynx/apidom-parser-adapter-openapi-yaml-2": "6.2.0",
323
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-0": "6.2.0",
324
+ "@speclynx/apidom-parser-adapter-openapi-yaml-3-1": "6.2.0",
325
+ "@speclynx/apidom-parser-adapter-overlay-json-1": "6.2.0",
326
+ "@speclynx/apidom-parser-adapter-overlay-yaml-1": "6.2.0",
327
+ "@speclynx/apidom-parser-adapter-yaml-1-2": "6.2.0",
328
+ "@speclynx/apidom-traverse": "6.2.0",
329
329
  "@swaggerexpert/arazzo-runtime-expression": "^3.2.0",
330
- "axios": "^1.20.0",
330
+ "axios": "^1.21.0",
331
331
  "picomatch": "^4.0.7",
332
332
  "ramda": "~0.32.0",
333
333
  "ramda-adjunct": "^6.1.0"
@@ -346,5 +346,5 @@
346
346
  "README.md",
347
347
  "CHANGELOG.md"
348
348
  ],
349
- "gitHead": "c25778097fd33bf5f0d61de6843a6240571f94b3"
349
+ "gitHead": "13bd86fba1e6c36dd97423e195c48994901c5422"
350
350
  }
@@ -22,20 +22,22 @@ const bundle = async (uri, options) => {
22
22
  const {
23
23
  refSet
24
24
  } = options.bundle;
25
- const sanitizedURI = url.sanitize(uri);
25
+ // strategies look up same-document references by a resolved, hence normalized, URI;
26
+ // the file is parsed as a whole document, so its URI has no fragment
27
+ const baseURI = url.normalize(url.stripHash(uri));
26
28
  const mergedOptions = (0, _util.merge)(options, {
27
29
  resolve: {
28
- baseURI: sanitizedURI
30
+ baseURI
29
31
  }
30
32
  });
31
33
  let parseResult;
32
34
 
33
35
  // if refSet was provided, use it to avoid unnecessary parsing
34
- if (refSet !== null && refSet.has(sanitizedURI)) {
36
+ if (refSet !== null && refSet.has(baseURI)) {
35
37
  // @ts-ignore
36
38
  ({
37
39
  value: parseResult
38
- } = refSet.find((0, _ramda.propEq)(sanitizedURI, 'uri')));
40
+ } = refSet.find((0, _ramda.propEq)(baseURI, 'uri')));
39
41
  } else {
40
42
  parseResult = await (0, _index.default)(uri, mergedOptions);
41
43
  }
@@ -14,20 +14,22 @@ const bundle = async (uri, options) => {
14
14
  const {
15
15
  refSet
16
16
  } = options.bundle;
17
- const sanitizedURI = url.sanitize(uri);
17
+ // strategies look up same-document references by a resolved, hence normalized, URI;
18
+ // the file is parsed as a whole document, so its URI has no fragment
19
+ const baseURI = url.normalize(url.stripHash(uri));
18
20
  const mergedOptions = mergeOptions(options, {
19
21
  resolve: {
20
- baseURI: sanitizedURI
22
+ baseURI
21
23
  }
22
24
  });
23
25
  let parseResult;
24
26
 
25
27
  // if refSet was provided, use it to avoid unnecessary parsing
26
- if (refSet !== null && refSet.has(sanitizedURI)) {
28
+ if (refSet !== null && refSet.has(baseURI)) {
27
29
  // @ts-ignore
28
30
  ({
29
31
  value: parseResult
30
- } = refSet.find(propEq(sanitizedURI, 'uri')));
32
+ } = refSet.find(propEq(baseURI, 'uri')));
31
33
  } else {
32
34
  parseResult = await parse(uri, mergedOptions);
33
35
  }
@@ -31,8 +31,10 @@ const dereferenceApiDOM = async (element, options) => {
31
31
  parseResult = new _apidomDatamodel.ParseResultElement([elementClone]);
32
32
  surrogateWrapping = true;
33
33
  }
34
+
35
+ // strategies look up same-document references by a resolved, hence normalized, URI
34
36
  const file = new _File.default({
35
- uri: options.resolve.baseURI,
37
+ uri: url.normalize(options.resolve.baseURI),
36
38
  parseResult,
37
39
  mediaType: options.parse.mediaType
38
40
  });
@@ -66,21 +68,22 @@ const dereference = async (uri, options) => {
66
68
  const {
67
69
  refSet
68
70
  } = options.dereference;
69
- const sanitizedURI = url.sanitize(uri);
71
+ // the file is parsed as a whole document, so its URI has no fragment
72
+ const baseURI = url.normalize(url.stripHash(uri));
70
73
  let parseResult;
71
74
 
72
75
  // if refSet was provided, use it to avoid unnecessary parsing
73
- if (refSet !== null && refSet.has(sanitizedURI)) {
76
+ if (refSet !== null && refSet.has(baseURI)) {
74
77
  // @ts-ignore
75
78
  ({
76
79
  value: parseResult
77
- } = refSet.find((0, _ramda.propEq)(sanitizedURI, 'uri')));
80
+ } = refSet.find((0, _ramda.propEq)(baseURI, 'uri')));
78
81
  } else {
79
82
  parseResult = await (0, _index.default)(uri, options);
80
83
  }
81
84
  const mergedOptions = (0, _util.merge)(options, {
82
85
  resolve: {
83
- baseURI: sanitizedURI
86
+ baseURI
84
87
  },
85
88
  dereference: {
86
89
  // if refSet was not provided, then we can work in mutable mode
@@ -23,8 +23,10 @@ export const dereferenceApiDOM = async (element, options) => {
23
23
  parseResult = new ParseResultElement([elementClone]);
24
24
  surrogateWrapping = true;
25
25
  }
26
+
27
+ // strategies look up same-document references by a resolved, hence normalized, URI
26
28
  const file = new File({
27
- uri: options.resolve.baseURI,
29
+ uri: url.normalize(options.resolve.baseURI),
28
30
  parseResult,
29
31
  mediaType: options.parse.mediaType
30
32
  });
@@ -57,21 +59,22 @@ const dereference = async (uri, options) => {
57
59
  const {
58
60
  refSet
59
61
  } = options.dereference;
60
- const sanitizedURI = url.sanitize(uri);
62
+ // the file is parsed as a whole document, so its URI has no fragment
63
+ const baseURI = url.normalize(url.stripHash(uri));
61
64
  let parseResult;
62
65
 
63
66
  // if refSet was provided, use it to avoid unnecessary parsing
64
- if (refSet !== null && refSet.has(sanitizedURI)) {
67
+ if (refSet !== null && refSet.has(baseURI)) {
65
68
  // @ts-ignore
66
69
  ({
67
70
  value: parseResult
68
- } = refSet.find(propEq(sanitizedURI, 'uri')));
71
+ } = refSet.find(propEq(baseURI, 'uri')));
69
72
  } else {
70
73
  parseResult = await parse(uri, options);
71
74
  }
72
75
  const mergedOptions = mergeOptions(options, {
73
76
  resolve: {
74
- baseURI: sanitizedURI
77
+ baseURI
75
78
  },
76
79
  dereference: {
77
80
  // if refSet was not provided, then we can work in mutable mode
@@ -68,6 +68,10 @@ const defaultOptions = {
68
68
  resolve: {
69
69
  /**
70
70
  * baseURI serves as a base for all relative URL found in ApiDOM references.
71
+ * It must be absolute: a file system path, a file:// URL or an HTTP(S) URL.
72
+ * Defaults to the current working directory (the page URL in browsers).
73
+ * A fragment marks where the ApiDOM fragment is located in its document;
74
+ * leave it out when passing a whole document.
71
75
  */
72
76
  baseURI: '',
73
77
  /**
@@ -63,6 +63,10 @@ const defaultOptions = {
63
63
  resolve: {
64
64
  /**
65
65
  * baseURI serves as a base for all relative URL found in ApiDOM references.
66
+ * It must be absolute: a file system path, a file:// URL or an HTTP(S) URL.
67
+ * Defaults to the current working directory (the page URL in browsers).
68
+ * A fragment marks where the ApiDOM fragment is located in its document;
69
+ * leave it out when passing a whole document.
66
70
  */
67
71
  baseURI: '',
68
72
  /**
@@ -12,7 +12,13 @@ var _url = require("../util/url.cjs");
12
12
  */
13
13
 
14
14
  const baseURILens = (0, _ramda.lens)((0, _ramda.path)(['resolve', 'baseURI']), (0, _ramda.assocPath)(['resolve', 'baseURI']));
15
- const baseURIDefault = baseURI => (0, _ramdaAdjunct.isEmptyString)(baseURI) ? (0, _url.cwd)() : baseURI;
15
+ // in Node cwd() is a raw file system path; encode it, so that it reads as a URI
16
+ // (in browsers it is the page URL, which may be an already encoded file:// URL)
17
+ const cwdURI = () => {
18
+ const dir = (0, _url.cwd)();
19
+ return (0, _url.isFileSystemPath)(dir) && (0, _url.getProtocol)(dir) !== 'file' ? (0, _url.fromFileSystemPath)(dir) : dir;
20
+ };
21
+ const baseURIDefault = baseURI => (0, _ramdaAdjunct.isEmptyString)(baseURI) ? cwdURI() : baseURI;
16
22
 
17
23
  /**
18
24
  * @public
@@ -1,12 +1,18 @@
1
1
  import { mergeDeepRight, lens, path, assocPath, over } from 'ramda';
2
2
  import { isEmptyString } from 'ramda-adjunct';
3
- import { cwd } from "../util/url.mjs";
3
+ import { cwd, getProtocol, isFileSystemPath, fromFileSystemPath } from "../util/url.mjs";
4
4
  /**
5
5
  * Algorithm for deep merging options.
6
6
  */
7
7
 
8
8
  const baseURILens = lens(path(['resolve', 'baseURI']), assocPath(['resolve', 'baseURI']));
9
- const baseURIDefault = baseURI => isEmptyString(baseURI) ? cwd() : baseURI;
9
+ // in Node cwd() is a raw file system path; encode it, so that it reads as a URI
10
+ // (in browsers it is the page URL, which may be an already encoded file:// URL)
11
+ const cwdURI = () => {
12
+ const dir = cwd();
13
+ return isFileSystemPath(dir) && getProtocol(dir) !== 'file' ? fromFileSystemPath(dir) : dir;
14
+ };
15
+ const baseURIDefault = baseURI => isEmptyString(baseURI) ? cwdURI() : baseURI;
10
16
 
11
17
  /**
12
18
  * @public
@@ -29,9 +29,10 @@ const resolveApiDOM = async (element, options) => {
29
29
  elementClone.classes.push('result');
30
30
  parseResult = new _apidomDatamodel.ParseResultElement([elementClone]);
31
31
  }
32
- const sanitizedURI = url.sanitize(url.stripHash(options.resolve.baseURI));
32
+
33
+ // strategies look up same-document references by a resolved, hence normalized, URI
33
34
  const file = new _File.default({
34
- uri: sanitizedURI,
35
+ uri: url.normalize(options.resolve.baseURI),
35
36
  parseResult,
36
37
  mediaType: options.parse.mediaType
37
38
  });
@@ -59,9 +60,11 @@ const resolveApiDOM = async (element, options) => {
59
60
  exports.resolveApiDOM = resolveApiDOM;
60
61
  const resolve = async (uri, options) => {
61
62
  const parseResult = await (0, _index.default)(uri, options);
63
+ // the file is parsed as a whole document, so its URI has no fragment
64
+ const baseURI = url.normalize(url.stripHash(uri));
62
65
  const mergedOptions = (0, _util.merge)(options, {
63
66
  resolve: {
64
- baseURI: url.sanitize(uri)
67
+ baseURI
65
68
  }
66
69
  });
67
70
  return resolveApiDOM(parseResult, mergedOptions);
@@ -21,9 +21,10 @@ export const resolveApiDOM = async (element, options) => {
21
21
  elementClone.classes.push('result');
22
22
  parseResult = new ParseResultElement([elementClone]);
23
23
  }
24
- const sanitizedURI = url.sanitize(url.stripHash(options.resolve.baseURI));
24
+
25
+ // strategies look up same-document references by a resolved, hence normalized, URI
25
26
  const file = new File({
26
- uri: sanitizedURI,
27
+ uri: url.normalize(options.resolve.baseURI),
27
28
  parseResult,
28
29
  mediaType: options.parse.mediaType
29
30
  });
@@ -50,9 +51,11 @@ export const resolveApiDOM = async (element, options) => {
50
51
  */
51
52
  const resolve = async (uri, options) => {
52
53
  const parseResult = await parse(uri, options);
54
+ // the file is parsed as a whole document, so its URI has no fragment
55
+ const baseURI = url.normalize(url.stripHash(uri));
53
56
  const mergedOptions = mergeOptions(options, {
54
57
  resolve: {
55
- baseURI: url.sanitize(uri)
58
+ baseURI
56
59
  }
57
60
  });
58
61
  return resolveApiDOM(parseResult, mergedOptions);
package/src/util/url.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.unsanitize = exports.toFileSystemPath = exports.stripHash = exports.sanitize = exports.resolve = exports.relative = exports.isURI = exports.isHttpUrl = exports.isFileSystemPath = exports.hasProtocol = exports.getProtocol = exports.getHash = exports.getExtension = exports.getBasename = exports.fromFileSystemPath = exports.cwd = void 0;
6
+ exports.unsanitize = exports.toFileSystemPath = exports.stripHash = exports.sanitize = exports.resolve = exports.relative = exports.normalize = exports.isURI = exports.isHttpUrl = exports.isFileSystemPath = exports.hasProtocol = exports.getProtocol = exports.getHash = exports.getExtension = exports.getBasename = exports.fromFileSystemPath = exports.cwd = void 0;
7
7
  var _ramda = require("ramda");
8
8
  var _ramdaAdjunct = require("ramda-adjunct");
9
9
  /**
@@ -118,21 +118,23 @@ exports.isURI = isURI;
118
118
  */
119
119
  const toFileSystemPath = (uri, options) => {
120
120
  // RegExp patterns to URL-decode special characters for local filesystem paths
121
- const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@'];
121
+ const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%3F/g, '?', /%40/g, '@'];
122
122
  const keepFileProtocol = (0, _ramda.propOr)(false, 'keepFileProtocol', options);
123
123
  const isWindowsPredicate = (0, _ramda.propOr)(isWindows, 'isWindows', options);
124
124
 
125
- // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
126
- let path = decodeURI(uri);
127
-
128
- // Step 2: Manually decode characters that are not decoded by `decodeURI`.
125
+ // Step 1: Manually decode characters that are not decoded by `decodeURI`.
129
126
  // This includes characters such as "#" and "?", which have special meaning in URLs,
130
- // but are just normal characters in a filesystem path.
127
+ // but are just normal characters in a filesystem path. It runs before `decodeURI`,
128
+ // so that an encoded "%" (e.g. "%253F") isn't decoded twice.
129
+ let path = uri;
131
130
  for (let i = 0; i < urlDecodePatterns.length; i += 2) {
132
131
  // @ts-ignore
133
132
  path = path.replace(urlDecodePatterns[i], urlDecodePatterns[i + 1]);
134
133
  }
135
134
 
135
+ // Step 2: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
136
+ path = decodeURI(path);
137
+
136
138
  // Step 3: If it's a "file://" URL, then format it consistently
137
139
  // or convert it to a local filesystem path
138
140
  let isFileUrl = path.substring(0, 7).toLowerCase() === 'file://';
@@ -278,6 +280,34 @@ const resolve = (from, to) => {
278
280
  return resolvedUrl.toString();
279
281
  };
280
282
 
283
+ /**
284
+ * Normalizes a URI the way `resolve` normalizes its result (WHATWG URL normalization):
285
+ * lowercases the scheme and the host of an absolute URL, drops a default port and
286
+ * removes dot segments. This is not RFC 3986 syntax-based normalization: existing
287
+ * percent-encoding is left as is. A file system path is sanitized first, so characters
288
+ * not allowed in URLs (e.g. spaces, `[`, `?`) are percent-encoded; a `file://` URL has
289
+ * its path sanitized the same way and stays a URL. The fragment is kept as given.
290
+ * A URI that can't be normalized is returned unchanged.
291
+ * @public
292
+ */
293
+ exports.resolve = resolve;
294
+ const normalize = uri => {
295
+ // getHash returns the root hash ('#') for a URI without fragment
296
+ const fragment = uri.includes('#') ? getHash(uri) : '';
297
+ const documentURI = stripHash(uri);
298
+ try {
299
+ let sanitizedURI = sanitize(documentURI);
300
+ // sanitize turns a file:// URL into a path; keep it a URL, as references
301
+ // resolved against it are, e.g. file:///C:/x.json rather than file://C:/x.json
302
+ if (getProtocol(documentURI) === 'file') {
303
+ sanitizedURI = `file://${sanitizedURI.startsWith('/') ? '' : '/'}${sanitizedURI}`;
304
+ }
305
+ return resolve('', sanitizedURI) + fragment;
306
+ } catch {
307
+ return uri;
308
+ }
309
+ };
310
+
281
311
  /**
282
312
  * Computes the URI reference that resolves to `to` against `from` — the inverse
283
313
  * of `resolve`: `resolve(from, relative(from, to)) === to`. The reference is
@@ -287,7 +317,7 @@ const resolve = (from, to) => {
287
317
  * returned unchanged.
288
318
  * @public
289
319
  */
290
- exports.resolve = resolve;
320
+ exports.normalize = normalize;
291
321
  const relative = (from, to) => {
292
322
  const fromURL = new URL(from, 'resolve://');
293
323
  const toURL = new URL(to, 'resolve://');
package/src/util/url.mjs CHANGED
@@ -108,21 +108,23 @@ export const isURI = uri => {
108
108
  */
109
109
  export const toFileSystemPath = (uri, options) => {
110
110
  // RegExp patterns to URL-decode special characters for local filesystem paths
111
- const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@'];
111
+ const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%3F/g, '?', /%40/g, '@'];
112
112
  const keepFileProtocol = propOr(false, 'keepFileProtocol', options);
113
113
  const isWindowsPredicate = propOr(isWindows, 'isWindows', options);
114
114
 
115
- // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
116
- let path = decodeURI(uri);
117
-
118
- // Step 2: Manually decode characters that are not decoded by `decodeURI`.
115
+ // Step 1: Manually decode characters that are not decoded by `decodeURI`.
119
116
  // This includes characters such as "#" and "?", which have special meaning in URLs,
120
- // but are just normal characters in a filesystem path.
117
+ // but are just normal characters in a filesystem path. It runs before `decodeURI`,
118
+ // so that an encoded "%" (e.g. "%253F") isn't decoded twice.
119
+ let path = uri;
121
120
  for (let i = 0; i < urlDecodePatterns.length; i += 2) {
122
121
  // @ts-ignore
123
122
  path = path.replace(urlDecodePatterns[i], urlDecodePatterns[i + 1]);
124
123
  }
125
124
 
125
+ // Step 2: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc.
126
+ path = decodeURI(path);
127
+
126
128
  // Step 3: If it's a "file://" URL, then format it consistently
127
129
  // or convert it to a local filesystem path
128
130
  let isFileUrl = path.substring(0, 7).toLowerCase() === 'file://';
@@ -263,6 +265,33 @@ export const resolve = (from, to) => {
263
265
  return resolvedUrl.toString();
264
266
  };
265
267
 
268
+ /**
269
+ * Normalizes a URI the way `resolve` normalizes its result (WHATWG URL normalization):
270
+ * lowercases the scheme and the host of an absolute URL, drops a default port and
271
+ * removes dot segments. This is not RFC 3986 syntax-based normalization: existing
272
+ * percent-encoding is left as is. A file system path is sanitized first, so characters
273
+ * not allowed in URLs (e.g. spaces, `[`, `?`) are percent-encoded; a `file://` URL has
274
+ * its path sanitized the same way and stays a URL. The fragment is kept as given.
275
+ * A URI that can't be normalized is returned unchanged.
276
+ * @public
277
+ */
278
+ export const normalize = uri => {
279
+ // getHash returns the root hash ('#') for a URI without fragment
280
+ const fragment = uri.includes('#') ? getHash(uri) : '';
281
+ const documentURI = stripHash(uri);
282
+ try {
283
+ let sanitizedURI = sanitize(documentURI);
284
+ // sanitize turns a file:// URL into a path; keep it a URL, as references
285
+ // resolved against it are, e.g. file:///C:/x.json rather than file://C:/x.json
286
+ if (getProtocol(documentURI) === 'file') {
287
+ sanitizedURI = `file://${sanitizedURI.startsWith('/') ? '' : '/'}${sanitizedURI}`;
288
+ }
289
+ return resolve('', sanitizedURI) + fragment;
290
+ } catch {
291
+ return uri;
292
+ }
293
+ };
294
+
266
295
  /**
267
296
  * Computes the URI reference that resolves to `to` against `from` — the inverse
268
297
  * of `resolve`: `resolve(from, relative(from, to)) === to`. The reference is
@@ -359,6 +359,18 @@ export declare class MaximumResolveDepthError extends ResolveError {
359
359
  */
360
360
  export declare const mergeOptions: (lObj: ApiDOMReferenceOptions, rObj: Record<string, any>) => ApiDOMReferenceOptions;
361
361
 
362
+ /**
363
+ * Normalizes a URI the way `resolve` normalizes its result (WHATWG URL normalization):
364
+ * lowercases the scheme and the host of an absolute URL, drops a default port and
365
+ * removes dot segments. This is not RFC 3986 syntax-based normalization: existing
366
+ * percent-encoding is left as is. A file system path is sanitized first, so characters
367
+ * not allowed in URLs (e.g. spaces, `[`, `?`) are percent-encoded; a `file://` URL has
368
+ * its path sanitized the same way and stays a URL. The fragment is kept as given.
369
+ * A URI that can't be normalized is returned unchanged.
370
+ * @public
371
+ */
372
+ declare const normalize: (uri: string) => string;
373
+
362
374
  /**
363
375
  * @public
364
376
  */
@@ -658,6 +670,7 @@ declare namespace url {
658
670
  stripHash,
659
671
  cwd,
660
672
  resolve_2 as resolve,
673
+ normalize,
661
674
  relative,
662
675
  sanitize,
663
676
  unsanitize
@@ -96,6 +96,17 @@ export declare const cwd: () => string;
96
96
  * @public
97
97
  */
98
98
  export declare const resolve: (from: string, to: string) => string;
99
+ /**
100
+ * Normalizes a URI the way `resolve` normalizes its result (WHATWG URL normalization):
101
+ * lowercases the scheme and the host of an absolute URL, drops a default port and
102
+ * removes dot segments. This is not RFC 3986 syntax-based normalization: existing
103
+ * percent-encoding is left as is. A file system path is sanitized first, so characters
104
+ * not allowed in URLs (e.g. spaces, `[`, `?`) are percent-encoded; a `file://` URL has
105
+ * its path sanitized the same way and stays a URL. The fragment is kept as given.
106
+ * A URI that can't be normalized is returned unchanged.
107
+ * @public
108
+ */
109
+ export declare const normalize: (uri: string) => string;
99
110
  /**
100
111
  * Computes the URI reference that resolves to `to` against `from` — the inverse
101
112
  * of `resolve`: `resolve(from, relative(from, to)) === to`. The reference is