@pubid/pubid 0.1.37 → 0.1.38

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.
@@ -1,6 +1,6 @@
1
1
  import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
2
2
  import { PubidDate, Language, Iteration } from "../../model/component.js";
3
- import { isoGrammar } from "./grammar.js";
3
+ import { isoGrammar, BALLOT_STAGE_CODES } from "./grammar.js";
4
4
  import { IsoBundledIdentifier, IsoDirectives, IsoDirectivesSupplement, IsoEdition, IsoPublisher, IsoSupplementIdentifier, IsoTcDocument, isoClassForType, locateStage, } from "./model.js";
5
5
  import { ISO_TYPED_STAGES } from "./stages.generated.js";
6
6
  /**
@@ -232,6 +232,15 @@ class IsoBuilder {
232
232
  }
233
233
  buildStandard(treeIn) {
234
234
  const tree = { ...treeIn };
235
+ // Historic TYPE-first ballot face ("ISO/IEC FDAM 1 14496-12:2004",
236
+ // "ISO/PDAM 19973-2", "ISO 15877-5:2009 FDAM 1"): the ballot_* keys
237
+ // are one amendment layer over the whole base, never a second
238
+ // supplement layer (the stored twins keep one level), so the layer
239
+ // is assembled here, before the supplement branch could read the
240
+ // ballot token as an outer wrapper.
241
+ if (tree["ballot_type_with_stage"] !== undefined) {
242
+ return this.buildBallotSupplement(tree);
243
+ }
235
244
  // Legacy ISO/R: publisher ISO + type "R".
236
245
  if (tree["iso_r_prefix"] !== undefined) {
237
246
  tree["publisher"] = "ISO";
@@ -280,8 +289,63 @@ class IsoBuilder {
280
289
  attrs["base"] = this.build(tree["base"]);
281
290
  }
282
291
  const identifier = new klass(attrs);
292
+ // The owner's no-number rule, applied on every construction path
293
+ // that routes through this builder (parse, URN, bundle members): a
294
+ // DAM-family ballot-stage amendment token with no number anywhere
295
+ // names Amendment 1 ("ISO 19973-2/PDAM", "ISO/IEC PDAM 12207"). The
296
+ // number is identity-bearing, so the default lands at build time and
297
+ // the wire hash serializes it — parse and fromHash(toHash) agree
298
+ // because both carry the filled number. The ballot stages are
299
+ // matched by the typed stage's code (the per-typed-stage key) —
300
+ // PDAM's stageCode is "cd", the harmonized range it lives in.
301
+ if (typeKey === "amd" && attrs["number"] === undefined &&
302
+ BALLOT_STAGE_CODES.includes(attrs["typedStageState"]?.entry.code ?? "")) {
303
+ identifier["number"] = "1";
304
+ }
283
305
  return identifier;
284
306
  }
307
+ /**
308
+ * Historic TYPE-first ballot face (owner ruling 2026-11-12, Option B
309
+ * decode-to-modern): "[P|F]?DAM [N] BASE [stamp]" decodes to ONE
310
+ * amendment over the WHOLE base, rendered as the canonical on-base
311
+ * slash face. The stage comes from the ballot token; the number from
312
+ * the TYPE token, then the on-base stamp, then the owner's no-number
313
+ * rule (a ballot face with no number anywhere names Amendment 1); the
314
+ * stamp's date is the ballot supplement's date. The base keeps every
315
+ * component it carried: a legacy slash-part stays a part of the base
316
+ * number ("ISO/IEC FPDAM 9075/1" → base "9075-1"), and the base's own
317
+ * type token stays the base's ("ISO/IEC PDAM TR 9573-11" → base
318
+ * "TR 9573-11") — nothing migrates into the supplement slot. The
319
+ * stamp's token letters (Amd/Am/…) are decode-away: the ballot's
320
+ * stage is the TYPE token's.
321
+ */
322
+ buildBallotSupplement(tree) {
323
+ const fields = {
324
+ type_with_stage: s(tree["ballot_type_with_stage"]),
325
+ };
326
+ const number = s(tree["ballot_number"]) ??
327
+ s(tree["ballot_stamp_number"]) ??
328
+ s(tree["ballot_stamp_dot_number"]) ??
329
+ "1";
330
+ fields["number_with_part"] = number;
331
+ // The on-base stamp: its date (or undated marker) is the ballot
332
+ // supplement's; its number feeds the precedence above; its token
333
+ // letters were matched uncaptured and decode away.
334
+ const stampDate = s(tree["ballot_stamp_date"]);
335
+ if (stampDate !== undefined) {
336
+ const [year, month, day] = stampDate.split("-");
337
+ fields["date"] = new PubidDate({ year, month, day });
338
+ }
339
+ else if (tree["ballot_stamp_undated_marker"] !== undefined) {
340
+ fields["date"] = new PubidDate({ undated: true });
341
+ }
342
+ const rest = {};
343
+ for (const [key, value] of Object.entries(tree)) {
344
+ if (!key.startsWith("ballot_"))
345
+ rest[key] = value;
346
+ }
347
+ return this.build({ ...fields, base: rest });
348
+ }
285
349
  buildDirectives(tree, state) {
286
350
  const attrs = this.baseAttrs(tree);
287
351
  const publisher = this.publisherOf(tree);
@@ -102,8 +102,14 @@ export class IsoIdentifier extends BaseIdentifier {
102
102
  const ts = this.typedStageState;
103
103
  if (ts === undefined)
104
104
  return false;
105
- if (ts.entry.longAbbr !== undefined && ts.originalAbbr?.startsWith(ts.entry.longAbbr))
105
+ if (ts.entry.longAbbr !== undefined && ts.originalAbbr !== undefined &&
106
+ (ts.originalAbbr.startsWith(ts.entry.longAbbr) ||
107
+ // The shorter long-family spellings ("Am", the alias of "Amd"
108
+ // the owner ruled renders canonical) are prefixes of the long
109
+ // abbr; the short family ("AMD") matches neither direction.
110
+ ts.entry.longAbbr.startsWith(ts.originalAbbr))) {
106
111
  return true;
112
+ }
107
113
  if (ts.entry.longAbbr !== undefined && ts.originalAbbr?.includes("Directives"))
108
114
  return true;
109
115
  return false;
@@ -32,7 +32,7 @@ export const ISO_TYPED_STAGES = [
32
32
  code: "prf_amd", stageCode: "prf", typeCode: "amd", abbr: ["PRF Amd"], harmonized: ["50.00"]
33
33
  }),
34
34
  TS("amd", {
35
- code: "published", stageCode: "published", typeCode: "amd", abbr: ["Amd", "AMD", "Amd."], shortAbbr: "AMD", longAbbr: "Amd", harmonized: ["60.00", "60.60"]
35
+ code: "published", stageCode: "published", typeCode: "amd", abbr: ["Amd", "AMD", "Amd.", "Am"], shortAbbr: "AMD", longAbbr: "Amd", harmonized: ["60.00", "60.60"]
36
36
  }),
37
37
  TS("cor", {
38
38
  code: "pwi_cor", stageCode: "proposal", typeCode: "cor", abbr: ["PWI Cor"], harmonized: ["00.00", "00.20", "00.60", "00.92", "00.93", "00.98", "00.99"]
@@ -709,8 +709,24 @@ function nistClass(spec) {
709
709
  return `${this.publisher ?? "NIST"} ${this.date_year ?? ""}-${this.date_month ?? ""}-${this.date_day ?? ""} ${this.dated_seq ?? ""}`;
710
710
  }
711
711
  if (spec.style === "fips") {
712
- let result = this.publisher !== undefined ? `${this.publisher} ` : "";
713
- result += this.seriesCode() ?? "";
712
+ // The official published face is "FIPS PUB <number>": FIPS is
713
+ // government-published and "PUB" (Federal Information Processing
714
+ // Standards PUBlication) is part of the official designation
715
+ // (owner ruling 2026-11-12). A spelled NIST publisher renders the
716
+ // official face; the historical NBS-prefixed rows keep
717
+ // "NBS FIPS <number>", and the bare publisher-less spelling keeps
718
+ // its short "FIPS <number>" face. The lead reads
719
+ // shortRenderPublisher() (the printed-publisher view), so a
720
+ // series-default publisher filled without being spelled (#704's
721
+ // ruby model) can never flip the bare face; the tail rides the
722
+ // lead.
723
+ const pub = this.shortRenderPublisher();
724
+ const series = this.seriesCode() ?? "";
725
+ let result = pub === "NIST"
726
+ ? "FIPS PUB"
727
+ : pub !== undefined && pub !== ""
728
+ ? `${pub} ${series}`
729
+ : series;
714
730
  let numberPart = this.number ?? "";
715
731
  const fy = /^(\d{1,3})-(\d{4})$/.exec(numberPart);
716
732
  if (fy !== null) {
@@ -137,6 +137,17 @@ export function preprocessNist(input) {
137
137
  cleaned = cleaned.replace(/^(NBS|NIST)(IR|FIPS|GCR|HB|MONO|MP|NCSTAR|NSRDS)/i, "$1 $2");
138
138
  cleaned = cleaned.replace(/\b(ir|sp|tn|hb|fips|ams|vts)\b/i, (m) => m.toUpperCase());
139
139
  cleaned = cleaned.replace(/\bLC\b(?!IRC)/g, "LCIRC");
140
+ // normalize_fips_pub_alias: the FIPS series' official printed face is
141
+ // "FIPS PUB <number>" — FIPS is government-published and "PUB" (Federal
142
+ // Information Processing Standards PUBlication) is part of the official
143
+ // designation — and the RENDERER emits that face directly (owner ruling
144
+ // 2026-11-12, mirroring the gem). The grammar reads the
145
+ // publisher-bearing spelling only through this fold (pubid#697): the
146
+ // printed spellings — "FIPS PUB 205" and the publisher-prefixed
147
+ // "NIST FIPS PUB 205" (pubid#702) — fold onto the one model "NIST FIPS
148
+ // <number>" the grammar already builds (same hash, same URN, same MR
149
+ // string). Anchored at the head; bare "FIPS" is untouched.
150
+ cleaned = cleaned.replace(/^(?:NIST )?FIPS PUB\b/i, "NIST FIPS");
140
151
  // normalize_lcirc_supplement_contexts
141
152
  cleaned = cleaned.replace(/\bNBS LCIRC\b(?=.*\b(?:supp?|sup\+|r\d+\/)\d)/, "NBS.LCIRC");
142
153
  cleaned = cleaned.replace(/\bNBS\.LCIRC\.(\d+r\d+\/\d{4})/, "NBS LCIRC $1");
@@ -20,11 +20,17 @@ function buildRules() {
20
20
  rule("edition_part", () => str("第").then(digits.then(str("."), digits).as("edition"), str("版")));
21
21
  rule("edition_latin", () => digits.then(str("."), digits).as("edition"));
22
22
  rule("edition", () => space.then(rules["edition_part"].or(rules["edition_latin"])));
23
+ // MLIT's volume label after the edition: 本編 (main volume) or
24
+ // 本編+付録 (main volume with appendix). ASCII or full-width (U+3000)
25
+ // space; ASCII "+" or full-width "+" (U+FF0B), which the builder
26
+ // normalizes to "+".
27
+ rule("volume_sep", () => str(" ").or(str(" ")));
28
+ rule("volume", () => rules["volume_sep"].then(str("本編").then(str("+").or(str("+")).then(str("付録")).maybe()).as("volume")));
23
29
  rule("annex_supplement", () => space.then(str("Annex")).then(space, match("[A-Z]").as("annex_letter")));
24
30
  rule("handbook", () => str("PLATEAU")
25
31
  .then(space, rules["doc_type"], space, rules["number"])
26
32
  .then(rules["annex"].maybe())
27
- .then(rules["edition"].maybe()));
33
+ .then(rules["edition"].then(rules["volume"].maybe()).maybe()));
28
34
  rule("identifier", () => rules["handbook"]
29
35
  .then(rules["annex_supplement"])
30
36
  .as("base")
@@ -42,14 +48,21 @@ function plateauClass(kind) {
42
48
  number: { type: "integer" },
43
49
  annex: { type: "integer" },
44
50
  edition: { type: "string" },
51
+ // The volume label MLIT prints after the edition: "本編" (the main
52
+ // volume alone) or "本編+付録" (main volume and appendix, one
53
+ // PDF). A nil volume means the publication has no volume label:
54
+ // "#00 第5.0版" does not subsume "#00 第5.0版 本編+付録".
55
+ volume: { type: "string" },
45
56
  });
46
- static mappings = keyValue({ wire: "number", to: "number" }, { wire: "edition", to: "edition" }, { wire: "annex", to: "annex" });
57
+ static mappings = keyValue({ wire: "number", to: "number" }, { wire: "edition", to: "edition" }, { wire: "annex", to: "annex" }, { wire: "volume", to: "volume" });
47
58
  render() {
48
59
  let result = `PLATEAU ${isHandbook ? "Handbook" : "Technical Report"} #${pad2(this.number)}`;
49
60
  if (this.annex !== undefined)
50
61
  result += `-${this.annex}`;
51
62
  if (isHandbook && this.edition !== undefined)
52
63
  result += ` 第${this.edition}版`;
64
+ if (this.volume !== undefined)
65
+ result += ` ${this.volume}`;
53
66
  return result;
54
67
  }
55
68
  }
@@ -91,6 +104,12 @@ export function plateauGrammarImplementation() {
91
104
  if (data["edition"] !== undefined && data["edition"] !== null && kind === "handbook") {
92
105
  attrs["edition"] = String(data["edition"]);
93
106
  }
107
+ // The grammar reads a volume label only after an edition, on the
108
+ // Handbook; the builder normalizes the ASCII "+" to the
109
+ // full-width "+" (MLIT prints full-width).
110
+ if (data["volume"] !== undefined && data["volume"] !== null && kind === "handbook") {
111
+ attrs["volume"] = String(data["volume"]).replaceAll("+", "+");
112
+ }
94
113
  return new KIND_CLASSES[kind](attrs);
95
114
  },
96
115
  };
@@ -15,6 +15,9 @@ export const ihoSchema = define("iho", (s) => {
15
15
  s.atom("part", "part");
16
16
  s.atom("part_edition", "parted");
17
17
  s.atom("part_year", "party");
18
+ s.atom("spec", "spec");
19
+ s.atom("spec_sub", "specsub");
20
+ s.atom("spec_variant", "specvar");
18
21
  s.atom("annex", "annex");
19
22
  s.atom("annex_edition", "anned");
20
23
  s.atom("annex_year", "annyear");
@@ -10,6 +10,7 @@ export const plateauSchema = define("plateau", (s) => {
10
10
  s.atom("publisher", "pub");
11
11
  s.atom("edition", "ed");
12
12
  s.atom("annex", "annex", "int");
13
+ s.atom("volume", "vol");
13
14
  s.type("pubid:plateau:annex", "annex", {
14
15
  core: [],
15
16
  atoms: [["letter", "letter"]],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
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",