identifier-js 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +4 -9
- package/index.js +99 -94
- package/normalization.md +33 -31
- package/package.json +1 -1
- package/readme.md +61 -63
package/index.d.ts
CHANGED
|
@@ -31,17 +31,13 @@ export const parseIri: (iri: string) => ParsedIdentifierComponents;
|
|
|
31
31
|
export const parseIriReference: (iriReference: string) => ParsedRelativeIdentifierComponents;
|
|
32
32
|
/** @throws {Error} If the absolute-IRI is invalid. */
|
|
33
33
|
export const parseAbsoluteIri: (iri: string) => ParsedAbsoluteIdentifierComponents;
|
|
34
|
-
/**
|
|
35
|
-
* @throws {Error} If the base or the reference is invalid.
|
|
36
|
-
*/
|
|
34
|
+
/** @throws {Error} If the base or the reference is invalid. */
|
|
37
35
|
export function resolveReference(reference: string, base: string, strict?: boolean, returnParts?: false): string;
|
|
38
36
|
export function resolveReference(reference: string, base: string, strict: boolean | undefined, returnParts: true): IdentifierComponents;
|
|
39
37
|
export function resolveReference(reference: string, base: string, strict: boolean | undefined, returnParts: boolean | undefined): string | IdentifierComponents;
|
|
40
38
|
/** @throws {Error} If the reference is invalid. */
|
|
41
39
|
export const toAbsoluteReference: (reference: string) => string;
|
|
42
|
-
/**
|
|
43
|
-
* @throws {Error} If the base or the reference is invalid.
|
|
44
|
-
*/
|
|
40
|
+
/** @throws {Error} If the base or the reference is invalid. */
|
|
45
41
|
export const toRelativeReference: (target: string, base: string) => string;
|
|
46
42
|
|
|
47
43
|
/** Map a parsed non-empty registered-name host to caller-owned text. */
|
|
@@ -94,17 +90,16 @@ export type AbsoluteIdentifierComponents = {
|
|
|
94
90
|
query?: string;
|
|
95
91
|
};
|
|
96
92
|
|
|
97
|
-
// Describe the scheme-specific captures returned by each complete or fragment-free URN grammar.
|
|
98
93
|
type UrnIdentifierComponents = {
|
|
99
94
|
scheme: string;
|
|
100
95
|
nid: string;
|
|
101
96
|
nss: string;
|
|
102
97
|
rComponent?: string;
|
|
103
98
|
qComponent?: string;
|
|
104
|
-
|
|
99
|
+
fComponent?: string;
|
|
105
100
|
};
|
|
106
101
|
|
|
107
|
-
type AbsoluteUrnIdentifierComponents = Omit<UrnIdentifierComponents, '
|
|
102
|
+
type AbsoluteUrnIdentifierComponents = Omit<UrnIdentifierComponents, 'fComponent'>;
|
|
108
103
|
|
|
109
104
|
export type ParsedIdentifierComponents = (IdentifierComponents | UrnIdentifierComponents) & NormalizableReference;
|
|
110
105
|
export type ParsedRelativeIdentifierComponents = (RelativeIdentifierComponents | UrnIdentifierComponents) & NormalizableReference;
|
package/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Validate UUIDs and parse, validate, normalize, resolve, and convert RFC 3986 URI, RFC 3987 IRI, and RFC 8141 URN references.
|
|
3
3
|
// A valid URI is always a valid IRI, subject to every implemented scheme's more specific grammar.
|
|
4
4
|
const { recursiveCompile } = require('url-templates');
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
//
|
|
5
|
+
const patternCache = new Map();
|
|
6
|
+
const dnsHostSchemesPattern = '(?:[hH][tT][tT][pP][sS]?|[wW][sS][sS]?|[fF][iI][lL][eE])';
|
|
7
|
+
// Define shared RFC 3986/3987 productions and helper productions used by UUID and scheme-specific grammars.
|
|
8
8
|
const commonRules = {
|
|
9
|
-
|
|
10
|
-
scheme: '(?!{
|
|
9
|
+
dnsHostSchemesPattern,
|
|
10
|
+
scheme: '(?!{dnsHostSchemesPattern}:)[a-zA-Z][a-zA-Z0-9+.-]*',
|
|
11
11
|
port: '\\d*',
|
|
12
12
|
IP_literal: '\\[(?:{IPv6address}|{IPvFuture})\\]',
|
|
13
13
|
IPv6address: '(?:(?:{h16}:){6}{ls32}|::(?:{h16}:){5}{ls32}|(?:(?:{h16})?)::(?:{h16}:){4}{ls32}|(?:(?:{h16}:)?{h16})?::(?:{h16}:){3}{ls32}|(?:(?:{h16}:){0,2}{h16})?::(?:{h16}:){2}{ls32}|(?:(?:{h16}:){0,3}{h16})?::(?:{h16}:){1}{ls32}|(?:(?:{h16}:){0,4}{h16})?::{ls32}|(?:(?:{h16}:){0,5}{h16})?::{h16}|(?:(?:{h16}:){0,6}{h16})?::)',
|
|
@@ -26,7 +26,7 @@ const commonRules = {
|
|
|
26
26
|
UUID: '{hex_digit}{8}-{hex_digit}{4}-{hex_digit}{4}-{hex_digit}{4}-{hex_digit}{12}',
|
|
27
27
|
UUID_v4: '{hex_digit}{8}-{hex_digit}{4}-4{hex_digit}{3}-[89abAB]{hex_digit}{3}-{hex_digit}{12}',
|
|
28
28
|
};
|
|
29
|
-
//
|
|
29
|
+
// Define RFC 3986 URI productions.
|
|
30
30
|
const uriRules = {
|
|
31
31
|
URI_reference: '(?:{URI}|{relative_ref})',
|
|
32
32
|
URI: '{absolute_URI}(?:#{fragment})?',
|
|
@@ -51,7 +51,7 @@ const uriRules = {
|
|
|
51
51
|
fragment: '(?:{pchar}|\/|\\?)*',
|
|
52
52
|
pchar: '(?:{unreserved}|{pct_encoded}|{sub_delims}|:|@)',
|
|
53
53
|
};
|
|
54
|
-
//
|
|
54
|
+
// Define RFC 3987 IRI productions.
|
|
55
55
|
const iriRules = {
|
|
56
56
|
IRI_reference: '(?:{IRI}|{irelative_ref})',
|
|
57
57
|
IRI: '{absolute_IRI}(?:#{ifragment})?',
|
|
@@ -79,9 +79,9 @@ const iriRules = {
|
|
|
79
79
|
iprivate: '[\\uE000-\\uF8FF\\u{F0000}-\\u{FFFFD}\\u{100000}-\\u{10FFFD}]',
|
|
80
80
|
ucschar: '[\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF\\u{10000}-\\u{1FFFD}\\u{20000}-\\u{2FFFD}\\u{30000}-\\u{3FFFD}\\u{40000}-\\u{4FFFD}\\u{50000}-\\u{5FFFD}\\u{60000}-\\u{6FFFD}\\u{70000}-\\u{7FFFD}\\u{80000}-\\u{8FFFD}\\u{90000}-\\u{9FFFD}\\u{A0000}-\\u{AFFFD}\\u{B0000}-\\u{BFFFD}\\u{C0000}-\\u{CFFFD}\\u{D0000}-\\u{DFFFD}\\u{E1000}-\\u{EFFFD}]',
|
|
81
81
|
};
|
|
82
|
-
// Define RFC 8141
|
|
82
|
+
// Define a closed RFC 8141 profile whose root overrides and direct fragment expression expose only URN captures.
|
|
83
83
|
const urnRules = {
|
|
84
|
-
scheme:
|
|
84
|
+
scheme: '[uU][rR][nN]',
|
|
85
85
|
URI_reference: '{URI}',
|
|
86
86
|
URI: '{namestring}',
|
|
87
87
|
absolute_URI: '{assigned_name}(?:{rq_components})?',
|
|
@@ -92,21 +92,21 @@ const urnRules = {
|
|
|
92
92
|
assigned_name: '{scheme}:{NID}:{NSS}',
|
|
93
93
|
NID: '{alpha_digit}{ldh}{0,30}{alpha_digit}',
|
|
94
94
|
ldh: '(?:{alpha_digit}|-)',
|
|
95
|
-
NSS: '{pchar}(?:{pchar}
|
|
95
|
+
NSS: '{pchar}(?:{pchar}|\/)*',
|
|
96
96
|
rq_components: '(?:[?][+]{r_component})?(?:[?]={q_component})?',
|
|
97
|
-
r_component: '{pchar}(?:{pchar}
|
|
98
|
-
q_component: '{pchar}(?:{pchar}
|
|
99
|
-
f_component:
|
|
97
|
+
r_component: '{pchar}(?:{pchar}|\/|[?](?!=))*',
|
|
98
|
+
q_component: '{pchar}(?:{pchar}|\/|[?])*',
|
|
99
|
+
f_component: uriRules.fragment,
|
|
100
100
|
};
|
|
101
|
-
//
|
|
101
|
+
// Compile character-repertoire checks used during URI/IRI percent-encoding normalization.
|
|
102
102
|
const uriUnreservedPattern = new RegExp(`^${commonRules.unreserved}$`);
|
|
103
103
|
const iriUcscharPattern = new RegExp(`^${iriRules.ucschar}$`, 'u');
|
|
104
104
|
const iriPrivatePattern = new RegExp(`^${iriRules.iprivate}$`, 'u');
|
|
105
105
|
// Apply the additional RFC 3987 Section 4.1 prose restriction outside the ABNF repertoire.
|
|
106
106
|
const forbiddenIriFormattingPattern = /^[\u200E\u200F\u202A-\u202E]$/u;
|
|
107
|
-
//
|
|
108
|
-
const
|
|
109
|
-
scheme:
|
|
107
|
+
// Restrict registered names for selected hierarchical schemes to DNS-style labels.
|
|
108
|
+
const dnsHostRules = {
|
|
109
|
+
scheme: dnsHostSchemesPattern,
|
|
110
110
|
reg_name: '(?:(?=.{1,255}(?:[:/?#]|$))(?:{a_label})(?:\\.{a_label})*)',
|
|
111
111
|
a_label: '(?:{alpha_digit})(?:(?:{alpha_digit}|-){0,61}(?:{alpha_digit}))?',
|
|
112
112
|
ireg_name: '(?:(?=.{1,255}(?:[:/?#]|$))(?:{u_label})(?:{u_separator}(?:{u_label}))*)',
|
|
@@ -115,13 +115,13 @@ const schemeSpecificRules = {
|
|
|
115
115
|
u_char: '[\\p{L}\\p{N}\\p{Mn}\\p{Mc}\\u200C\\u200D\\u00B7\\u0375\\u30FB\\u05F3\\u05F4]',
|
|
116
116
|
};
|
|
117
117
|
// Recognize RFC 8089's empty file authority without weakening other scheme host policies.
|
|
118
|
-
const emptyFileHostRules = Object.assign({},
|
|
118
|
+
const emptyFileHostRules = Object.assign({}, dnsHostRules, {
|
|
119
119
|
scheme: '[fF][iI][lL][eE]',
|
|
120
120
|
reg_name: '',
|
|
121
121
|
ireg_name: '',
|
|
122
122
|
});
|
|
123
|
-
//
|
|
124
|
-
const
|
|
123
|
+
// Map grammar productions to the public named captures returned by parsers.
|
|
124
|
+
const captureGroupNames = {
|
|
125
125
|
scheme: 'scheme',
|
|
126
126
|
port: 'port',
|
|
127
127
|
authority: 'authority',
|
|
@@ -148,20 +148,23 @@ const groupNames = {
|
|
|
148
148
|
NSS: 'nss',
|
|
149
149
|
r_component: 'rComponent',
|
|
150
150
|
q_component: 'qComponent',
|
|
151
|
+
f_component: 'fComponent',
|
|
151
152
|
};
|
|
152
|
-
// Detect schemes
|
|
153
|
-
const
|
|
154
|
-
//
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
// Detect schemes whose registered names use the DNS-host grammar.
|
|
154
|
+
const usesDnsHostRules = (string) => new RegExp('^' + dnsHostSchemesPattern + ':').test(string);
|
|
155
|
+
// Grammar profiles: '' is generic, 's' uses DNS-host rules, 'f' permits an empty file host, and 'u' is URN.
|
|
156
|
+
const grammarProfile = (string) => (string.slice(0, 4).toLowerCase() === 'urn:' ? 'u' : string.slice(0, 8).toLowerCase() === 'file:///' ? 'f' : usesDnsHostRules(string) ? 's' : '');
|
|
157
|
+
// Select and merge the rules for the active grammar profile.
|
|
158
|
+
const grammarRules = (profile) => Object.assign({}, commonRules, uriRules, iriRules, profile === 'u' ? urnRules : profile === 'f' ? emptyFileHostRules : profile ? dnsHostRules : {});
|
|
159
|
+
// Compile and execute a grammar with named component captures.
|
|
158
160
|
const parse = (string, rule) => {
|
|
159
161
|
if (typeof string !== 'string') throw new TypeError(`Invalid ${rule.replace('_', '-')} type: must be a string.`);
|
|
160
|
-
const profile =
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
const profile = grammarProfile(string);
|
|
163
|
+
// Wrap each public component production in its associated named capture.
|
|
164
|
+
const addNamedCapture = (key) => (captureGroupNames[key] ? `(?<${captureGroupNames[key]}>${grammarRules(profile)[key]})` : grammarRules(profile)[key]);
|
|
165
|
+
const cacheKey = '_' + profile + rule;
|
|
166
|
+
if (!patternCache.has(cacheKey)) patternCache.set(cacheKey, new RegExp(`^${recursiveCompile(grammarRules(profile), rule, addNamedCapture)}$`, 'u'));
|
|
167
|
+
const match = patternCache.get(cacheKey).exec(string);
|
|
165
168
|
if (match) {
|
|
166
169
|
Object.defineProperty(match.groups, 'normalize', {
|
|
167
170
|
// Normalize this parsed result only when its optional method is called.
|
|
@@ -173,17 +176,17 @@ const parse = (string, rule) => {
|
|
|
173
176
|
}
|
|
174
177
|
throw new SyntaxError(`Invalid ${rule.replace('_', '-')}: ${string}`);
|
|
175
178
|
};
|
|
176
|
-
//
|
|
179
|
+
// Compile and test a capture-free grammar for validation.
|
|
177
180
|
const validate = (string, rule) => {
|
|
178
181
|
if (typeof string !== 'string') throw new TypeError(`Invalid ${rule.replace('_', '-')} type: must be a string.`);
|
|
179
|
-
const profile =
|
|
180
|
-
const
|
|
181
|
-
if (!
|
|
182
|
-
if (
|
|
182
|
+
const profile = grammarProfile(string);
|
|
183
|
+
const cacheKey = profile + rule;
|
|
184
|
+
if (!patternCache.has(cacheKey)) patternCache.set(cacheKey, new RegExp(`^${recursiveCompile(grammarRules(profile), rule)}$`, 'u'));
|
|
185
|
+
if (patternCache.get(cacheKey).test(string)) return true;
|
|
183
186
|
throw new SyntaxError(`Invalid ${rule.replace('_', '-')}: ${string}`);
|
|
184
187
|
};
|
|
185
|
-
//
|
|
186
|
-
function
|
|
188
|
+
// Serialize scheme, authority, path, query, and fragment slots using RFC 3986 delimiters.
|
|
189
|
+
function composeReference(parts = {}) {
|
|
187
190
|
let result = '';
|
|
188
191
|
if (parts.scheme) result += parts.scheme + ':';
|
|
189
192
|
if (parts.authority !== undefined && parts.authority !== null) result += '//' + parts.authority;
|
|
@@ -192,7 +195,8 @@ function compose(parts = {}) {
|
|
|
192
195
|
if (parts.fragment !== undefined && parts.fragment !== null) result += '#' + parts.fragment;
|
|
193
196
|
return result;
|
|
194
197
|
}
|
|
195
|
-
//
|
|
198
|
+
// Local abbreviations: idx is an index and seg is a path segment.
|
|
199
|
+
// Remove complete dot segments using the RFC 3986 Section 5.2.4 algorithm.
|
|
196
200
|
function removeDotSegments(path) {
|
|
197
201
|
const output = [];
|
|
198
202
|
let input = path ?? '';
|
|
@@ -237,8 +241,9 @@ function removeDotSegments(path) {
|
|
|
237
241
|
}
|
|
238
242
|
return output.join('');
|
|
239
243
|
}
|
|
240
|
-
//
|
|
241
|
-
|
|
244
|
+
// RFC 3986 resolution notation: B is the base, R is the reference, and T is the target.
|
|
245
|
+
// Resolve a reference according to RFC 3986 Section 5.2.
|
|
246
|
+
function resolveReference(reference, base, strict = true, returnParts = false) {
|
|
242
247
|
let B;
|
|
243
248
|
if (typeof base === 'string') {
|
|
244
249
|
B = parse(base, 'IRI');
|
|
@@ -287,14 +292,14 @@ function resolveReference(reference, base, strict = true, parts = false) {
|
|
|
287
292
|
}
|
|
288
293
|
T.fragment = R.fragment;
|
|
289
294
|
}
|
|
290
|
-
if (
|
|
291
|
-
return
|
|
295
|
+
if (returnParts) return T;
|
|
296
|
+
return composeReference(T);
|
|
292
297
|
}
|
|
293
298
|
// Convert a complete IRI to fragment-free form without changing its other components.
|
|
294
299
|
function toAbsoluteReference(string) {
|
|
295
300
|
const result = parse(string, 'IRI');
|
|
296
301
|
result.fragment = undefined;
|
|
297
|
-
return
|
|
302
|
+
return composeReference(result);
|
|
298
303
|
}
|
|
299
304
|
// Generate a relative reference when resolution is stable, otherwise retain the absolute target.
|
|
300
305
|
const toRelativeReference = (target, base) => {
|
|
@@ -373,27 +378,27 @@ function normalizePercentEncoding(value, decodeUnreserved = true) {
|
|
|
373
378
|
}
|
|
374
379
|
// Expand any accepted IPv6 spelling into eight numeric 16-bit fields.
|
|
375
380
|
function parseIPv6Words(address) {
|
|
376
|
-
let
|
|
381
|
+
let addressText = address;
|
|
377
382
|
// Convert a dotted-decimal tail to the same two-field representation used by every later step.
|
|
378
|
-
const lastColon =
|
|
379
|
-
const lastSegment =
|
|
383
|
+
const lastColon = addressText.lastIndexOf(':');
|
|
384
|
+
const lastSegment = addressText.slice(lastColon + 1);
|
|
380
385
|
if (lastSegment.includes('.')) {
|
|
381
386
|
const octets = lastSegment.split('.');
|
|
382
|
-
const
|
|
383
|
-
const
|
|
384
|
-
|
|
387
|
+
const highWord = Number(octets[0]) * 0x100 + Number(octets[1]);
|
|
388
|
+
const lowWord = Number(octets[2]) * 0x100 + Number(octets[3]);
|
|
389
|
+
addressText = `${addressText.slice(0, lastColon + 1)}${highWord.toString(16)}:${lowWord.toString(16)}`;
|
|
385
390
|
}
|
|
386
391
|
|
|
387
|
-
const
|
|
388
|
-
const leftText =
|
|
389
|
-
const rightText =
|
|
392
|
+
const compressionIndex = addressText.indexOf('::');
|
|
393
|
+
const leftText = compressionIndex === -1 ? addressText : addressText.slice(0, compressionIndex);
|
|
394
|
+
const rightText = compressionIndex === -1 ? '' : addressText.slice(compressionIndex + 2);
|
|
390
395
|
const left = leftText ? leftText.split(':') : [];
|
|
391
396
|
const right = rightText ? rightText.split(':') : [];
|
|
392
397
|
const words = [];
|
|
393
398
|
// Retain every explicit field before the compressed zero run.
|
|
394
399
|
for (const field of left) words.push(Number.parseInt(field, 16));
|
|
395
400
|
// Expand the single compression marker to the required number of zero fields.
|
|
396
|
-
if (
|
|
401
|
+
if (compressionIndex !== -1) {
|
|
397
402
|
// Fill the omitted field count determined from both explicit sides.
|
|
398
403
|
for (let index = left.length + right.length; index < 8; index++) words.push(0);
|
|
399
404
|
}
|
|
@@ -423,7 +428,7 @@ function serializeIPv6Words(words) {
|
|
|
423
428
|
if (bestLength < 2) bestStart = -1;
|
|
424
429
|
|
|
425
430
|
const fields = [];
|
|
426
|
-
//
|
|
431
|
+
// Render each field without leading hexadecimal zeroes.
|
|
427
432
|
for (const word of words) fields.push(word.toString(16));
|
|
428
433
|
if (bestStart === -1) return fields.join(':');
|
|
429
434
|
const before = fields.slice(0, bestStart).join(':');
|
|
@@ -435,11 +440,11 @@ function normalizeIPv6Address(address) {
|
|
|
435
440
|
const words = parseIPv6Words(address);
|
|
436
441
|
// Detect standardized prefixes that identify an embedded IPv4 address from address bits alone.
|
|
437
442
|
const low32 = words[6] * 0x10000 + words[7];
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
if (
|
|
443
|
+
const isCompatible = words[0] === 0 && words[1] === 0 && words[2] === 0 && words[3] === 0 && words[4] === 0 && words[5] === 0 && low32 > 1;
|
|
444
|
+
const isMapped = words[0] === 0 && words[1] === 0 && words[2] === 0 && words[3] === 0 && words[4] === 0 && words[5] === 0xFFFF;
|
|
445
|
+
const isTranslated = words[0] === 0 && words[1] === 0 && words[2] === 0 && words[3] === 0 && words[4] === 0xFFFF && words[5] === 0;
|
|
446
|
+
const isWellKnownNat64 = words[0] === 0x64 && words[1] === 0xFF9B && words[2] === 0 && words[3] === 0 && words[4] === 0 && words[5] === 0;
|
|
447
|
+
if (isCompatible || isMapped || isTranslated || isWellKnownNat64) {
|
|
443
448
|
const prefix = serializeIPv6Words(words.slice(0, 6));
|
|
444
449
|
const ipv4 = `${words[6] >>> 8}.${words[6] & 0xFF}.${words[7] >>> 8}.${words[7] & 0xFF}`;
|
|
445
450
|
return prefix.endsWith(':') ? prefix + ipv4 : `${prefix}:${ipv4}`;
|
|
@@ -480,20 +485,20 @@ function normalizeHost(host, mapRegName) {
|
|
|
480
485
|
if (!mapRegName && isIPv4Address(result)) result = normalizePercentEncoding(encodedResult, false);
|
|
481
486
|
return /[^\x00-\x7F]/u.test(result) ? result : lowercaseAsciiHost(result);
|
|
482
487
|
}
|
|
483
|
-
//
|
|
488
|
+
// Omit an empty or default port only for HTTP and WebSocket schemes.
|
|
484
489
|
function normalizePort(scheme, port) {
|
|
485
490
|
if (port === undefined) return undefined;
|
|
486
491
|
const defaultPort = scheme === 'http' || scheme === 'ws' ? '80' : scheme === 'https' || scheme === 'wss' ? '443' : undefined;
|
|
487
492
|
if (defaultPort !== undefined && (port === '' || port.replace(/^0+(?=\d)/, '') === defaultPort)) return undefined;
|
|
488
493
|
return port;
|
|
489
494
|
}
|
|
490
|
-
// Encode each non-ASCII Unicode scalar as uppercase UTF-8 percent triplets.
|
|
491
|
-
function
|
|
495
|
+
// Encode each non-ASCII Unicode scalar as uppercase UTF-8 percent triplets for URI output.
|
|
496
|
+
function encodeNonAsciiForUri(component) {
|
|
492
497
|
// Process complete code points so supplementary characters produce one UTF-8 sequence.
|
|
493
498
|
return component.replace(/[^\x00-\x7F]/gu, (character) => encodeURIComponent(character).toUpperCase());
|
|
494
499
|
}
|
|
495
|
-
// Decode the maximal RFC 3987
|
|
496
|
-
function
|
|
500
|
+
// Decode the maximal RFC 3987 character repertoire allowed by one IRI component.
|
|
501
|
+
function decodeUriTextToIri(component, allowPrivateUse = false) {
|
|
497
502
|
let result = '';
|
|
498
503
|
// Inspect each normalized percent triplet as either ASCII or the lead of one strict UTF-8 scalar.
|
|
499
504
|
for (let index = 0; index < component.length; index++) {
|
|
@@ -509,28 +514,28 @@ function decodeUriComponentToIri(component, allowPrivate = false) {
|
|
|
509
514
|
continue;
|
|
510
515
|
}
|
|
511
516
|
const sequenceLength = octet >= 0xC2 && octet <= 0xDF ? 2 : octet >= 0xE0 && octet <= 0xEF ? 3 : octet >= 0xF0 && octet <= 0xF4 ? 4 : 0;
|
|
512
|
-
let
|
|
517
|
+
let encodedSequence = '';
|
|
513
518
|
// Collect exactly one candidate scalar without consuming malformed trailing input.
|
|
514
519
|
for (let sequenceIndex = 0; sequenceIndex < sequenceLength; sequenceIndex++) {
|
|
515
520
|
const position = index + sequenceIndex * 3;
|
|
516
521
|
if (component[position] !== '%' || !/^[0-9A-F]{2}$/.test(component.slice(position + 1, position + 3))) {
|
|
517
|
-
|
|
522
|
+
encodedSequence = '';
|
|
518
523
|
break;
|
|
519
524
|
}
|
|
520
|
-
|
|
525
|
+
encodedSequence += component.slice(position, position + 3);
|
|
521
526
|
}
|
|
522
527
|
let character;
|
|
523
|
-
if (
|
|
528
|
+
if (encodedSequence) {
|
|
524
529
|
try {
|
|
525
|
-
character = decodeURIComponent(
|
|
530
|
+
character = decodeURIComponent(encodedSequence);
|
|
526
531
|
} catch {
|
|
527
532
|
character = undefined;
|
|
528
533
|
}
|
|
529
534
|
}
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
535
|
+
const isAllowedIriCharacter = character !== undefined && !forbiddenIriFormattingPattern.test(character) && (iriUcscharPattern.test(character) || (allowPrivateUse && iriPrivatePattern.test(character)));
|
|
536
|
+
if (isAllowedIriCharacter) {
|
|
532
537
|
result += character;
|
|
533
|
-
index +=
|
|
538
|
+
index += encodedSequence.length - 1;
|
|
534
539
|
} else {
|
|
535
540
|
result += `%${hexadecimal}`;
|
|
536
541
|
index += 2;
|
|
@@ -538,7 +543,7 @@ function decodeUriComponentToIri(component, allowPrivate = false) {
|
|
|
538
543
|
}
|
|
539
544
|
return result;
|
|
540
545
|
}
|
|
541
|
-
// Rebuild authority from normalized
|
|
546
|
+
// Rebuild authority from normalized userinfo, host, and port while preserving their presence.
|
|
542
547
|
function normalizeAuthority(parts, scheme, mapRegName) {
|
|
543
548
|
if (parts.authority === undefined) return undefined;
|
|
544
549
|
const userinfo = parts.userinfo === undefined ? undefined : normalizePercentEncoding(parts.userinfo);
|
|
@@ -561,12 +566,12 @@ function normalizeParsedReference(parts, options = {}) {
|
|
|
561
566
|
if (parts.nid !== undefined) {
|
|
562
567
|
const rComponent = parts.rComponent === undefined ? undefined : normalizePercentEncoding(parts.rComponent, false);
|
|
563
568
|
const qComponent = parts.qComponent === undefined ? undefined : normalizePercentEncoding(parts.qComponent, false);
|
|
564
|
-
const
|
|
565
|
-
return
|
|
569
|
+
const rqComponentText = rComponent !== undefined ? `+${rComponent}${qComponent === undefined ? '' : `?=${qComponent}`}` : qComponent === undefined ? undefined : `=${qComponent}`;
|
|
570
|
+
return composeReference({ scheme: parts.scheme.toLowerCase(), path: `${parts.nid.toLowerCase()}:${normalizePercentEncoding(parts.nss, false)}`, query: rqComponentText, fragment: parts.fComponent === undefined ? undefined : normalizePercentEncoding(parts.fComponent, false) });
|
|
566
571
|
}
|
|
567
572
|
// Normalize each component independently so encoded delimiters cannot become structure.
|
|
568
573
|
const scheme = parts.scheme === undefined ? undefined : parts.scheme.toLowerCase();
|
|
569
|
-
const
|
|
574
|
+
const normalizedParts = {
|
|
570
575
|
scheme,
|
|
571
576
|
authority: normalizeAuthority(parts, scheme, mapRegName),
|
|
572
577
|
path: normalizePercentEncoding(parts.path),
|
|
@@ -574,31 +579,31 @@ function normalizeParsedReference(parts, options = {}) {
|
|
|
574
579
|
fragment: parts.fragment === undefined ? undefined : normalizePercentEncoding(parts.fragment),
|
|
575
580
|
};
|
|
576
581
|
// Use the slash form defined for an empty HTTP or WebSocket authority path.
|
|
577
|
-
if (
|
|
582
|
+
if (normalizedParts.authority !== undefined && normalizedParts.path === '' && (scheme === 'http' || scheme === 'https' || scheme === 'ws' || scheme === 'wss')) normalizedParts.path = '/';
|
|
578
583
|
// Limit dot-segment removal to paths whose standalone interpretation remains stable.
|
|
579
|
-
const rootlessRelativePath =
|
|
584
|
+
const rootlessRelativePath = normalizedParts.scheme === undefined && normalizedParts.authority === undefined && normalizedParts.path.length > 0 && !normalizedParts.path.startsWith('/');
|
|
580
585
|
if (!rootlessRelativePath) {
|
|
581
|
-
const reducedPath = removeDotSegments(
|
|
586
|
+
const reducedPath = removeDotSegments(normalizedParts.path);
|
|
582
587
|
// Preserve a no-authority path when reduction would reparse it as an authority.
|
|
583
|
-
if (
|
|
588
|
+
if (normalizedParts.authority !== undefined || !reducedPath.startsWith('//')) normalizedParts.path = reducedPath;
|
|
584
589
|
}
|
|
585
|
-
// Select an explicit target representation only after
|
|
590
|
+
// Select an explicit target representation only after component normalization is complete.
|
|
586
591
|
if (transform === 'URI') {
|
|
587
|
-
//
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
if (
|
|
591
|
-
if (
|
|
592
|
+
// Percent-encode every non-ASCII authority, path, query, and fragment scalar for URI output.
|
|
593
|
+
if (normalizedParts.authority !== undefined) normalizedParts.authority = encodeNonAsciiForUri(normalizedParts.authority);
|
|
594
|
+
normalizedParts.path = encodeNonAsciiForUri(normalizedParts.path);
|
|
595
|
+
if (normalizedParts.query !== undefined) normalizedParts.query = encodeNonAsciiForUri(normalizedParts.query);
|
|
596
|
+
if (normalizedParts.fragment !== undefined) normalizedParts.fragment = encodeNonAsciiForUri(normalizedParts.fragment);
|
|
592
597
|
} else if (transform === 'IRI') {
|
|
593
598
|
// Decode valid UTF-8 percent sequences only where the destination component permits their scalar.
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
if (
|
|
597
|
-
if (
|
|
599
|
+
if (normalizedParts.authority !== undefined) normalizedParts.authority = decodeUriTextToIri(normalizedParts.authority);
|
|
600
|
+
normalizedParts.path = decodeUriTextToIri(normalizedParts.path);
|
|
601
|
+
if (normalizedParts.query !== undefined) normalizedParts.query = decodeUriTextToIri(normalizedParts.query, true);
|
|
602
|
+
if (normalizedParts.fragment !== undefined) normalizedParts.fragment = decodeUriTextToIri(normalizedParts.fragment);
|
|
598
603
|
}
|
|
599
|
-
return
|
|
604
|
+
return composeReference(normalizedParts);
|
|
600
605
|
}
|
|
601
|
-
//
|
|
606
|
+
// Expose the public validation, parsing, resolution, and conversion API.
|
|
602
607
|
module.exports = {
|
|
603
608
|
isUUID: (string) => validate(string, 'UUID'),
|
|
604
609
|
isUUIDv4: (string) => validate(string, 'UUID_v4'),
|
package/normalization.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# URI and IRI normalization
|
|
2
2
|
|
|
3
|
-
Parsed URI and IRI results expose `normalize()` for syntax
|
|
3
|
+
Parsed URI and IRI results expose `normalize()` for generic syntax normalization, scheme-specific HTTP and WebSocket forms, the separate RFC 8141 URN normalization path, and optional RFC 3987 URI/IRI representation transformation. The method returns a string and leaves the parsed components unchanged.
|
|
4
4
|
|
|
5
5
|
## API
|
|
6
6
|
|
|
@@ -44,13 +44,13 @@ parsed.path;
|
|
|
44
44
|
// /%7e/a/../b
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## Generic URI and IRI syntax normalization
|
|
48
48
|
|
|
49
49
|
| Behavior | Implementation | Source |
|
|
50
50
|
| --- | --- | --- |
|
|
51
51
|
| Case normalization | Lowercase the scheme and an ASCII-only host. Uppercase hexadecimal letters in percent triplets. | [RFC 3986 §6.2.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.1), [RFC 3987 §5.3.2.1](https://www.rfc-editor.org/rfc/rfc3987#section-5.3.2.1) |
|
|
52
|
-
| Percent-encoded unreserved characters |
|
|
53
|
-
| Path segments | Apply the RFC dot-segment algorithm where a generic
|
|
52
|
+
| Percent-encoded unreserved characters | Decode percent triplets representing ASCII letters, digits, `-`, `.`, `_`, or `~`. Retain percent encoding for reserved octets. | [RFC 3986 §§2.2–2.4 and 6.2.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.2), [RFC 3987 §5.3.2.3](https://www.rfc-editor.org/rfc/rfc3987#section-5.3.2.3) |
|
|
53
|
+
| Path segments | Apply the RFC dot-segment algorithm where a parsed generic reference can be normalized independently. Preserve unresolved rootless-relative path semantics. | [RFC 3986 §§5.2.4 and 6.2.2.3](https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4), [RFC 3987 §5.3.2.4](https://www.rfc-editor.org/rfc/rfc3987#section-5.3.2.4) |
|
|
54
54
|
| Component recomposition | Emit authority, query, and fragment delimiters from component presence, including present-empty components. | [RFC 3986 §5.3](https://www.rfc-editor.org/rfc/rfc3986#section-5.3) |
|
|
55
55
|
| IPv6 text | Suppress leading zeroes, compress the longest zero run with first-run tie breaking, and use lowercase hexadecimal. Known embedded-IPv4 forms use mixed notation. | [RFC 5952 §§4–5](https://www.rfc-editor.org/rfc/rfc5952#section-4) |
|
|
56
56
|
| IRI-to-URI output | With `transform: 'URI'`, encode non-ASCII authority, path, query, and fragment characters as uppercase UTF-8 percent triplets. | [RFC 3987 §3.1](https://www.rfc-editor.org/rfc/rfc3987#section-3.1) |
|
|
@@ -58,31 +58,9 @@ parsed.path;
|
|
|
58
58
|
|
|
59
59
|
Without a mapper, normalization retains the parser's host classification as an IP literal, IPv4 address, or registered name. IPvFuture literals use generic host case normalization. Existing non-ASCII IRI host text is retained unless the registered-name mapper supplies another value.
|
|
60
60
|
|
|
61
|
-
##
|
|
61
|
+
## Scheme-specific normalization
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
| Input component | Output | Source |
|
|
66
|
-
| --- | --- | --- |
|
|
67
|
-
| Scheme | Convert `urn` to lowercase. | [RFC 8141 §3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
68
|
-
| NID | Convert ASCII letters to lowercase. | [RFC 8141 §§2.1 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
69
|
-
| NSS | Uppercase hexadecimal letters in percent triplets without decoding any octet. Preserve literal case, slash structure, and dot segments. | [RFC 8141 §§2.2 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
70
|
-
| r-, q-, and f-components | Retain the components and their delimiters, uppercasing hexadecimal letters in percent triplets without decoding. | [RFC 8141 §2.3](https://www.rfc-editor.org/rfc/rfc8141#section-2.3), [RFC 3986 §6.2.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.1) |
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
const { parseUri } = require('identifier-js');
|
|
74
|
-
|
|
75
|
-
parseUri('URN:EXAMPLE:a%62/./b/../C?+r%2f?=q%2f#f%2f').normalize();
|
|
76
|
-
// urn:example:a%62/./b/../C?+r%2F?=q%2F#f%2F
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
RFC 8141 URNs remain ASCII, including when parsed through an IRI operation. Consequently, `transform: 'URI'` and `transform: 'IRI'` produce the same URN representation, and `mapRegName` is not called because a URN has no authority or registered-name host.
|
|
80
|
-
|
|
81
|
-
For a parsed URN, the current scheme-specific fields are the normalization input. The NSS and optional-component values stay opaque except for percent-triplet letter case. The method leaves every property unchanged.
|
|
82
|
-
|
|
83
|
-
Normalization is not a URN-equivalence API. RFC 8141 equivalence compares the normalized assigned-name and ignores r-, q-, and f-components; namespace definitions can add further equivalence rules. This method instead retains those optional components in its returned string. The package does not implement generic or namespace-specific URN-equivalence comparison.
|
|
84
|
-
|
|
85
|
-
## HTTP and HTTPS
|
|
63
|
+
### HTTP and HTTPS
|
|
86
64
|
|
|
87
65
|
For `http` and `https`, normalization applies the generic rules and these scheme rules:
|
|
88
66
|
|
|
@@ -101,7 +79,7 @@ https://example.com:00443/a → https://example.com/a
|
|
|
101
79
|
http://example.com:/a → http://example.com/a
|
|
102
80
|
```
|
|
103
81
|
|
|
104
|
-
|
|
82
|
+
### WS and WSS
|
|
105
83
|
|
|
106
84
|
For `ws` and `wss`, normalization applies the generic rules and these scheme rules:
|
|
107
85
|
|
|
@@ -118,11 +96,35 @@ wss://example.com:00443/chat → wss://example.com/chat
|
|
|
118
96
|
ws://example.com?channel=updates → ws://example.com/?channel=updates
|
|
119
97
|
```
|
|
120
98
|
|
|
99
|
+
### URNs
|
|
100
|
+
|
|
101
|
+
A parsed value under the case-insensitive `urn` scheme takes the separate RFC 8141 normalization path using its captured `scheme`, `nid`, `nss`, `rComponent`, `qComponent`, and `fComponent` properties.
|
|
102
|
+
|
|
103
|
+
| Input component | Output | Source |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| Scheme | Convert `urn` to lowercase. | [RFC 8141 §3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
106
|
+
| NID | Convert ASCII letters to lowercase. | [RFC 8141 §§2.1 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
107
|
+
| NSS | Uppercase hexadecimal letters in percent triplets without decoding any octet. Preserve literal case, slash structure, and dot segments. | [RFC 8141 §§2.2 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
|
|
108
|
+
| r-, q-, and f-components | Retain the components and their delimiters, uppercasing hexadecimal letters in percent triplets without decoding. | [RFC 8141 §2.3](https://www.rfc-editor.org/rfc/rfc8141#section-2.3), [RFC 3986 §6.2.2.1](https://www.rfc-editor.org/rfc/rfc3986#section-6.2.2.1) |
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const { parseUri } = require('identifier-js');
|
|
112
|
+
|
|
113
|
+
parseUri('URN:EXAMPLE:a%62/./b/../C?+r%2f?=q%2f#f%2f').normalize();
|
|
114
|
+
// urn:example:a%62/./b/../C?+r%2F?=q%2F#f%2F
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
RFC 8141 URNs remain ASCII, including when parsed through an IRI operation. Consequently, `transform: 'URI'` and `transform: 'IRI'` produce the same URN representation, and `mapRegName` is not called because a URN has no authority or registered-name host.
|
|
118
|
+
|
|
119
|
+
For a parsed URN, the current URN-specific fields are the normalization input. The NSS and optional-component values stay opaque except for percent-triplet letter case. The method leaves every property unchanged.
|
|
120
|
+
|
|
121
|
+
Normalization is not a URN-equivalence API. RFC 8141 equivalence compares the normalized assigned name and ignores r-, q-, and f-components; namespace definitions can add further equivalence rules. This method instead retains those optional components in its returned string. The package does not implement generic or namespace-specific URN-equivalence comparison.
|
|
122
|
+
|
|
121
123
|
## Registered-name mapping and representation transformation
|
|
122
124
|
|
|
123
125
|
For a non-empty registered-name host, `options.mapRegName` is called once with the current host spelling before built-in normalization. IP literals, IPv4 addresses, absent hosts, and empty hosts bypass the mapper.
|
|
124
126
|
|
|
125
|
-
The mapper owns the returned text and all registered-name validation, representation, and host-kind policy. This package enforces only the declared string return type. It does not check whether mapper output is non-empty, remains a registered name, introduces component delimiters, resembles an IP address, or satisfies
|
|
127
|
+
The mapper owns the returned text and all registered-name validation, representation, and host-kind policy. This package enforces only the declared string return type. It does not check whether mapper output is non-empty, remains a registered name, introduces component delimiters, resembles an IP address, or satisfies the DNS-host grammar. The returned string then receives percent-triplet and ASCII host-case normalization. Mapper exceptions propagate unchanged.
|
|
126
128
|
|
|
127
129
|
```js
|
|
128
130
|
const mapped = parseIriReference('x://example').normalize({
|
|
@@ -155,7 +157,7 @@ ACE-to-Unicode and Unicode-to-ACE registered-name conversion remain application
|
|
|
155
157
|
|
|
156
158
|
## Verification
|
|
157
159
|
|
|
158
|
-
The normalization suite covers URI and IRI parser results, component presence, percent triplets, dot segments, host kinds, RFC 5952 output, HTTP and WebSocket scheme rules, registered-name mapping, both RFC representation transformations, malformed UTF-8 retention, component-specific Unicode repertoires, component non-mutation, round trips, and idempotence.
|
|
160
|
+
The normalization suite covers generic URI and IRI parser results, RFC 8141 URNs, component presence, percent triplets, dot segments, host kinds, RFC 5952 output, HTTP and WebSocket scheme rules, registered-name mapping, both RFC representation transformations, malformed UTF-8 retention, component-specific Unicode repertoires, component non-mutation, round trips, and idempotence.
|
|
159
161
|
|
|
160
162
|
```sh
|
|
161
163
|
npm test
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
title: Identifier JS
|
|
4
4
|
|
|
5
|
-
description: RFC 3986/3987 URI and IRI tools with
|
|
5
|
+
description: RFC 3986/3987 URI and IRI tools with RFC 8141 URN grammar and normalization support.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Identifier JS
|
|
10
10
|
|
|
11
|
-
`identifier-js` is a URI/IRI parser, validator, normalizer, resolver, and
|
|
11
|
+
`identifier-js` is a URI/IRI parser, validator, normalizer, resolver, and reference converter based on RFC [3986](https://www.rfc-editor.org/rfc/rfc3986) and RFC [3987](https://www.rfc-editor.org/rfc/rfc3987), with an RFC [8141](https://www.rfc-editor.org/rfc/rfc8141) URN grammar profile. HTTP, WebSocket, and `file` identifiers use the documented DNS-host grammar where applicable. It provides:
|
|
12
12
|
|
|
13
13
|
- URI and IRI validation, including RFC 8141 URN namestring syntax;
|
|
14
|
-
- parsed generic URI/IRI components and
|
|
15
|
-
- conservative syntax normalization,
|
|
14
|
+
- parsed generic URI/IRI components and URN-specific components;
|
|
15
|
+
- conservative syntax normalization, scheme-specific forms, and a registered-name extension point;
|
|
16
16
|
- RFC 3986 reference resolution and dot-segment removal;
|
|
17
17
|
- relative-reference generation with resolution round-trip guarantees for supported forms;
|
|
18
18
|
- UUID and UUIDv4 lexical validation;
|
|
@@ -28,7 +28,7 @@ npm install identifier-js
|
|
|
28
28
|
|
|
29
29
|
## API
|
|
30
30
|
|
|
31
|
-
Every validator returns `true`
|
|
31
|
+
Every validator returns `true` for valid input and otherwise throws. Parsing and reference operations also throw when their input does not satisfy the required grammar.
|
|
32
32
|
|
|
33
33
|
### Validate URI syntax
|
|
34
34
|
|
|
@@ -143,45 +143,6 @@ console.log(parseIri('https://usér@例え.テスト:8443/résumé?lang=fr#profi
|
|
|
143
143
|
|
|
144
144
|
</details>
|
|
145
145
|
|
|
146
|
-
### Validate and parse RFC 8141 URNs
|
|
147
|
-
|
|
148
|
-
URNs use the existing URI and IRI operations because a URN is a URI under the `urn` scheme. Values with a case-insensitive `urn:` prefix are validated against RFC 8141 namestring syntax; no separate `isUrn` or `parseUrn` API is exported.
|
|
149
|
-
|
|
150
|
-
```text
|
|
151
|
-
urn:NID:NSS[?+r-component][?=q-component][#f-component]
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
The NID contains 2–32 ASCII characters, starts and ends with a letter or digit, and permits letters, digits, or hyphens internally. The NSS begins with an RFC 3986 `pchar` and then permits `pchar` or `/`. The ordered r- and q-components also begin with `pchar` and then permit `pchar`, `/`, or `?`, while an f-component can be empty. The first `?=` sequence after an r-component starts the q-component, and any other question mark outside an optional component is rejected.
|
|
155
|
-
|
|
156
|
-
<details>
|
|
157
|
-
<summary><strong>API behavior and examples</strong></summary>
|
|
158
|
-
|
|
159
|
-
```js
|
|
160
|
-
const { isUri, isIri, parseUri } = require('identifier-js');
|
|
161
|
-
|
|
162
|
-
const value = 'URN:Example:a%2f/../B?+service?x?=key=value#part';
|
|
163
|
-
console.log(isUri(value)); // true
|
|
164
|
-
console.log(isIri(value)); // true
|
|
165
|
-
|
|
166
|
-
const parsed = parseUri(value);
|
|
167
|
-
console.log(parsed.scheme); // URN
|
|
168
|
-
console.log(parsed.nid); // Example
|
|
169
|
-
console.log(parsed.nss); // a%2f/../B
|
|
170
|
-
console.log(parsed.rComponent); // service?x
|
|
171
|
-
console.log(parsed.qComponent); // key=value
|
|
172
|
-
console.log(parsed.fragment); // part
|
|
173
|
-
console.log(parsed.normalize());
|
|
174
|
-
// urn:example:a%2F/../B?+service?x?=key=value#part
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
URN parse results expose `nid`, `nss`, `rComponent`, and `qComponent`, while the RFC-defined f-component is exposed as `fragment`. They do not expose generic `path` or `query` aliases. To require a URN after parsing a value accepted as a general URI, check `parsed.scheme.toLowerCase() === 'urn'`.
|
|
178
|
-
|
|
179
|
-
URNs remain ASCII even through the IRI operations. Callers representing non-ASCII names must first encode them as UTF-8 and then percent-encode the resulting octets; lexical validation does not decode or verify those octet sequences.
|
|
180
|
-
|
|
181
|
-
Validation is deliberately lexical and namespace-independent. Success does not prove that an NID is registered or otherwise legitimate, that an NSS obeys a namespace's additional syntax and canonicalization rules, or that the name was legitimately assigned.
|
|
182
|
-
|
|
183
|
-
</details>
|
|
184
|
-
|
|
185
146
|
### Resolve a reference
|
|
186
147
|
|
|
187
148
|
Resolve a URI or IRI reference against an absolute base using RFC 3986 §5.
|
|
@@ -212,7 +173,7 @@ console.log(resolveReference('?page=2', 'https://example.com/items?page=1#curren
|
|
|
212
173
|
|
|
213
174
|
Empty authorities, queries, and fragments are preserved during recomposition.
|
|
214
175
|
|
|
215
|
-
|
|
176
|
+
`resolveReference` does not apply when either input uses the `urn` scheme. URN resolution services are outside this package's scope.
|
|
216
177
|
|
|
217
178
|
</details>
|
|
218
179
|
|
|
@@ -242,7 +203,7 @@ console.log(relative); // ../images/logo.svg
|
|
|
242
203
|
|
|
243
204
|
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. Complete dot segments in either path also trigger this fallback because RFC resolution removes them. For those inputs, resolving the result produces the same identifier as resolving the target directly; lexical dot-segment spelling is not preserved.
|
|
244
205
|
|
|
245
|
-
|
|
206
|
+
`toAbsoluteReference` and `toRelativeReference` do not apply when an input uses the `urn` scheme. Relative-URN semantics are outside this package's scope.
|
|
246
207
|
|
|
247
208
|
</details>
|
|
248
209
|
|
|
@@ -284,7 +245,7 @@ Normalization implements RFC 3986 and RFC 3987 syntax normalization for scheme a
|
|
|
284
245
|
|
|
285
246
|
URN normalization lowercases the scheme and NID, uppercases percent-triplet hexadecimal letters without decoding, and preserves NSS case, slashes, and dot segments. The r-, q-, and f-components are retained, so normalized-string equality is not the RFC 8141 URN-equivalence procedure. Namespace-specific equivalence and URN resolution are outside this package's scope. URI/IRI transformation and registered-name mapping options do not alter authority-free, ASCII-only URNs.
|
|
286
247
|
|
|
287
|
-
For a non-empty registered-name host, `mapRegName` receives the current host spelling before built-in normalization. The mapper exclusively owns validation, representation, and host-kind policy for its returned string. Apart from enforcing the declared string return type, this package does not check whether mapper output is non-empty, remains a registered name, introduces delimiters, resembles an IP address, or satisfies
|
|
248
|
+
For a non-empty registered-name host, `mapRegName` receives the current host spelling before built-in normalization. The mapper exclusively owns validation, representation, and host-kind policy for its returned string. Apart from enforcing the declared string return type, this package does not check whether mapper output is non-empty, remains a registered name, introduces delimiters, resembles an IP address, or satisfies the DNS-host grammar.
|
|
288
249
|
|
|
289
250
|
With `transform: 'URI'`, non-ASCII userinfo, mapper output, path, query, and fragment text becomes uppercase UTF-8 percent triplets under RFC 3987 §3.1. With `transform: 'IRI'`, eligible percent-encoded ASCII unreserved characters and strictly legal UTF-8 sequences become IRI characters under RFC 3987 §3.2; reserved, malformed, disallowed, and non-UTF-8 octets remain encoded. Private-use characters are decoded only in queries, and forbidden bidirectional formatting characters remain encoded. A mapper can supply the desired Unicode or ASCII hostname representation; this package does not enforce that policy or validate the complete normalized result.
|
|
290
251
|
|
|
@@ -319,27 +280,27 @@ console.log(isUUIDv4('123e4567-e89b-42d3-9456-426614174000')); // true
|
|
|
319
280
|
|
|
320
281
|
The parser builds its validation logic from declarative RFC grammar fragments:
|
|
321
282
|
|
|
322
|
-
1. Select the
|
|
323
|
-
2.
|
|
324
|
-
3. Add named captures for the components exposed by
|
|
283
|
+
1. Select the generic, DNS-host, empty-file-host, or URN grammar profile.
|
|
284
|
+
2. Merge and recursively expand grammar references through `url-templates`.
|
|
285
|
+
3. Add named captures for the public components exposed by parsing.
|
|
325
286
|
4. Compile the complete expression with Unicode support.
|
|
326
|
-
5. Cache expressions by operation, grammar rule, and
|
|
287
|
+
5. Cache expressions by operation, grammar rule, and grammar profile.
|
|
327
288
|
6. Validate with `RegExp.test()` or parse with `RegExp.exec()`.
|
|
328
|
-
7. Resolve references by component inheritance, path merging, dot-segment removal, and
|
|
289
|
+
7. Resolve references by component inheritance, path merging, dot-segment removal, and component recomposition.
|
|
329
290
|
|
|
330
291
|
<details>
|
|
331
292
|
<summary><strong>Lazy compilation and cache behavior</strong></summary>
|
|
332
293
|
|
|
333
|
-
Parsing and validation use separate cached expressions because parsing requires named groups and validation does not.
|
|
294
|
+
Parsing and validation use separate cached expressions because parsing requires named groups and validation does not. Each grammar profile also uses separate entries.
|
|
334
295
|
|
|
335
|
-
The first call for an operation
|
|
296
|
+
The first call for an operation, grammar rule, and grammar profile 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.
|
|
336
297
|
|
|
337
298
|
</details>
|
|
338
299
|
|
|
339
300
|
<details>
|
|
340
|
-
<summary><strong>
|
|
301
|
+
<summary><strong>DNS-host grammar profile</strong></summary>
|
|
341
302
|
|
|
342
|
-
The following schemes trigger DNS-style ASCII or Unicode label rules instead of the
|
|
303
|
+
The following schemes trigger DNS-style ASCII or Unicode label rules instead of the generic `reg-name` grammar:
|
|
343
304
|
|
|
344
305
|
- `http`
|
|
345
306
|
- `https`
|
|
@@ -347,9 +308,46 @@ The following schemes trigger DNS-style ASCII or Unicode label rules instead of
|
|
|
347
308
|
- `wss`
|
|
348
309
|
- `file`
|
|
349
310
|
|
|
350
|
-
Matching is case-insensitive. Other valid schemes use generic RFC 3986/3987 registered-name syntax
|
|
311
|
+
Matching is case-insensitive. Other valid schemes use generic RFC 3986/3987 registered-name syntax. RFC 8089's empty `file` authority is accepted when followed by an absolute path, as in `file:///path`; empty hosts remain rejected for HTTP and WebSocket schemes.
|
|
351
312
|
|
|
352
|
-
Parsing validates DNS-style label shape and the selected RFC 3987 Unicode character classes. A registered-name mapper runs later during optional normalization, and its returned string is not submitted to
|
|
313
|
+
Parsing validates DNS-style label shape and the selected RFC 3987 Unicode character classes. A registered-name mapper runs later during optional normalization, and its returned string is not submitted to the DNS-host grammar again.
|
|
314
|
+
|
|
315
|
+
</details>
|
|
316
|
+
|
|
317
|
+
<details>
|
|
318
|
+
<summary><strong>URN grammar profile</strong></summary>
|
|
319
|
+
|
|
320
|
+
URNs use the existing URI and IRI operations because a URN is a URI under the `urn` scheme. Values with a case-insensitive `urn:` prefix select the closed RFC 8141 grammar profile; no separate `isUrn` or `parseUrn` API is exported.
|
|
321
|
+
|
|
322
|
+
```text
|
|
323
|
+
urn:NID:NSS[?+r-component][?=q-component][#f-component]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The NID contains 2–32 ASCII characters, starts and ends with a letter or digit, and permits letters, digits, or hyphens internally. The NSS begins with an RFC 3986 `pchar` and then permits `pchar` or `/`. The ordered r- and q-components also begin with `pchar` and then permit `pchar`, `/`, or `?`, while an f-component can be empty. The first `?=` sequence after an r-component starts the q-component, and any other question mark outside an optional component is rejected.
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
const { isUri, isIri, parseUri } = require('identifier-js');
|
|
330
|
+
|
|
331
|
+
const value = 'URN:Example:a%2f/../B?+service?x?=key=value#part';
|
|
332
|
+
console.log(isUri(value)); // true
|
|
333
|
+
console.log(isIri(value)); // true
|
|
334
|
+
|
|
335
|
+
const parsed = parseUri(value);
|
|
336
|
+
console.log(parsed.scheme); // URN
|
|
337
|
+
console.log(parsed.nid); // Example
|
|
338
|
+
console.log(parsed.nss); // a%2f/../B
|
|
339
|
+
console.log(parsed.rComponent); // service?x
|
|
340
|
+
console.log(parsed.qComponent); // key=value
|
|
341
|
+
console.log(parsed.fComponent); // part
|
|
342
|
+
console.log(parsed.normalize());
|
|
343
|
+
// urn:example:a%2F/../B?+service?x?=key=value#part
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
URN parse results expose `nid`, `nss`, `rComponent`, `qComponent`, and `fComponent`. They do not expose generic `authority`, `userinfo`, `host`, `port`, `path`, `query`, or `fragment` fields. To require a URN after parsing a value accepted as a general URI, check `parsed.scheme.toLowerCase() === 'urn'`.
|
|
347
|
+
|
|
348
|
+
URNs remain ASCII even through the IRI operations. Callers representing non-ASCII names must first encode them as UTF-8 and then percent-encode the resulting octets; lexical validation does not decode or verify those octet sequences.
|
|
349
|
+
|
|
350
|
+
Validation is deliberately lexical and namespace-independent. Success does not prove that an NID is registered or otherwise legitimate, that an NSS obeys a namespace's additional syntax and canonicalization rules, or that the name was legitimately assigned.
|
|
353
351
|
|
|
354
352
|
</details>
|
|
355
353
|
|
|
@@ -463,9 +461,9 @@ RFC 9562 lists database keys, filenames, system identifiers, and transaction ide
|
|
|
463
461
|
<details>
|
|
464
462
|
<summary><strong>Validation and parsing</strong></summary>
|
|
465
463
|
|
|
466
|
-
-
|
|
467
|
-
-
|
|
468
|
-
- Values with the case-insensitive `urn` scheme
|
|
464
|
+
- The generic URI grammar profile follows RFC 3986 character and component syntax; HTTP, WebSocket, and `file` schemes select the documented DNS-host rules.
|
|
465
|
+
- The generic IRI grammar profile follows the RFC 3987 Unicode extensions to URI syntax; HTTP, WebSocket, and `file` schemes select the documented DNS-host rules.
|
|
466
|
+
- Values with the case-insensitive `urn` scheme select the closed RFC 8141 grammar profile and expose `nid`, `nss`, `rComponent`, `qComponent`, and `fComponent` fields through the URI and IRI parsers.
|
|
469
467
|
- URN validation establishes generic lexical syntax only, not namespace registration, namespace-specific syntax, assignment, resolution, or equivalence.
|
|
470
468
|
- Validators return `true` or throw at the first grammar violation.
|
|
471
469
|
- `absolute-URI` and `absolute-IRI` use the fragment-free grammar defined by their RFCs; complete URI and IRI operations accept fragments.
|
|
@@ -483,7 +481,7 @@ RFC 9562 lists database keys, filenames, system identifiers, and transaction ide
|
|
|
483
481
|
- `toAbsoluteReference` removes the fragment from an identifier containing a scheme.
|
|
484
482
|
- `toRelativeReference` generates a reference whose RFC resolution equals the target resolution for supported forms.
|
|
485
483
|
- `normalize()` implements the applicable case, percent-encoding, and path-segment rules from RFC 3986 §§6.2.2.1–6.2.2.3 and RFC 3987 §§5.3.2.1, 5.3.2.3–5.3.2.4, RFC 3987 §§3.1–3.2 URI/IRI representation transformation, RFC 5952 IPv6 text, RFC 9110 HTTP(S) port/path forms, RFC 6455 WS(S) port/resource-name forms, and RFC 8141 scheme/NID/percent-triplet normalization without NSS decoding or path reduction.
|
|
486
|
-
-
|
|
484
|
+
- Reference resolution and absolute/relative reference conversion do not apply to `urn` inputs; RFC 8141 URN resolution services and URN-equivalence APIs are not implemented.
|
|
487
485
|
|
|
488
486
|
</details>
|
|
489
487
|
|
|
@@ -523,7 +521,7 @@ Run `gh workspace-data load` again to refresh materialized data after public-dat
|
|
|
523
521
|
|
|
524
522
|
### Tests
|
|
525
523
|
|
|
526
|
-
The active suite contains 3,157 tests covering URI/IRI validation and parsing, RFC 8141 URN syntax and normalization, generic normalization, bidirectional URI/IRI representation transformation,
|
|
524
|
+
The active suite contains 3,157 tests covering URI/IRI validation and parsing, RFC 8141 URN syntax and normalization, generic normalization, bidirectional URI/IRI representation transformation, DNS-host grammar, IPv4, IPv6, IPvFuture, ports, UUIDs, RFC 3986 resolution examples, empty components, absolute conversion, and relative-reference round trips, including 2,646 generated combinations of target/base paths, query-presence states, and target-fragment states across equivalent URI and IRI families.
|
|
527
525
|
|
|
528
526
|
<details>
|
|
529
527
|
<summary><strong>Test details</strong></summary>
|