@pubid/pubid 0.1.19 → 0.1.21
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.
|
@@ -29,7 +29,13 @@ export function checkCase(testCase, impl) {
|
|
|
29
29
|
for (const alias of testCase.nonNormalizedAliases ?? []) {
|
|
30
30
|
try {
|
|
31
31
|
const aliased = impl.parse(alias.spelling);
|
|
32
|
-
|
|
32
|
+
// A multi-round chain walks another render/parse round per
|
|
33
|
+
// recorded `rounds` until it reaches the canonical human.
|
|
34
|
+
let human = aliased.toHuman();
|
|
35
|
+
for (let round = 1; round < (alias.rounds ?? 1); round += 1) {
|
|
36
|
+
human = impl.parse(human).toHuman();
|
|
37
|
+
}
|
|
38
|
+
if (human !== testCase.representations.human) {
|
|
33
39
|
mismatches.push(`${label} alias ${alias.spelling}`);
|
|
34
40
|
}
|
|
35
41
|
}
|
package/dist/corpus/types.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface Representations {
|
|
|
13
13
|
export interface Spelling {
|
|
14
14
|
spelling: string;
|
|
15
15
|
style?: string;
|
|
16
|
+
/** Render/parse rounds the spelling needs to reach the canonical human;
|
|
17
|
+
* present only above 1 (a documented multi-round normalization chain). */
|
|
18
|
+
rounds?: number;
|
|
16
19
|
}
|
|
17
20
|
export interface ErrorExpectation {
|
|
18
21
|
/** Neutral rejection code - never an implementation class. */
|
|
@@ -501,11 +501,15 @@ class IeeeBuilder {
|
|
|
501
501
|
// An explicit joint stage on the draft ("=DFDIS.3") IS the stage —
|
|
502
502
|
// it outranks the ordinal ladder, which classifies IEEE-internal
|
|
503
503
|
// drafts only (docs/IEEE-DRAFT-STAGES.md §1.3).
|
|
504
|
+
let draftVersionPresent = false;
|
|
504
505
|
if (parsed["draft"] !== undefined) {
|
|
505
506
|
let draftData = parsed["draft"];
|
|
506
507
|
if (Array.isArray(draftData)) {
|
|
507
508
|
draftData = mergeParsedArray(draftData);
|
|
508
509
|
}
|
|
510
|
+
// A verbatim draft designator string is a version in itself.
|
|
511
|
+
if (!isObj(draftData))
|
|
512
|
+
draftVersionPresent = true;
|
|
509
513
|
if (isObj(draftData) && draftData["draft_iso_stage"] !== undefined) {
|
|
510
514
|
const stage = extractValue(draftData["draft_iso_stage"]) ?? "";
|
|
511
515
|
// The doubled-D alias ("DFDIS") carries its own draft marker;
|
|
@@ -516,9 +520,21 @@ class IeeeBuilder {
|
|
|
516
520
|
: stage;
|
|
517
521
|
}
|
|
518
522
|
if (isObj(draftData) && draftData["draft_version"] !== undefined) {
|
|
523
|
+
draftVersionPresent = true;
|
|
519
524
|
const dv = draftData["draft_version"];
|
|
520
525
|
const version = Array.isArray(dv) ? dv.map((v) => extractValue(v) ?? "").join("") : extractValue(dv);
|
|
521
526
|
if (version !== undefined) {
|
|
527
|
+
// A draft version that IS an ISO stage token ("/DCD1" and
|
|
528
|
+
// "/CD1" both capture "CD1") takes that stage on a plain IEEE
|
|
529
|
+
// document (docs/IEEE-DRAFT-STAGES.md §1.3, "DDIS" = DIS); a
|
|
530
|
+
// joint ISO-led stage-word draft keeps its generic project
|
|
531
|
+
// stage.
|
|
532
|
+
const pubs = parsed["publishers"];
|
|
533
|
+
const copubs = isObj(pubs) ? pubs["copublishers"] : undefined;
|
|
534
|
+
const joint = copubs !== undefined && (Array.isArray(copubs) ? copubs.length > 0 : true);
|
|
535
|
+
const stageMatch = /^(FDIS|FCD|CDV|DIS|CD|WD|PWI|NP)\d*$/.exec(version);
|
|
536
|
+
if (!joint && stageMatch !== null)
|
|
537
|
+
return stageMatch[1];
|
|
522
538
|
// relaton's hyphenated "/D-2" spelling captures the dash inside
|
|
523
539
|
// draft_version; the stage registry keys on "D2". A date-embedded
|
|
524
540
|
// version ("3-2017-07") keys on its leading ordinal.
|
|
@@ -534,7 +550,7 @@ class IeeeBuilder {
|
|
|
534
550
|
return "P";
|
|
535
551
|
if (typeValue === "Std")
|
|
536
552
|
return "Std";
|
|
537
|
-
if (typeValue === "Draft Std" &&
|
|
553
|
+
if (typeValue === "Draft Std" && !draftVersionPresent && parsed["digit_draft"] === undefined) {
|
|
538
554
|
return "D1";
|
|
539
555
|
}
|
|
540
556
|
if (/^No\.?$/.test(typeValue))
|
|
@@ -1259,6 +1259,15 @@ export function nistGrammarImplementation() {
|
|
|
1259
1259
|
const { cleaned, format } = preprocessNist(input);
|
|
1260
1260
|
const base = cleaned.replace(/\s*\(all parts\)\s*$/, "");
|
|
1261
1261
|
const tree = toBuilderTree(cachedRuntime(base));
|
|
1262
|
+
// The reference's language_code rule captures its separator with
|
|
1263
|
+
// the code (".uppl"); the baked nist artifact labels the code
|
|
1264
|
+
// alone. Restore the reference's slice for a trailing dot-prefixed
|
|
1265
|
+
// code, so the renderer prints the canonical double-dot face
|
|
1266
|
+
// ("NIST.SP.955-S..uppl").
|
|
1267
|
+
const dotted = /\.([a-z]{2,4})$/.exec(base);
|
|
1268
|
+
if (dotted && tree["translation"] === dotted[1]) {
|
|
1269
|
+
tree["translation"] = `.${dotted[1]}`;
|
|
1270
|
+
}
|
|
1262
1271
|
const marked = base !== cleaned
|
|
1263
1272
|
? { ...tree, all_parts: true }
|
|
1264
1273
|
: tree;
|
|
@@ -266,8 +266,11 @@ export function preprocessNist(input) {
|
|
|
266
266
|
// normalize_legacy_corpus_spellings: catalogue forms the grammar and
|
|
267
267
|
// the renderers cannot round-trip on their own. Each rule mirrors an
|
|
268
268
|
// already-parseable spelling of the same document.
|
|
269
|
-
// The mr
|
|
270
|
-
//
|
|
269
|
+
// The reference's mr translation CODE keeps its own leading dot
|
|
270
|
+
// (".uppl") and its renderer adds the separator dot, so the canonical
|
|
271
|
+
// face is "955-S..uppl". The grammar reads the collapsed single-dot
|
|
272
|
+
// spelling and captures the dot with the code; collapse here so a
|
|
273
|
+
// double-dot input reads the same way.
|
|
271
274
|
cleaned = cleaned.replace(/\.\./g, ".");
|
|
272
275
|
// Trailing-dot addendum spelling; the canonical render comes from
|
|
273
276
|
// the lowercase ".add" parse ("NBS.TN.467pt1.Add." -> ".add").
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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());
|
|
529
|
+
}
|
|
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}`;
|
|
502
535
|
}
|
|
503
|
-
|
|
504
|
-
|
|
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();
|
|
505
558
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
559
|
+
const year = parts.shift();
|
|
560
|
+
text += `-${org} ${number}`;
|
|
561
|
+
if (part !== undefined)
|
|
562
|
+
text += `-${part}`;
|
|
563
|
+
if (year !== undefined)
|
|
564
|
+
text += `:${year}`;
|
|
509
565
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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)}`);
|
|
575
|
+
}
|
|
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,50 @@ 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}`;
|
|
544
650
|
return parse(text);
|
|
545
651
|
},
|
|
546
652
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pubid/pubid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
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",
|