identifier-js 0.0.1 → 0.0.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 ADDED
@@ -0,0 +1,55 @@
1
+ export const isUUID: (string: string) => boolean;
2
+ export const isUUIDv4: (string: string) => boolean;
3
+
4
+ export const isUri: (uri: string) => boolean;
5
+ export const isUriReference: (uriReference: string) => boolean;
6
+ export const isAbsoluteUri: (uri: string) => boolean;
7
+
8
+ export const parseUri: (uri: string) => IdentifierComponents;
9
+ export const parseUriReference: (uriReference: string) => RelativeIdentifierComponents;
10
+ export const parseAbsoluteUri: (uri: string) => AbsoluteIdentifierComponents;
11
+
12
+ export const isIri: (iri: string) => boolean;
13
+ export const isIriReference: (iriReference: string) => boolean;
14
+ export const isAbsoluteIri: (iri: string) => boolean;
15
+
16
+ export const parseIri: (iri: string) => IdentifierComponents;
17
+ export const parseIriReference: (iriReference: string) => RelativeIdentifierComponents;
18
+ export const parseAbsoluteIri: (iri: string) => AbsoluteIdentifierComponents;
19
+
20
+ export const normalizeReference: (reference: string) => string;
21
+ export const resolveReference: (reference: string, base: string, strict?: boolean, returnParts?: boolean) => string;
22
+ export const toAbsoluteReference: (reference: string) => string;
23
+ export const toRelativeReference: (target: string, base: string) => string;
24
+
25
+ type IdentifierComponents = {
26
+ scheme: string;
27
+ authority: string;
28
+ userinfo?: string;
29
+ host: string;
30
+ port?: string;
31
+ path: string;
32
+ query?: string;
33
+ fragment?: string;
34
+ };
35
+
36
+ type RelativeIdentifierComponents = {
37
+ scheme?: string;
38
+ authority?: string;
39
+ userinfo?: string;
40
+ host?: string;
41
+ port?: string;
42
+ path: string;
43
+ query?: string;
44
+ fragment?: string;
45
+ };
46
+
47
+ type AbsoluteIdentifierComponents = {
48
+ scheme: string;
49
+ authority: string;
50
+ userinfo?: string;
51
+ host: string;
52
+ port?: string;
53
+ path: string;
54
+ query?: string;
55
+ };
package/index.js ADDED
@@ -0,0 +1,270 @@
1
+ 'use strict';
2
+ // a valid URI is always a valid IRI
3
+ const { recursiveCompile } = require('url-templates');
4
+ const patterns = new Map();
5
+ // RFC3986/RFC3987 common rules
6
+ const commonRules = {
7
+ scheme: '[a-zA-Z][a-zA-Z0-9+.-]*',
8
+ port: '[0-9]*',
9
+ IP_literal: '\\[(?:{IPv6address}|{IPvFuture})\\]',
10
+ 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})?::)',
11
+ ls32: '(?:{h16}:{h16}|{IPv4address})',
12
+ h16: '{hexdig}{1,4}',
13
+ IPv4address: '(?:{dec_octet}\\.){3}{dec_octet}',
14
+ dec_octet: '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)',
15
+ IPvFuture: 'v{hexdig}+\\.(?:{unreserved}|{sub_delims}|:)+',
16
+ unreserved: '[a-zA-Z0-9_.~-]',
17
+ reserved: '(?:{gen_delims}|{sub_delims})',
18
+ pct_encoded: '%{hexdig}{2}',
19
+ gen_delims: '[:/?#[\\]@]',
20
+ sub_delims: "[!&'()*+,;=$]",
21
+ hexdig: '[0-9A-Fa-f]',
22
+ uuid: '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}',
23
+ uuid_v4: '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}',
24
+ };
25
+ // RFC3986 rules
26
+ const uriRules = {
27
+ URI_reference: '(?:{URI}|{relative_ref})',
28
+ URI: '{absolute_URI}(?:#{fragment})?',
29
+ absolute_URI: '{scheme}:{hier_part}(?:\\?{query})?',
30
+ relative_ref: '{relative_part}(?:\\?{query})?(?:#{fragment})?',
31
+ hier_part: '(?:\/\/{authority}{path_abempty}|{path_absolute}|{path_rootless}|{path_empty})',
32
+ relative_part: '(?:\/\/{authority}{path_abempty}|{path_absolute}|{path_noscheme}|{path_empty})',
33
+ authority: '(?:{userinfo}@)?{host}(?::{port})?',
34
+ host: '(?:{IP_literal}|{IPv4address}|{reg_name})',
35
+ userinfo: '(?:{unreserved}|{pct_encoded}|{sub_delims}|:)*',
36
+ reg_name: '(?:{unreserved}|{pct_encoded}|{sub_delims})*',
37
+ path: '(?:{path_abempty}|{path_absolute}|{path_noscheme}|{path_rootless}|{path_empty})',
38
+ path_abempty: '(?:\/{segment})*',
39
+ path_absolute: '\/(?:{segment_nz}(?:\/{segment})*)?',
40
+ path_noscheme: '{segment_nz_nc}(?:\/{segment})*',
41
+ path_rootless: '{segment_nz}(?:\/{segment})*',
42
+ path_empty: '',
43
+ segment: '{pchar}*',
44
+ segment_nz: '{pchar}+',
45
+ segment_nz_nc: '(?:{unreserved}|{pct_encoded}|{sub_delims}|@)+',
46
+ query: '(?:{pchar}|\/|\\?)*',
47
+ fragment: '(?:{pchar}|\/|\\?)*',
48
+ pchar: '(?:{unreserved}|{pct_encoded}|{sub_delims}|:|@)',
49
+ };
50
+ // RFC3987 rules
51
+ const iriRules = {
52
+ IRI_reference: '(?:{IRI}|{irelative_ref})',
53
+ IRI: '{absolute_IRI}(?:#{ifragment})?',
54
+ absolute_IRI: '{scheme}:{ihier_part}(?:\\?{iquery})?',
55
+ irelative_ref: '(?:{irelative_part}(?:\\?{iquery})?(?:#{ifragment})?)',
56
+ ihier_part: '(?:\/\/{iauthority}{ipath_abempty}|{ipath_absolute}|{ipath_rootless}|{ipath_empty})',
57
+ irelative_part: '(?:\/\/{iauthority}{ipath_abempty}|{ipath_absolute}|{ipath_noscheme}|{ipath_empty})',
58
+ iauthority: '(?:{iuserinfo}@)?{ihost}(?::{port})?',
59
+ iuserinfo: '(?:{iunreserved}|{pct_encoded}|{sub_delims}|:)*',
60
+ ihost: '(?:{IP_literal}|{IPv4address}|{ireg_name})',
61
+ ireg_name: '(?:{iunreserved}|{pct_encoded}|{sub_delims})*',
62
+ ipath: '(?:{ipath_abempty}|{ipath_absolute}|{ipath_noscheme}|{ipath_rootless}|{ipath_empty})',
63
+ ipath_empty: '',
64
+ ipath_rootless: '{isegment_nz}(?:\/{isegment})*',
65
+ ipath_noscheme: '{isegment_nz_nc}(?:\/{isegment})*',
66
+ ipath_absolute: '\/(?:{isegment_nz}(?:\/{isegment})*)?',
67
+ ipath_abempty: '(?:\/{isegment})*',
68
+ isegment_nz_nc: '(?:{iunreserved}|{pct_encoded}|{sub_delims}|@)+',
69
+ isegment_nz: '{ipchar}+',
70
+ isegment: '{ipchar}*',
71
+ iquery: '(?:{ipchar}|{iprivate}|\/|\\?)*',
72
+ ifragment: '(?:{ipchar}|\/|\\?)*',
73
+ ipchar: '(?:{iunreserved}|{pct_encoded}|{sub_delims}|:|@)',
74
+ iunreserved: '(?:[a-zA-Z0-9._~-]|{ucschar})',
75
+ iprivate: '[\\uE000-\\uF8FF\\u{F0000}-\\u{FFFFD}\\u{100000}-\\u{10FFFD}]',
76
+ 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}]',
77
+ };
78
+ // pattern RFC group names
79
+ const groupNames = {
80
+ scheme: 'scheme',
81
+ port: 'port',
82
+ authority: 'authority',
83
+ host: 'host',
84
+ userinfo: 'userinfo',
85
+ query: 'query',
86
+ fragment: 'fragment',
87
+ iauthority: 'authority',
88
+ ihost: 'host',
89
+ iuserinfo: 'userinfo',
90
+ iquery: 'query',
91
+ ifragment: 'fragment',
92
+ path_abempty: 'path',
93
+ path_absolute: 'path',
94
+ path_noscheme: 'path',
95
+ path_rootless: 'path',
96
+ path_empty: 'path',
97
+ ipath_abempty: 'path',
98
+ ipath_absolute: 'path',
99
+ ipath_noscheme: 'path',
100
+ ipath_rootless: 'path',
101
+ ipath_empty: 'path',
102
+ };
103
+ // all rules into one map
104
+ const rules = Object.assign({}, commonRules, uriRules, iriRules);
105
+ const addNames = (key) => (groupNames[key] ? `(?<${groupNames[key]}>${rules[key]})` : rules[key]);
106
+ // parse (slower, it uses regex.exec and includes named capture groups)
107
+ const parse = (string, rule) => {
108
+ if (!patterns.has('_' + rule)) patterns.set('_' + rule, new RegExp(`^${recursiveCompile(rules, rule, addNames)}$`, 'u'));
109
+ const match = patterns.get('_' + rule).exec(string);
110
+ if (match) return match.groups;
111
+ throw new TypeError(`Invalid ${rule.replace('_', '-')}: ${string}`);
112
+ };
113
+ // validate (faster, it uses regex.test and does not include named capture groups)
114
+ const validate = (string, rule) => {
115
+ if (!patterns.has(rule)) patterns.set(rule, new RegExp(`^${recursiveCompile(rules, rule)}$`, 'u'));
116
+ return patterns.get(rule).test(string);
117
+ };
118
+ // compose as per RFC 3986 Section 5.3 (component recomposition)
119
+ function compose(parts = {}) {
120
+ let result = '';
121
+ if (parts.scheme) result += parts.scheme + ':';
122
+ if (parts.authority) result += '//' + parts.authority;
123
+ result += parts.path || '';
124
+ if (parts.query) result += '?' + parts.query;
125
+ if (parts.fragment) result += '#' + parts.fragment;
126
+ return result;
127
+ }
128
+ // remove dot segments algorithm per RFC 3986 Section 5.2.4 (loop and replace)
129
+ function removeDotSegments(path) {
130
+ const output = [];
131
+ let input = path;
132
+ while (input.length > 0) {
133
+ if (input.startsWith('../')) input = input.slice(3);
134
+ else if (input.startsWith('./')) input = input.slice(2);
135
+ else if (input.startsWith('/./')) input = input.replace('/./', '/');
136
+ else if (input === '/.') input = '/';
137
+ else if (input.startsWith('/../')) {
138
+ input = input.replace('/../', '/');
139
+ if (output.length) output.pop();
140
+ } else if (input === '/..') {
141
+ input = '/';
142
+ if (output.length) output.pop();
143
+ } else if (input === '.' || input === '..') {
144
+ input = '';
145
+ } else {
146
+ // move first segment from input to output
147
+ let seg = '';
148
+ if (input[0] === '/') {
149
+ // keep leading '/'
150
+ const idx = input.indexOf('/', 1);
151
+ if (idx === -1) {
152
+ seg = input;
153
+ input = '';
154
+ } else {
155
+ seg = input.slice(0, idx);
156
+ input = input.slice(idx);
157
+ }
158
+ } else {
159
+ const idx = input.indexOf('/');
160
+ if (idx === -1) {
161
+ seg = input;
162
+ input = '';
163
+ } else {
164
+ seg = input.slice(0, idx);
165
+ input = input.slice(idx);
166
+ }
167
+ }
168
+ output.push(seg);
169
+ }
170
+ }
171
+ return output.join('');
172
+ }
173
+ // resolve as per RFC https://datatracker.ietf.org/doc/html/rfc3986#section-5.2
174
+ function resolveReference(reference, base, strict = true, parts = false) {
175
+ let B;
176
+ if (typeof base === 'string') {
177
+ B = parse(base, 'IRI');
178
+ } else {
179
+ B = Object.assign({}, base);
180
+ }
181
+ if (!B.scheme) throw new Error('Expected an URI/IRI (with scheme) as base.');
182
+
183
+ let R;
184
+ if (typeof reference === 'string') {
185
+ R = parse(reference, 'IRI_reference');
186
+ } else {
187
+ R = Object.assign({}, reference);
188
+ }
189
+
190
+ let T;
191
+ if (R.scheme && (strict || R.scheme !== B.scheme)) {
192
+ T = R;
193
+ } else {
194
+ T = {};
195
+ T.scheme = B.scheme;
196
+ if (R.authority !== undefined && R.authority !== null) {
197
+ T.authority = R.authority;
198
+ T.path = R.path;
199
+ T.query = R.query;
200
+ } else {
201
+ T.authority = B.authority;
202
+ if (R.path && R.path.length > 0) {
203
+ if (R.path.startsWith('/')) {
204
+ T.path = R.path;
205
+ } else if (B.authority !== undefined && B.authority !== null && (!B.path || B.path.length === 0)) {
206
+ T.path = '/' + R.path;
207
+ } else {
208
+ // merge base path and ref path
209
+ const idx = B.path ? B.path.lastIndexOf('/') : -1;
210
+ const prefix = idx !== -1 ? B.path.slice(0, idx + 1) : '';
211
+ T.path = prefix + R.path;
212
+ }
213
+ T.query = R.query;
214
+ } else {
215
+ T.path = B.path;
216
+ if (R.query !== undefined && R.query !== null) T.query = R.query;
217
+ else T.query = B.query;
218
+ }
219
+ }
220
+ T.fragment = R.fragment;
221
+ }
222
+ T.path = removeDotSegments(T.path || '');
223
+ if (parts) return T;
224
+ return compose(T);
225
+ }
226
+ // reverse logic for resolve
227
+ const toRelativeReference = (target, base) => {
228
+ const B = parse(base, 'absolute_IRI');
229
+ const T = parse(target, 'IRI');
230
+ if (T.scheme !== B.scheme || T.authority !== B.authority) return target;
231
+ let result;
232
+ if (B.path === T.path) {
233
+ result = '';
234
+ } else {
235
+ const baseSegments = B.path.split('/');
236
+ const targetSegments = T.path.split('/');
237
+ let position = 0;
238
+ while (baseSegments[position] === targetSegments[position] && position < baseSegments.length - 1 && position < targetSegments.length - 1) {
239
+ position++;
240
+ }
241
+ const segments = [];
242
+ for (let index = position + 1; index < baseSegments.length; index++) segments.push('..');
243
+ for (let index = position; index < targetSegments.length; index++) segments.push(targetSegments[index]);
244
+ result = segments.join('/');
245
+ }
246
+ if (T.query !== undefined) result += `?${T.query}`;
247
+ if (T.fragment !== undefined) result += `#${T.fragment}`;
248
+ return result;
249
+ };
250
+ // export
251
+ module.exports = {
252
+ isUUID: (string) => validate(string, 'uuid'),
253
+ isUUIDv4: (string) => validate(string, 'uuid_v4'),
254
+ isUri: (string) => validate(string, 'URI'),
255
+ isUriReference: (string) => validate(string, 'URI_reference'),
256
+ isAbsoluteUri: (string) => validate(string, 'absolute_URI'),
257
+ parseUri: (string) => parse(string, 'URI'),
258
+ parseUriReference: (string) => parse(string, 'URI_reference'),
259
+ parseAbsoluteUri: (string) => parse(string, 'absolute_URI'),
260
+ isIri: (string) => validate(string, 'IRI'),
261
+ isIriReference: (string) => validate(string, 'IRI_reference'),
262
+ isAbsoluteIri: (string) => validate(string, 'absolute_IRI'),
263
+ parseIri: (string) => parse(string, 'IRI'),
264
+ parseIriReference: (string) => parse(string, 'IRI_reference'),
265
+ parseAbsoluteIri: (string) => parse(string, 'absolute_IRI'),
266
+ resolveReference, // not yet decided if the return should be normalized or not, and in what extent
267
+ normalizeReference: (string) => string, // not done yet
268
+ toAbsoluteReference: (string) => resolveReference('', string),
269
+ toRelativeReference,
270
+ };
package/package.json CHANGED
@@ -1,35 +1,39 @@
1
- {
2
- "name": "identifier-js",
3
- "version": "0.0.1",
4
- "description": "A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.",
5
- "keywords": [
6
- "IRI",
7
- "URI",
8
- "IRI-reference",
9
- "URI-reference",
10
- "ipv4",
11
- "ipv6",
12
- "uuid",
13
- "parser",
14
- "validator",
15
- "RFC",
16
- "3987",
17
- "RFC",
18
- "3986"
19
- ],
20
- "homepage": "https://github.com/SorinGFS/identifier-js#readme",
21
- "bugs": {
22
- "url": "https://github.com/SorinGFS/identifier-js/issues"
23
- },
24
- "repository": {
25
- "type": "git",
26
- "url": "git+https://github.com/SorinGFS/identifier-js.git"
27
- },
28
- "license": "MIT",
29
- "author": "SorinGFS",
30
- "type": "commonjs",
31
- "main": "index.js",
32
- "scripts": {
33
- "test": "echo \"Error: no test specified\" && exit 1"
34
- }
35
- }
1
+ {
2
+ "name": "identifier-js",
3
+ "version": "0.0.2",
4
+ "description": "A RFC3986 / RFC3987 compliant fast parser/validator/resolver/composer for NodeJS and browser.",
5
+ "keywords": [
6
+ "IRI",
7
+ "URI",
8
+ "IRI-reference",
9
+ "URI-reference",
10
+ "ipv4",
11
+ "ipv6",
12
+ "uuid",
13
+ "parser",
14
+ "validator",
15
+ "RFC",
16
+ "3987",
17
+ "RFC",
18
+ "3986"
19
+ ],
20
+ "homepage": "https://github.com/SorinGFS/identifier-js#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/SorinGFS/identifier-js/issues"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/SorinGFS/identifier-js.git"
27
+ },
28
+ "license": "MIT",
29
+ "author": "SorinGFS",
30
+ "type": "commonjs",
31
+ "main": "index.js",
32
+ "scripts": {
33
+ "update-deps": "npx npm-check-updates -u && npm install",
34
+ "test": "node tests/vitest-setup && npm test"
35
+ },
36
+ "dependencies": {
37
+ "url-templates": "^1.0.4"
38
+ }
39
+ }
package/readme.md CHANGED
@@ -15,3 +15,85 @@ A fully RFC [3986](https://datatracker.ietf.org/doc/html/rfc3986.html)/[3897](ht
15
15
  ```bash title="console"
16
16
  npm i identifier-js
17
17
  ```
18
+
19
+ ## API
20
+
21
+ Exports are documented below.
22
+
23
+ ### URI (RFC 3986)
24
+
25
+ #### Validation
26
+
27
+ Validate a URI:
28
+ - isUri: (value: string) => boolean
29
+
30
+ Validate a URI reference (absolute or relative):
31
+ - isUriReference: (value: string) => boolean
32
+
33
+ Validate an absolute URI (must include scheme):
34
+ - isAbsoluteUri: (value: string) => boolean
35
+
36
+ #### Parsing
37
+
38
+ Parse a URI into structured components:
39
+ - parseUri: (value: string) => IdentifierComponents
40
+
41
+ Parse a URI reference into structured components:
42
+ - parseUriReference: (value: string) => RelativeIdentifierComponents
43
+
44
+ Parse an absolute URI (must include scheme):
45
+ - parseAbsoluteUri: (value: string) => AbsoluteIdentifierComponents
46
+
47
+ ### IRI (RFC 3987)
48
+
49
+ #### Validation
50
+
51
+ Validate an IRI:
52
+ - isIri: (value: string) => boolean
53
+
54
+ Validate an IRI reference (absolute or relative):
55
+ - isIriReference: (value: string) => boolean
56
+
57
+ Validate an absolute IRI (must include scheme):
58
+ - isAbsoluteIri: (value: string) => boolean
59
+
60
+ #### Parsing
61
+
62
+ Parse an IRI into structured components:
63
+ - parseIri: (value: string) => IdentifierComponents
64
+
65
+ Parse an IRI reference into structured components:
66
+ - parseIriReference: (value: string) => RelativeIdentifierComponents
67
+
68
+ Parse an absolute IRI (must include scheme):
69
+ - parseAbsoluteIri: (value: string) => AbsoluteIdentifierComponents
70
+
71
+ ### Reference Utilities (URI/IRI)
72
+
73
+ Normalize a URI or IRI reference:
74
+ - normalizeReference: (reference: string) => string
75
+
76
+ Resolve a reference against a base identifier:
77
+ - resolveReference: (reference: string, base: string, strict?: boolean, returnParts?: boolean) => string
78
+
79
+ **Note:**
80
+ - strict enables strict resolution behavior.
81
+ - returnParts returns structured components instead of a string.
82
+
83
+ Convert a reference into absolute form:
84
+ - toAbsoluteReference: (reference: string) => string
85
+
86
+ Compute a relative reference from base to target:
87
+ - toRelativeReference: (target: string, base: string) => string
88
+
89
+ ### UUID
90
+
91
+ Validate a UUID (any version):
92
+ - isUUID: (value: string) => boolean
93
+
94
+ Validate a UUID version 4:
95
+ - isUUIDv4: (value: string) => boolean
96
+
97
+ ## Testing
98
+
99
+ - npm test