@pubid/pubid 0.1.20 → 0.1.22
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/dist/flavors/iso/index.js +265 -1
- package/dist/flavors/iso/model.d.ts +1 -1
- package/dist/flavors/iso/model.js +16 -7
- package/dist/flavors/nist/model.js +4 -1
- package/dist/urn/parsers.js +181 -50
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
|
|
|
2
2
|
import { PubidDate, Language, Iteration } from "../../model/component.js";
|
|
3
3
|
import { isoGrammar } from "./grammar.js";
|
|
4
4
|
import { IsoBundledIdentifier, IsoDirectives, IsoDirectivesSupplement, IsoEdition, IsoPublisher, IsoSupplementIdentifier, IsoTcDocument, isoClassForType, locateStage, } from "./model.js";
|
|
5
|
+
import { ISO_TYPED_STAGES } from "./stages.generated.js";
|
|
5
6
|
/**
|
|
6
7
|
* Port of lib/pubid/iso/{builder,normalizer}.rb. Class dispatch follows
|
|
7
8
|
* the Ruby Builder: joint → Combined (unreachable by the corpus),
|
|
@@ -246,7 +247,14 @@ class IsoBuilder {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
const state = typeStateFromToken(s(tree["type_with_stage"]), "is");
|
|
249
|
-
|
|
250
|
+
// The explicit URN type token pins the class: a stage's abbreviation
|
|
251
|
+
// can overwrite type_with_stage with an IS-only stage ("WDA" for
|
|
252
|
+
// 90.93), and the document must stay a TR/TS (the Ruby builder reads
|
|
253
|
+
// the same document_type hint).
|
|
254
|
+
const documentType = s(tree["document_type"]);
|
|
255
|
+
const typeKey = documentType !== undefined
|
|
256
|
+
? documentType.toLowerCase()
|
|
257
|
+
: state?.entry.typeCode ?? "is";
|
|
250
258
|
if (typeKey === "dir-sup")
|
|
251
259
|
return this.buildDirectivesSupplement(tree, state);
|
|
252
260
|
if (typeKey === "dir")
|
|
@@ -334,8 +342,264 @@ function parseHuman(input) {
|
|
|
334
342
|
}
|
|
335
343
|
return new IsoBuilder().build(tree);
|
|
336
344
|
}
|
|
345
|
+
// --- URN ingestion (lib/pubid/iso/urn_parser.rb) ----------------------------
|
|
346
|
+
// RFC 5141-bis typed-stage abbreviations → stage codes. "DIS"/"ADIS" are
|
|
347
|
+
// ISO ladder stages the reference maps; everything else mirrors the gem's
|
|
348
|
+
// TYPED_STAGE_REVERSE_MAP verbatim.
|
|
349
|
+
const URN_STAGE_ABBRS = {
|
|
350
|
+
WD: "wd", WDS: "wds", CD: "cd", CDV: "cdv", DIS: "dis", FDIS: "fdis",
|
|
351
|
+
PDAM: "pdam", DAM: "dam", FDAM: "fdamd", DCOR: "dcor", FDCOR: "fdcor",
|
|
352
|
+
CDTS: "cdts", DTS: "dts", FDTS: "fdts",
|
|
353
|
+
};
|
|
354
|
+
const URN_TYPE_TOKENS = new Set([
|
|
355
|
+
"tr", "ts", "pas", "guide", "dir", "dir-sup", "iwa-sup", "isp", "iwa", "r", "data",
|
|
356
|
+
]);
|
|
357
|
+
const URN_SUPP_TYPES = new Set(["amd", "cor", "sup", "add"]);
|
|
358
|
+
// The URN type tokens are the gem's type keys; the ts leaf for ISO/R is
|
|
359
|
+
// keyed "rec".
|
|
360
|
+
const URN_TYPE_KEYS = { r: "rec" };
|
|
361
|
+
function urnStageByHarmonized(code) {
|
|
362
|
+
const candidates = ISO_TYPED_STAGES.filter((s) => s.harmonized?.includes(code));
|
|
363
|
+
// The gem prefers the IS family when a code is shared across families
|
|
364
|
+
// (pubid#570 — the registry order is load-order dependent there).
|
|
365
|
+
return candidates.find((s) => s.typeCode === "is") ?? candidates[0];
|
|
366
|
+
}
|
|
367
|
+
// When the URN gives a harmonized code and no type token, the document is
|
|
368
|
+
// an IS: prefer the non-published IS stages (PRF over the published IS for
|
|
369
|
+
// 60.00), then the stage covering the most codes (WD over WDS).
|
|
370
|
+
function urnIsStageByHarmonized(code) {
|
|
371
|
+
const candidates = ISO_TYPED_STAGES.filter((s) => s.typeCode === "is" && s.harmonized?.includes(code));
|
|
372
|
+
if (candidates.length === 0)
|
|
373
|
+
return undefined;
|
|
374
|
+
const nonPublished = candidates.filter((s) => s.stageCode !== "published");
|
|
375
|
+
const pool = nonPublished.length > 0 ? nonPublished : candidates;
|
|
376
|
+
let best = pool[0];
|
|
377
|
+
for (const candidate of pool) {
|
|
378
|
+
if ((candidate.harmonized?.length ?? 0) > (best.harmonized?.length ?? 0))
|
|
379
|
+
best = candidate;
|
|
380
|
+
}
|
|
381
|
+
return best;
|
|
382
|
+
}
|
|
383
|
+
// The URN carries parts as "10164:-5-1-1" or "9001-1"; split into
|
|
384
|
+
// number / part / subpart (a subpart keeps its dashes).
|
|
385
|
+
function parseUrnNumberPart(raw) {
|
|
386
|
+
if (raw === undefined)
|
|
387
|
+
return [undefined, undefined, undefined];
|
|
388
|
+
if (/^\w+:-/.test(raw)) {
|
|
389
|
+
const segments = raw.split(":-");
|
|
390
|
+
const tail = segments.slice(1).join("-").split("-");
|
|
391
|
+
return [
|
|
392
|
+
segments[0],
|
|
393
|
+
tail[0],
|
|
394
|
+
tail.length > 1 ? tail.slice(1).join("-") : undefined,
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
if (raw.includes("-")) {
|
|
398
|
+
const segments = raw.split("-");
|
|
399
|
+
return [
|
|
400
|
+
segments[0],
|
|
401
|
+
segments[1],
|
|
402
|
+
segments.length > 2 ? segments.slice(2).join("-") : undefined,
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
return [raw, undefined, undefined];
|
|
406
|
+
}
|
|
407
|
+
function parseIsoUrn(urn) {
|
|
408
|
+
if (!urn.startsWith("urn:iso:std:"))
|
|
409
|
+
throw new Error(`Invalid ISO URN: ${urn}`);
|
|
410
|
+
let parts = urn.slice("urn:iso:std:".length).split(":");
|
|
411
|
+
// A trailing "ser" marks an all-parts identifier.
|
|
412
|
+
let allParts = false;
|
|
413
|
+
if (parts[parts.length - 1] === "ser") {
|
|
414
|
+
parts = parts.slice(0, -1);
|
|
415
|
+
allParts = true;
|
|
416
|
+
}
|
|
417
|
+
const publishers = (parts.shift() ?? "").split("-").map((p) => p.toUpperCase());
|
|
418
|
+
let typeCode;
|
|
419
|
+
if (parts[0] !== undefined && URN_TYPE_TOKENS.has(parts[0].toLowerCase())) {
|
|
420
|
+
typeCode = parts.shift().toLowerCase();
|
|
421
|
+
}
|
|
422
|
+
let [number, part, subpart] = parseUrnNumberPart(parts.shift());
|
|
423
|
+
let year;
|
|
424
|
+
if (/^\d{4}$/.test(parts[0] ?? ""))
|
|
425
|
+
year = parts.shift();
|
|
426
|
+
// URN-style part notation can trail the year (":10164:2019:-22").
|
|
427
|
+
if ((parts[0] ?? "").startsWith("-")) {
|
|
428
|
+
const partStr = parts.shift();
|
|
429
|
+
[number, part, subpart] = parseUrnNumberPart(`${number}${partStr}`);
|
|
430
|
+
}
|
|
431
|
+
if (year === undefined && /^\d{4}$/.test(parts[0] ?? ""))
|
|
432
|
+
year = parts.shift();
|
|
433
|
+
let edition;
|
|
434
|
+
if ((parts[0] ?? "").startsWith("ed-"))
|
|
435
|
+
edition = parts.shift().slice(3);
|
|
436
|
+
let languages;
|
|
437
|
+
if (parts[0] !== undefined && !/^\d+$/.test(parts[0]) &&
|
|
438
|
+
!/^(amd|cor|sup|add|v\d+|stage-|ed-)/i.test(parts[0]) &&
|
|
439
|
+
!(parts[0].toUpperCase() in URN_STAGE_ABBRS) &&
|
|
440
|
+
!/^[A-Za-z]+\.\d+$/.test(parts[0])) {
|
|
441
|
+
languages = parts.shift().split(",").map((l) => l.toUpperCase()).join("/");
|
|
442
|
+
}
|
|
443
|
+
// Stage: either "stage-XX.XX[.vN]" (harmonized) or a typed abbreviation
|
|
444
|
+
// ("WD", "CD.2").
|
|
445
|
+
let stageCode;
|
|
446
|
+
let stageFromAbbr;
|
|
447
|
+
let stageIteration;
|
|
448
|
+
if ((parts[0] ?? "").startsWith("stage-")) {
|
|
449
|
+
const stageStr = parts.shift();
|
|
450
|
+
stageCode = stageStr.slice(6).replace(/\.v\d+$/i, "");
|
|
451
|
+
const iteration = /\.v(\d+)$/i.exec(stageStr);
|
|
452
|
+
if (iteration !== null)
|
|
453
|
+
stageIteration = iteration[1];
|
|
454
|
+
}
|
|
455
|
+
else if ((parts[0] !== undefined && parts[0].toUpperCase() in URN_STAGE_ABBRS) ||
|
|
456
|
+
(parts[0] !== undefined && /^[A-Za-z]+\.\d+$/.test(parts[0]) &&
|
|
457
|
+
(parts[0].toUpperCase().split(".")[0] ?? "") in URN_STAGE_ABBRS)) {
|
|
458
|
+
const stageAbbr = parts[0].toUpperCase();
|
|
459
|
+
// A supplement-family stage directly before its own supplement type
|
|
460
|
+
// token ("...:FDAM:amd:1:v1") names the SUPPLEMENT's stage: consuming
|
|
461
|
+
// it as the base's stage builds the base as a base-less supplement,
|
|
462
|
+
// which cannot render (the gem guards the same way).
|
|
463
|
+
const family = locateStage(stageAbbr.split(".")[0] ?? "")?.typeCode;
|
|
464
|
+
const supplementFollows = (family !== undefined &&
|
|
465
|
+
["amd", "cor", "suppl", "add"].includes(family)) &&
|
|
466
|
+
parts.slice(1).some((token) => URN_SUPP_TYPES.has(token.toLowerCase()) &&
|
|
467
|
+
(URN_TYPE_KEYS[token.toLowerCase()] ?? token.toLowerCase()) === family);
|
|
468
|
+
if (!supplementFollows) {
|
|
469
|
+
parts.shift();
|
|
470
|
+
if (stageAbbr.includes(".")) {
|
|
471
|
+
const abbrPart = stageAbbr.split(".")[0];
|
|
472
|
+
const iterationPart = stageAbbr.split(".")[1];
|
|
473
|
+
stageCode = URN_STAGE_ABBRS[abbrPart];
|
|
474
|
+
stageFromAbbr = abbrPart;
|
|
475
|
+
stageIteration = iterationPart;
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
stageCode = URN_STAGE_ABBRS[stageAbbr];
|
|
479
|
+
stageFromAbbr = stageAbbr;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Supplements (amd, cor, sup, add), left-to-right: each wraps the previous.
|
|
484
|
+
const supplements = [];
|
|
485
|
+
while (parts.length > 0) {
|
|
486
|
+
const supp = {};
|
|
487
|
+
if ((parts[0] ?? "").startsWith("stage-")) {
|
|
488
|
+
supp.stage = parts.shift().slice(6).replace(/\.v\d+$/i, "");
|
|
489
|
+
}
|
|
490
|
+
else if (parts[0] !== undefined && parts[0].toUpperCase() in URN_STAGE_ABBRS) {
|
|
491
|
+
supp.stage = URN_STAGE_ABBRS[parts.shift().toUpperCase()];
|
|
492
|
+
}
|
|
493
|
+
if (parts[0] !== undefined && URN_SUPP_TYPES.has(parts[0].toLowerCase())) {
|
|
494
|
+
supp.type = parts.shift().toLowerCase();
|
|
495
|
+
}
|
|
496
|
+
if (/^\d+$/.test(parts[0] ?? "")) {
|
|
497
|
+
if (/^\d{4}$/.test(parts[0]))
|
|
498
|
+
supp.year = parts.shift();
|
|
499
|
+
else
|
|
500
|
+
supp.number = parts.shift();
|
|
501
|
+
}
|
|
502
|
+
if ((parts[0] ?? "").startsWith("v")) {
|
|
503
|
+
const version = parts.shift().slice(1);
|
|
504
|
+
supp.number = version.split(".")[0];
|
|
505
|
+
}
|
|
506
|
+
if (supp.year === undefined && /^\d{4}$/.test(parts[0] ?? ""))
|
|
507
|
+
supp.year = parts.shift();
|
|
508
|
+
if (parts[0] !== undefined && !/^\d+$/.test(parts[0]) &&
|
|
509
|
+
!/^(amd|cor|sup|add|v\d+|stage-)/i.test(parts[0])) {
|
|
510
|
+
supp.languages = parts.shift().split(",").map((l) => l.toUpperCase()).join("/");
|
|
511
|
+
}
|
|
512
|
+
supplements.push(supp);
|
|
513
|
+
}
|
|
514
|
+
// Builder tree, mirroring the gem's build_identifier.
|
|
515
|
+
const base = {};
|
|
516
|
+
if (publishers[0] !== undefined && publishers[0] !== "") {
|
|
517
|
+
base["publisher"] = publishers[0];
|
|
518
|
+
if (publishers.length > 1) {
|
|
519
|
+
base["copublishers"] = publishers.slice(1).map((c) => ({ copublisher: c }));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (part !== undefined || subpart !== undefined) {
|
|
523
|
+
base["number_with_part"] = [number, part, subpart].filter((p) => p !== undefined).join("-");
|
|
524
|
+
}
|
|
525
|
+
else if (number !== undefined) {
|
|
526
|
+
base["number"] = number;
|
|
527
|
+
}
|
|
528
|
+
if (year !== undefined)
|
|
529
|
+
base["date"] = year;
|
|
530
|
+
if (edition !== undefined)
|
|
531
|
+
base["edition"] = edition;
|
|
532
|
+
if (typeCode !== undefined && typeCode !== "is") {
|
|
533
|
+
base["type_with_stage"] = typeCode.toUpperCase();
|
|
534
|
+
// Pin the document class against the stage block below overwriting
|
|
535
|
+
// type_with_stage (the Ruby builder reads the same hint).
|
|
536
|
+
base["document_type"] = (URN_TYPE_KEYS[typeCode] ?? typeCode).toUpperCase();
|
|
537
|
+
}
|
|
538
|
+
if (stageCode !== undefined) {
|
|
539
|
+
let stage;
|
|
540
|
+
if (stageFromAbbr !== undefined) {
|
|
541
|
+
stage = locateStage(stageFromAbbr) ??
|
|
542
|
+
ISO_TYPED_STAGES.find((s) => s.stageCode === stageCode);
|
|
543
|
+
}
|
|
544
|
+
else if (typeCode === undefined || typeCode === "is") {
|
|
545
|
+
stage = urnIsStageByHarmonized(stageCode);
|
|
546
|
+
// FCD is a legacy spelling of the DIS stage in URN form.
|
|
547
|
+
if (stageCode === "40.00" && stage?.stageCode === "fcd") {
|
|
548
|
+
stage = ISO_TYPED_STAGES.find((s) => s.stageCode === "dis" && s.harmonized?.includes("40.00")) ?? stage;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
stage ??= urnStageByHarmonized(stageCode);
|
|
552
|
+
if (stage !== undefined)
|
|
553
|
+
base["type_with_stage"] = stage.abbr[0] ?? "";
|
|
554
|
+
else
|
|
555
|
+
base["stage"] = stageCode;
|
|
556
|
+
if (stageIteration !== undefined)
|
|
557
|
+
base["stage_iteration"] = stageIteration;
|
|
558
|
+
}
|
|
559
|
+
if (languages !== undefined)
|
|
560
|
+
base["languages"] = languages;
|
|
561
|
+
let tree = base;
|
|
562
|
+
for (const supp of supplements) {
|
|
563
|
+
// A bare language token with no supplement identity names the BASE
|
|
564
|
+
// document (the gem merges it the same way — a wrapper would drop it).
|
|
565
|
+
if (supp.type === undefined && supp.stage === undefined &&
|
|
566
|
+
supp.number === undefined && supp.year === undefined &&
|
|
567
|
+
supp.languages !== undefined) {
|
|
568
|
+
base["languages"] = supp.languages;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const suppTree = {};
|
|
572
|
+
if (supp.stage !== undefined) {
|
|
573
|
+
const stage = ISO_TYPED_STAGES.find((s) => s.stageCode === supp.stage);
|
|
574
|
+
suppTree["type_with_stage"] = stage !== undefined
|
|
575
|
+
? stage.abbr[0] ?? ""
|
|
576
|
+
: supp.stage.toUpperCase();
|
|
577
|
+
}
|
|
578
|
+
if (supp.type !== undefined && suppTree["type_with_stage"] === undefined) {
|
|
579
|
+
suppTree["type_with_stage"] = supp.type.toUpperCase();
|
|
580
|
+
}
|
|
581
|
+
if (supp.number !== undefined)
|
|
582
|
+
suppTree["number"] = supp.number;
|
|
583
|
+
if (supp.year !== undefined)
|
|
584
|
+
suppTree["date"] = supp.year;
|
|
585
|
+
if (supp.languages !== undefined)
|
|
586
|
+
suppTree["languages"] = supp.languages;
|
|
587
|
+
tree = { base: tree, ...suppTree };
|
|
588
|
+
}
|
|
589
|
+
const identifier = new IsoBuilder().build(tree);
|
|
590
|
+
if (allParts) {
|
|
591
|
+
identifier.all_parts = true;
|
|
592
|
+
}
|
|
593
|
+
return identifier;
|
|
594
|
+
}
|
|
337
595
|
export function isoGrammarImplementation() {
|
|
338
596
|
return {
|
|
597
|
+
// Inverse of the ISO UrnGenerator (lib/pubid/iso/urn_parser.rb): the
|
|
598
|
+
// reference rebuilds a builder hash straight from the positional URN
|
|
599
|
+
// fields, so the port does the same against the shared IsoBuilder.
|
|
600
|
+
parseUrn(urn) {
|
|
601
|
+
return parseIsoUrn(urn);
|
|
602
|
+
},
|
|
339
603
|
parse(input) {
|
|
340
604
|
return parseHuman(input);
|
|
341
605
|
},
|
|
@@ -61,7 +61,7 @@ export declare abstract class IsoIdentifier extends BaseIdentifier {
|
|
|
61
61
|
protected stageString(): string;
|
|
62
62
|
/** Render with an EXPLICIT long-format flag (the supplement
|
|
63
63
|
* renderer propagates its own context to the base). */
|
|
64
|
-
renderWithFormat(formatLong: boolean): string;
|
|
64
|
+
renderWithFormat(formatLong: boolean, withLanguages?: boolean): string;
|
|
65
65
|
/** Ruby SupplementRenderer renders the base with the wrapper's
|
|
66
66
|
* detected context (stage_format_long propagates down the chain;
|
|
67
67
|
* with_date being non-nil pins it against re-detection — a nested
|
|
@@ -119,7 +119,7 @@ export class IsoIdentifier extends BaseIdentifier {
|
|
|
119
119
|
}
|
|
120
120
|
/** Render with an EXPLICIT long-format flag (the supplement
|
|
121
121
|
* renderer propagates its own context to the base). */
|
|
122
|
-
renderWithFormat(formatLong) {
|
|
122
|
+
renderWithFormat(formatLong, withLanguages = true) {
|
|
123
123
|
let result = "";
|
|
124
124
|
if (this.typeKey() === "iwa") {
|
|
125
125
|
const stage = this.typedStageState === undefined ? "" : stageAbbr(this.typedStageState.entry, formatLong);
|
|
@@ -141,7 +141,8 @@ export class IsoIdentifier extends BaseIdentifier {
|
|
|
141
141
|
const num = this.numberPortion(true);
|
|
142
142
|
if (num !== "")
|
|
143
143
|
result += result === "" ? num : ` ${num}`;
|
|
144
|
-
|
|
144
|
+
if (withLanguages)
|
|
145
|
+
result += this.languagePortion(this.languageSingle());
|
|
145
146
|
if (this.all_parts === true)
|
|
146
147
|
result += " (all parts)";
|
|
147
148
|
return result;
|
|
@@ -152,7 +153,11 @@ export class IsoIdentifier extends BaseIdentifier {
|
|
|
152
153
|
* amendment renders "AMD" under a "CD Cor" top but "Amd" under a
|
|
153
154
|
* "Cor." top). */
|
|
154
155
|
renderAsSupplementBase(long) {
|
|
155
|
-
|
|
156
|
+
// The reference re-renders a nested base from the four propagated
|
|
157
|
+
// options alone, and that rebuilt context defaults to
|
|
158
|
+
// with_language_code :none — only the outermost identifier renders
|
|
159
|
+
// its own language portion.
|
|
160
|
+
return this.renderWithFormat(long, false);
|
|
156
161
|
}
|
|
157
162
|
stageStringFor(formatLong) {
|
|
158
163
|
const ts = this.typedStageState;
|
|
@@ -618,12 +623,15 @@ export class IsoSupplementIdentifier extends IsoSingleIdentifier {
|
|
|
618
623
|
});
|
|
619
624
|
static mappings = SUPPLEMENT_MAPPINGS;
|
|
620
625
|
render() {
|
|
621
|
-
return this.renderSupplement(this.stageFormatLong());
|
|
626
|
+
return this.renderSupplement(this.stageFormatLong(), false);
|
|
622
627
|
}
|
|
623
628
|
renderAsSupplementBase(long) {
|
|
624
|
-
return this.renderSupplement(long);
|
|
629
|
+
return this.renderSupplement(long, true);
|
|
625
630
|
}
|
|
626
|
-
|
|
631
|
+
// The reference renders a nested base from a rebuilt context whose
|
|
632
|
+
// with_language_code defaults to :none, so only the outermost
|
|
633
|
+
// identifier of a chain renders its language portion.
|
|
634
|
+
renderSupplement(long, asBase) {
|
|
627
635
|
const base = this.base;
|
|
628
636
|
const baseStr = base === undefined ? "" : base.renderAsSupplementBase(long);
|
|
629
637
|
const ts = this.typedStageState;
|
|
@@ -635,7 +643,8 @@ export class IsoSupplementIdentifier extends IsoSingleIdentifier {
|
|
|
635
643
|
result += " ";
|
|
636
644
|
result += num;
|
|
637
645
|
}
|
|
638
|
-
|
|
646
|
+
if (!asBase)
|
|
647
|
+
result += this.languagePortion(this.languageSingle());
|
|
639
648
|
return result;
|
|
640
649
|
}
|
|
641
650
|
}
|
|
@@ -688,7 +688,10 @@ function nistClass(spec) {
|
|
|
688
688
|
}
|
|
689
689
|
renderMr() {
|
|
690
690
|
if (spec.style === "fips") {
|
|
691
|
-
|
|
691
|
+
// The MR form keeps a parsed publisher ("NIST.FIPS.197-upd1" is the
|
|
692
|
+
// DOI suffix NIST publishes); a bare FIPS invents none.
|
|
693
|
+
const series = this.seriesCode() ?? "";
|
|
694
|
+
let result = this.publisher !== undefined ? `${this.publisher}.${series}` : series;
|
|
692
695
|
if (this.number !== undefined)
|
|
693
696
|
result += `.${this.number}`;
|
|
694
697
|
if (this.part instanceof NistPart)
|
package/dist/urn/parsers.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { splitParts, withYear } from "./types.js";
|
|
2
|
-
import { grammarImplementation } from "../flavors/index.js";
|
|
3
|
-
import { NationalAdoption } from "../flavors/evs/model.js";
|
|
4
2
|
// Each parser mirrors lib/pubid/<flavor>/urn_parser.rb#parse_urn exactly —
|
|
5
3
|
// including its lossiness (dropped segments stay dropped) and its error
|
|
6
4
|
// cases. The parity fixture (test/fixtures/urn-parity.json) pins the
|
|
7
5
|
// reference behavior for ~93k corpus URNs.
|
|
6
|
+
// The adopted CEN spine consumes its own supplements; their urn segments
|
|
7
|
+
// carry the consolidation marker ("plus" = "+A1", its absence = "/A1") —
|
|
8
|
+
// cen_cenelec/urn_generator.rb's supplement_segments.
|
|
9
|
+
function adoptedSupplement(parts) {
|
|
10
|
+
let faces = "";
|
|
11
|
+
while (["plus", "slash", "amd", "cor", "frag"].includes(parts[0] ?? "")) {
|
|
12
|
+
let sep = "/";
|
|
13
|
+
let kind;
|
|
14
|
+
if (parts[0] === "plus" || parts[0] === "slash") {
|
|
15
|
+
sep = parts.shift() === "plus" ? "+" : "/";
|
|
16
|
+
kind = parts.shift();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
kind = parts.shift();
|
|
20
|
+
}
|
|
21
|
+
const num = /^\d{1,3}$/.test(parts[0] ?? "") ? parts.shift() : undefined;
|
|
22
|
+
const yr = /^\d{4}$/.test(parts[0] ?? "") ? parts.shift() : undefined;
|
|
23
|
+
const iter = num ?? "";
|
|
24
|
+
const year = yr !== undefined ? `:${yr}` : "";
|
|
25
|
+
if (kind === "amd")
|
|
26
|
+
faces += `${sep}A${iter}${year}`;
|
|
27
|
+
else if (kind === "cor")
|
|
28
|
+
faces += `${sep}AC${iter}${year}`;
|
|
29
|
+
else
|
|
30
|
+
faces += ` FRAG${iter}`;
|
|
31
|
+
}
|
|
32
|
+
return faces;
|
|
33
|
+
}
|
|
8
34
|
export const URN_PARSERS = {
|
|
9
35
|
// lib/pubid/ansi/urn_parser.rb
|
|
10
36
|
ansi: {
|
|
@@ -459,64 +485,111 @@ export const URN_PARSERS = {
|
|
|
459
485
|
return parse(text);
|
|
460
486
|
},
|
|
461
487
|
},
|
|
462
|
-
// lib/pubid/evs/urn_parser.rb —
|
|
463
|
-
//
|
|
488
|
+
// lib/pubid/evs/urn_parser.rb — reassemble the identifier's human text
|
|
489
|
+
// (draft prefix, adopted org, national annex, language, supplements)
|
|
490
|
+
// and delegate to the EVS grammar, exactly like the reference.
|
|
464
491
|
evs: {
|
|
465
492
|
prefix: "urn:evs:",
|
|
466
493
|
reconstruct(body, parse) {
|
|
467
494
|
const parts = splitParts(body);
|
|
468
|
-
const
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
}
|
|
495
|
+
const draft = parts[0] === "pr";
|
|
496
|
+
if (draft)
|
|
497
|
+
parts.shift();
|
|
472
498
|
const orgs = {
|
|
473
499
|
iso: "ISO",
|
|
474
500
|
"iso-iec": "ISO/IEC",
|
|
475
501
|
iec: "IEC",
|
|
476
502
|
cispr: "CISPR",
|
|
477
503
|
};
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
504
|
+
const types = { ts: "TS", tr: "TR" };
|
|
505
|
+
// The trailing language and the EVS-level national annex peel
|
|
506
|
+
// before the body dispatch — both are EVS-level, never CEN
|
|
507
|
+
// segments.
|
|
508
|
+
let language;
|
|
509
|
+
const last = parts[parts.length - 1];
|
|
510
|
+
if (parts.length > 1 && last !== undefined && /^(et-en|et|en)$/.test(last)) {
|
|
511
|
+
language = parts.pop();
|
|
481
512
|
}
|
|
482
|
-
let
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
513
|
+
let naPresent = false;
|
|
514
|
+
let naYear;
|
|
515
|
+
const naIndex = parts.indexOf("na");
|
|
516
|
+
if (naIndex >= 0) {
|
|
517
|
+
parts.splice(naIndex, 1);
|
|
518
|
+
naPresent = true;
|
|
519
|
+
if (/^\d{4}$/.test(parts[naIndex] ?? ""))
|
|
520
|
+
naYear = parts.splice(naIndex, 1)[0];
|
|
488
521
|
}
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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)}`);
|
|
522
|
+
const head = parts.shift() ?? "";
|
|
523
|
+
let text = draft ? "prEVS" : "EVS";
|
|
524
|
+
if (/^en$/i.test(head)) {
|
|
525
|
+
text += "-EN";
|
|
526
|
+
const orgParts = [];
|
|
527
|
+
while (parts[0] !== undefined && /^[a-z][a-z-]*$/.test(parts[0])) {
|
|
528
|
+
orgParts.push(parts.shift());
|
|
502
529
|
}
|
|
503
|
-
if (
|
|
504
|
-
|
|
530
|
+
if (orgParts.length) {
|
|
531
|
+
const org = orgs[orgParts.join("-")];
|
|
532
|
+
if (org === undefined)
|
|
533
|
+
throw new Error(`unknown adopted org in ${JSON.stringify(body)}`);
|
|
534
|
+
text += ` ${org}`;
|
|
505
535
|
}
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
|
|
536
|
+
const number = parts.shift();
|
|
537
|
+
if (number === undefined)
|
|
538
|
+
throw new Error(`missing number in ${JSON.stringify(body)}`);
|
|
539
|
+
text += ` ${number}`;
|
|
540
|
+
const year = parts.shift();
|
|
541
|
+
if (year !== undefined)
|
|
542
|
+
text += `:${year}`;
|
|
543
|
+
text += adoptedSupplement(parts);
|
|
544
|
+
}
|
|
545
|
+
else if (head === "ts" || head === "tr") {
|
|
546
|
+
const number = parts.shift();
|
|
547
|
+
const year = parts.shift();
|
|
548
|
+
text += `-${types[head]} ${number}`;
|
|
549
|
+
if (year !== undefined)
|
|
550
|
+
text += `:${year}`;
|
|
551
|
+
}
|
|
552
|
+
else if (head === "iso" || head === "iec" || head === "cispr") {
|
|
553
|
+
const org = orgs[head];
|
|
554
|
+
const number = parts.shift();
|
|
555
|
+
let part;
|
|
556
|
+
if (/^\d+$/.test(parts[0] ?? "") && !/^\d{4}$/.test(parts[0]) && parts.length > 1) {
|
|
557
|
+
part = parts.shift();
|
|
558
|
+
}
|
|
559
|
+
const year = parts.shift();
|
|
560
|
+
text += `-${org} ${number}`;
|
|
561
|
+
if (part !== undefined)
|
|
562
|
+
text += `-${part}`;
|
|
563
|
+
if (year !== undefined)
|
|
564
|
+
text += `:${year}`;
|
|
565
|
+
}
|
|
566
|
+
else if (/^\d/.test(head)) {
|
|
567
|
+
const number = head;
|
|
568
|
+
const year = parts.shift();
|
|
569
|
+
text += ` ${number}`;
|
|
570
|
+
if (year !== undefined)
|
|
571
|
+
text += `:${year}`;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
throw new Error(`unsupported shape in ${JSON.stringify(body)}`);
|
|
509
575
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
576
|
+
if (parts.length)
|
|
577
|
+
throw new Error(`trailing segments in ${JSON.stringify(body)}`);
|
|
578
|
+
if (naPresent) {
|
|
579
|
+
text += "+NA";
|
|
580
|
+
if (naYear !== undefined)
|
|
581
|
+
text += `:${naYear}`;
|
|
582
|
+
}
|
|
583
|
+
if (language !== undefined)
|
|
584
|
+
text += ` ${language}`;
|
|
585
|
+
return parse(text);
|
|
516
586
|
},
|
|
517
587
|
},
|
|
518
|
-
// lib/pubid/nist/urn_parser.rb —
|
|
519
|
-
//
|
|
588
|
+
// lib/pubid/nist/urn_parser.rb — the generator dot-joins the identity's
|
|
589
|
+
// fields after the number; reassemble the human spelling the flavor
|
|
590
|
+
// parser reads (edition/volume glue onto the number, a revision glues
|
|
591
|
+
// after them, ".supp" marks supplementability, ".supp-<v>" is "sup<v>",
|
|
592
|
+
// insert/errata glue).
|
|
520
593
|
nist: {
|
|
521
594
|
prefix: "urn:nist:",
|
|
522
595
|
reconstruct(body, parse) {
|
|
@@ -530,17 +603,75 @@ export const URN_PARSERS = {
|
|
|
530
603
|
const typeToken = parts[0] ?? "";
|
|
531
604
|
const payload = parts[1] ?? "";
|
|
532
605
|
const label = typeUpcase[typeToken.toLowerCase()] ?? typeToken.toUpperCase();
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
606
|
+
const tokens = payload.split(".");
|
|
607
|
+
// Ruby's String#split drops trailing empty fields; JS keeps them, and a
|
|
608
|
+
// trailing "." would become a stray marker that the flavor rejects.
|
|
609
|
+
while (tokens.length > 0 && tokens[tokens.length - 1] === "")
|
|
610
|
+
tokens.pop();
|
|
611
|
+
const code = tokens.shift() ?? "";
|
|
612
|
+
let marker = "";
|
|
613
|
+
let part;
|
|
614
|
+
let edition;
|
|
615
|
+
let volume;
|
|
616
|
+
let revision;
|
|
617
|
+
let supplement;
|
|
618
|
+
for (const token of tokens) {
|
|
619
|
+
const supp = /^supp-(.+)$/is.exec(token);
|
|
620
|
+
if (/^r\d+$/i.test(token))
|
|
621
|
+
revision = token;
|
|
622
|
+
else if (/^e\w+$/i.test(token))
|
|
623
|
+
edition = token;
|
|
624
|
+
else if (/^v\d+$/i.test(token))
|
|
625
|
+
volume = token;
|
|
626
|
+
else if (supp !== null)
|
|
627
|
+
supplement = supp[1];
|
|
628
|
+
else if (token === "supp")
|
|
629
|
+
continue; // supplementability marker
|
|
630
|
+
else if (token === "insert" || token === "errata")
|
|
631
|
+
marker += token;
|
|
632
|
+
else if (/^[A-Z]$/.test(token))
|
|
633
|
+
part = token; // part letter, glued to the code
|
|
634
|
+
else if (/^[A-Z][a-z]+\d{4}$/.test(token)) {
|
|
635
|
+
// the edition's additional text ("13e2.June1908")
|
|
636
|
+
if (edition !== undefined)
|
|
637
|
+
edition = `${edition}.${token}`;
|
|
638
|
+
}
|
|
639
|
+
else
|
|
640
|
+
marker += `.${token}`;
|
|
641
|
+
}
|
|
537
642
|
// The NBS-era series carry the NBS imprint in their canonical form
|
|
538
643
|
// ("NBS CSM 1") — the rebuild names the publisher the document
|
|
539
644
|
// carries, or the flavor parse rejects its own URN's rebuild.
|
|
540
645
|
const publisher = typeToken.toLowerCase() === "csm" ? "NBS" : "NIST";
|
|
541
|
-
let text = `${publisher} ${label} ${code}`;
|
|
542
|
-
|
|
543
|
-
|
|
646
|
+
let text = `${publisher} ${label} ${code}${part ?? ""}${marker}`;
|
|
647
|
+
text += `${edition ?? ""}${volume ?? ""}${revision ?? ""}`;
|
|
648
|
+
if (supplement !== undefined)
|
|
649
|
+
text += `sup${supplement}`;
|
|
650
|
+
return parse(text);
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
// lib/pubid/easc/urn_parser.rb — "urn:easc:<series>[:v]:<number>[:<year>]";
|
|
654
|
+
// the human face renders the Cyrillic series label
|
|
655
|
+
// ("ПМГ 02-2008", "ПМГ В 31-2001", "РМГ 151-2025").
|
|
656
|
+
easc: {
|
|
657
|
+
prefix: "urn:easc:",
|
|
658
|
+
reconstruct(body, parse) {
|
|
659
|
+
const parts = body.split(":");
|
|
660
|
+
const series = parts.shift() ?? "";
|
|
661
|
+
if (series === "") {
|
|
662
|
+
throw new Error(`EASC URN missing series: ${JSON.stringify(body)}`);
|
|
663
|
+
}
|
|
664
|
+
const label = series.toUpperCase() === "PMG" ? "ПМГ" : "РМГ";
|
|
665
|
+
let rest = parts;
|
|
666
|
+
let separator = " ";
|
|
667
|
+
if (/^v$/i.test(rest[0] ?? "")) {
|
|
668
|
+
separator = " В ";
|
|
669
|
+
rest = rest.slice(1);
|
|
670
|
+
}
|
|
671
|
+
const number = rest[0] ?? "";
|
|
672
|
+
let text = `${label}${separator}${number}`;
|
|
673
|
+
if (rest[1] !== undefined)
|
|
674
|
+
text += `-${rest[1]}`;
|
|
544
675
|
return parse(text);
|
|
545
676
|
},
|
|
546
677
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pubid/pubid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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",
|