identifier-js 0.0.13 → 0.0.14

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/index.js CHANGED
@@ -140,10 +140,10 @@ const validate = (string, rule) => {
140
140
  function compose(parts = {}) {
141
141
  let result = '';
142
142
  if (parts.scheme) result += parts.scheme + ':';
143
- if (parts.authority) result += '//' + parts.authority;
144
- result += parts.path || '';
145
- if (parts.query) result += '?' + parts.query;
146
- if (parts.fragment) result += '#' + parts.fragment;
143
+ if (parts.authority !== undefined && parts.authority !== null) result += '//' + parts.authority;
144
+ result += parts.path ?? '';
145
+ if (parts.query !== undefined && parts.query !== null) result += '?' + parts.query;
146
+ if (parts.fragment !== undefined && parts.fragment !== null) result += '#' + parts.fragment;
147
147
  return result;
148
148
  }
149
149
  // remove dot segments algorithm per RFC 3986 Section 5.2.4 (loop and replace)
@@ -251,21 +251,40 @@ const toRelativeReference = (target, base) => {
251
251
  if (T.scheme !== B.scheme || T.authority !== B.authority) return target;
252
252
  let result;
253
253
  if (B.path === T.path) {
254
- result = '';
254
+ if (T.query === undefined && B.query !== undefined) {
255
+ // Use an explicit path to prevent the base query from being inherited.
256
+ if (T.path.startsWith('/')) result = T.path;
257
+ else if (T.path) {
258
+ const segment = T.path.slice(T.path.lastIndexOf('/') + 1);
259
+ result = segment && !segment.includes(':') ? segment : `./${segment}`;
260
+ } else if (T.authority !== undefined) result = `//${T.authority}`;
261
+ else return target;
262
+ } else result = '';
263
+ } else if (!T.path) {
264
+ // A network-path reference is required to represent an empty path without inheritance.
265
+ if (T.authority !== undefined) result = `//${T.authority}`;
266
+ else return target;
255
267
  } else {
256
268
  const baseSegments = B.path.split('/');
257
269
  const targetSegments = T.path.split('/');
258
270
  let position = 0;
271
+ // Find the common path prefix before constructing the relative traversal.
259
272
  while (baseSegments[position] === targetSegments[position] && position < baseSegments.length - 1 && position < targetSegments.length - 1) {
260
273
  position++;
261
274
  }
262
275
  const segments = [];
276
+ // Backtrack from the base resource to the common path prefix.
263
277
  for (let index = position + 1; index < baseSegments.length; index++) segments.push('..');
278
+ // Append the target path after the common prefix.
264
279
  for (let index = position; index < targetSegments.length; index++) segments.push(targetSegments[index]);
265
280
  result = segments.join('/');
281
+ if (!result) result = T.path.startsWith('/') ? T.path : './';
282
+ else if (/^[^/]*:/.test(result)) result = './' + result;
266
283
  }
267
284
  if (T.query !== undefined) result += `?${T.query}`;
268
285
  if (T.fragment !== undefined) result += `#${T.fragment}`;
286
+ // Parent traversal would convert a rootless path into an absolute path during resolution.
287
+ if (T.authority === undefined && !T.path.startsWith('/') && result.startsWith('..')) return target;
269
288
  return result;
270
289
  };
271
290
  // export
@@ -0,0 +1,299 @@
1
+ # Identifier normalization research notes
2
+
3
+ These notes summarize the standards and design choices that should be resolved before implementing `normalizeReference`. They are research material, not the current API contract.
4
+
5
+ ## Central constraint
6
+
7
+ There is no universal canonical form for every URI or IRI. RFC 3986 defines comparison in levels because a transformation that is safe for one scheme or application can merge distinct identifiers in another.
8
+
9
+ A normalizer should minimize false negatives without creating false positives:
10
+
11
+ 1. **Simple comparison** — compare characters exactly.
12
+ 2. **Syntax-based normalization** — apply transformations licensed by generic URI syntax.
13
+ 3. **Scheme-based normalization** — apply additional equivalences defined by a scheme.
14
+ 4. **Protocol-based normalization** — use equivalences established by observed protocol behavior.
15
+
16
+ `normalizeReference` should implement only an explicitly selected level. Protocol-derived normalization does not belong in a deterministic identifier library.
17
+
18
+ Reference: [RFC 3986 §6](https://www.rfc-editor.org/rfc/rfc3986#section-6).
19
+
20
+ ## Generic syntax-based normalization
21
+
22
+ The following transformations are suitable candidates for a generic URI profile.
23
+
24
+ ### Scheme and host case
25
+
26
+ - Lowercase the scheme.
27
+ - Lowercase the host.
28
+ - Do not lowercase userinfo, path, query, or fragment.
29
+ - Preserve Unicode component case unless a scheme or external policy defines equivalence.
30
+
31
+ Example:
32
+
33
+ ```text
34
+ HTTP://WWW.EXAMPLE.COM/ → http://www.example.com/
35
+ ```
36
+
37
+ Reference: RFC 3986 §6.2.2.1.
38
+
39
+ ### Percent-triplet case
40
+
41
+ Uppercase hexadecimal letters in every valid percent triplet:
42
+
43
+ ```text
44
+ %3a → %3A
45
+ %2f → %2F
46
+ ```
47
+
48
+ This changes presentation, not the represented octet.
49
+
50
+ Reference: RFC 3986 §§2.1 and 6.2.2.1.
51
+
52
+ ### Decode percent-encoded unreserved characters
53
+
54
+ Decode a percent triplet only when it represents an ASCII unreserved character:
55
+
56
+ ```text
57
+ ALPHA / DIGIT / "-" / "." / "_" / "~"
58
+ ```
59
+
60
+ Examples:
61
+
62
+ ```text
63
+ %63 → c
64
+ %7E → ~
65
+ ```
66
+
67
+ Do not generically decode reserved characters. `%2F` and `/`, for example, can have different structural meanings.
68
+
69
+ Parse components before decoding so an encoded delimiter cannot be mistaken for syntax. Never decode the same data twice.
70
+
71
+ References: RFC 3986 §§2.2–2.4 and 6.2.2.2.
72
+
73
+ ### Remove dot segments
74
+
75
+ Apply the RFC 3986 §5.2.4 algorithm to the parsed path:
76
+
77
+ ```text
78
+ /a/b/c/./../../g → /a/g
79
+ ```
80
+
81
+ Only complete `.` and `..` path segments are special. Do not process similar text in a query or fragment:
82
+
83
+ ```text
84
+ g?y/../x
85
+ g#s/../x
86
+ ```
87
+
88
+ References: RFC 3986 §§5.2.4 and 6.2.2.3.
89
+
90
+ ### Preserve component presence
91
+
92
+ Recomposition must distinguish an absent component from a present but empty component:
93
+
94
+ ```text
95
+ https://example.com/path
96
+ https://example.com/path?
97
+ https://example.com/path#
98
+ ```
99
+
100
+ The same applies to an empty authority. Component delimiters must be emitted based on definedness, not truthiness.
101
+
102
+ Reference: RFC 3986 §5.3.
103
+
104
+ ## Scheme-based profiles
105
+
106
+ Scheme-specific transformations should not run unless the scheme profile is selected or automatic scheme handling is part of the documented contract.
107
+
108
+ ### HTTP and HTTPS
109
+
110
+ RFC 9110 permits these normal forms:
111
+
112
+ - lowercase scheme and host;
113
+ - remove port `80` from `http`;
114
+ - remove port `443` from `https`;
115
+ - remove an explicitly empty port;
116
+ - use `/` when authority is present and path is empty;
117
+ - decode percent-encoded unreserved characters;
118
+ - preserve all other component case.
119
+
120
+ Examples:
121
+
122
+ ```text
123
+ HTTP://Example.COM:80 → http://example.com/
124
+ https://example.com:443/a → https://example.com/a
125
+ ```
126
+
127
+ An HTTP request target does not include a fragment, but identifier normalization should not silently discard a fragment unless the selected operation specifically produces a request target.
128
+
129
+ Userinfo in HTTP and HTTPS targets is deprecated and should be rejected or reported by an HTTP policy rather than silently normalized away.
130
+
131
+ Reference: [RFC 9110 §§4.2.1–4.2.5](https://www.rfc-editor.org/rfc/rfc9110#section-4.2).
132
+
133
+ ### WS and WSS
134
+
135
+ Potential scheme-specific rules include:
136
+
137
+ - default port `80` for `ws`;
138
+ - default port `443` for `wss`;
139
+ - `/` as the resource path when the path is empty;
140
+ - no fragment identifiers;
141
+ - IDN-to-ASCII handling under an explicitly selected hostname policy.
142
+
143
+ Fragment rejection is validation, not normalization. A normalizer should not repair a WebSocket URI by silently deleting its fragment.
144
+
145
+ Reference: [RFC 6455 §3](https://www.rfc-editor.org/rfc/rfc6455#section-3).
146
+
147
+ ### File
148
+
149
+ A generic `file` normalizer is not recommended. Behavior varies by platform and filesystem:
150
+
151
+ - local empty authority versus `localhost`;
152
+ - POSIX roots;
153
+ - Windows drive-letter case and UNC paths;
154
+ - path case sensitivity;
155
+ - backslash handling;
156
+ - platform-specific Unicode normalization;
157
+ - reserved device names and namespace paths.
158
+
159
+ Require an explicit platform profile before applying these transformations.
160
+
161
+ Reference: [RFC 8089](https://www.rfc-editor.org/rfc/rfc8089).
162
+
163
+ ### Other schemes
164
+
165
+ Apply only generic syntax normalization unless the scheme's authoritative specification defines additional equivalences. Do not infer default ports, path case rules, or authority behavior from a similar scheme.
166
+
167
+ ## IRI and Unicode decisions
168
+
169
+ RFC 3987 recommends that creators produce IRIs in NFC, but comparison code must not arbitrarily normalize an existing Unicode IRI. Normalizing third-party text can merge identifiers that were intentionally distinct.
170
+
171
+ Recommended policy:
172
+
173
+ - do not apply Unicode normalization by default;
174
+ - offer NFC only as an explicit creation or application-policy option;
175
+ - do not offer NFKC as a generic identifier transformation;
176
+ - retain the original IRI when a normalized form is generated only as a comparison key.
177
+
178
+ ### IRI-to-URI mapping is a separate operation
179
+
180
+ Mapping an IRI to a URI is not merely normalization:
181
+
182
+ 1. encode non-ASCII `ucschar` and `iprivate` characters as UTF-8;
183
+ 2. percent-encode each UTF-8 octet as `%HH`;
184
+ 3. preserve existing valid percent triplets and URI-allowed characters;
185
+ 4. apply an explicit IDNA policy to internationalized hostnames when required.
186
+
187
+ The reverse operation must decode only valid UTF-8 and must preserve encoded reserved characters. It must not guess legacy encodings.
188
+
189
+ Reference: [RFC 3987 §§3 and 5](https://www.rfc-editor.org/rfc/rfc3987).
190
+
191
+ ## Transformations excluded by default
192
+
193
+ A generic normalizer should not:
194
+
195
+ - decode percent-encoded reserved characters;
196
+ - lowercase userinfo, paths, queries, or fragments;
197
+ - sort, deduplicate, or reinterpret query parameters;
198
+ - remove empty query or fragment delimiters;
199
+ - add or remove trailing slashes without scheme authority;
200
+ - apply Unicode NFC or NFKC automatically;
201
+ - convert an internationalized hostname without a defined IDNA version and policy;
202
+ - remove userinfo rather than reporting it;
203
+ - infer filesystem semantics for `file`;
204
+ - infer equivalence from redirects or successful retrievals;
205
+ - convert a relative reference without a supplied base.
206
+
207
+ ## Suggested API design
208
+
209
+ Avoid a single aggressive operation. Two viable designs are:
210
+
211
+ ### Explicit profiles
212
+
213
+ ```js
214
+ normalizeReference(reference, {
215
+ profile: 'generic', // generic | http | https | ws | wss
216
+ unicodeNormalization: false,
217
+ });
218
+ ```
219
+
220
+ A `file` profile should additionally require a platform policy.
221
+
222
+ ### Separate operations
223
+
224
+ ```js
225
+ normalizeReference(reference); // generic syntax only
226
+ normalizeHttpReference(reference); // HTTP/HTTPS scheme rules
227
+ normalizeWebSocketReference(reference); // WS/WSS scheme rules
228
+ iriToUri(reference, options); // explicit representation mapping
229
+ ```
230
+
231
+ Separate operations are harder to misuse and make compatibility changes more visible. A profile-based API is easier to extend. The final choice should follow the expected consumers.
232
+
233
+ ## Suggested generic processing order
234
+
235
+ 1. Parse and retain whether authority, query, and fragment are absent or empty.
236
+ 2. Lowercase scheme and host where generic syntax permits it.
237
+ 3. Normalize percent-triplet hexadecimal case.
238
+ 4. Decode percent-encoded ASCII unreserved characters component by component.
239
+ 5. Remove dot segments from the path.
240
+ 6. Apply an explicitly selected scheme profile.
241
+ 7. Apply Unicode normalization only when explicitly requested.
242
+ 8. Recompose while preserving empty components.
243
+ 9. Validate the normalized output under the same identifier and scheme policy.
244
+ 10. Optionally verify idempotence:
245
+
246
+ ```js
247
+ normalizeReference(normalizeReference(value)) === normalizeReference(value)
248
+ ```
249
+
250
+ ## Acceptance scenarios
251
+
252
+ A generic profile should include at least these cases:
253
+
254
+ ```text
255
+ HTTP://Example.COM/%7euser → http://example.com/~user
256
+ http://example.com/a/./b/../c → http://example.com/a/c
257
+ http://example.com/path? → http://example.com/path?
258
+ http://example.com/path# → http://example.com/path#
259
+ http://example.com/%2F → http://example.com/%2F
260
+ ```
261
+
262
+ An HTTP profile can additionally include:
263
+
264
+ ```text
265
+ http://example.com:80 → http://example.com/
266
+ https://example.com:443/a → https://example.com/a
267
+ ```
268
+
269
+ Cases that must remain distinct under generic normalization include:
270
+
271
+ ```text
272
+ http://example.com/a ≠ http://example.com/a/
273
+ http://example.com/path ≠ http://example.com/path?
274
+ http://example.com/%2F ≠ http://example.com//
275
+ http://example.com/A ≠ http://example.com/a
276
+ ```
277
+
278
+ Testing should cover URI and IRI forms, empty components, rootless paths, percent triplets, Unicode supplementary characters, idempotence, and normalization followed by parsing/recomposition.
279
+
280
+ ## Open decisions
281
+
282
+ Before implementation, decide:
283
+
284
+ 1. Whether `normalizeReference` is generic-only or selects profiles automatically by scheme.
285
+ 2. Whether output preserves the input category: URI versus IRI.
286
+ 3. Whether relative references are normalized in place or require a base and become absolute.
287
+ 4. Whether Unicode NFC is offered, and for which creation contexts.
288
+ 5. Whether IDNA conversion belongs here or in a dedicated hostname dependency.
289
+ 6. Whether comparison keys and display/transport identifiers use separate APIs.
290
+ 7. Whether unsupported scheme profiles throw, fall back to generic rules, or require an explicit option.
291
+ 8. Whether current callers can rely on `normalizeReference` remaining an identity function until a major release.
292
+
293
+ ## Authoritative references
294
+
295
+ - [RFC 3986 — Uniform Resource Identifier: Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
296
+ - [RFC 3987 — Internationalized Resource Identifiers](https://www.rfc-editor.org/rfc/rfc3987)
297
+ - [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
298
+ - [RFC 6455 — The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
299
+ - [RFC 8089 — The `file` URI Scheme](https://www.rfc-editor.org/rfc/rfc8089)
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "identifier-js",
3
- "version": "0.0.13",
4
- "description": "A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.",
5
- "keywords": [
6
- "IRI",
7
- "URI",
8
- "IRI-reference",
9
- "URI-reference",
10
- "ipv4",
11
- "ipv6",
12
- "uuid",
13
- "parser",
14
- "validator",
15
- "RFC3986",
16
- "RFC3987"
17
- ],
18
- "homepage": "https://github.com/SorinGFS/identifier-js#readme",
19
- "bugs": {
20
- "url": "https://github.com/SorinGFS/identifier-js/issues"
21
- },
22
- "repository": {
23
- "type": "git",
24
- "url": "git+https://github.com/SorinGFS/identifier-js.git"
25
- },
26
- "license": "MIT",
27
- "author": "SorinGFS",
28
- "type": "commonjs",
29
- "main": "index.js",
30
- "scripts": {
31
- "update-deps": "npx npm-check-updates -u && npm install",
32
- "test": "node tests/vitest-setup && npm test"
33
- },
34
- "dependencies": {
35
- "url-templates": "^1.0.4"
36
- },
37
- "engines": {
38
- "node": ">=18.0.0"
39
- },
40
- "engineStrict": true
41
- }
1
+ {
2
+ "name": "identifier-js",
3
+ "version": "0.0.14",
4
+ "description": "A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.",
5
+ "keywords": [
6
+ "IRI",
7
+ "URI",
8
+ "IRI-reference",
9
+ "URI-reference",
10
+ "ipv4",
11
+ "ipv6",
12
+ "uuid",
13
+ "parser",
14
+ "validator",
15
+ "RFC3986",
16
+ "RFC3987"
17
+ ],
18
+ "homepage": "https://github.com/SorinGFS/identifier-js#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/SorinGFS/identifier-js/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/SorinGFS/identifier-js.git"
25
+ },
26
+ "license": "MIT",
27
+ "author": "SorinGFS",
28
+ "type": "commonjs",
29
+ "main": "index.js",
30
+ "scripts": {
31
+ "update-deps": "npx npm-check-updates -u && npm install",
32
+ "test": "node tests/vitest-setup && npm test"
33
+ },
34
+ "dependencies": {
35
+ "url-templates": "^1.0.5"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "engineStrict": true
41
+ }
package/readme.md CHANGED
@@ -1,110 +1,449 @@
1
- ---
2
-
3
- title: Identifier JS
4
-
5
- description: A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- A fully RFC [3986](https://datatracker.ietf.org/doc/html/rfc3986.html)/[3897](https://datatracker.ietf.org/doc/html/rfc3987.html) compliant URI/IRI parser, validator, resolver and composer, along with other identifier utilities. This library implements the following [IANA registered](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml) schemes:
12
- - `http` defined by [RFC9110, Section 4.2.1](https://datatracker.ietf.org/doc/html/rfc9110#section-4.2.1)
13
- - `https` defined by [RFC9110, Section 4.2.2](https://datatracker.ietf.org/doc/html/rfc9110#section-4.2.2)
14
- - `ws` defined by [RFC6455, Section 3](https://datatracker.ietf.org/doc/html/rfc6455#section-3)
15
- - `wss` defined by [RFC6455, Section 3](https://datatracker.ietf.org/doc/html/rfc6455#section-3)
16
- - `file` defined by [RFC8089, Section 2](https://datatracker.ietf.org/doc/html/rfc8089#section-2)
17
-
18
- Other schemes that are `IANA registered` schemes and compliant with the generic `URI` or `IRI` syntax are also supported. As for the identifiers that are not `IANA registered`, but compliant with the generic `URI` or `IRI` syntax, the preferred schemes are `uri` and respectively `iri`.
19
-
20
- ## Install
21
-
22
- ### Requirements
23
-
24
- - `regular expression with unicode support`, available in modern browsers and `NodeJS 18+`
25
-
26
- ```bash title="console"
27
- npm i identifier-js
28
- ```
29
-
30
- ## API
31
-
32
- Exports are documented below.
33
-
34
- ### URI (RFC 3986)
35
-
36
- #### Validation
37
-
38
- Validate a URI:
39
- - isUri: (value: string) => boolean
40
-
41
- Validate a URI reference (absolute or relative):
42
- - isUriReference: (value: string) => boolean
43
-
44
- Validate an absolute URI (must include scheme):
45
- - isAbsoluteUri: (value: string) => boolean
46
-
47
- #### Parsing
48
-
49
- Parse a URI into structured components:
50
- - parseUri: (value: string) => IdentifierComponents
51
-
52
- Parse a URI reference into structured components:
53
- - parseUriReference: (value: string) => RelativeIdentifierComponents
54
-
55
- Parse an absolute URI (must include scheme):
56
- - parseAbsoluteUri: (value: string) => AbsoluteIdentifierComponents
57
-
58
- ### IRI (RFC 3987)
59
-
60
- #### Validation
61
-
62
- Validate an IRI:
63
- - isIri: (value: string) => boolean
64
-
65
- Validate an IRI reference (absolute or relative):
66
- - isIriReference: (value: string) => boolean
67
-
68
- Validate an absolute IRI (must include scheme):
69
- - isAbsoluteIri: (value: string) => boolean
70
-
71
- #### Parsing
72
-
73
- Parse an IRI into structured components:
74
- - parseIri: (value: string) => IdentifierComponents
75
-
76
- Parse an IRI reference into structured components:
77
- - parseIriReference: (value: string) => RelativeIdentifierComponents
78
-
79
- Parse an absolute IRI (must include scheme):
80
- - parseAbsoluteIri: (value: string) => AbsoluteIdentifierComponents
81
-
82
- ### Reference Utilities (URI/IRI)
83
-
84
- Normalize a URI or IRI reference:
85
- - normalizeReference: (reference: string) => string
86
-
87
- Resolve a reference against a base identifier:
88
- - resolveReference: (reference: string, base: string, strict?: boolean, returnParts?: boolean) => string
89
-
90
- **Note:**
91
- - strict (default: `true`) enables strict resolution behavior.
92
- - returnParts (default: `false`) returns structured components instead of a string.
93
-
94
- Convert a reference into absolute form:
95
- - toAbsoluteReference: (reference: string) => string
96
-
97
- Compute a relative reference from base to target:
98
- - toRelativeReference: (target: string, base: string) => string
99
-
100
- ### UUID
101
-
102
- Validate a UUID (any version):
103
- - isUUID: (value: string) => boolean
104
-
105
- Validate a UUID version 4:
106
- - isUUIDv4: (value: string) => boolean
107
-
108
- ## Testing
109
-
110
- - npm test
1
+ ---
2
+
3
+ title: Identifier JS
4
+
5
+ description: An RFC 3986 and RFC 3987 parser, validator, and reference resolver for Node.js and browser bundles.
6
+
7
+ ---
8
+
9
+ # Identifier JS
10
+
11
+ `identifier-js` is a fully RFC [3986](https://www.rfc-editor.org/rfc/rfc3986) and RFC [3987](https://www.rfc-editor.org/rfc/rfc3987) compliant URI/IRI parser, validator, resolver, and composer. It provides:
12
+
13
+ - URI and IRI validation;
14
+ - parsed identifier components;
15
+ - RFC 3986 reference resolution and dot-segment removal;
16
+ - relative-reference generation with round-trip guarantees for supported forms;
17
+ - UUID and UUIDv4 lexical validation;
18
+ - lazily compiled and cached regular expressions.
19
+
20
+ The package is synchronous, CommonJS, and supports Node.js 18 or newer. Browser use requires a bundler or runtime that supports CommonJS dependencies and Unicode regular expressions.
21
+
22
+ ## Install
23
+
24
+ ```bash title="console"
25
+ npm install identifier-js
26
+ ```
27
+
28
+ ## API
29
+
30
+ Every validator returns `true` or throws at the first detected violation. Parsing and reference operations also throw when their input does not satisfy the required grammar.
31
+
32
+ ### Validate URI syntax
33
+
34
+ Validate a complete URI, a URI reference, or an absolute URI without a fragment.
35
+
36
+ <details>
37
+ <summary><strong>API and examples</strong></summary>
38
+
39
+ ```ts
40
+ isUri(value: string): true
41
+ isUriReference(value: string): true
42
+ isAbsoluteUri(value: string): true
43
+ ```
44
+
45
+ ```js
46
+ const { isUri, isUriReference, isAbsoluteUri } = require('identifier-js');
47
+
48
+ console.log(isUri('https://example.com/path?query#fragment')); // true
49
+ console.log(isUriReference('../asset?version=2')); // true
50
+ console.log(isAbsoluteUri('https://example.com/path?query')); // true
51
+ ```
52
+
53
+ An `absolute-URI` is the fragment-free grammar defined by RFC 3986. Use `isUri` when a fragment is allowed.
54
+
55
+ </details>
56
+
57
+ ### Parse URI components
58
+
59
+ Parse URI syntax into scheme, authority, userinfo, host, port, path, query, and fragment components.
60
+
61
+ <details>
62
+ <summary><strong>API and examples</strong></summary>
63
+
64
+ ```ts
65
+ parseUri(value: string): IdentifierComponents
66
+ parseUriReference(value: string): RelativeIdentifierComponents
67
+ parseAbsoluteUri(value: string): AbsoluteIdentifierComponents
68
+ ```
69
+
70
+ ```js
71
+ const { parseUri } = require('identifier-js');
72
+
73
+ console.log(parseUri('https://user@example.com:8443/a?b#c'));
74
+ // {
75
+ // scheme: 'https',
76
+ // authority: 'user@example.com:8443',
77
+ // userinfo: 'user',
78
+ // host: 'example.com',
79
+ // port: '8443',
80
+ // path: '/a',
81
+ // query: 'b',
82
+ // fragment: 'c'
83
+ // }
84
+ ```
85
+
86
+ Absent optional components are returned as `undefined`. Present but empty query and fragment components are returned as empty strings.
87
+
88
+ </details>
89
+
90
+ ### Validate IRI syntax
91
+
92
+ Validate Unicode-capable identifiers using RFC 3987 grammar.
93
+
94
+ <details>
95
+ <summary><strong>API and examples</strong></summary>
96
+
97
+ ```ts
98
+ isIri(value: string): true
99
+ isIriReference(value: string): true
100
+ isAbsoluteIri(value: string): true
101
+ ```
102
+
103
+ ```js
104
+ const { isIri, isIriReference } = require('identifier-js');
105
+
106
+ console.log(isIri('https://例え.テスト/資料?項目=値#概要')); // true
107
+ console.log(isIriReference('../résumé')); // true
108
+ ```
109
+
110
+ IRI support permits RFC 3987 Unicode ranges in applicable components. Complete IDNA processing and conversion to a transport URI are separate responsibilities.
111
+
112
+ </details>
113
+
114
+ ### Parse IRI components
115
+
116
+ Parse an IRI while preserving its Unicode component values.
117
+
118
+ <details>
119
+ <summary><strong>API and examples</strong></summary>
120
+
121
+ ```ts
122
+ parseIri(value: string): IdentifierComponents
123
+ parseIriReference(value: string): RelativeIdentifierComponents
124
+ parseAbsoluteIri(value: string): AbsoluteIdentifierComponents
125
+ ```
126
+
127
+ ```js
128
+ const { parseIri } = require('identifier-js');
129
+
130
+ console.log(parseIri('https://usér@例え.テスト:8443/résumé?lang=fr#profil'));
131
+ // {
132
+ // scheme: 'https',
133
+ // authority: 'usér@例え.テスト:8443',
134
+ // userinfo: 'usér',
135
+ // host: '例え.テスト',
136
+ // port: '8443',
137
+ // path: '/résumé',
138
+ // query: 'lang=fr',
139
+ // fragment: 'profil'
140
+ // }
141
+ ```
142
+
143
+ </details>
144
+
145
+ ### Resolve a reference
146
+
147
+ Resolve a URI or IRI reference against an absolute base using RFC 3986 §5.
148
+
149
+ <details>
150
+ <summary><strong>API and examples</strong></summary>
151
+
152
+ ```ts
153
+ resolveReference(
154
+ reference: string,
155
+ base: string,
156
+ strict?: boolean,
157
+ returnParts?: boolean
158
+ ): string | Record<string, string | undefined>
159
+ ```
160
+
161
+ ```js
162
+ const { resolveReference } = require('identifier-js');
163
+
164
+ console.log(resolveReference('../images/logo.svg', 'https://example.com/docs/api/page'));
165
+ // https://example.com/docs/images/logo.svg
166
+
167
+ console.log(resolveReference('?page=2', 'https://example.com/items?page=1#current'));
168
+ // https://example.com/items?page=2
169
+ ```
170
+
171
+ `strict` defaults to `true`. In strict mode, a reference containing a scheme replaces the base identifier even when both schemes are equal. `returnParts` defaults to `false`; when enabled at runtime, the function returns the resolved component object.
172
+
173
+ Empty authorities, queries, and fragments are preserved during recomposition.
174
+
175
+ </details>
176
+
177
+ ### Produce absolute and relative forms
178
+
179
+ Remove a base fragment or derive a relative reference that resolves back to a target.
180
+
181
+ <details>
182
+ <summary><strong>API and examples</strong></summary>
183
+
184
+ ```ts
185
+ toAbsoluteReference(reference: string): string
186
+ toRelativeReference(target: string, base: string): string
187
+ ```
188
+
189
+ ```js
190
+ const { toAbsoluteReference, toRelativeReference } = require('identifier-js');
191
+
192
+ console.log(toAbsoluteReference('https://example.com/a/../b#section'));
193
+ // https://example.com/b
194
+
195
+ const target = 'https://example.com/docs/images/logo.svg';
196
+ const base = 'https://example.com/docs/api/page';
197
+ const relative = toRelativeReference(target, base);
198
+ console.log(relative); // ../images/logo.svg
199
+ ```
200
+
201
+ When no safe rootless relative form can round-trip to the target, `toRelativeReference` returns the absolute target. Different schemes or authorities also return the target unchanged.
202
+
203
+ </details>
204
+
205
+ ### Normalization status
206
+
207
+ `normalizeReference` is reserved for future normalization policy and currently returns its input unchanged.
208
+
209
+ <details>
210
+ <summary><strong>Current behavior and research</strong></summary>
211
+
212
+ ```ts
213
+ normalizeReference(reference: string): string
214
+ ```
215
+
216
+ ```js
217
+ const { normalizeReference } = require('identifier-js');
218
+
219
+ console.log(normalizeReference('HTTP://Example.COM/a/../b'));
220
+ // HTTP://Example.COM/a/../b
221
+ ```
222
+
223
+ URI/IRI normalization has multiple standards-defined levels and scheme-specific tradeoffs. See [`normalization.md`](normalization.md) for implementation options, unsafe transformations, suggested profiles, and acceptance scenarios.
224
+
225
+ </details>
226
+
227
+ ### Validate UUID text
228
+
229
+ Validate the canonical UUID text shape or the stricter UUIDv4 version and variant fields.
230
+
231
+ <details>
232
+ <summary><strong>API and examples</strong></summary>
233
+
234
+ ```ts
235
+ isUUID(value: string): true
236
+ isUUIDv4(value: string): true
237
+ ```
238
+
239
+ ```js
240
+ const { isUUID, isUUIDv4 } = require('identifier-js');
241
+
242
+ console.log(isUUID('99c17cbb-656f-564a-940f-1a4568f03487')); // true
243
+ console.log(isUUIDv4('123e4567-e89b-42d3-9456-426614174000')); // true
244
+ ```
245
+
246
+ `isUUID` validates the `8-4-4-4-12` hexadecimal layout without restricting the version or variant fields. `isUUIDv4` requires version `4` and the RFC variant nibble `8`, `9`, `a`, or `b`.
247
+
248
+ </details>
249
+
250
+ ## Processing model
251
+
252
+ The parser builds its validation logic from declarative RFC grammar fragments:
253
+
254
+ 1. Select generic URI/IRI rules or the package's scheme-specific hostname policy.
255
+ 2. Recursively expand grammar references through `url-templates`.
256
+ 3. Add named capture groups when parsing is requested.
257
+ 4. Compile the complete expression with Unicode support.
258
+ 5. Cache the expression by operation, grammar rule, and scheme-policy class.
259
+ 6. Validate with `RegExp.test()` or parse with `RegExp.exec()`.
260
+ 7. Resolve references by component inheritance, path merging, dot-segment removal, and definedness-preserving recomposition.
261
+
262
+ <details>
263
+ <summary><strong>Lazy compilation and cache behavior</strong></summary>
264
+
265
+ Parsing and validation use separate cached expressions because parsing requires named groups and validation does not. Generic and scheme-specific identifiers also use separate entries.
266
+
267
+ The first call for an operation/rule/policy combination includes recursive grammar expansion and regular-expression compilation. Later calls reuse the cached expression and are considerably faster. No regular expressions are generated during package import.
268
+
269
+ </details>
270
+
271
+ <details>
272
+ <summary><strong>Scheme-specific hostname policy</strong></summary>
273
+
274
+ The following schemes trigger DNS-style ASCII or Unicode label rules instead of the fully generic `reg-name` grammar:
275
+
276
+ - `http`
277
+ - `https`
278
+ - `ws`
279
+ - `wss`
280
+ - `file`
281
+
282
+ Matching is case-insensitive. Other valid schemes use generic RFC 3986/3987 registered-name syntax.
283
+
284
+ This policy validates label shape and selected Unicode character classes. It is not complete IDNA processing and does not replace normalization, contextual, bidi, registry, or Punycode validation.
285
+
286
+ </details>
287
+
288
+ ## Real-world use cases
289
+
290
+ ### Follow HTTP redirects and resource locations
291
+
292
+ HTTP `Location` values are URI references and can be relative to the original target URI.
293
+
294
+ <details>
295
+ <summary><strong>Example and context</strong></summary>
296
+
297
+ ```js
298
+ const { resolveReference } = require('identifier-js');
299
+
300
+ const requestUrl = 'https://example.com/account/profile';
301
+ const location = '../login?return=profile';
302
+ console.log(resolveReference(location, requestUrl));
303
+ // https://example.com/login?return=profile
304
+ ```
305
+
306
+ RFC 9110 uses URI references in `Location`, `Content-Location`, and `Referer`. Correct resolution requires component parsing, path merging, query inheritance rules, and dot-segment removal.
307
+
308
+ </details>
309
+
310
+ ### Keep document trees portable
311
+
312
+ Relative references let documents and assets move together without rewriting every internal link.
313
+
314
+ <details>
315
+ <summary><strong>Example and context</strong></summary>
316
+
317
+ ```js
318
+ const { resolveReference, toRelativeReference } = require('identifier-js');
319
+
320
+ const documentUrl = 'https://example.com/manual/chapters/intro.html';
321
+ const imageUrl = 'https://example.com/manual/images/diagram.svg';
322
+ const relative = toRelativeReference(imageUrl, documentUrl);
323
+ console.log(relative); // ../images/diagram.svg
324
+ console.log(resolveReference(relative, documentUrl)); // original image URL
325
+ ```
326
+
327
+ RFC 3986 identifies portable hypertext document trees as a central use for relative references.
328
+
329
+ </details>
330
+
331
+ ### Process internationalized identifiers
332
+
333
+ IRI parsing preserves native-script host, path, query, and fragment text for interfaces and internationalized content.
334
+
335
+ <details>
336
+ <summary><strong>Example and context</strong></summary>
337
+
338
+ ```js
339
+ const { parseIri } = require('identifier-js');
340
+
341
+ const parts = parseIri('https://例え.テスト/検索?q=資料#結果');
342
+ console.log(parts.host); // 例え.テスト
343
+ console.log(parts.path); // /検索
344
+ ```
345
+
346
+ RFC 3987 uses IRIs for internationalized identification while requiring mapping to URIs when a protocol accepts only URI syntax. Cache lookup, browser history, XML identifiers, and indexing are cited comparison contexts.
347
+
348
+ </details>
349
+
350
+ ### Validate and route protocol identifiers
351
+
352
+ Component parsing supports policy decisions without unsafe string splitting.
353
+
354
+ <details>
355
+ <summary><strong>Example and context</strong></summary>
356
+
357
+ ```js
358
+ const { parseUri } = require('identifier-js');
359
+
360
+ const parts = parseUri('wss://example.com:8443/events?channel=updates');
361
+ console.log(parts.scheme); // wss
362
+ console.log(parts.host); // example.com
363
+ console.log(parts.port); // 8443
364
+ console.log(parts.path); // /events
365
+ ```
366
+
367
+ Applications can inspect scheme, authority, path, and query before selecting a connector, enforcing an allowlist, constructing an HTTP request target, or routing to a service. Protocol-specific security and semantic validation remains the application's responsibility.
368
+
369
+ </details>
370
+
371
+ ### Validate externally supplied UUID text
372
+
373
+ Lexical UUID checks are useful at API, configuration, and storage boundaries.
374
+
375
+ <details>
376
+ <summary><strong>Example and context</strong></summary>
377
+
378
+ ```js
379
+ const { isUUIDv4 } = require('identifier-js');
380
+
381
+ try {
382
+ isUUIDv4('123e4567-e89b-42d3-9456-426614174000');
383
+ console.log('valid UUIDv4');
384
+ } catch (error) {
385
+ console.error(error.message);
386
+ }
387
+ ```
388
+
389
+ RFC 9562 lists database keys, filenames, system identifiers, and transaction identifiers among common UUID uses. UUID validation does not establish authorization, unpredictability, uniqueness, or safe use as a capability token.
390
+
391
+ </details>
392
+
393
+ ## Intentional behavior and limitations
394
+
395
+ <details>
396
+ <summary><strong>Validation and parsing boundaries</strong></summary>
397
+
398
+ - Validators return `true` or throw; they do not return `false`.
399
+ - URI functions reject non-ASCII characters where RFC 3986 permits only URI syntax. Use the IRI functions for RFC 3987 Unicode ranges.
400
+ - `absolute-URI` and `absolute-IRI` exclude fragments by definition. The complete `URI` and `IRI` functions permit fragments.
401
+ - Scheme-specific processing currently specializes hostname syntax; it does not implement every protocol rule for HTTP, WebSocket, or `file` identifiers.
402
+ - Scheme-specific Unicode labels are not complete IDNA validation.
403
+ - Ports are restricted to an empty value or the numeric range 0–65535. Generic RFC 3986 syntax itself permits any sequence of digits.
404
+ - Generic registered names may contain syntax that DNS-style hostnames reject.
405
+ - Parsing separates components before any application-level percent decoding.
406
+ - The library does not perform network, DNS, filesystem, registry, or authorization checks.
407
+
408
+ </details>
409
+
410
+ <details>
411
+ <summary><strong>Resolution and conversion boundaries</strong></summary>
412
+
413
+ - Resolution uses IRI grammar, so Unicode references and bases are accepted.
414
+ - Empty authorities, queries, and fragments remain distinct from absent components.
415
+ - `strict = false` enables RFC 3986 backward-compatible handling when a reference repeats the base scheme.
416
+ - `toAbsoluteReference` requires an identifier containing a scheme and removes its fragment through empty-reference resolution.
417
+ - `toRelativeReference` compares scheme and authority text exactly; it does not normalize them first.
418
+ - `toRelativeReference` may return an absolute target when a rootless relative path cannot preserve identity.
419
+ - `normalizeReference` is intentionally an identity function until a normalization contract is selected.
420
+
421
+ </details>
422
+
423
+ <details>
424
+ <summary><strong>UUID boundaries</strong></summary>
425
+
426
+ - `isUUID` validates canonical hexadecimal layout only; it does not enforce a known version or the RFC variant.
427
+ - `isUUIDv4` validates the version and variant fields but does not assess random-number quality.
428
+ - A syntactically valid UUID is not proof of uniqueness, integrity, authenticity, or authorization.
429
+
430
+ </details>
431
+
432
+ ## Qualification
433
+
434
+ The current suite contains 418 active tests covering URI/IRI validation and parsing, scheme-specific hosts, IPv4, IPv6, ports, UUIDs, RFC 3986 resolution examples, empty components, absolute conversion, and relative-reference round trips.
435
+
436
+ The reference-conversion changes were additionally checked against 2,646 combinations of paths, absent/empty/non-empty queries, and absent/empty/non-empty fragments.
437
+
438
+ ## Authoritative references
439
+
440
+ - [RFC 3986 — Uniform Resource Identifier: Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
441
+ - [RFC 3987 — Internationalized Resource Identifiers](https://www.rfc-editor.org/rfc/rfc3987)
442
+ - [RFC 9110 — HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
443
+ - [RFC 6455 — The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
444
+ - [RFC 8089 — The `file` URI Scheme](https://www.rfc-editor.org/rfc/rfc8089)
445
+ - [RFC 9562 — Universally Unique IDentifiers](https://www.rfc-editor.org/rfc/rfc9562)
446
+
447
+ ## Disclaimer
448
+
449
+ Validation establishes conformance with this implementation's syntax and policy. It does not establish that an identifier is registered, reachable, trustworthy, safe to dereference, or appropriate for a particular protocol operation.
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
  import id from '../index.js';
3
3
 
4
+ // Verify reference resolution behavior beyond the RFC's published examples.
4
5
  const resolveTests = [
5
6
  ['urn:some:ip:prop', 'urn:some:ip:prop', 'urn:some:ip:prop'],
6
7
  ['urn:some:ip:prop', 'urn:some:other:prop', 'urn:some:ip:prop'],
@@ -13,4 +14,24 @@ describe('resolveReference', () => {
13
14
  expect(subject).to.equal(expected);
14
15
  });
15
16
  });
17
+
18
+ // Preserve an explicitly empty query while replacing the base query.
19
+ test('Preserves an empty query component', () => {
20
+ expect(id.resolveReference('?', 'https://example.com/path?old#fragment')).to.equal('https://example.com/path?');
21
+ });
22
+
23
+ // Preserve an explicitly empty fragment after inheriting the base query.
24
+ test('Preserves an empty fragment component', () => {
25
+ expect(id.resolveReference('#', 'https://example.com/path?query#old')).to.equal('https://example.com/path?query#');
26
+ });
27
+
28
+ // Preserve an empty query inherited by an empty reference.
29
+ test('Preserves an inherited empty query component', () => {
30
+ expect(id.resolveReference('', 'https://example.com/path?#fragment')).to.equal('https://example.com/path?');
31
+ });
32
+
33
+ // Preserve the delimiter that distinguishes an empty authority from no authority.
34
+ test('Preserves an empty authority component', () => {
35
+ expect(id.resolveReference('uri:///target', 'uri:/base')).to.equal('uri:///target');
36
+ });
16
37
  });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from 'vitest';
2
2
  import id from '../index.js';
3
3
 
4
+ // Verify relative-reference generation and round trips through reference resolution.
4
5
  describe('toRelativeReference IRI', () => {
5
6
  test.each([
6
7
  ['https://examplé.com/var/lib', 'https://examplé.com', '/var/lib'],
@@ -48,3 +49,25 @@ describe('toRelativeReference URI', () => {
48
49
  expect(id.resolveReference(relative, base)).to.equal(target); // sanity check
49
50
  });
50
51
  });
52
+
53
+ // Verify empty components and path forms that require explicit inheritance control.
54
+ describe('toRelativeReference component presence', () => {
55
+ // Require every generated reference to have the expected form and resolve back to its target.
56
+ test.each([
57
+ ['clears a query on the same absolute path', 'https://example.com/a/item', 'https://example.com/a/item?old', '/a/item'],
58
+ ['preserves an empty target query', 'https://example.com/a/item?', 'https://example.com/a/item?old', '?'],
59
+ ['preserves an empty target fragment', 'https://example.com/a/item#', 'https://example.com/a/item', '#'],
60
+ ['clears a query on an empty path', 'https://example.com', 'https://example.com?old', '//example.com'],
61
+ ['clears a query on a colon-containing rootless path', 'urn:a:b', 'urn:a:b?old', './a:b'],
62
+ ['produces an empty path from a non-empty base path', 'https://example.com', 'https://example.com/a', '//example.com'],
63
+ ['produces a root path from a single-segment base path', 'https://example.com/', 'https://example.com/a', '/'],
64
+ ['preserves a trailing slash on a rootless directory', 'urn:a/', 'urn:a/b', './'],
65
+ ['protects a colon-containing first path segment', 'urn:a:b', 'urn:c', './a:b'],
66
+ ['falls back for an empty rootless target path', 'urn:', 'urn:a', 'urn:'],
67
+ ['falls back when rootless parent traversal changes path form', 'urn:a', 'urn:a/', 'urn:a'],
68
+ ])('%s', (description, target, base, expected) => {
69
+ const relative = id.toRelativeReference(target, base);
70
+ expect(relative).to.equal(expected);
71
+ expect(id.resolveReference(relative, base)).to.equal(target);
72
+ });
73
+ });