@pubid/pubid 0.1.33 → 0.1.35

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.
Files changed (118) hide show
  1. package/artifact/corpus.json.gz +0 -0
  2. package/dist/flavors/api/grammar.js +9 -2
  3. package/dist/flavors/api/index.js +31 -2
  4. package/dist/flavors/bipm/index.js +89 -14
  5. package/dist/flavors/bsi/model.js +13 -9
  6. package/dist/flavors/iec/index.js +15 -4
  7. package/dist/flavors/iec/model.js +26 -10
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +2 -0
  10. package/dist/model/identifier.d.ts +3 -0
  11. package/dist/model/identifier.js +6 -0
  12. package/dist/pubid-urn/decoder.d.ts +27 -0
  13. package/dist/pubid-urn/decoder.js +165 -0
  14. package/dist/pubid-urn/encoder.d.ts +30 -0
  15. package/dist/pubid-urn/encoder.js +197 -0
  16. package/dist/pubid-urn/errors.d.ts +15 -0
  17. package/dist/pubid-urn/errors.js +21 -0
  18. package/dist/pubid-urn/escape.d.ts +19 -0
  19. package/dist/pubid-urn/escape.js +65 -0
  20. package/dist/pubid-urn/index.d.ts +65 -0
  21. package/dist/pubid-urn/index.js +216 -0
  22. package/dist/pubid-urn/paths.d.ts +26 -0
  23. package/dist/pubid-urn/paths.js +129 -0
  24. package/dist/pubid-urn/schema.d.ts +88 -0
  25. package/dist/pubid-urn/schema.js +186 -0
  26. package/dist/pubid-urn/schemas/adobe.d.ts +8 -0
  27. package/dist/pubid-urn/schemas/adobe.js +15 -0
  28. package/dist/pubid-urn/schemas/amca.d.ts +8 -0
  29. package/dist/pubid-urn/schemas/amca.js +19 -0
  30. package/dist/pubid-urn/schemas/ansi.d.ts +8 -0
  31. package/dist/pubid-urn/schemas/ansi.js +30 -0
  32. package/dist/pubid-urn/schemas/api.d.ts +8 -0
  33. package/dist/pubid-urn/schemas/api.js +25 -0
  34. package/dist/pubid-urn/schemas/ashrae.d.ts +8 -0
  35. package/dist/pubid-urn/schemas/ashrae.js +28 -0
  36. package/dist/pubid-urn/schemas/asme.d.ts +8 -0
  37. package/dist/pubid-urn/schemas/asme.js +26 -0
  38. package/dist/pubid-urn/schemas/astm.d.ts +8 -0
  39. package/dist/pubid-urn/schemas/astm.js +38 -0
  40. package/dist/pubid-urn/schemas/bipm.d.ts +8 -0
  41. package/dist/pubid-urn/schemas/bipm.js +34 -0
  42. package/dist/pubid-urn/schemas/bsi.d.ts +8 -0
  43. package/dist/pubid-urn/schemas/bsi.js +98 -0
  44. package/dist/pubid-urn/schemas/calconnect.d.ts +8 -0
  45. package/dist/pubid-urn/schemas/calconnect.js +16 -0
  46. package/dist/pubid-urn/schemas/ccsds.d.ts +8 -0
  47. package/dist/pubid-urn/schemas/ccsds.js +18 -0
  48. package/dist/pubid-urn/schemas/cencenelec.d.ts +8 -0
  49. package/dist/pubid-urn/schemas/cencenelec.js +39 -0
  50. package/dist/pubid-urn/schemas/cie.d.ts +8 -0
  51. package/dist/pubid-urn/schemas/cie.js +41 -0
  52. package/dist/pubid-urn/schemas/csa.d.ts +8 -0
  53. package/dist/pubid-urn/schemas/csa.js +45 -0
  54. package/dist/pubid-urn/schemas/doi.d.ts +8 -0
  55. package/dist/pubid-urn/schemas/doi.js +14 -0
  56. package/dist/pubid-urn/schemas/easc.d.ts +8 -0
  57. package/dist/pubid-urn/schemas/easc.js +16 -0
  58. package/dist/pubid-urn/schemas/ecma.d.ts +8 -0
  59. package/dist/pubid-urn/schemas/ecma.js +17 -0
  60. package/dist/pubid-urn/schemas/etsi.d.ts +8 -0
  61. package/dist/pubid-urn/schemas/etsi.js +23 -0
  62. package/dist/pubid-urn/schemas/evs.d.ts +8 -0
  63. package/dist/pubid-urn/schemas/evs.js +30 -0
  64. package/dist/pubid-urn/schemas/gb.d.ts +8 -0
  65. package/dist/pubid-urn/schemas/gb.js +18 -0
  66. package/dist/pubid-urn/schemas/gost.d.ts +8 -0
  67. package/dist/pubid-urn/schemas/gost.js +23 -0
  68. package/dist/pubid-urn/schemas/iala.d.ts +8 -0
  69. package/dist/pubid-urn/schemas/iala.js +26 -0
  70. package/dist/pubid-urn/schemas/iana.d.ts +8 -0
  71. package/dist/pubid-urn/schemas/iana.js +13 -0
  72. package/dist/pubid-urn/schemas/idf.d.ts +8 -0
  73. package/dist/pubid-urn/schemas/idf.js +34 -0
  74. package/dist/pubid-urn/schemas/iec.d.ts +8 -0
  75. package/dist/pubid-urn/schemas/iec.js +61 -0
  76. package/dist/pubid-urn/schemas/ieee.d.ts +8 -0
  77. package/dist/pubid-urn/schemas/ieee.js +116 -0
  78. package/dist/pubid-urn/schemas/ietf.d.ts +8 -0
  79. package/dist/pubid-urn/schemas/ietf.js +17 -0
  80. package/dist/pubid-urn/schemas/iho.d.ts +8 -0
  81. package/dist/pubid-urn/schemas/iho.js +35 -0
  82. package/dist/pubid-urn/schemas/index.d.ts +9 -0
  83. package/dist/pubid-urn/schemas/index.js +97 -0
  84. package/dist/pubid-urn/schemas/isbn.d.ts +8 -0
  85. package/dist/pubid-urn/schemas/isbn.js +14 -0
  86. package/dist/pubid-urn/schemas/iso.d.ts +8 -0
  87. package/dist/pubid-urn/schemas/iso.js +46 -0
  88. package/dist/pubid-urn/schemas/itu.d.ts +8 -0
  89. package/dist/pubid-urn/schemas/itu.js +61 -0
  90. package/dist/pubid-urn/schemas/jcgm.d.ts +8 -0
  91. package/dist/pubid-urn/schemas/jcgm.js +21 -0
  92. package/dist/pubid-urn/schemas/jis.d.ts +8 -0
  93. package/dist/pubid-urn/schemas/jis.js +24 -0
  94. package/dist/pubid-urn/schemas/nist.d.ts +8 -0
  95. package/dist/pubid-urn/schemas/nist.js +90 -0
  96. package/dist/pubid-urn/schemas/oasis.d.ts +8 -0
  97. package/dist/pubid-urn/schemas/oasis.js +17 -0
  98. package/dist/pubid-urn/schemas/ogc.d.ts +8 -0
  99. package/dist/pubid-urn/schemas/ogc.js +14 -0
  100. package/dist/pubid-urn/schemas/oiml.d.ts +8 -0
  101. package/dist/pubid-urn/schemas/oiml.js +39 -0
  102. package/dist/pubid-urn/schemas/omg.d.ts +8 -0
  103. package/dist/pubid-urn/schemas/omg.js +15 -0
  104. package/dist/pubid-urn/schemas/plateau.d.ts +8 -0
  105. package/dist/pubid-urn/schemas/plateau.js +20 -0
  106. package/dist/pubid-urn/schemas/sae.d.ts +8 -0
  107. package/dist/pubid-urn/schemas/sae.js +18 -0
  108. package/dist/pubid-urn/schemas/tgpp.d.ts +8 -0
  109. package/dist/pubid-urn/schemas/tgpp.js +17 -0
  110. package/dist/pubid-urn/schemas/un.d.ts +8 -0
  111. package/dist/pubid-urn/schemas/un.js +13 -0
  112. package/dist/pubid-urn/schemas/w3c.d.ts +8 -0
  113. package/dist/pubid-urn/schemas/w3c.js +23 -0
  114. package/dist/pubid-urn/schemas/xsf.d.ts +8 -0
  115. package/dist/pubid-urn/schemas/xsf.js +12 -0
  116. package/dist/pubid-urn/slug.d.ts +20 -0
  117. package/dist/pubid-urn/slug.js +47 -0
  118. package/package.json +2 -1
