@pubid/pubid 0.1.23 → 0.1.25

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.
Binary file
@@ -118,6 +118,14 @@ function buildRules() {
118
118
  rule("tc_type", () => literalAlternation(TC_TYPES).as("tc_type"));
119
119
  rule("sc_type", () => str("SC/QC").or(str("SC")).as("sc_type"));
120
120
  rule("wg_type", () => literalAlternation(WG_TYPES).as("wg_type"));
121
+ // Bare TC reference (pubid-iso#285): "184/SC 4/WG 12 N10291" — the
122
+ // ISO/TC route without the ORIGINATOR prefix; the SC segment is
123
+ // required so bare numbers cannot leak into the route.
124
+ rule("bare_tc_document", () => digits.as("tc_number")
125
+ .then(str("/"), rules["sc_type"], space, digits.as("sc_number"))
126
+ .then(str("/"), rules["wg_type"], space, digits.as("wg_number")).maybe()
127
+ .then(space, str("N"), space.maybe(), digits.as("number"))
128
+ .then(str(":"), yearDigits.as("year")).maybe());
121
129
  rule("tc_subcommittee_part", () => str("/")
122
130
  .then(rules["sc_type"], space, digits.as("sc_number"), str("/"), rules["wg_type"], space, digits.as("wg_number"))
123
131
  .or(str("/").then(rules["sc_type"], space, digits.as("sc_number"))));
@@ -200,7 +208,7 @@ function buildRules() {
200
208
  .as("base")
201
209
  .then(space.maybe(), str("|"), space.maybe(), rules["idf_identifier"])
202
210
  .as("joint_identifier"));
203
- rule("identifier", () => rules["nsb_iso_identifier"].or(rules["tc_document"], rules["directives_identifiers"], rules["jcgm_identifier"], rules["iso_r_supplement_identifier"], rules["iso_r_identifier"], rules["supplement_supplement_supplement_identifier"], rules["supplement_supplement_identifier"], rules["supplement_identifier"], rules["joint_identifier"], rules["identifier_copublishers"]));
211
+ rule("identifier", () => rules["bare_tc_document"].or(rules["nsb_iso_identifier"], rules["tc_document"], rules["directives_identifiers"], rules["jcgm_identifier"], rules["iso_r_supplement_identifier"], rules["iso_r_identifier"], rules["supplement_supplement_supplement_identifier"], rules["supplement_supplement_identifier"], rules["supplement_identifier"], rules["joint_identifier"], rules["identifier_copublishers"]));
204
212
  rule("root", () => rules["identifier"]);
205
213
  return rules;
206
214
  }
@@ -147,6 +147,13 @@ class IsoBuilder {
147
147
  }
148
148
  if (tree["supplements"] !== undefined)
149
149
  return this.buildBundled(tree);
150
+ // Bare TC reference (pubid-iso#285): tc_type is absent by
151
+ // construction; sc_type is the route's required segment and cannot
152
+ // appear on a standard identifier. Canonicalizes the publisher to ISO.
153
+ if (tree["tc_type"] === undefined && tree["sc_type"] !== undefined && tree["tc_number"] !== undefined) {
154
+ tree["tc_type"] = "TC";
155
+ return this.buildTc(tree);
156
+ }
150
157
  if (tree["tc_type"] !== undefined)
151
158
  return this.buildTc(tree);
152
159
  return this.buildStandard(tree);
@@ -499,12 +506,25 @@ function parseIsoUrn(urn) {
499
506
  else
500
507
  supp.number = parts.shift();
501
508
  }
502
- if ((parts[0] ?? "").startsWith("v")) {
503
- const version = parts.shift().slice(1);
504
- supp.number = version.split(".")[0];
505
- }
509
+ // The legacy year-overloaded wire ("amd:2007:v2" -- the number rode
510
+ // the version slot): a v-token with no preceding number carries the
511
+ // NUMBER. The RFC 5141-bis order ("amd:1:2007:v2") keeps the number
512
+ // in its own slot, so a v-token after a number is the true
513
+ // <supplversion>.
514
+ const consumeV = () => {
515
+ if ((parts[0] ?? "").startsWith("v")) {
516
+ const value = parts.shift().slice(1).split(".")[0];
517
+ if (supp.number === undefined)
518
+ supp.number = value;
519
+ else
520
+ supp.version = value;
521
+ }
522
+ };
523
+ consumeV();
506
524
  if (supp.year === undefined && /^\d{4}$/.test(parts[0] ?? ""))
