@pubid/pubid 0.1.23 → 0.1.24

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);
@@ -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.24",
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": [