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 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
- /** Resolve with generic RFC 3986 semantics; this does not invoke a URN resolution service.
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
- /** Derive a generic URI reference without scheme-specific relative-URN semantics.
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
- fragment?: string;
99
+ fComponent?: string;
105
100
  };
106
101
 
107
- type AbsoluteUrnIdentifierComponents = Omit<UrnIdentifierComponents, 'fragment'>;
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
- // Parse, validate, normalize, resolve, and convert RFC 3986 URI, RFC 3987 IRI, and RFC 8141 URN references.
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 patterns = new Map();
6
- const implemented_schemes = '(?:[hH][tT][tT][pP][sS]?|[wW][sS][sS]?|[fF][iI][lL][eE]|[uU][rR][nN])';
7
- // RFC3986/RFC3987 common rules + https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2:~:text=DNS%29%2E-,A,of%20%5BRFC1123%5D%2E
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
- implemented_schemes,
10
- scheme: '(?!{implemented_schemes}:)[a-zA-Z][a-zA-Z0-9+.-]*',
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
- // RFC3986 rules
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
- // RFC3987 rules
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 productions and URI/IRI root overrides for the conditional URN profile.
82
+ // Define a closed RFC 8141 profile whose root overrides and direct fragment expression expose only URN captures.
83
83
  const urnRules = {
84
- scheme: implemented_schemes,
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: '{fragment}',
97
+ r_component: '{pchar}(?:{pchar}|\/|[?](?!=))*',
98
+ q_component: '{pchar}(?:{pchar}|\/|[?])*',
99
+ f_component: uriRules.fragment,
100
100
  };
101
- // Reuse the grammar repertoires when selecting URI octets safe for IRI output.
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
- // scheme specific URI reg_name and IRI ireg_name
108
- const schemeSpecificRules = {
109
- scheme: implemented_schemes,
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({}, schemeSpecificRules, {
118
+ const emptyFileHostRules = Object.assign({}, dnsHostRules, {
119
119
  scheme: '[fF][iI][lL][eE]',
120
120
  reg_name: '',
121
121
  ireg_name: '',
122
122
  });
123
- // pattern RFC group names
124
- const groupNames = {
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 for which the package implements grammar beyond generic URI/IRI syntax.
153
- const isSpecificScheme = (string) => new RegExp('^' + implemented_schemes + ':').test(string);
154
- // Select and merge generic, DNS-host, empty-file-host, or URN grammar profiles.
155
- const schemeProfile = (string) => (string.slice(0, 4).toLowerCase() === 'urn:' ? 'u' : string.slice(0, 8).toLowerCase() === 'file:///' ? 'f' : isSpecificScheme(string) ? 's' : '');
156
- const rules = (profile) => Object.assign({}, commonRules, uriRules, iriRules, profile === 'u' ? urnRules : profile === 'f' ? emptyFileHostRules : profile ? schemeSpecificRules : {});
157
- // parse (slower, it uses regex.exec and includes named capture groups)
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 = schemeProfile(string);
161
- const addNames = (key) => (groupNames[key] ? `(?<${groupNames[key]}>${rules(profile)[key]})` : rules(profile)[key]);
162
- const ruleId = '_' + profile + rule;
163
- if (!patterns.has(ruleId)) patterns.set(ruleId, new RegExp(`^${recursiveCompile(rules(profile), rule, addNames)}$`, 'u'));
164
- const match = patterns.get(ruleId).exec(string);
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
- // validate (faster, it uses regex.test and does not include named capture groups)
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 = schemeProfile(string);
180
- const ruleId = profile + rule;
181
- if (!patterns.has(ruleId)) patterns.set(ruleId, new RegExp(`^${recursiveCompile(rules(profile), rule)}$`, 'u'));
182
- if (patterns.get(ruleId).test(string)) return true;
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
- // compose as per RFC 3986 Section 5.3 (component recomposition)
186
- function compose(parts = {}) {
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
- // remove dot segments algorithm per RFC 3986 Section 5.2.4 (loop and replace)
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
- // resolve as per RFC https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
241
- function resolveReference(reference, base, strict = true, parts = false) {
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 (parts) return T;
291
- return compose(T);
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 compose(result);
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 expanded = address;
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 = expanded.lastIndexOf(':');
379
- const lastSegment = expanded.slice(lastColon + 1);
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 high = Number(octets[0]) * 0x100 + Number(octets[1]);
383
- const low = Number(octets[2]) * 0x100 + Number(octets[3]);
384
- expanded = `${expanded.slice(0, lastColon + 1)}${high.toString(16)}:${low.toString(16)}`;
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 compression = expanded.indexOf('::');
388
- const leftText = compression === -1 ? expanded : expanded.slice(0, compression);
389
- const rightText = compression === -1 ? '' : expanded.slice(compression + 2);
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 (compression !== -1) {
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
- // Suppress every leading zero by converting each field through its numeric value.
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 compatible = words[0] === 0 && words[1] === 0 && words[2] === 0 && words[3] === 0 && words[4] === 0 && words[5] === 0 && low32 > 1;
439
- const mapped = words[0] === 0 && words[1] === 0 && words[2] === 0 && words[3] === 0 && words[4] === 0 && words[5] === 0xFFFF;
440
- const translated = words[0] === 0 && words[1] === 0 && words[2] === 0 && words[3] === 0 && words[4] === 0xFFFF && words[5] === 0;
441
- const nat64 = words[0] === 0x64 && words[1] === 0xFF9B && words[2] === 0 && words[3] === 0 && words[4] === 0 && words[5] === 0;
442
- if (compatible || mapped || translated || nat64) {
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
- // Remove an empty or default-valued HTTP or WebSocket port.
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 encodeIriComponent(component) {
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 URI octet repertoire allowed by one IRI component.
496
- function decodeUriComponentToIri(component, allowPrivate = false) {
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 encoded = '';
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
- encoded = '';
522
+ encodedSequence = '';
518
523
  break;
519
524
  }
520
- encoded += component.slice(position, position + 3);
525
+ encodedSequence += component.slice(position, position + 3);
521
526
  }
522
527
  let character;
523
- if (encoded) {
528
+ if (encodedSequence) {
524
529
  try {
525
- character = decodeURIComponent(encoded);
530
+ character = decodeURIComponent(encodedSequence);
526
531
  } catch {
527
532
  character = undefined;
528
533
  }
529
534
  }
530
- const allowed = character !== undefined && !forbiddenIriFormattingPattern.test(character) && (iriUcscharPattern.test(character) || (allowPrivate && iriPrivatePattern.test(character)));
531
- if (allowed) {
535
+ const isAllowedIriCharacter = character !== undefined && !forbiddenIriFormattingPattern.test(character) && (iriUcscharPattern.test(character) || (allowPrivateUse && iriPrivatePattern.test(character)));
536
+ if (isAllowedIriCharacter) {
532
537
  result += character;
533
- index += encoded.length - 1;
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 values while preserving other empty component delimiters.
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 query = rComponent !== undefined ? `+${rComponent}${qComponent === undefined ? '' : `?=${qComponent}`}` : qComponent === undefined ? undefined : `=${qComponent}`;
565
- return compose({ scheme: parts.scheme.toLowerCase(), path: `${parts.nid.toLowerCase()}:${normalizePercentEncoding(parts.nss, false)}`, query, fragment: parts.fragment === undefined ? undefined : normalizePercentEncoding(parts.fragment, false) });
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 normalized = {
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 (normalized.authority !== undefined && normalized.path === '' && (scheme === 'http' || scheme === 'https' || scheme === 'ws' || scheme === 'wss')) normalized.path = '/';
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 = normalized.scheme === undefined && normalized.authority === undefined && normalized.path.length > 0 && !normalized.path.startsWith('/');
584
+ const rootlessRelativePath = normalizedParts.scheme === undefined && normalizedParts.authority === undefined && normalizedParts.path.length > 0 && !normalizedParts.path.startsWith('/');
580
585
  if (!rootlessRelativePath) {
581
- const reducedPath = removeDotSegments(normalized.path);
586
+ const reducedPath = removeDotSegments(normalizedParts.path);
582
587
  // Preserve a no-authority path when reduction would reparse it as an authority.
583
- if (normalized.authority !== undefined || !reducedPath.startsWith('//')) normalized.path = reducedPath;
588
+ if (normalizedParts.authority !== undefined || !reducedPath.startsWith('//')) normalizedParts.path = reducedPath;
584
589
  }
585
- // Select an explicit target representation only after syntax and scheme normalization is complete.
590
+ // Select an explicit target representation only after component normalization is complete.
586
591
  if (transform === 'URI') {
587
- // Map every non-ASCII authority, path, query, and fragment scalar under RFC 3987 URI output.
588
- if (normalized.authority !== undefined) normalized.authority = encodeIriComponent(normalized.authority);
589
- normalized.path = encodeIriComponent(normalized.path);
590
- if (normalized.query !== undefined) normalized.query = encodeIriComponent(normalized.query);
591
- if (normalized.fragment !== undefined) normalized.fragment = encodeIriComponent(normalized.fragment);
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 (normalized.authority !== undefined) normalized.authority = decodeUriComponentToIri(normalized.authority);
595
- normalized.path = decodeUriComponentToIri(normalized.path);
596
- if (normalized.query !== undefined) normalized.query = decodeUriComponentToIri(normalized.query, true);
597
- if (normalized.fragment !== undefined) normalized.fragment = decodeUriComponentToIri(normalized.fragment);
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 compose(normalized);
604
+ return composeReference(normalizedParts);
600
605
  }
601
- // export
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-based normalization, the implemented HTTP, HTTPS, WS, WSS, and URN scheme rules, and optional RFC 3987 URI/IRI representation transformation. The method returns a string and leaves the parsed components unchanged.
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
- ## RFC syntax normalization
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 | For generic URI/IRI components, decode percent triplets representing ASCII letters, digits, `-`, `.`, `_`, or `~`. Retain percent encoding for reserved octets. URNs use the non-decoding rules below. | [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 generic parsed reference can be normalized independently. Preserve unresolved rootless-relative path semantics and every URN NSS segment. | [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), [RFC 8141 §§2.2 and 3.1](https://www.rfc-editor.org/rfc/rfc8141#section-3.1) |
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
- ## URNs
61
+ ## Scheme-specific normalization
62
62
 
63
- A parsed value under the case-insensitive `urn` scheme takes a separate RFC 8141 normalization path using its captured `scheme`, `nid`, `nss`, `rComponent`, `qComponent`, and `fragment` properties.
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
- ## WS and WSS
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 a scheme-specific hostname grammar. The returned string then receives percent-triplet and ASCII host-case normalization. Mapper exceptions propagate unchanged.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "identifier-js",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A fast RFC 3986/3987 URI/IRI parser, validator, normalizer, resolver, and composer with RFC 8141 URN syntax support.",
5
5
  "keywords": [
6
6
  "IRI",
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 scheme-specific RFC 8141 URN syntax and normalization support.
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 composer based on RFC [3986](https://www.rfc-editor.org/rfc/rfc3986) and RFC [3987](https://www.rfc-editor.org/rfc/rfc3987), with scheme-specific RFC [8141](https://www.rfc-editor.org/rfc/rfc8141) URN support. Its recognized HTTP, WebSocket, and `file` schemes retain the documented hostname-policy restrictions below. It provides:
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 scheme-specific URN components;
15
- - conservative syntax normalization, recognized-scheme forms, and a registered-name extension point;
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` or throws at the first detected violation. Parsing and reference operations also throw when their input does not satisfy the required grammar.
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
- This function performs generic RFC 3986 reference resolution only. It does not invoke a URN resolution service or implement scheme-specific URN resolution semantics.
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
- These conversion functions retain their generic URI-reference behavior. They do not construct, resolve, or interpret scheme-specific relative URNs.
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 a scheme-specific hostname grammar.
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 applicable generic or scheme-specific URI/IRI syntax.
323
- 2. Recursively expand grammar references through `url-templates`.
324
- 3. Add named captures for the components exposed by the selected syntax.
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 scheme class.
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 definedness-preserving recomposition.
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. Generic and scheme-specific identifiers also use separate entries.
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/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.
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>Scheme-specific hostname policy</strong></summary>
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 fully generic `reg-name` grammar:
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; URNs instead follow RFC 8141 namestring 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.
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 this hostname policy again.
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
- - Generic URI syntax follows RFC 3986 character and component grammar; HTTP, WebSocket, and `file` schemes apply the documented hostname restrictions.
467
- - Generic IRI syntax follows the RFC 3987 Unicode extensions to URI grammar; HTTP, WebSocket, and `file` schemes apply the documented hostname restrictions.
468
- - Values with the case-insensitive `urn` scheme follow RFC 8141 namestring syntax and expose NID, NSS, r-component, q-component, and fragment fields through the URI and IRI parsers.
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
- - RFC 3986 reference resolution and relative-reference generation receive no URN-specific semantics; RFC 8141 URN resolution services and URN-equivalence APIs are not implemented.
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, scheme-specific hosts, 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.
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>