Binary file
@@ -53,9 +53,16 @@ function buildRules() {
53
53
  rule("typeless_identifier", () => ref(rules, "publisher")
54
54
  .then(docTail())
55
55
  .then(ref(rules, "reaffirmation").maybe()));
56
- rule("second_identifier", () => ref(rules, "doc_type")
56
+ rule("second_identifier", () =>
57
+ // The reference's grammar binds the second side's captures under
58
+ // second_* names (second_type/second_number/second_part), so the
59
+ // builder can fold first+second and let the second side overwrite.
60
+ ref(rules, "doc_type").as("second_type")
57
61
  .then(ref(rules, "space"))
58
- .then(docTail()));
62
+ .then(match("[0-9A-Z]").repeat(1, Infinity).as("second_number")
63
+ .then((ref(rules, "dash").then((match("[0-9A-Z]").repeat(1, Infinity)).as("second_part"))).maybe())
64
+ .then(ref(rules, "part_notation").maybe())
65
+ .then(ref(rules, "date_dash").or(ref(rules, "date_colon")).maybe())));
59
66
  rule("combined_identifier", () => ref(rules, "mpms_identifier")
60
67
  .or(ref(rules, "typed_identifier"))
61
68
  .or(ref(rules, "typeless_identifier"))
@@ -52,13 +52,42 @@ function assignAttributes(identifier, data) {
52
52
  self[key] = realized;
53
53
  }
54
54
  }
55
+ // The bound grammar wraps the combined form's first/second identifiers
56
+ // under "first"/"second"; the reference folds both wrappers into one
57
+ // hash in parse order, so the second side's number/part/year overwrite
58
+ // the first's — and select_class saw no top-level :type, so a combined
59
+ // form always built the typeless class. Reproduce both.
60
+ const SECOND_KEYS = ["second_number", "second_part", "second_year", "second_type"];
61
+ const SECOND_OVERWRITES = {
62
+ second_number: "number",
63
+ second_part: "part",
64
+ second_year: "year",
65
+ };
66
+ function unflattenCombined(data) {
67
+ if (!SECOND_KEYS.some((k) => k in data))
68
+ return data;
69
+ const out = { ...data };
70
+ delete out["type"];
71
+ for (const [source, target] of Object.entries(SECOND_OVERWRITES)) {
72
+ if (source in out)
73
+ out[target] = out[source];
74
+ }
75
+ return out;
76
+ }
55
77
  export function buildApiIdentifier(tree) {
56
78
  const data = flatten(tree);
57
- const typeStr = strv(data["type"]);
79
+ const first = isObj(data["first"]) ? data["first"] : undefined;
80
+ const second = isObj(data["second"]) ? data["second"] : undefined;
81
+ // Parse-order fold: the first side's captures, then the second's.
82
+ const combined = { ...data, ...(first ?? {}), ...(second ?? {}) };
83
+ delete combined["first"];
84
+ delete combined["second"];
85
+ const unflattened = unflattenCombined(combined);
86
+ const typeStr = strv(unflattened["type"]);
58
87
  const klass = (typeStr !== undefined ? TYPE_CLASS_MAP[typeStr] : undefined)
59
88
  ?? TypelessStandardClass;
60
89
  const identifier = new klass();
61
- assignAttributes(identifier, data);
90
+ assignAttributes(identifier, unflattened);
62
91
  return identifier;
63
92
  }
64
93
  export function apiGrammarImplementation() {
@@ -4,13 +4,14 @@ import { BaseIdentifier, registerType } from "../../model/identifier.js";
4
4
  import { extendAttributes, keyValue } from "../../model/attribute.js";
5
5
  import { BaseUrnGenerator } from "../../model/urn-generator.js";
6
6
  /**
7
- * 1:1 port of lib/pubid/bipm/ — 90 corpus rows across six families:
7
+ * 1:1 port of lib/pubid/bipm/ — corpus rows across six families:
8
8
  * committee documents (REC/RES/DECN/ACT/DECL in short and long EN/FR
9
- * forms), meetings (EN ordinals, FR <sup>e</sup> réunion), the
10
- * Metrologia journal, the SI Brochure and its derived products, mises
11
- * en pratique (incl. Rapport BIPM-YYYY/NN), and CC guides. Historic
12
- * group names (CCDS) and two-letter language codes (EN/FR) normalize
13
- * at parse time and never reach the hash.
9
+ * forms, with the appendix/annex complement and the publisher-prefixed
10
+ * decision spelling), meetings (EN ordinals and double-dash forms, FR
11
+ * <sup>e</sup> réunion), the Metrologia journal, the SI Brochure and
12
+ * its derived products, mises en pratique (incl. Rapport BIPM-YYYY/NN),
13
+ * and CC guides. Historic group names (CCDS) and two-letter language
14
+ * codes (EN/FR) normalize at parse time and never reach the hash.
14
15
  */
15
16
  const GROUPS = [
16
17
  "CIPM", "CGPM", "JCRB", "CCTF", "CCQM", "CCT", "CCL", "CCAUV",
@@ -66,15 +67,31 @@ function buildRules() {
66
67
  const connective = () => str("de la").or(str("du"));
67
68
  const ordinal = () => str("st").or(str("nd"), str("rd"), str("th"));
68
69
  const meetingWordFr = () => str("Réunion").or(str("réunion"));
69
- // The trailing date is optional so "CCTF REC 2" parses date-less.
70
+ // The trailing date is optional so "CCTF REC 2" parses date-less. The
71
+ // appendix/annex complement tail ("CIPM DECN 112 A3.5 (2022, F)")
72
+ // rides the number: "A" + digits [ "." + digits ].
70
73
  rule("committee_short", () => rules["group"]
71
- .then(space, rules["type_abbrev"], space.then(rules["number"]).maybe(), space.then(rules["year_paren"]).maybe())
74
+ .then(space, rules["type_abbrev"], space.then(rules["number"], space
75
+ .then(str("A").then(digits.as("appendix"), str(".").then(digits.as("annex")).maybe()))
76
+ .maybe()).maybe(), space.then(rules["year_paren"]).maybe())
72
77
  .as("committee_short"));
78
+ // The publisher-prefixed decision form the corpus cites
79
+ // ("BIPM DECN CIPM/101-1 (2012, EN)"): a literal BIPM publisher lead,
80
+ // the outcome type, then the body-qualified resource-specific-id
81
+ // "{body}/{id}". It names the same document as the unprefixed
82
+ // "<body> <TYPE> <id>" form, so the builder dispatches it through the
83
+ // same CommitteeDocument path.
84
+ rule("committee_prefixed", () => str("BIPM")
85
+ .then(space, rules["type_abbrev"], space, rules["group"], str("/"), rules["number"], space.then(rules["year_paren"]).maybe())
86
+ .as("committee_prefixed"));
73
87
  rule("committee_long_en", () => rules["group"]
74
88
  .then(space, rules["type_name_en"], space.then(rules["number"]).maybe(), space.then(rules["year_paren_nolang"]).maybe())
75
89
  .as("committee_long_en"));
76
90
  rule("committee_long_fr", () => rules["type_name_fr"]
77
91
  .then(space.then(rules["number"]).maybe(), space, connective(), space, rules["group"], space.then(rules["year_paren_nolang"]).maybe())
92
+ // The corpus-cited French body-qualified form ("Décision
93
+ // CIPM/101-1 (2012)").
94
+ .or(rules["type_name_fr"].then(space, rules["group"], str("/"), rules["number"], space.then(rules["year_paren_nolang"]).maybe()))
78
95
  .as("committee_long_fr"));
79
96
  // Loose consumer form: a French type name in group-leading order.
80
97
  rule("committee_group_fr", () => rules["group"]
@@ -87,6 +104,9 @@ function buildRules() {
87
104
  rule("meeting_en", () => rules["group"]
88
105
  .then(space, rules["number"], ordinal(), space, str("Meeting"), space.then(rules["year_paren"]).maybe())
89
106
  .or(rules["group"].then(space, str("Meeting"), space, rules["number"], space.then(rules["year_paren"]).maybe()))
107
+ // The double-dash consumer spellings ("CGPM -- Meeting 1 (1889)").
108
+ .or(rules["group"].then(space, str("--"), space, rules["number"], ordinal(), space, str("Meeting"), space.then(rules["year_paren"]).maybe()))
109
+ .or(rules["group"].then(space, str("--"), space, str("Meeting"), space, rules["number"], space.then(rules["year_paren"]).maybe()))
90
110
  .as("meeting_en"));
91
111
  rule("meeting_fr", () => rules["group"]
92
112
  .then(space, rules["number"], str("<sup>e</sup>"), space, str("réunion"), space.then(rules["year_paren"]).maybe())
@@ -102,10 +122,23 @@ function buildRules() {
102
122
  rule("edition", () => digits.then(str("e")).as("edition"));
103
123
  rule("version", () => str("v").then(match("[0-9.]").repeat(1, Infinity)).as("version"));
104
124
  rule("years", () => digits.then(str("/").then(digits).maybe()).as("years"));
105
- rule("si_brochure", () => str("BIPM SI Brochure")
125
+ rule("si_brochure", () =>
126
+ // The guide's French publication identifier leads with
127
+ // "Brochure sur le SI" instead of the English brochure name; all
128
+ // three leads render in the canonical "BIPM SI Brochure …" spelling.
129
+ str("BIPM SI Brochure")
106
130
  .or(str("SI Brochure"))
107
- .then(space, str("sur le SI").then(space).maybe(), rules["edition"], space, rules["version"], space, str("("), rules["years"], str(", "), lang(), str(")"))
131
+ .or(str("Brochure sur le SI"))
132
+ .then(space, str("sur le SI").then(space).maybe(), rules["edition"],
133
+ // The version and the language code are optional (the guide's
134
+ // language-independent form, "SI Brochure 9e (2019, E)").
135
+ space.then(rules["version"]).maybe(), space, str("("), rules["years"], str(", ").then(lang()).maybe(), str(")"))
108
136
  .as("si_brochure"));
137
+ // The corpus-cited ordinal-edition spelling ("SI Brochure 9th edition
138
+ // (2019)") normalizes the English ordinal onto the canonical "9e".
139
+ rule("si_brochure_ordinal_edition", () => str("SI Brochure")
140
+ .then(space, digits.then(ordinal()).as("ordinal_edition"), space, str("edition"), space.then(rules["year_paren"]).maybe())
141
+ .as("si_brochure_ordinal_edition"));
109
142
  rule("brochure_variant", () => str("Appendix")
110
143
  .then(space, digits)
111
144
  .or(str("Concise"), str("FAQ"))
@@ -134,7 +167,7 @@ function buildRules() {
134
167
  .or(guideBody())
135
168
  .as("guide"));
136
169
  rule("group_leading", () => rules["meeting_en"].or(rules["meeting_fr"], rules["committee_short"], rules["committee_long_en"], rules["committee_group_fr"], rules["committee_bare"]));
137
- rule("identifier", () => rules["metrologia"].or(rules["si_brochure"], rules["si_brochure_variant"], rules["si_brochure_section"], rules["mep"], rules["guide"], rules["committee_long_fr"], rules["group_leading"]));
170
+ rule("identifier", () => rules["metrologia"].or(rules["si_brochure"], rules["si_brochure_variant"], rules["si_brochure_section"], rules["si_brochure_ordinal_edition"], rules["mep"], rules["guide"], rules["committee_long_fr"], rules["committee_prefixed"], rules["group_leading"]));
138
171
  rule("root", () => rules["identifier"]);
139
172
  return rules;
140
173
  }
@@ -183,6 +216,16 @@ function ordinalSuffix(number) {
183
216
  function frenchConnective(group) {
184
217
  return group === "CGPM" ? "de la" : "du";
185
218
  }
219
+ /** The appendix/annex complement folds into the identifier segment as
220
+ * "-a{appendix}[.{annex}]" (ruby committee_number_segment); a bare
221
+ * number is digits and dashes only, so the suffix is unambiguous. */
222
+ function committeeNumberSegment(id) {
223
+ if (id.number === undefined)
224
+ return "";
225
+ if (id.appendix === undefined)
226
+ return id.number;
227
+ return `${id.number}-a${id.appendix}${id.annex === undefined ? "" : `.${id.annex}`}`;
228
+ }
186
229
  export class BipmIdentifier extends BaseIdentifier {
187
230
  static attributes = extendAttributes(BaseIdentifier, BIPM_ATTRS);
188
231
  static mappings = BIPM_MAPPINGS;
@@ -205,7 +248,7 @@ class BipmUrnGenerator extends BaseUrnGenerator {
205
248
  if (id instanceof BipmCommitteeDocument) {
206
249
  const parts = [
207
250
  "urn", "bipm", id.group?.toLowerCase() ?? "",
208
- id.type_code?.toLowerCase() ?? "", id.number ?? "",
251
+ id.type_code?.toLowerCase() ?? "", committeeNumberSegment(id),
209
252
  ];
210
253
  if (id.year !== undefined)
211
254
  parts.push(String(id.year));
@@ -248,7 +291,15 @@ class BipmCommitteeDocument extends BipmIdentifier {
248
291
  }
249
292
  const lang = this.language === undefined ? "" : `, ${this.language}`;
250
293
  const type = this.type_code === undefined ? "" : ` ${this.type_code}`;
251
- return `${this.group ?? ""}${type}${this.numberSegment()}${this.yearSegment(lang)}`;
294
+ return `${this.group ?? ""}${type}${this.numberSegment()}${this.appendixSegment()}${this.yearSegment(lang)}`;
295
+ }
296
+ /** The complement tail "{identifier} A{appendix}[.{annex}]" — parts and
297
+ * subparts are dash forms inside `number` itself; appendices and
298
+ * annexes are this separate tail. */
299
+ appendixSegment() {
300
+ if (this.appendix === undefined)
301
+ return "";
302
+ return ` A${this.appendix}${this.annex === undefined ? "" : `.${this.annex}`}`;
252
303
  }
253
304
  }
254
305
  registerType(BipmCommitteeDocument);
@@ -294,7 +345,8 @@ class BipmSiBrochure extends BipmIdentifier {
294
345
  }
295
346
  const phrase = this.language === "F" ? "sur le SI " : "";
296
347
  const lang = this.language === undefined ? "" : `, ${this.language}`;
297
- return `BIPM SI Brochure ${phrase}${this.edition} ${this.version} (${this.years}${lang})`;
348
+ const version = this.version === undefined ? "" : ` ${this.version}`;
349
+ return `BIPM SI Brochure ${phrase}${this.edition}${version} (${this.years}${lang})`;
298
350
  }
299
351
  }
300
352
  registerType(BipmSiBrochure);
@@ -336,6 +388,12 @@ class BipmBuilder {
336
388
  };
337
389
  if (tree["committee_short"] !== undefined)
338
390
  return this.buildCommittee(node("committee_short"), "short");
391
+ if (tree["committee_prefixed"] !== undefined) {
392
+ // The corpus's "BIPM DECN CIPM/101-1 (2012, EN)" publisher-prefixed
393
+ // spelling names the same document as the unprefixed short form; the
394
+ // BIPM lead is the constant publisher, so it carries no attribute.
395
+ return this.buildCommittee(node("committee_prefixed"), "short");
396
+ }
339
397
  if (tree["committee_long_en"] !== undefined)
340
398
  return this.buildCommittee(node("committee_long_en"), "long", "E");
341
399
  if (tree["committee_bare"] !== undefined)
@@ -373,6 +431,21 @@ class BipmBuilder {
373
431
  const n = node("si_brochure_section");
374
432
  return new BipmSiBrochure({ part: s(n["part"]) });
375
433
  }
434
+ if (tree["si_brochure_ordinal_edition"] !== undefined) {
435
+ // The guide prints the edition "9e"; the corpus-cited "9th edition"
436
+ // spelling is the English ordinal form of the same edition,
437
+ // normalized onto the canonical "9e" value. The version is absent
438
+ // from the input, so it stays nil and drops from the hash.
439
+ const n = node("si_brochure_ordinal_edition");
440
+ const edition = `${(s(n["ordinal_edition"]) ?? "").replace(/(st|nd|rd|th)$/, "")}e`;
441
+ const year = s(n["year"]);
442
+ return new BipmSiBrochure({
443
+ number: edition,
444
+ edition,
445
+ ...(year !== undefined ? { years: year } : {}),
446
+ language: normalizeLanguage(s(n["language"])),
447
+ });
448
+ }
376
449
  if (tree["mep"] !== undefined) {
377
450
  const n = node("mep");
378
451
  const mepCode = s(n["mep_code"]);
@@ -407,6 +480,8 @@ class BipmBuilder {
407
480
  ...(year !== undefined ? { year: Number(year) } : {}),
408
481
  language: normalizeLanguage(language ?? s(n["language"])),
409
482
  form,
483
+ appendix: s(n["appendix"]),
484
+ annex: s(n["annex"]),
410
485
  });
411
486
  }
412
487
  buildMeeting(n, language) {
@@ -776,15 +776,6 @@ export const ConsolidatedIdentifierClass = bsiClassWith({ kind: "consolidated-id
776
776
  .replace(/ - TC$/, "")
777
777
  .replace(/ PDF$/, "")
778
778
  .replace(/ \([A-Z][a-z]+\)$/, "");
779
- // The gem serializes only the `type` block onto the consolidated
780
- // wrapper (the members carry their own full typed-stage trio).
781
- ConsolidatedIdentifierClass.compactHash =
782
- (model, hash) => {
783
- const ts = model.typedStage;
784
- if (ts === undefined || ts.code === "pubbs")
785
- return;
786
- hash["type"] = { name: ts.name, abbr: ts.abbr[0], type_code: ts.typeCode };
787
- };
788
779
  for (const supp of supplements) {
789
780
  if (supp instanceof Amendment) {
790
781
  if (supp.amd_suffix_form) {
@@ -838,6 +829,19 @@ export const ConsolidatedIdentifierClass = bsiClassWith({ kind: "consolidated-id
838
829
  result += ` (R${ry})`;
839
830
  return result;
840
831
  });
832
+ // The gem serializes only the `type` block onto the consolidated
833
+ // wrapper (the members carry their own full typed-stage trio). This
834
+ // override is installed at load time, NOT inside the render closure: a
835
+ // fromHash -> toHash of a not-yet-rendered consolidated id must
836
+ // serialize identically (the pubid-scheme URN's canonical gate,
837
+ // pubid-ts#161, depends on it).
838
+ ConsolidatedIdentifierClass.compactHash =
839
+ (model, hash) => {
840
+ const ts = model.typedStage;
841
+ if (ts === undefined || ts.code === "pubbs")
842
+ return;
843
+ hash["type"] = { name: ts.name, abbr: ts.abbr[0], type_code: ts.typeCode };
844
+ };
841
845
  // ConsolidatedIdentifier#to_urn override (renderer.rb class method):
842
846
  // base URN plus per-supplement segments.
843
847
  export function consolidatedUrn(id) {
@@ -133,12 +133,16 @@ class IecBuilder {
133
133
  const kind = typedStage.typeCode === "wp" ? "is" : typedStage.typeCode;
134
134
  const attrs = {};
135
135
  this.commonAttrs(tree, attrs);
136
- // TRF with an embedded CISPR document: the CISPR number is runtime-only
137
- // (hash carries just year/publisher; the URN names the CISPR document).
136
+ // TRF with an embedded CISPR document: the form itself has no number
137
+ // — the document it reports on is built as the nested identifier and
138
+ // serializes under `cispr_identifier` (pubid#687). The runtime
139
+ // columns stay for the renderer and the official-URN slot.
138
140
  if (tree["trf_org"] !== undefined) {
139
141
  const cisprComponents = numberComponents(tree["number_with_part"]);
140
142
  attrs["cispr_number"] = cisprComponents["number"];
141
143
  attrs["cispr_part"] = cisprComponents["part"];
144
+ const cispr = new IEC_CLASSES.KIND_CLASSES["is"]({ publisher: "CISPR", ...cisprComponents });
145
+ attrs["cispr_identifier"] = cispr;
142
146
  delete attrs["number"];
143
147
  delete attrs["part"];
144
148
  delete attrs["subpart"];
@@ -300,9 +304,14 @@ class IecBuilder {
300
304
  });
301
305
  }
302
306
  buildWorkingProgramme(tree) {
307
+ // The grammar captures a trailing space on wp_type ("PWI TR 100-36"
308
+ // -> "TR "); ruby strips it at the construction site (its builder's
309
+ // :wp_type cast) so the stored value — and the hash and URN — is the
310
+ // stripped form, an all-space value dropping out entirely.
311
+ const wpTypeRaw = s(tree["wp_type"])?.replace(/\s+$/, "");
303
312
  const attrs = {
304
313
  wp_stage: s(tree["wp_stage"]),
305
- wp_type: s(tree["wp_type"]),
314
+ wp_type: wpTypeRaw === "" ? undefined : wpTypeRaw,
306
315
  };
307
316
  if (tree["wp_raw"] !== undefined) {
308
317
  // TC-style name ("SyCCOMM-1"): split the trailing number as the part.
@@ -329,7 +338,9 @@ class IecBuilder {
329
338
  buildWorkingDocument(tree) {
330
339
  const attrs = {
331
340
  technical_committee: s(tree["technical_committee"]),
332
- wd_number: s(tree["wd_number"]),
341
+ // The document number is the inherited `number` (pubid#687: the
342
+ // parallel wd_number slot used to serialize nowhere).
343
+ number: s(tree["wd_number"]),
333
344
  wd_language: s(tree["wd_language"]),
334
345
  wd_stage: s(tree["wd_stage"]),
335
346
  };
@@ -190,7 +190,10 @@ function iecSingleClass(kind) {
190
190
  registerType(IecSingleIdentifier);
191
191
  return IecSingleIdentifier;
192
192
  }
193
- /** Working documents: the TC/wd fields are runtime-only (hash is bare). */
193
+ /** Working documents: the committee/number/language/stage identity
194
+ * serializes (pubid#687) — the document number is the inherited
195
+ * `number`, and the five plain strings sit in the hash under their own
196
+ * keys (ruby WorkingDocument's key_value block, render_default off). */
194
197
  function iecWorkingDocumentClass() {
195
198
  class IecWorkingDocument extends IecIdentifier {
196
199
  static polymorphicName = "pubid:iec:working-document";
@@ -199,11 +202,10 @@ function iecWorkingDocumentClass() {
199
202
  wp_stage: { type: "string" },
200
203
  wp_type: { type: "string" },
201
204
  technical_committee: { type: "string" },
202
- wd_number: { type: "string" },
203
205
  wd_language: { type: "string" },
204
206
  wd_stage: { type: "string" },
205
207
  });
206
- static mappings = keyValue({ wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "subpart", to: "subpart" }, { wire: "edition", to: "edition" });
208
+ static mappings = keyValue({ wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "subpart", to: "subpart" }, { wire: "edition", to: "edition" }, { wire: "technical_committee", to: "technical_committee" }, { wire: "wd_language", to: "wd_language" }, { wire: "wd_stage", to: "wd_stage" }, { wire: "wp_stage", to: "wp_stage" }, { wire: "wp_type", to: "wp_type" });
207
209
  render() {
208
210
  if (this.wp_stage !== undefined) {
209
211
  const parts = [this.wp_stage];
@@ -225,8 +227,8 @@ function iecWorkingDocumentClass() {
225
227
  const parts = [];
226
228
  if (this.technical_committee !== undefined)
227
229
  parts.push(this.technical_committee);
228
- if (this.wd_number !== undefined) {
229
- let numPart = this.wd_number;
230
+ if (this.number !== undefined) {
231
+ let numPart = String(this.number);
230
232
  if (this.wd_language !== undefined)
231
233
  numPart += `(${this.wd_language})`;
232
234
  parts.push(numPart);
@@ -249,6 +251,10 @@ function iecTrfClass() {
249
251
  cispr_number: { type: "string" },
250
252
  cispr_part: { type: "string" },
251
253
  trf_info: { type: "string" },
254
+ // The embedded CISPR document is part of the form's identity and
255
+ // serializes under its own key (pubid#687), dispatching on the
256
+ // nested _type like ruby's polymorphic attribute.
257
+ cispr_identifier: { type: IecIdentifier },
252
258
  });
253
259
  static mappings = keyValue({ wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "subpart", to: "subpart" }, { wire: "year", to: "date", toWire: (m) => m["date"]?.year, fromWire: yearFromWire }, { wire: "month", to: "date", toWire: (m) => m["date"]?.month, fromWire: () => undefined }, { wire: "day", to: "date", toWire: (m) => m["date"]?.day, fromWire: () => undefined }, {
254
260
  wire: "publisher",
@@ -263,13 +269,23 @@ function iecTrfClass() {
263
269
  return list !== undefined && list.length > 0 ? list : undefined;
264
270
  },
265
271
  fromWire: (h) => (Array.isArray(h["copublishers"]) ? h["copublishers"].map(String) : undefined),
266
- });
272
+ }, { wire: "cispr_identifier", to: "cispr_identifier" });
267
273
  render() {
268
274
  let result = this.publisherPortion() + " ";
269
- if (this.cispr_number !== undefined) {
270
- let numStr = this.cispr_number;
271
- if (this.cispr_part !== undefined && this.cispr_part !== "")
272
- numStr += `-${this.cispr_part}`;
275
+ // The parse path carries the CISPR document in the runtime columns;
276
+ // a fromHash-built form carries the nested identifier — read the
277
+ // document from whichever one is set (ruby renderer).
278
+ const cispr = this.cispr_identifier;
279
+ const cisprNumber = this.cispr_number ??
280
+ (cispr?.number === undefined || cispr.number === null ? undefined : String(cispr.number));
281
+ const cisprPart = this.cispr_part ??
282
+ (cispr?.part === undefined || cispr.part === null || cispr.part === ""
283
+ ? undefined
284
+ : String(cispr.part));
285
+ if (cisprNumber !== undefined) {
286
+ let numStr = cisprNumber;
287
+ if (cisprPart !== undefined && cisprPart !== "")
288
+ numStr += `-${cisprPart}`;
273
289
  let cisprStr = `CISPR ${numStr}`;
274
290
  if (this.date !== undefined)
275
291
  cisprStr += `:${this.date.render()}`;
package/dist/index.d.ts CHANGED
@@ -8,3 +8,4 @@ export { checkCase, runFlavor, runCorpus } from "./conformance/runner.js";
8
8
  export { corpusModeImplementation, CorpusBackedIdentifier } from "./implementations/corpus-mode.js";
9
9
  export { grammarImplementation } from "./flavors/index.js";
10
10
  export { subsetMatch, valueMatch, exactMatch } from "./model/subset-match.js";
11
+ export * as PubidUrn from "./pubid-urn/index.js";
package/dist/index.js CHANGED
@@ -8,3 +8,5 @@ export { corpusModeImplementation, CorpusBackedIdentifier } from "./implementati
8
8
  // (pubid/pubid-ts#63 - the estate serving codec).
9
9
  export { grammarImplementation } from "./flavors/index.js";
10
10
  export { subsetMatch, valueMatch, exactMatch } from "./model/subset-match.js";
11
+ // The pubid-scheme URN codec and the slug form (pubid-ts#161).
12
+ export * as PubidUrn from "./pubid-urn/index.js";
@@ -58,6 +58,9 @@ export declare abstract class BaseIdentifier {
58
58
  toString(): string;
59
59
  /** Ruby to_urn: the flavor's UrnGenerator, else the base template. */
60
60
  toUrn(): string;
61
+ /** The pubid-scheme URN (pubid#656): the lossless form,
62
+ * `PubidUrn.parse(this.toPubidUrn()).toHash == this.toHash`. */
63
+ toPubidUrn(): string;
61
64
  fromHash(hash: Record<string, unknown>): BaseIdentifier;
62
65
  /** SubsetMatch: an all-parts reference skips the part attributes. */
63
66
  subsetAllPartsWildcard(): boolean;
@@ -20,6 +20,7 @@
20
20
  import { FLAT_SCALAR_COMPONENTS, FLAT_SCALAR_FIELDS, BASE_ATTRIBUTES } from "./attribute.js";
21
21
  import { Component } from "./component.js";
22
22
  import { BaseUrnGenerator } from "./urn-generator.js";
23
+ import { generate as generatePubidUrn } from "../pubid-urn/index.js";
23
24
  const TYPE_REGISTRY = new Map();
24
25
  /** Register a concrete identifier class under its polymorphic _type (the *_TYPE_MAP role). */
25
26
  export function registerType(klass) {
@@ -107,6 +108,11 @@ export class BaseIdentifier {
107
108
  const Generator = this.constructor.urnGenerator ?? BaseUrnGenerator;
108
109
  return new Generator(this).generate();
109
110
  }
111
+ /** The pubid-scheme URN (pubid#656): the lossless form,
112
+ * `PubidUrn.parse(this.toPubidUrn()).toHash == this.toHash`. */
113
+ toPubidUrn() {
114
+ return generatePubidUrn(this.toHash());
115
+ }
110
116
  fromHash(hash) {
111
117
  return this.constructor.fromHash(hash);
112
118
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reads the body of a pubid-scheme URN back into an identifier hash —
3
+ * the TS mirror of Pubid::PubidUrn::Decoder (lib/pubid/pubid_urn/decoder.rb).
4
+ * The inverse of Encoder; strict about structure. Canonical form (atom
5
+ * order, escapes, trailing "~") is checked by PubidUrn.parse, which
6
+ * re-encodes the result and compares.
7
+ */
8
+ export declare class Decoder {
9
+ private readonly body;
10
+ constructor(body: string);
11
+ /** The identifier hash (`_type` and `base` included). */
12
+ toIdentifierHash(): Record<string, unknown>;
13
+ private decodeIdentifier;
14
+ private nodeSchema;
15
+ private decodeCore;
16
+ private coreValue;
17
+ private decodeAtoms;
18
+ private atomValue;
19
+ /** The value of a :string, :bool or list atom, or of a facet. */
20
+ private slotValue;
21
+ private scalar;
22
+ private boolean;
23
+ private integer;
24
+ private nested;
25
+ private decodeFacets;
26
+ private fail;
27
+ }