@pubid/pubid 0.1.5 → 0.1.7

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.
@@ -0,0 +1,566 @@
1
+ import { splitParts, withYear } from "./types.js";
2
+ import { grammarImplementation } from "../flavors/index.js";
3
+ import { NationalAdoption } from "../flavors/evs/model.js";
4
+ // Each parser mirrors lib/pubid/<flavor>/urn_parser.rb#parse_urn exactly —
5
+ // including its lossiness (dropped segments stay dropped) and its error
6
+ // cases. The parity fixture (test/fixtures/urn-parity.json) pins the
7
+ // reference behavior for ~93k corpus URNs.
8
+ export const URN_PARSERS = {
9
+ // lib/pubid/ansi/urn_parser.rb
10
+ ansi: {
11
+ prefix: "urn:ansi:",
12
+ reconstruct(body, parse) {
13
+ const [code, year] = splitParts(body);
14
+ return parse(withYear(`ANSI ${code}`, year));
15
+ },
16
+ },
17
+ // lib/pubid/ccsds/urn_parser.rb — the body is the full suffix.
18
+ ccsds: {
19
+ prefix: "urn:ccsds:",
20
+ reconstruct(body, parse) {
21
+ // The reference rejects cor-supplemented forms at its text parser;
22
+ // mirrored (fixture-pinned gap).
23
+ if (/:cor\./.test(body)) {
24
+ throw new Error(`CCSDS URN rejected by the reference: ${JSON.stringify(body)}`);
25
+ }
26
+ return parse(`CCSDS ${body}`);
27
+ },
28
+ },
29
+ // lib/pubid/cen_cenelec/urn_parser.rb — namespace "cen"; type token
30
+ // uppercased before the number.
31
+ cen_cenelec: {
32
+ prefix: "urn:cen:",
33
+ reconstruct(body, parse) {
34
+ const [typeToken = "", number, year] = splitParts(body);
35
+ return parse(withYear(`${typeToken.toUpperCase()} ${number}`, year));
36
+ },
37
+ },
38
+ // lib/pubid/cie/urn_parser.rb — leading lowercase publisher + trailing
39
+ // "sep.<x>" separator tokens are dropped.
40
+ cie: {
41
+ prefix: "urn:cie:",
42
+ reconstruct(body, parse) {
43
+ const parts = splitParts(body).filter((p) => !p.startsWith("sep."));
44
+ const [, code, year] = parts;
45
+ return parse(withYear(`CIE ${code}`, year));
46
+ },
47
+ },
48
+ // lib/pubid/ashrae/urn_parser.rb — "ASHRAE Standard <number>-<year>".
49
+ ashrae: {
50
+ prefix: "urn:ashrae:",
51
+ reconstruct(body, parse) {
52
+ const [number, year] = splitParts(body);
53
+ let text = `ASHRAE Standard ${number}`;
54
+ if (year)
55
+ text += `-${year}`;
56
+ return parse(text);
57
+ },
58
+ },
59
+ // lib/pubid/asme/urn_parser.rb — "ASME <code>-<year>".
60
+ asme: {
61
+ prefix: "urn:asme:",
62
+ reconstruct(body, parse) {
63
+ const [, code, year] = splitParts(body);
64
+ let text = `ASME ${code}`;
65
+ if (year)
66
+ text += `-${year}`;
67
+ return parse(text);
68
+ },
69
+ },
70
+ // lib/pubid/astm/urn_parser.rb — year rendered to its last two digits.
71
+ astm: {
72
+ prefix: "urn:astm:",
73
+ reconstruct(body, parse) {
74
+ const [, code, year] = splitParts(body);
75
+ let text = `ASTM ${code}`;
76
+ if (year && year.length >= 2)
77
+ text += `-${year.slice(-2)}`;
78
+ return parse(text);
79
+ },
80
+ },
81
+ // lib/pubid/iana/urn_parser.rb — "<registry>[/<sub-registry>]".
82
+ iana: {
83
+ prefix: "urn:iana:",
84
+ reconstruct(body, parse) {
85
+ const [registry = "", subRegistry] = splitParts(body);
86
+ return parse(subRegistry ? `${registry}/${subRegistry}` : registry);
87
+ },
88
+ },
89
+ // lib/pubid/idf/urn_parser.rb — empty segments dropped.
90
+ idf: {
91
+ prefix: "urn:idf:",
92
+ reconstruct(body, parse) {
93
+ const parts = splitParts(body).filter((p) => p !== "");
94
+ const [number = "", year] = parts;
95
+ return parse(withYear(`IDF ${number}`, year));
96
+ },
97
+ },
98
+ // lib/pubid/plateau/urn_parser.rb — "PLATEAU <Type> #<number>[-<annex>]".
99
+ plateau: {
100
+ prefix: "urn:plateau:",
101
+ reconstruct(body, parse) {
102
+ const parts = splitParts(body);
103
+ const typeToken = parts[0] ?? "";
104
+ const number = parts[1] ?? "";
105
+ const annex = parts[2];
106
+ const typeMap = {
107
+ handbook: "Handbook",
108
+ tr: "Technical Report",
109
+ an: "Annex",
110
+ };
111
+ let text = `PLATEAU ${typeMap[typeToken] ?? (typeToken.charAt(0).toUpperCase() + typeToken.slice(1))} #${number}`;
112
+ if (annex)
113
+ text += `-${annex}`;
114
+ return parse(text);
115
+ },
116
+ },
117
+ // lib/pubid/tgpp/urn_parser.rb — empty release/version segments dropped.
118
+ tgpp: {
119
+ prefix: "urn:3gpp:",
120
+ reconstruct(body, parse) {
121
+ const [type = "", code = "", release, version] = splitParts(body);
122
+ let result = `${type.toUpperCase()} ${code}`;
123
+ if (release !== undefined && release !== "")
124
+ result += `:${release}`;
125
+ if (version !== undefined && version !== "")
126
+ result += `/${version}`;
127
+ return parse(result);
128
+ },
129
+ },
130
+ // lib/pubid/w3c/urn_parser.rb — a known type token hyphenates the rest.
131
+ w3c: {
132
+ prefix: "urn:w3c:",
133
+ reconstruct(body, parse) {
134
+ const known = ["note", "dnote", "wd", "cr", "crd", "rec", "pr", "per", "spsd", "obsl"];
135
+ const parts = splitParts(body);
136
+ let text;
137
+ if (parts.length > 1 && known.includes(parts[0] ?? "")) {
138
+ text = `W3C ${parts.shift().toUpperCase()}-${parts.shift() ?? ""}`;
139
+ }
140
+ else {
141
+ text = `W3C ${parts.shift()}`;
142
+ }
143
+ if (parts.length)
144
+ text += `-${parts.shift()}`;
145
+ return parse(text);
146
+ },
147
+ },
148
+ // lib/pubid/xsf/urn_parser.rb — "XEP <number>".
149
+ xsf: {
150
+ prefix: "urn:xsf:",
151
+ reconstruct(body, parse) {
152
+ const [, number] = splitParts(body);
153
+ return parse(`XEP ${number}`);
154
+ },
155
+ },
156
+ // lib/pubid/calconnect/urn_parser.rb — "CC[/series] <number>:<date>".
157
+ calconnect: {
158
+ prefix: "urn:calconnect:",
159
+ reconstruct(body, parse) {
160
+ const parts = splitParts(body);
161
+ const date = parts.pop();
162
+ const number = parts.pop();
163
+ const series = parts.pop();
164
+ let text = "CC";
165
+ if (series !== undefined)
166
+ text += `/${series}`;
167
+ text += ` ${number}:${date}`;
168
+ return parse(text);
169
+ },
170
+ },
171
+ // lib/pubid/api/urn_parser.rb — "API STD <number>[-<part>]:<year>".
172
+ api: {
173
+ prefix: "urn:api:",
174
+ reconstruct(body, parse) {
175
+ const parts = splitParts(body);
176
+ const number = parts[1] ?? "";
177
+ let idx = 2;
178
+ let part = "";
179
+ if (parts[idx]?.startsWith("-")) {
180
+ part = parts[idx] ?? "";
181
+ idx += 1;
182
+ }
183
+ const year = parts[idx];
184
+ let text = `API STD ${number}${part}`;
185
+ if (year)
186
+ text += `:${year}`;
187
+ return parse(text);
188
+ },
189
+ },
190
+ // lib/pubid/jis/urn_parser.rb — "JIS <code>[:<year>]".
191
+ jis: {
192
+ prefix: "urn:jis:",
193
+ reconstruct(body, parse) {
194
+ const [code = "", year] = splitParts(body);
195
+ return parse(withYear(`JIS ${code}`, year));
196
+ },
197
+ },
198
+ // lib/pubid/ietf/urn_parser.rb — rfc/bcp/std/fyi/id; unknown types raise.
199
+ ietf: {
200
+ prefix: "urn:ietf:",
201
+ reconstruct(body, parse) {
202
+ const parts = splitParts(body);
203
+ const type = parts.shift() ?? "";
204
+ let text;
205
+ if (type === "rfc") {
206
+ text = `RFC ${parts[0] ?? ""}`;
207
+ }
208
+ else if (type === "bcp" || type === "std" || type === "fyi") {
209
+ text = `${type.toUpperCase()} ${parts[0] ?? ""}`;
210
+ }
211
+ else if (type === "id") {
212
+ const [name, version] = parts;
213
+ text = version ? `${name}-${version}` : (name ?? "");
214
+ }
215
+ else {
216
+ throw new Error(`Invalid IETF URN type: ${JSON.stringify(type)}`);
217
+ }
218
+ return parse(text);
219
+ },
220
+ },
221
+ // lib/pubid/ecma/urn_parser.rb — optional tr/mem tag, labelled
222
+ // part-/ed-/vol- segments.
223
+ ecma: {
224
+ prefix: "urn:ecma:",
225
+ reconstruct(body, parse) {
226
+ const parts = splitParts(body);
227
+ const tag = parts[0] === "tr" || parts[0] === "mem" ? parts.shift() : undefined;
228
+ const number = parts.shift() ?? "";
229
+ const shiftLabelled = (label) => parts[0]?.startsWith(label) ? parts.shift().slice(label.length) : undefined;
230
+ const part = shiftLabelled("part-");
231
+ const edition = shiftLabelled("ed-");
232
+ const volume = shiftLabelled("vol-");
233
+ let base;
234
+ if (tag === "tr")
235
+ base = `ECMA TR/${number}`;
236
+ else if (tag === "mem")
237
+ base = `ECMA MEM/${number}`;
238
+ else
239
+ base = part ? `ECMA-${number}-${part}` : `ECMA-${number}`;
240
+ if (edition)
241
+ base += ` ed${edition}`;
242
+ if (volume)
243
+ base += ` vol${volume}`;
244
+ return parse(base);
245
+ },
246
+ },
247
+ // lib/pubid/gost/urn_parser.rb — "r" prefix selects the national
248
+ // standard; both classes render "GOST [R ]<number>-<year>".
249
+ gost: {
250
+ prefix: "urn:gost:std:",
251
+ reconstruct(body, parse) {
252
+ const parts = splitParts(body);
253
+ if (parts[0] === "r") {
254
+ const number = parts[1] ?? "";
255
+ const year = parts[2];
256
+ return parse(year ? `GOST R ${number}-${year}` : `GOST R ${number}`);
257
+ }
258
+ const number = parts[0] ?? "";
259
+ const year = parts[1];
260
+ return parse(year ? `GOST ${number}-${year}` : `GOST ${number}`);
261
+ },
262
+ },
263
+ // lib/pubid/iho/urn_parser.rb — "IHO <TYPE>-<number> [<Part label>
264
+ // <value>] [<version>]".
265
+ iho: {
266
+ prefix: "urn:iho:",
267
+ reconstruct(body, parse) {
268
+ const partLabels = {
269
+ part: "Part",
270
+ ap: "Ap.",
271
+ annex: "Annex",
272
+ suppl: "Suppl",
273
+ };
274
+ const parts = splitParts(body);
275
+ const typeToken = (parts[0] ?? "").toUpperCase();
276
+ const number = parts[1] ?? "";
277
+ let idx = 2;
278
+ let partLabel;
279
+ const kindToken = parts[idx];
280
+ if (kindToken !== undefined && /^[a-z]+\./i.test(kindToken)) {
281
+ const [kind = "", value = ""] = kindToken.split(".", 2);
282
+ const label = partLabels[kind.toLowerCase()] ?? (kind.charAt(0).toUpperCase() + kind.slice(1));
283
+ partLabel = `${label} ${value}`;
284
+ idx += 1;
285
+ }
286
+ const version = parts[idx];
287
+ let text = `IHO ${typeToken}-${number}`;
288
+ if (partLabel)
289
+ text += ` ${partLabel}`;
290
+ if (version)
291
+ text += ` ${version}`;
292
+ return parse(text);
293
+ },
294
+ },
295
+ // lib/pubid/oasis/urn_parser.rb — %20/%5D decoded after the namespace.
296
+ oasis: {
297
+ prefix: "urn:oasis:",
298
+ reconstruct(body, parse) {
299
+ const slug = body.replace(/%20|%5D/g, (m) => (m === "%20" ? " " : "]"));
300
+ return parse(`OASIS ${slug}`);
301
+ },
302
+ },
303
+ // lib/pubid/ogc/urn_parser.rb — "<year>-<number>[<revision>]".
304
+ ogc: {
305
+ prefix: "urn:ogc:",
306
+ reconstruct(body, parse) {
307
+ const [year = "", number = "", revision] = splitParts(body);
308
+ let text = `${year}-${number}`;
309
+ if (revision !== undefined)
310
+ text += revision;
311
+ return parse(text);
312
+ },
313
+ },
314
+ // lib/pubid/amca/urn_parser.rb — keyed tokens (copub./rev./reaff./interp.),
315
+ // a bare numeric year, and a trailing type token.
316
+ amca: {
317
+ prefix: "urn:amca:",
318
+ reconstruct(body, parse) {
319
+ const parts = splitParts(body);
320
+ const number = parts.shift() ?? "";
321
+ const first = parts[0];
322
+ const year = first !== undefined && /^\d+$/.test(first) ? parts.shift() : undefined;
323
+ const keyed = {};
324
+ const keyedTokens = [];
325
+ for (const token of parts) {
326
+ if (token.includes(".")) {
327
+ const [k, ...v] = token.split(".", 2);
328
+ keyed[k] = v.join(".");
329
+ keyedTokens.push(token);
330
+ }
331
+ }
332
+ const type = parts.filter((t) => !keyedTokens.includes(t)).pop();
333
+ const publisher = (keyed["copub"] ?? "amca").toUpperCase();
334
+ let text;
335
+ if (type === "interpretation") {
336
+ const code = keyed["interp"]?.toUpperCase();
337
+ text = [publisher, number, code, "Interp"].filter(Boolean).join(" ");
338
+ }
339
+ else {
340
+ const titles = {
341
+ standard: "Standard",
342
+ publication: "Publication",
343
+ };
344
+ text = [publisher, titles[type ?? ""], number].filter(Boolean).join(" ");
345
+ if (year)
346
+ text += `-${year}`;
347
+ if (keyed["rev"])
348
+ text += ` (Rev. ${keyed["rev"]})`;
349
+ if (keyed["reaff"])
350
+ text += ` (R${keyed["reaff"]})`;
351
+ }
352
+ return parse(text);
353
+ },
354
+ },
355
+ // lib/pubid/itu/urn_parser.rb — "report" segment between sector and code
356
+ // marks an ITU-R Report; otherwise "ITU-<SECTOR> <code>".
357
+ itu: {
358
+ prefix: "urn:itu:",
359
+ reconstruct(body, parse) {
360
+ const parts = splitParts(body);
361
+ const sector = parts[0] ?? "";
362
+ if (parts[1] === "report") {
363
+ return parse(`Report ITU-${sector.toUpperCase()} ${parts[2] ?? ""}`);
364
+ }
365
+ return parse(`ITU-${sector.toUpperCase()} ${parts[1] ?? ""}`);
366
+ },
367
+ },
368
+ // lib/pubid/iala/urn_parser.rb — annex(-letter) / ed.<x> / single-letter
369
+ // language segments after the code.
370
+ iala: {
371
+ prefix: "urn:mrn:iala:pub:",
372
+ reconstruct(body, parse) {
373
+ const parts = splitParts(body);
374
+ const code = (parts.shift() ?? "").toUpperCase();
375
+ let annexForm;
376
+ let annexLetter;
377
+ let edition;
378
+ let language;
379
+ for (const seg of parts) {
380
+ const annexMatch = seg.match(/^annex(-([a-z]))?$/i);
381
+ if (annexMatch) {
382
+ annexLetter = annexMatch[2]?.toUpperCase();
383
+ annexForm = annexLetter ? "ANNEX" : "Annex";
384
+ }
385
+ else if (/^ed\.?/i.test(seg)) {
386
+ edition = seg.replace(/^ed\.?/i, "");
387
+ }
388
+ else if (/^[a-z]$/i.test(seg)) {
389
+ language = seg.toUpperCase();
390
+ }
391
+ }
392
+ let text = `IALA ${code}`;
393
+ if (annexForm)
394
+ text += ` ${annexForm}`;
395
+ if (annexLetter)
396
+ text += ` ${annexLetter}`;
397
+ if (edition)
398
+ text += ` Ed ${edition}`;
399
+ if (language)
400
+ text += ` (${language})`;
401
+ return parse(text);
402
+ },
403
+ },
404
+ // lib/pubid/bsi/urn_parser.rb — publisher token + number + year +
405
+ // 3-segment supplement slices (amd/cor/add).
406
+ bsi: {
407
+ prefix: "urn:bsi:",
408
+ reconstruct(body, parse) {
409
+ const abbr = { amd: "Amd", cor: "Cor", add: "Add" };
410
+ const parts = splitParts(body);
411
+ const [publisherToken = "", number = "", year, ...supplementParts] = parts;
412
+ let text = `${publisherToken.toUpperCase()} ${number}`;
413
+ if (year)
414
+ text += `:${year}`;
415
+ for (let i = 0; i < supplementParts.length; i += 3) {
416
+ const type = supplementParts[i] ?? "";
417
+ const num = supplementParts[i + 1];
418
+ const suppYear = supplementParts[i + 2];
419
+ const label = abbr[type.toLowerCase()] ?? (type.charAt(0).toUpperCase() + type.slice(1));
420
+ let suffix = num ? ` ${num}` : "";
421
+ if (suppYear)
422
+ suffix += `:${suppYear}`;
423
+ text += `/${label}${suffix}`;
424
+ }
425
+ return parse(text);
426
+ },
427
+ },
428
+ // lib/pubid/csa/urn_parser.rb — format.<sep> token picks the year
429
+ // separator; the token itself is dropped.
430
+ csa: {
431
+ prefix: "urn:csa:",
432
+ reconstruct(body, parse) {
433
+ const parts = splitParts(body);
434
+ const separators = { dash: "-", colon: ":" };
435
+ const formatToken = parts.find((t) => t.startsWith("format."));
436
+ const separator = separators[(formatToken?.split(".")[1]) ?? ""] ?? "-";
437
+ const payload = parts.filter((t) => !t.startsWith("format."));
438
+ const [, code, year] = payload;
439
+ let text = `CSA ${code}`;
440
+ if (year)
441
+ text += `${separator}${year}`;
442
+ return parse(text);
443
+ },
444
+ },
445
+ // lib/pubid/etsi/urn_parser.rb — "ETSI <TYPE> <code> [VERSION] [(date)]".
446
+ etsi: {
447
+ prefix: "urn:etsi:",
448
+ reconstruct(body, parse) {
449
+ const parts = splitParts(body);
450
+ const typeToken = (parts[0] ?? "").toUpperCase();
451
+ const code = parts[1] ?? "";
452
+ const version = parts[2];
453
+ const date = parts[3];
454
+ let text = `ETSI ${typeToken} ${code}`;
455
+ if (version !== undefined)
456
+ text += ` ${version.toUpperCase()}`;
457
+ if (date !== undefined)
458
+ text += ` (${date})`;
459
+ return parse(text);
460
+ },
461
+ },
462
+ // lib/pubid/evs/urn_parser.rb — EN [org] number[:year] with an optional
463
+ // amd/cor iteration tail; the rebuilt text is parsed as a CEN adoption.
464
+ evs: {
465
+ prefix: "urn:evs:",
466
+ reconstruct(body, parse) {
467
+ const parts = splitParts(body);
468
+ const type = parts.shift() ?? "";
469
+ if (type.toLowerCase() !== "en") {
470
+ throw new Error(`unsupported type in ${JSON.stringify(body)}`);
471
+ }
472
+ const orgs = {
473
+ iso: "ISO",
474
+ "iso-iec": "ISO/IEC",
475
+ iec: "IEC",
476
+ cispr: "CISPR",
477
+ };
478
+ const orgParts = [];
479
+ while (parts[0] !== undefined && /^[a-z][a-z-]*$/.test(parts[0])) {
480
+ orgParts.push(parts.shift());
481
+ }
482
+ let text = "EN";
483
+ if (orgParts.length) {
484
+ const org = orgs[orgParts.join("-")];
485
+ if (org === undefined)
486
+ throw new Error(`unknown adopted org in ${JSON.stringify(body)}`);
487
+ text += ` ${org}`;
488
+ }
489
+ const number = parts.shift();
490
+ if (number === undefined)
491
+ throw new Error(`missing number in ${JSON.stringify(body)}`);
492
+ text += ` ${number}`;
493
+ const year = parts.shift();
494
+ if (year)
495
+ text += `:${year}`;
496
+ if (parts.length) {
497
+ const yearTail = parts[parts.length - 1]?.match(/^\d{4}$/) ? parts.pop() : undefined;
498
+ const kind = parts.shift();
499
+ const iteration = parts.shift();
500
+ if (kind !== "amd" && kind !== "cor") {
501
+ throw new Error(`malformed supplement in ${JSON.stringify(body)}`);
502
+ }
503
+ if (iteration === undefined || parts.length) {
504
+ throw new Error(`malformed supplement in ${JSON.stringify(body)}`);
505
+ }
506
+ text += kind === "amd" ? `/A${iteration}` : `/AC${iteration}`;
507
+ if (yearTail)
508
+ text += `:${yearTail}`;
509
+ }
510
+ // The reference constructs NationalAdoption around a parsed CEN
511
+ // base (lib/pubid/evs/urn_parser.rb) rather than re-parsing the
512
+ // rebuilt text as EVS.
513
+ const cen = grammarImplementation("cen_cenelec");
514
+ const base = cen.parse(text);
515
+ return new NationalAdoption({ base });
516
+ },
517
+ },
518
+ // lib/pubid/nist/urn_parser.rb — type token upcased via the reference
519
+ // map; ".supp" stripped; ".r<N>" revision suffix split off the payload.
520
+ nist: {
521
+ prefix: "urn:nist:",
522
+ reconstruct(body, parse) {
523
+ const typeUpcase = {
524
+ sp: "SP", fips: "FIPS", ir: "IR", nistcir: "NISTCIR", nbs: "NBS",
525
+ nistir: "NISTIR", csf: "CSF", gcr: "GCR", itl: "ITL", jres: "JRES",
526
+ lcirc: "LCIRC", mon: "MONO", ms: "MS", nsrds: "NSRDS", tn: "TN",
527
+ wp: "WP",
528
+ };
529
+ const parts = splitParts(body);
530
+ const typeToken = parts[0] ?? "";
531
+ const payload = parts[1] ?? "";
532
+ const label = typeUpcase[typeToken.toLowerCase()] ?? typeToken.toUpperCase();
533
+ const stripped = payload.replace(/\.supp$/, "");
534
+ const revMatch = stripped.match(/^(.*)\.(r\d+)$/i);
535
+ const code = revMatch ? revMatch[1] : stripped;
536
+ const revision = revMatch ? revMatch[2] : undefined;
537
+ let text = `NIST ${label} ${code}`;
538
+ if (revision)
539
+ text += revision;
540
+ return parse(text);
541
+ },
542
+ },
543
+ // lib/pubid/ieee/urn_parser.rb — "IEEE Std <code>[-<year>]". The fixture
544
+ // pins the reference's own parse failures for narrative/artifact rows.
545
+ ieee: {
546
+ prefix: "urn:ieee:",
547
+ reconstruct(body, parse) {
548
+ const parts = splitParts(body);
549
+ const [, code, year] = parts;
550
+ let text = `IEEE Std ${code}`;
551
+ if (year)
552
+ text += `-${year}`;
553
+ return parse(text);
554
+ },
555
+ },
556
+ };
557
+ export function lookupUrnParser(flavor) {
558
+ return URN_PARSERS[flavor];
559
+ }
560
+ export function parseUrnFor(def, urn, parse) {
561
+ const prefix = def.prefix;
562
+ if (!urn.toLowerCase().startsWith(prefix)) {
563
+ throw new Error(`Invalid ${def.prefix.toUpperCase()} URN: ${JSON.stringify(urn)}`);
564
+ }
565
+ return def.reconstruct(urn.slice(prefix.length), parse);
566
+ }
@@ -0,0 +1,12 @@
1
+ import type { Identifier } from "../conformance/implementation.js";
2
+ export type UrnParserDef = {
3
+ /** Full URN prefix including the trailing colon
4
+ * (gem UrnParser PREFIX / "urn:" + flavor_name + ":"). */
5
+ prefix: string;
6
+ /** Rebuild the human text (or parse directly) from the URN body. */
7
+ reconstruct: (body: string, parse: (text: string) => Identifier) => Identifier;
8
+ };
9
+ /** Split the body on ":" (gem Base#split_parts). */
10
+ export declare function splitParts(body: string): string[];
11
+ /** Reconstruct "<TEXT>:<year>" (the common trailing-year shape). */
12
+ export declare function withYear(text: string, year: string | undefined): string;
@@ -0,0 +1,8 @@
1
+ /** Split the body on ":" (gem Base#split_parts). */
2
+ export function splitParts(body) {
3
+ return body.split(":");
4
+ }
5
+ /** Reconstruct "<TEXT>:<year>" (the common trailing-year shape). */
6
+ export function withYear(text, year) {
7
+ return year ? `${text}:${year}` : text;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "TypeScript PubID identifiers: resolution, normalization and canonical hashes for every identifier in the pubid-testsuite corpus (41 standards bodies), with a conformance runner mirroring the Ruby reference",
5
5
  "type": "module",
6
6
  "license": "MIT",