507
525
  supp.year = parts.shift();
526
+ // a v-token after the year would otherwise start a type-less wrap
527
+ consumeV();
508
528
  if (parts[0] !== undefined && !/^\d+$/.test(parts[0]) &&
509
529
  !/^(amd|cor|sup|add|v\d+|stage-)/i.test(parts[0])) {
510
530
  supp.languages = parts.shift().split(",").map((l) => l.toUpperCase()).join("/");
@@ -570,10 +590,16 @@ function parseIsoUrn(urn) {
570
590
  }
571
591
  const suppTree = {};
572
592
  if (supp.stage !== undefined) {
573
- const stage = ISO_TYPED_STAGES.find((s) => s.stageCode === supp.stage);
593
+ // A harmonized code resolves within the supplement's own type
594
+ // family -- the IS-family default would turn a staged Corrigendum
595
+ // into a plain IS (mirrors the gem's build_identifier).
596
+ const stage = /^\d\d\.\d\d$/.test(supp.stage)
597
+ ? ISO_TYPED_STAGES.find((st) => st.harmonized?.includes(supp.stage) && st.typeKey === supp.type) ??
598
+ ISO_TYPED_STAGES.find((st) => st.harmonized?.includes(supp.stage) && st.typeKey === "is")
599
+ : ISO_TYPED_STAGES.find((st) => st.stageCode === supp.stage);
574
600
  suppTree["type_with_stage"] = stage !== undefined
575
601
  ? stage.abbr[0] ?? ""
576
- : supp.stage.toUpperCase();
602
+ : supp.type?.toUpperCase() ?? supp.stage.toUpperCase();
577
603
  }
578
604
  if (supp.type !== undefined && suppTree["type_with_stage"] === undefined) {
579
605
  suppTree["type_with_stage"] = supp.type.toUpperCase();
@@ -582,6 +608,8 @@ function parseIsoUrn(urn) {
582
608
  suppTree["number"] = supp.number;
583
609
  if (supp.year !== undefined)
584
610
  suppTree["date"] = supp.year;
611
+ if (supp.version !== undefined)
612
+ suppTree["supplement_version"] = supp.version;
585
613
  if (supp.languages !== undefined)
586
614
  suppTree["languages"] = supp.languages;
587
615
  tree = { base: tree, ...suppTree };
@@ -465,14 +465,9 @@ class IsoUrnGenerator extends BaseUrnGenerator {
465
465
  const stageCode = ts.entry.stageCode;
466
466
  if (stageCode === "published")
467
467
  return undefined;
468
- const typeKey = id.typeKey();
469
- const combinedKey = typeKey !== "is" ? `${stageCode}${typeKey}` : stageCode;
470
- const stageAbbrUrn = TYPED_STAGE_MAP[combinedKey] ?? TYPED_STAGE_MAP[stageCode];
471
- if (stageAbbrUrn !== undefined) {
472
- if (id.stage_iteration !== undefined)
473
- return `${stageAbbrUrn}.${id.stage_iteration.render() ?? ""}`;
474
- return stageAbbrUrn;
475
- }
468
+ // RFC 5141 carries the stage as the harmonized code only; the typed
469
+ // abbreviations live in the human face. Legacy abbreviation URNs are
470
+ // accepted on ingestion and re-canonicalized here.
476
471
  const harmonized = ts.entry.harmonized;
477
472
  if (harmonized === undefined || harmonized.length === 0)
478
473
  return undefined;
@@ -558,17 +553,15 @@ class IsoUrnGenerator extends BaseUrnGenerator {
558
553
  parts.push(stageComp);
559
554
  parts.push(supp.typeKey() === "suppl" || supp.typeKey() === "add" ? "sup" : supp.typeKey());
560
555
  const iter = supp.typeKey() === "suppl" || supp.stage_iteration === undefined ? undefined : supp.stage_iteration.render();
556
+ // RFC 5141 supplement slots in order: number, year (the bis
557
+ // additive extension), then the published version.
558
+ if (supp.number !== undefined)
559
+ parts.push(supp.number);
561
560
  const d = urnDate(supp);
562
- if (d !== undefined) {
561
+ if (d !== undefined)
563
562
  parts.push(d);
564
- if (supp.number !== undefined)
565
- parts.push(iter !== undefined ? `v${supp.number}.${iter}` : `v${supp.number}`);
566
- }
567
- else {
568
- if (supp.number !== undefined)
569
- parts.push(supp.number);
570
- parts.push(iter !== undefined ? `v1.${iter}` : "v1");
571
- }
563
+ const version = supp.supplement_version ?? 1;
564
+ parts.push(iter !== undefined ? `v${version}.${iter}` : `v${version}`);
572
565
  if (supp.languages !== undefined && supp.languages.length > 0) {
573
566
  parts.push(supp.languages.map((l) => l.code).join(","));
574
567
  }
@@ -616,10 +609,14 @@ const supplementPublisherMappings = () => [
616
609
  },
617
610
  },
618
611
  ];
619
- const SUPPLEMENT_MAPPINGS = keyValue(...ISO_MAPPINGS, ...supplementPublisherMappings(), stageMapping(), { wire: "base", to: "base" });
612
+ const SUPPLEMENT_MAPPINGS = keyValue(...ISO_MAPPINGS, ...supplementPublisherMappings(), stageMapping(), { wire: "base", to: "base" },
613
+ // omitted when unset, so parsed human faces keep their hashes
614
+ { wire: "supplement_version", to: "supplement_version" });
620
615
  export class IsoSupplementIdentifier extends IsoSingleIdentifier {
621
616
  static attributes = extendAttributes(IsoSingleIdentifier, {
622
617
  base: { type: IsoIdentifier },
618
+ // The RFC 5141 <supplversion> (corrected version of the supplement).
619
+ supplement_version: { type: "string" },
623
620
  });
624
621
  static mappings = SUPPLEMENT_MAPPINGS;
625
622
  render() {
@@ -301,6 +301,14 @@ function cast(type, value, h, policy, series = "") {
301
301
  }
302
302
  case "part_number":
303
303
  return s(value);
304
+ case "n_letter_part": {
305
+ // Part type "n" with a letter value (pubid#555): the "1nA" /
306
+ // "1-1nA" annex tokens capture whole, the caster strips the n.
307
+ const v = s(value);
308
+ if (v === undefined)
309
+ return undefined;
310
+ return { part: new NistPart({ type: "n", value: v.replace(/^n/, "").toUpperCase() }) };
311
+ }
304
312
  case "letter_number": {
305
313
  if (!isObj(value))
306
314
  return undefined;
@@ -413,6 +421,11 @@ function cast(type, value, h, policy, series = "") {
413
421
  case "appendix":
414
422
  case "errata":
415
423
  case "index":
424
+ // The component's value is the long form either way (pubid#584):
425
+ // both spellings parse, the render prints the abbreviated indx.
426
+ if (s(value) === "indx")
427
+ return "index";
428
+ return s(value);
416
429
  case "insert":
417
430
  return s(value);
418
431
  case "revision": {
@@ -56,6 +56,8 @@ export declare abstract class BaseIdentifier {
56
56
  /** Ruby to_urn: the flavor's UrnGenerator, else the base template. */
57
57
  toUrn(): string;
58
58
  fromHash(hash: Record<string, unknown>): BaseIdentifier;
59
+ /** SubsetMatch: an all-parts reference skips the part attributes. */
60
+ subsetAllPartsWildcard(): boolean;
59
61
  protected classAttributes(): AttributeTable;
60
62
  /** Wire key for an attribute: custom mapping or the attribute name. */
61
63
  protected wireKeyFor(name: string): {
@@ -104,6 +104,10 @@ export class BaseIdentifier {
104
104
  fromHash(hash) {
105
105
  return this.constructor.fromHash(hash);
106
106
  }
107
+ /** SubsetMatch: an all-parts reference skips the part attributes. */
108
+ subsetAllPartsWildcard() {
109
+ return Boolean(this["all_parts"]);
110
+ }
107
111
  classAttributes() {
108
112
  return this.constructor.attributes;
109
113
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Subset match: `subsetMatch(reference, candidate)` — the TypeScript port of
3
+ * Pubid::SubsetMatch (lib/pubid/subset_match.rb), the semantics behind Ruby's
4
+ * `Identifier#===`.
5
+ *
6
+ * The match is true when every part that `reference` states matches
7
+ * `candidate`. A part the reference leaves nil or empty is a wildcard. The
8
+ * operator is NOT symmetric: the receiver is the reference (`ISO 9001` is
9
+ * matched by `ISO 9001:2015`, not the other way round).
10
+ *
11
+ * Rules (see the Ruby module for the normative docs):
12
+ * - Both objects must be of the same concrete class; a reference never falls
13
+ * back to the base document of a wrapper.
14
+ * - A default value is stated: `ISO 9001` (published stage) does not match
15
+ * `ISO/DIS 9001`.
16
+ * - A collection matches by position, and the reference may be shorter
17
+ * (`ISO/IEC 9001` matches `ISO/IEC/IEEE 9001`; `ISO/IEEE 9001` does not).
18
+ * - A STRICT attribute (class `subsetStrict` declaration) is exempt from the
19
+ * wildcards: the reference always states it, a stated collection must match
20
+ * in full, and nested matches run in BOTH directions with the class's own
21
+ * rule.
22
+ * - A reference with `allParts` set skips part/parts/subpart/allParts.
23
+ * - Nested identifiers and components recurse with their own match; every
24
+ * other value compares with deep scalar equality.
25
+ */
26
+ import { BaseIdentifier } from "./identifier.js";
27
+ import { Component } from "./component.js";
28
+ /** One attribute: theirs satisfies the reference value mine. */
29
+ export declare function valueMatch(mine: unknown, theirs: unknown): boolean;
30
+ /** Strict attributes compare exactly, composing nested matches both ways. */
31
+ export declare function exactMatch(mine: unknown, theirs: unknown): boolean;
32
+ /**
33
+ * True when `candidate` holds every part that `reference` states. Both must
34
+ * be identifiers or components of the same concrete class.
35
+ */
36
+ export declare function subsetMatch(reference: BaseIdentifier | Component, candidate: unknown): boolean;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Subset match: `subsetMatch(reference, candidate)` — the TypeScript port of
3
+ * Pubid::SubsetMatch (lib/pubid/subset_match.rb), the semantics behind Ruby's
4
+ * `Identifier#===`.
5
+ *
6
+ * The match is true when every part that `reference` states matches
7
+ * `candidate`. A part the reference leaves nil or empty is a wildcard. The
8
+ * operator is NOT symmetric: the receiver is the reference (`ISO 9001` is
9
+ * matched by `ISO 9001:2015`, not the other way round).
10
+ *
11
+ * Rules (see the Ruby module for the normative docs):
12
+ * - Both objects must be of the same concrete class; a reference never falls
13
+ * back to the base document of a wrapper.
14
+ * - A default value is stated: `ISO 9001` (published stage) does not match
15
+ * `ISO/DIS 9001`.
16
+ * - A collection matches by position, and the reference may be shorter
17
+ * (`ISO/IEC 9001` matches `ISO/IEC/IEEE 9001`; `ISO/IEEE 9001` does not).
18
+ * - A STRICT attribute (class `subsetStrict` declaration) is exempt from the
19
+ * wildcards: the reference always states it, a stated collection must match
20
+ * in full, and nested matches run in BOTH directions with the class's own
21
+ * rule.
22
+ * - A reference with `allParts` set skips part/parts/subpart/allParts.
23
+ * - Nested identifiers and components recurse with their own match; every
24
+ * other value compares with deep scalar equality.
25
+ */
26
+ import { BaseIdentifier } from "./identifier.js";
27
+ import { Component } from "./component.js";
28
+ /** Attributes an all-parts reference does not restrict. */
29
+ const ALL_PARTS_ATTRIBUTES = ["part", "parts", "subpart", "allParts"];
30
+ function isSubsetMatchable(v) {
31
+ return v instanceof BaseIdentifier || v instanceof Component;
32
+ }
33
+ /** True for undefined/null and for an empty string or collection. */
34
+ function blank(value) {
35
+ if (value === undefined || value === null)
36
+ return true;
37
+ if (typeof value === "string")
38
+ return value === "";
39
+ if (Array.isArray(value))
40
+ return value.length === 0;
41
+ return false;
42
+ }
43
+ /**
44
+ * Exact equality for non-identifier values: scalars by identity, plain
45
+ * objects and arrays structurally (Ruby's `==` on the lutaml values).
46
+ */
47
+ function scalarEqual(mine, theirs) {
48
+ if (mine === theirs)
49
+ return true;
50
+ if (Array.isArray(mine) && Array.isArray(theirs)) {
51
+ return mine.length === theirs.length && mine.every((v, i) => scalarEqual(v, theirs[i]));
52
+ }
53
+ if (Array.isArray(mine) || Array.isArray(theirs))
54
+ return false;
55
+ if (mine !== null && theirs !== null &&
56
+ typeof mine === "object" && typeof theirs === "object") {
57
+ const a = mine;
58
+ const b = theirs;
59
+ const keys = Object.keys(a);
60
+ if (keys.length !== Object.keys(b).length)
61
+ return false;
62
+ return keys.every((k) => k in b && scalarEqual(a[k], b[k]));
63
+ }
64
+ return false;
65
+ }
66
+ /** A shorter reference collection matches the leading elements. */
67
+ function collectionMatch(mine, theirs) {
68
+ if (!Array.isArray(theirs))
69
+ return false;
70
+ return mine.length <= theirs.length && mine.every((v, i) => valueMatch(v, theirs[i]));
71
+ }
72
+ /** A strict collection matches in full, never as a prefix. */
73
+ function exactCollectionMatch(mine, theirs) {
74
+ if (!Array.isArray(theirs))
75
+ return false;
76
+ return mine.length === theirs.length && mine.every((v, i) => exactMatch(v, theirs[i]));
77
+ }
78
+ /** One attribute: theirs satisfies the reference value mine. */
79
+ export function valueMatch(mine, theirs) {
80
+ if (blank(mine))
81
+ return true;
82
+ if (Array.isArray(mine))
83
+ return collectionMatch(mine, theirs);
84
+ if (isSubsetMatchable(mine))
85
+ return subsetMatch(mine, theirs);
86
+ return scalarEqual(mine, theirs);
87
+ }
88
+ /** Strict attributes compare exactly, composing nested matches both ways. */
89
+ export function exactMatch(mine, theirs) {
90
+ if (blank(mine) && blank(theirs))
91
+ return true;
92
+ if (Array.isArray(mine))
93
+ return exactCollectionMatch(mine, theirs);
94
+ if (isSubsetMatchable(mine)) {
95
+ return subsetMatch(mine, theirs) && isSubsetMatchable(theirs) && subsetMatch(theirs, mine);
96
+ }
97
+ return scalarEqual(mine, theirs);
98
+ }
99
+ /**
100
+ * True when `candidate` holds every part that `reference` states. Both must
101
+ * be identifiers or components of the same concrete class.
102
+ */
103
+ export function subsetMatch(reference, candidate) {
104
+ if (reference === candidate)
105
+ return true;
106
+ if (!(candidate instanceof reference.constructor))
107
+ return false;
108
+ const ctor = reference.constructor;
109
+ // Components (and any class without a declared attribute table) match
110
+ // conservatively: equal serialized forms. A component that wants the
111
+ // wildcard/strict rules declares a `static attributes` table.
112
+ if (!ctor.attributes || typeof ctor.attributes !== "object") {
113
+ const a = reference.toWire?.() ?? null;
114
+ const b = candidate?.toWire?.() ?? null;
115
+ return JSON.stringify(a) === JSON.stringify(b);
116
+ }
117
+ const strict = new Set(ctor.subsetStrict ?? []);
118
+ const ignored = new Set([
119
+ ...(ctor.subsetIgnoredAttributes ?? []),
120
+ ...(reference.subsetAllPartsWildcard?.()
121
+ ? ALL_PARTS_ATTRIBUTES
122
+ : []),
123
+ ]);
124
+ return Object.keys(ctor.attributes).every((name) => {
125
+ if (ignored.has(name))
126
+ return true;
127
+ const mine = reference[name];
128
+ const theirs = candidate[name];
129
+ if (strict.has(name))
130
+ return exactMatch(mine, theirs);
131
+ return valueMatch(mine, theirs);
132
+ });
133
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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",
@@ -14,7 +14,8 @@
14
14
  "test": "tsc -p tsconfig.test.json && node --test dist-test/test/*.test.js",
15
15
  "conformance": "tsc -p tsconfig.json && node dist/conformance/cli.js",
16
16
  "prepublishOnly": "tsc -p tsconfig.json",
17
- "artifact": "tsc -p tsconfig.json && node scripts/build-artifact.mjs"
17
+ "artifact": "tsc -p tsconfig.json && node scripts/build-artifact.mjs",
18
+ "doc:api": "node scripts/api-reference.mjs"
18
19
  },
19
20
  "dependencies": {
20
21
  "parsanol": "^0.10.4",
@@ -22,6 +23,7 @@
22
23
  },
23
24
  "devDependencies": {
24
25
  "@types/node": "^20.14.0",
26
+ "typedoc": "^0.28.20",
25
27
  "typescript": "^5.6.0"
26
28
  },
27
29
  "files": [