identifier-js 0.4.1 → 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.js +99 -99
- package/normalization.md +33 -31
- package/package.json +1 -1
- package/readme.md +58 -60
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,32 +79,34 @@ 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
84
|
scheme: '[uU][rR][nN]',
|
|
85
|
+
URI_reference: '{URI}',
|
|
85
86
|
URI: '{namestring}',
|
|
86
87
|
absolute_URI: '{assigned_name}(?:{rq_components})?',
|
|
88
|
+
IRI_reference: '{IRI}',
|
|
87
89
|
IRI: '{URI}',
|
|
88
90
|
absolute_IRI: '{absolute_URI}',
|
|
89
91
|
namestring: '{assigned_name}(?:{rq_components})?(?:#{f_component})?',
|
|
90
92
|
assigned_name: '{scheme}:{NID}:{NSS}',
|
|
91
93
|
NID: '{alpha_digit}{ldh}{0,30}{alpha_digit}',
|
|
92
94
|
ldh: '(?:{alpha_digit}|-)',
|
|
93
|
-
NSS: '{pchar}(?:{pchar}
|
|
95
|
+
NSS: '{pchar}(?:{pchar}|\/)*',
|
|
94
96
|
rq_components: '(?:[?][+]{r_component})?(?:[?]={q_component})?',
|
|
95
|
-
r_component: '{pchar}(?:{pchar}
|
|
96
|
-
q_component: '{pchar}(?:{pchar}
|
|
97
|
-
f_component:
|
|
97
|
+
r_component: '{pchar}(?:{pchar}|\/|[?](?!=))*',
|
|
98
|
+
q_component: '{pchar}(?:{pchar}|\/|[?])*',
|
|
99
|
+
f_component: uriRules.fragment,
|
|
98
100
|
};
|
|
99
|
-
//
|
|
101
|
+
// Compile character-repertoire checks used during URI/IRI percent-encoding normalization.
|
|
100
102
|
const uriUnreservedPattern = new RegExp(`^${commonRules.unreserved}$`);
|
|
101
103
|
const iriUcscharPattern = new RegExp(`^${iriRules.ucschar}$`, 'u');
|
|
102
104
|
const iriPrivatePattern = new RegExp(`^${iriRules.iprivate}$`, 'u');
|
|
103
105
|
// Apply the additional RFC 3987 Section 4.1 prose restriction outside the ABNF repertoire.
|
|
104
106
|
const forbiddenIriFormattingPattern = /^[\u200E\u200F\u202A-\u202E]$/u;
|
|
105
|
-
//
|
|
106
|
-
const
|
|
107
|
-
scheme:
|
|
107
|
+
// Restrict registered names for selected hierarchical schemes to DNS-style labels.
|
|
108
|
+
const dnsHostRules = {
|
|
109
|
+
scheme: dnsHostSchemesPattern,
|
|
108
110
|
reg_name: '(?:(?=.{1,255}(?:[:/?#]|$))(?:{a_label})(?:\\.{a_label})*)',
|
|
109
111
|
a_label: '(?:{alpha_digit})(?:(?:{alpha_digit}|-){0,61}(?:{alpha_digit}))?',
|
|
110
112
|
ireg_name: '(?:(?=.{1,255}(?:[:/?#]|$))(?:{u_label})(?:{u_separator}(?:{u_label}))*)',
|
|
@@ -113,13 +115,13 @@ const schemeSpecificRules = {
|
|
|
113
115
|
u_char: '[\\p{L}\\p{N}\\p{Mn}\\p{Mc}\\u200C\\u200D\\u00B7\\u0375\\u30FB\\u05F3\\u05F4]',
|
|
114
116
|
};
|
|
115
117
|
// Recognize RFC 8089's empty file authority without weakening other scheme host policies.
|
|
116
|
-
const emptyFileHostRules = Object.assign({},
|
|
118
|
+
const emptyFileHostRules = Object.assign({}, dnsHostRules, {
|
|
117
119
|
scheme: '[fF][iI][lL][eE]',
|
|
118
120
|
reg_name: '',
|
|
119
121
|
ireg_name: '',
|
|
120
122
|
});
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
+
// Map grammar productions to the public named captures returned by parsers.
|
|
124
|
+
const captureGroupNames = {
|
|
123
125
|
scheme: 'scheme',
|
|
124
126
|
port: 'port',
|
|
125
127
|
authority: 'authority',
|
|
@@ -148,25 +150,21 @@ const groupNames = {
|
|
|
148
150
|
q_component: 'qComponent',
|
|
149
151
|
f_component: 'fComponent',
|
|
150
152
|
};
|
|
151
|
-
//
|
|
152
|
-
const
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
// Select and merge
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
// 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.
|
|
159
160
|
const parse = (string, rule) => {
|
|
160
161
|
if (typeof string !== 'string') throw new TypeError(`Invalid ${rule.replace('_', '-')} type: must be a string.`);
|
|
161
|
-
const profile =
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const ruleId = '_' + profile + rule;
|
|
168
|
-
if (!patterns.has(ruleId)) patterns.set(ruleId, new RegExp(`^${recursiveCompile(rules(profile), rule, addNames)}$`, 'u'));
|
|
169
|
-
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);
|
|
170
168
|
if (match) {
|
|
171
169
|
Object.defineProperty(match.groups, 'normalize', {
|
|
172
170
|
// Normalize this parsed result only when its optional method is called.
|
|
@@ -178,17 +176,17 @@ const parse = (string, rule) => {
|
|
|
178
176
|
}
|
|
179
177
|
throw new SyntaxError(`Invalid ${rule.replace('_', '-')}: ${string}`);
|
|
180
178
|
};
|
|
181
|
-
//
|
|
179
|
+
// Compile and test a capture-free grammar for validation.
|
|
182
180
|
const validate = (string, rule) => {
|
|
183
181
|
if (typeof string !== 'string') throw new TypeError(`Invalid ${rule.replace('_', '-')} type: must be a string.`);
|
|
184
|
-
const profile =
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
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;
|
|
188
186
|
throw new SyntaxError(`Invalid ${rule.replace('_', '-')}: ${string}`);
|
|
189
187
|
};
|
|
190
|
-
//
|
|
191
|
-
function
|
|
188
|
+
// Serialize scheme, authority, path, query, and fragment slots using RFC 3986 delimiters.
|
|
189
|
+
function composeReference(parts = {}) {
|
|
192
190
|
let result = '';
|
|
193
191
|
if (parts.scheme) result += parts.scheme + ':';
|
|
194
192
|
if (parts.authority !== undefined && parts.authority !== null) result += '//' + parts.authority;
|
|
@@ -197,7 +195,8 @@ function compose(parts = {}) {
|
|
|
197
195
|
if (parts.fragment !== undefined && parts.fragment !== null) result += '#' + parts.fragment;
|
|
198
196
|
return result;
|
|
199
197
|
}
|
|
200
|
-
//
|
|
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.
|
|
201
200
|
function removeDotSegments(path) {
|
|
202
201
|
const output = [];
|
|
203
202
|
let input = path ?? '';
|
|
@@ -242,8 +241,9 @@ function removeDotSegments(path) {
|
|
|
242
241
|
}
|
|
243
242
|
return output.join('');
|
|
244
243
|
}
|
|
245
|
-
//
|
|
246
|
-
|
|
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) {
|
|
247
247
|
let B;
|
|
248
248
|
if (typeof base === 'string') {
|
|
249
249
|
B = parse(base, 'IRI');
|
|
@@ -292,14 +292,14 @@ function resolveReference(reference, base, strict = true, parts = false) {
|
|
|
292
292
|
}
|
|
293
293
|
T.fragment = R.fragment;
|
|
294
294
|
}
|
|
295
|
-
if (
|
|
296
|
-
return
|
|
295
|
+
if (returnParts) return T;
|
|
296
|
+
return composeReference(T);
|
|
297
297
|
}
|
|
298
298
|
// Convert a complete IRI to fragment-free form without changing its other components.
|
|
299
299
|
function toAbsoluteReference(string) {
|
|
300
300
|
const result = parse(string, 'IRI');
|
|
301
301
|
result.fragment = undefined;
|
|
302
|
-
return
|
|
302
|
+
return composeReference(result);
|
|
303
303
|
}
|
|
304
304
|
// Generate a relative reference when resolution is stable, otherwise retain the absolute target.
|
|
305
305
|
const toRelativeReference = (target, base) => {
|
|
@@ -378,27 +378,27 @@ function normalizePercentEncoding(value, decodeUnreserved = true) {
|
|
|
378
378
|
}
|
|
379
379
|
// Expand any accepted IPv6 spelling into eight numeric 16-bit fields.
|
|
380
380
|
function parseIPv6Words(address) {
|
|
381
|
-
let
|
|
381
|
+
let addressText = address;
|
|
382
382
|
// Convert a dotted-decimal tail to the same two-field representation used by every later step.
|
|
383
|
-
const lastColon =
|
|
384
|
-
const lastSegment =
|
|
383
|
+
const lastColon = addressText.lastIndexOf(':');
|
|
384
|
+
const lastSegment = addressText.slice(lastColon + 1);
|
|
385
385
|
if (lastSegment.includes('.')) {
|
|
386
386
|
const octets = lastSegment.split('.');
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
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)}`;
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
const
|
|
393
|
-
const leftText =
|
|
394
|
-
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);
|
|
395
395
|
const left = leftText ? leftText.split(':') : [];
|
|
396
396
|
const right = rightText ? rightText.split(':') : [];
|
|
397
397
|
const words = [];
|
|
398
398
|
// Retain every explicit field before the compressed zero run.
|
|
399
399
|
for (const field of left) words.push(Number.parseInt(field, 16));
|
|
400
400
|
// Expand the single compression marker to the required number of zero fields.
|
|
401
|
-
if (
|
|
401
|
+
if (compressionIndex !== -1) {
|
|
402
402
|
// Fill the omitted field count determined from both explicit sides.
|
|
403
403
|
for (let index = left.length + right.length; index < 8; index++) words.push(0);
|
|
404
404
|
}
|
|
@@ -428,7 +428,7 @@ function serializeIPv6Words(words) {
|
|
|
428
428
|
if (bestLength < 2) bestStart = -1;
|
|
429
429
|
|
|
430
430
|
const fields = [];
|
|
431
|
-
//
|
|
431
|
+
// Render each field without leading hexadecimal zeroes.
|
|
432
432
|
for (const word of words) fields.push(word.toString(16));
|
|
433
433
|
if (bestStart === -1) return fields.join(':');
|
|
434
434
|
const before = fields.slice(0, bestStart).join(':');
|
|
@@ -440,11 +440,11 @@ function normalizeIPv6Address(address) {
|
|
|
440
440
|
const words = parseIPv6Words(address);
|
|
441
441
|
// Detect standardized prefixes that identify an embedded IPv4 address from address bits alone.
|
|
442
442
|
const low32 = words[6] * 0x10000 + words[7];
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
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) {
|
|
448
448
|
const prefix = serializeIPv6Words(words.slice(0, 6));
|
|
449
449
|
const ipv4 = `${words[6] >>> 8}.${words[6] & 0xFF}.${words[7] >>> 8}.${words[7] & 0xFF}`;
|
|
450
450
|
return prefix.endsWith(':') ? prefix + ipv4 : `${prefix}:${ipv4}`;
|
|
@@ -485,20 +485,20 @@ function normalizeHost(host, mapRegName) {
|
|
|
485
485
|
if (!mapRegName && isIPv4Address(result)) result = normalizePercentEncoding(encodedResult, false);
|
|
486
486
|
return /[^\x00-\x7F]/u.test(result) ? result : lowercaseAsciiHost(result);
|
|
487
487
|
}
|
|
488
|
-
//
|
|
488
|
+
// Omit an empty or default port only for HTTP and WebSocket schemes.
|
|
489
489
|
function normalizePort(scheme, port) {
|
|
490
490
|
if (port === undefined) return undefined;
|
|
491
491
|
const defaultPort = scheme === 'http' || scheme === 'ws' ? '80' : scheme === 'https' || scheme === 'wss' ? '443' : undefined;
|
|
492
492
|
if (defaultPort !== undefined && (port === '' || port.replace(/^0+(?=\d)/, '') === defaultPort)) return undefined;
|
|
493
493
|
return port;
|
|
494
494
|
}
|
|
495
|
-
// Encode each non-ASCII Unicode scalar as uppercase UTF-8 percent triplets.
|
|
496
|
-
function
|
|
495
|
+
// Encode each non-ASCII Unicode scalar as uppercase UTF-8 percent triplets for URI output.
|
|
496
|
+
function encodeNonAsciiForUri(component) {
|
|
497
497
|
// Process complete code points so supplementary characters produce one UTF-8 sequence.
|
|
498
498
|
return component.replace(/[^\x00-\x7F]/gu, (character) => encodeURIComponent(character).toUpperCase());
|
|
499
499
|
}
|
|
500
|
-
// Decode the maximal RFC 3987
|
|
501
|
-
function
|
|
500
|
+
// Decode the maximal RFC 3987 character repertoire allowed by one IRI component.
|
|
501
|
+
function decodeUriTextToIri(component, allowPrivateUse = false) {
|
|
502
502
|
let result = '';
|
|
503
503
|
// Inspect each normalized percent triplet as either ASCII or the lead of one strict UTF-8 scalar.
|
|
504
504
|
for (let index = 0; index < component.length; index++) {
|
|
@@ -514,28 +514,28 @@ function decodeUriComponentToIri(component, allowPrivate = false) {
|
|
|
514
514
|
continue;
|
|
515
515
|
}
|
|
516
516
|
const sequenceLength = octet >= 0xC2 && octet <= 0xDF ? 2 : octet >= 0xE0 && octet <= 0xEF ? 3 : octet >= 0xF0 && octet <= 0xF4 ? 4 : 0;
|
|
517
|
-
let
|
|
517
|
+
let encodedSequence = '';
|
|
518
518
|
// Collect exactly one candidate scalar without consuming malformed trailing input.
|
|
519
519
|
for (let sequenceIndex = 0; sequenceIndex < sequenceLength; sequenceIndex++) {
|
|
520
520
|
const position = index + sequenceIndex * 3;
|
|
521
521
|
if (component[position] !== '%' || !/^[0-9A-F]{2}$/.test(component.slice(position + 1, position + 3))) {
|
|
522
|
-
|
|
522
|
+
encodedSequence = '';
|
|
523
523
|
break;
|
|
524
524
|
}
|
|
525
|
-
|
|
525
|
+
encodedSequence += component.slice(position, position + 3);
|
|
526
526
|
}
|
|
527
527
|
let character;
|
|
528
|
-
if (
|
|
528
|
+
if (encodedSequence) {
|
|
529
529
|
try {
|
|
530
|
-
character = decodeURIComponent(
|
|
530
|
+
character = decodeURIComponent(encodedSequence);
|
|
531
531
|
} catch {
|
|
532
532
|
character = undefined;
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
|
-
const
|
|
536
|
-
if (
|
|
535
|
+
const isAllowedIriCharacter = character !== undefined && !forbiddenIriFormattingPattern.test(character) && (iriUcscharPattern.test(character) || (allowPrivateUse && iriPrivatePattern.test(character)));
|
|
536
|
+
if (isAllowedIriCharacter) {
|
|
537
537
|
result += character;
|
|
538
|
-
index +=
|
|
538
|
+
index += encodedSequence.length - 1;
|
|
539
539
|
} else {
|
|
540
540
|
result += `%${hexadecimal}`;
|
|
541
541
|
index += 2;
|
|
@@ -543,7 +543,7 @@ function decodeUriComponentToIri(component, allowPrivate = false) {
|
|
|
543
543
|
}
|
|
544
544
|
return result;
|
|
545
545
|
}
|
|
546
|
-
// Rebuild authority from normalized
|
|
546
|
+
// Rebuild authority from normalized userinfo, host, and port while preserving their presence.
|
|
547
547
|
function normalizeAuthority(parts, scheme, mapRegName) {
|
|
548
548
|
if (parts.authority === undefined) return undefined;
|
|
549
549
|
const userinfo = parts.userinfo === undefined ? undefined : normalizePercentEncoding(parts.userinfo);
|
|
@@ -566,12 +566,12 @@ function normalizeParsedReference(parts, options = {}) {
|
|
|
566
566
|
if (parts.nid !== undefined) {
|
|
567
567
|
const rComponent = parts.rComponent === undefined ? undefined : normalizePercentEncoding(parts.rComponent, false);
|
|
568
568
|
const qComponent = parts.qComponent === undefined ? undefined : normalizePercentEncoding(parts.qComponent, false);
|
|
569
|
-
const
|
|
570
|
-
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) });
|
|
571
571
|
}
|
|
572
572
|
// Normalize each component independently so encoded delimiters cannot become structure.
|
|
573
573
|
const scheme = parts.scheme === undefined ? undefined : parts.scheme.toLowerCase();
|
|
574
|
-
const
|
|
574
|
+
const normalizedParts = {
|
|
575
575
|
scheme,
|
|
576
576
|
authority: normalizeAuthority(parts, scheme, mapRegName),
|
|
577
577
|
path: normalizePercentEncoding(parts.path),
|
|
@@ -579,31 +579,31 @@ function normalizeParsedReference(parts, options = {}) {
|
|
|
579
579
|
fragment: parts.fragment === undefined ? undefined : normalizePercentEncoding(parts.fragment),
|
|
580
580
|
};
|
|
581
581
|
// Use the slash form defined for an empty HTTP or WebSocket authority path.
|
|
582
|
-
if (
|
|
582
|
+
if (normalizedParts.authority !== undefined && normalizedParts.path === '' && (scheme === 'http' || scheme === 'https' || scheme === 'ws' || scheme === 'wss')) normalizedParts.path = '/';
|
|
583
583
|
// Limit dot-segment removal to paths whose standalone interpretation remains stable.
|
|
584
|
-
const rootlessRelativePath =
|
|
584
|
+
const rootlessRelativePath = normalizedParts.scheme === undefined && normalizedParts.authority === undefined && normalizedParts.path.length > 0 && !normalizedParts.path.startsWith('/');
|
|
585
585
|
if (!rootlessRelativePath) {
|
|
586
|
-
const reducedPath = removeDotSegments(
|
|
586
|
+
const reducedPath = removeDotSegments(normalizedParts.path);
|
|
587
587
|
// Preserve a no-authority path when reduction would reparse it as an authority.
|
|
588
|
-
if (
|
|
588
|
+
if (normalizedParts.authority !== undefined || !reducedPath.startsWith('//')) normalizedParts.path = reducedPath;
|
|
589
589
|
}
|
|
590
|
-
// Select an explicit target representation only after
|
|
590
|
+
// Select an explicit target representation only after component normalization is complete.
|
|
591
591
|
if (transform === 'URI') {
|
|
592
|
-
//
|
|
593
|
-
if (
|
|
594
|
-
|
|
595
|
-
if (
|
|
596
|
-
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);
|
|
597
597
|
} else if (transform === 'IRI') {
|
|
598
598
|
// Decode valid UTF-8 percent sequences only where the destination component permits their scalar.
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
if (
|
|
602
|
-
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);
|
|
603
603
|
}
|
|
604
|
-
return
|
|
604
|
+
return composeReference(normalizedParts);
|
|
605
605
|
}
|
|
606
|
-
//
|
|
606
|
+
// Expose the public validation, parsing, resolution, and conversion API.
|
|
607
607
|
module.exports = {
|
|
608
608
|
isUUID: (string) => validate(string, 'UUID'),
|
|
609
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.fComponent); // 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`, `qComponent`, and `fComponent`. They do not expose generic `path`, `query`, or `fragment` 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.
|
|
@@ -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.
|
|
@@ -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>
|