@pubid/pubid 0.1.4 → 0.1.5

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
@@ -18,7 +18,14 @@ export declare class GbIdentifier extends BaseIdentifier {
18
18
  readonly number: string;
19
19
  readonly part: string | undefined;
20
20
  readonly date: PubidDate | undefined;
21
- readonly all_parts: boolean | undefined;
22
21
  render(): string;
23
22
  }
23
+ export declare class GbAllPartsIdentifier extends BaseIdentifier {
24
+ static polymorphicName: string;
25
+ static get attributes(): import("../../model/attribute.js").AttributeTable;
26
+ static get mappings(): never[];
27
+ readonly identifiers: GbIdentifier[];
28
+ render(): string;
29
+ toUrn(): string;
30
+ }
24
31
  export declare function gbGrammarImplementation(): FlavorImplementation;
@@ -53,9 +53,17 @@ export class GbIdentifier extends BaseIdentifier {
53
53
  mandate: { type: "string" },
54
54
  number: { type: "string" },
55
55
  part: { type: "string" },
56
- all_parts: { type: "boolean", default: false },
57
56
  });
58
- static mappings = keyValue({ wire: "publisher", to: "publisher" }, { wire: "mandate", to: "mandate" }, { wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "date", to: "date" }, { wire: "all_parts", to: "all_parts" });
57
+ static mappings = keyValue({
58
+ wire: "publisher",
59
+ to: "publisher",
60
+ // The flat wire form is the Publisher's body scalar; rebuild the
61
+ // component on deserialize (lutaml coerces it on the Ruby side).
62
+ fromWire: (hash) => {
63
+ const value = hash["publisher"];
64
+ return typeof value === "string" ? new Publisher({ body: value }) : value;
65
+ },
66
+ }, { wire: "mandate", to: "mandate" }, { wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "date", to: "date" });
59
67
  // GB's flat-scalar entry: the Publisher serializes as its body scalar.
60
68
  static flatScalarComponents = { publisher: "publisher" };
61
69
  static flatScalarFields = { publisher: "body" };
@@ -68,17 +76,56 @@ export class GbIdentifier extends BaseIdentifier {
68
76
  numberPortion += `.${this.part}`;
69
77
  if (this.date !== undefined && this.date.year !== undefined)
70
78
  numberPortion += `-${this.date.year}`;
71
- let result = `${code} ${numberPortion}`;
72
- if (this.all_parts)
73
- result += " (all parts)";
74
- return result;
79
+ return `${code} ${numberPortion}`;
75
80
  }
76
81
  }
77
82
  registerType(GbIdentifier);
83
+ // The document a member names: without its part and its edition (the
84
+ // attributes an all-parts reference ignores).
85
+ function gbDocumentOf(member) {
86
+ const r = member;
87
+ return new GbIdentifier({
88
+ publisher: r["publisher"],
89
+ mandate: r["mandate"],
90
+ number: r["number"],
91
+ });
92
+ }
93
+ // "GB/T 5606 (all parts)" names every part of one document: the wrapper
94
+ // holds the member identifiers (gem #433 - the mark is a class, not a
95
+ // flag), renders the document plus the suffix, and URNs as the document.
96
+ export class GbAllPartsIdentifier extends BaseIdentifier {
97
+ static polymorphicName = "pubid:gb:all-parts";
98
+ static get attributes() {
99
+ return extendAttributes(BaseIdentifier, {
100
+ identifiers: { type: GbIdentifier, collection: true },
101
+ });
102
+ }
103
+ static get mappings() {
104
+ return [{ wire: "identifiers", to: "identifiers" }];
105
+ }
106
+ render() {
107
+ const first = this.identifiers[0];
108
+ return `${first === undefined ? "" : gbDocumentOf(first).render()} (all parts)`;
109
+ }
110
+ toUrn() {
111
+ const first = this.identifiers[0];
112
+ return gbDocumentOf(first).toUrn();
113
+ }
114
+ }
115
+ registerType(GbAllPartsIdentifier);
78
116
  class GbBuilder extends BaseBuilder {
79
117
  defaultIdentifierClass() {
80
118
  return GbIdentifier;
81
119
  }
120
+ build(tree) {
121
+ const id = super.build(tree);
122
+ if (tree["all_parts"] === undefined)
123
+ return id;
124
+ // "(all parts)" wraps the document it parsed.
125
+ return new GbAllPartsIdentifier({
126
+ identifiers: [id],
127
+ });
128
+ }
82
129
  cast(key, value) {
83
130
  if (key === "publisher_code") {
84
131
  // Split an inline /T or /Z mandate out of the code.
@@ -92,7 +139,8 @@ class GbBuilder extends BaseBuilder {
92
139
  return value === undefined || value === null ? null : { date: new PubidDate({ year: String(value) }) };
93
140
  }
94
141
  if (key === "all_parts") {
95
- return { all_parts: String(value) !== "" };
142
+ // Presence marker only: the wrapper carries the members.
143
+ return value;
96
144
  }
97
145
  return value;
98
146
  }
@@ -42,6 +42,10 @@ function buildRules() {
42
42
  const dashMaybe = () => dash.maybe();
43
43
  const monthName = () => literalAlternation(MONTHS);
44
44
  const absent = (p) => p.absent();
45
+ // The ISO/IEC draft-stage vocabulary of the D= notation
46
+ // (docs/IEEE-DRAFT-STAGES.md §1.2). Ordered so the longest token wins
47
+ // ("FDIS" over "DIS").
48
+ const stageVocab = () => literalAlternation(["FDIS", "CDV", "PWI", "DIS", "WD", "NP", "CD"]);
45
49
  rule("year_digits", () => str("19").or(str("20")).then(digit.repeat(2, 2)).then(lower.repeat(0, 2)).then(absent(digits)));
46
50
  rule("month_numeric", () => str("0").then(match("[1-9]")).or(str("1").then(match("[0-2]"))));
47
51
  rule("date_with_month_text", () => monthName().as("month").then(space).then(R("year_digits").as("year")));
@@ -128,6 +132,16 @@ function buildRules() {
128
132
  rule("draft", () => (ref(rules, "draft_prefix")
129
133
  .then(ref(rules, "draft_version").repeat(1, 2))
130
134
  .then(dot.then(digits.as("revision")).maybe())
135
+ .then(
136
+ // The compound both-systems form (docs/IEEE-DRAFT-STAGES.md
137
+ // §1.3): "=DDIS.3" - IEEE draft ordinal = draft of the ISO/IEC
138
+ // stage, with its iteration. The glued "=DDIS3" / "=DDIS-3"
139
+ // spellings are accepted aliases of "=DDIS.3".
140
+ str("=")
141
+ .then(str("D").then(stageVocab().as("draft_iso_stage"))
142
+ .or(stageVocab().as("draft_iso_stage")))
143
+ .then((dot.or(dash)).maybe().then(digits.as("draft_iso_iteration")).maybe())
144
+ .maybe())
131
145
  .then(ref(rules, "draft_date").maybe())).as("draft"));
132
146
  rule("revision_suffix", () => slash.then(str("R")).then(dash).then(match("[0-9A-Za-z]").repeat(1, Infinity).as("revision")));
133
147
  rule("edition", () => (comma.then(R("year_digits").as("edition_year")).then(str(" Edition"))).or(space.or(dash)
@@ -230,13 +244,38 @@ function buildRules() {
230
244
  .then(match("[^\\n]").repeat(1, Infinity).as("content")));
231
245
  rule("joint_development_ieee_format", () => literalAlternation(["ISO/IEC/IEEE", "ISO/IEEE", "IEC/IEEE", "IEEE/CSA"]).as("joint_publishers")
232
246
  .then(space)
233
- .then(str("P"))
247
+ // P = project (the document is a draft): identity-bearing, so it
248
+ // is captured and preserved, never silently consumed.
249
+ .then(str("P").as("project_marker"))
234
250
  .then(digits.as("number"))
235
251
  .then((dot.or(dash)).then(digits.as("part")).maybe())
236
252
  .then(slash.then(digits).then(dot.then(digits).maybe())
237
253
  .then(dash.then(digits.as("draft_version")).maybe()).maybe())
238
- .then(slash.then(str("D")).then(digits.as("draft_version"))
239
- .or(comma.then(literalAlternation(["CDV", "FDIS", "CD", "DIS"]).as("iec_stage")).then(digits.maybe().as("stage_iteration"))).maybe())
254
+ .then(
255
+ // Variant 1b: the ordinal-less stage draft "D=CDV[:2020]" -
256
+ // D (draft) = CDV (the IEC stage it drafts). The year rides in
257
+ // the draft clause (a distinct key, so the builder keeps the
258
+ // date inside the designator).
259
+ slash.then(str("D")).then(str("="))
260
+ .then(stageVocab().as("draft_iso_stage"))
261
+ .then(str(":").then(R("year_digits").as("draft_stage_year")).maybe())
262
+ .or(
263
+ // Variant 1: /D8 notation (original), with the compound
264
+ // both-systems suffix "=DDIS.3" (docs/IEEE-DRAFT-STAGES.md
265
+ // §1.3). The doubled-D spelling is consumed and echoed: the
266
+ // joint form prints the stage as spelled.
267
+ slash.then(str("D")).then(digits.as("draft_version"))
268
+ .then(str("=")
269
+ .then(str("D").as("draft_stage_d").then(stageVocab().as("draft_iso_stage"))
270
+ .or(stageVocab().as("draft_iso_stage")))
271
+ .then((dot.or(dash)).maybe().then(digits.as("draft_iso_iteration")).maybe())
272
+ .maybe()))
273
+ .or(
274
+ // Variant 2: , CDV1 notation (comma before stage code) —
275
+ // the stage draft of the named ISO/IEC stage, its iteration
276
+ comma.then(literalAlternation(["CDV", "FDIS", "CD", "DIS"]).as("iec_stage"))
277
+ .then(digits.maybe().as("stage_iteration")))
278
+ .maybe())
240
279
  .then(ref(rules, "edition").maybe())
241
280
  .then((dash.then(R("year_digits").as("year"))).or(comma.maybe().then(space).then(monthName().as("month")).then(spaceMaybe())
242
281
  .then(R("year_digits").as("year"))).maybe())
@@ -270,14 +309,27 @@ function buildRules() {
270
309
  .then(isoStage().then(stdNoise()).then(space)
271
310
  .or(str("").as("iso_published").then(spaceMaybe())))
272
311
  .or(stagedOnly.as("joint_publishers").then(space).then(isoStage()).then(stdNoise()).then(space)))
273
- .then(str("P").maybe())
312
+ .then(str("P").as("project_marker").maybe())
274
313
  .then(digits.as("number"))
275
314
  .then((dot.then(absent(R("year_digits"))).then(digits.as("part")))
276
315
  .or(dash.then(str("").as("part_dash")).then(absent(R("year_digits")))
277
316
  .then(digits.as("part")))
278
317
  .maybe())
279
318
  .then(dateClause())
280
- .then((slash.then(str("D")).then(dashMaybe())
319
+ .then(
320
+ // The ISO/IEC edition marker may sit between the year and an
321
+ // amendment/draft tail ("8802.11:2012 (E)/Amd 1-2014").
322
+ (spaceMaybe().then(str("("))
323
+ .then(match("[^)]").repeat(1, Infinity).as("edition_marker"))
324
+ .then(str(")"))).maybe())
325
+ .then(
326
+ // The stage-draft clause of the ISO-led print: "/D=WD.5" - D
327
+ // (draft) = the ISO/IEC stage it drafts, with its iteration
328
+ // (docs/IEEE-DRAFT-STAGES.md §1.2).
329
+ (slash.then(str("D")).then(str("="))
330
+ .then(stageVocab().as("draft_iso_stage"))
331
+ .then(dot.then(digits.then(match("[A-Za-z]").repeat(0, 1)).as("draft_iso_iteration")).maybe()))
332
+ .or(slash.then(str("D")).then(dashMaybe())
281
333
  .then(match("[0-9.]").repeat(1, Infinity).as("draft_version"))
282
334
  .then(((comma.or(space)).then(monthName().as("draft_month")).then(space)
283
335
  .then(R("year_digits").as("draft_year"))).or(comma.then(R("year_digits").as("draft_year"))).maybe())).maybe())
@@ -288,12 +340,30 @@ function buildRules() {
288
340
  .then(ref(rules, "revision_suffix").maybe())
289
341
  .then(ref(rules, "parenthetical").maybe());
290
342
  });
343
+ // The stage-less ISO/IEC label with a colon year and the ISO/IEC
344
+ // edition marker: "ISO/IEC 13210:1994 (E)" - the ISO/IEC portion of a
345
+ // double-labeled standard. The (E) parenthetical is REQUIRED: without it
346
+ // this spelling is the ISO flavor's own form and must not be stolen.
347
+ rule("joint_development_iso_iec_edition", () => str("ISO/IEC").as("joint_publishers")
348
+ .then(space)
349
+ .then(str("").as("iso_published"))
350
+ .then(str("P").as("project_marker").maybe())
351
+ .then(digits.as("number"))
352
+ // The part may be dot- or dash-joined ("8802-9"); a dash-year is
353
+ // not a part. A distinct key: the iso-format route's part_dash
354
+ // prints as a dot in the joint code; only the label keeps its dash.
355
+ .then((dot.then(digits.as("part")))
356
+ .or(dash.then(str("").as("iec_label_dash")).then(absent(R("year_digits"))).then(digits.as("part")))
357
+ .maybe())
358
+ .then(str(":").then(spaceMaybe()).then(R("year_digits").as("year")))
359
+ .then(spaceMaybe())
360
+ .then(ref(rules, "parenthetical")));
291
361
  rule("joint_development_embedded_stage", () => literalAlternation([
292
362
  "ISO/IEC/IEEE", "IEEE/ISO/IEC", "IEEE/IEC/ISO", "ISO/IEEE",
293
363
  "IEC/IEEE", "IEEE/IEC", "ISO/IEC", "IEEE",
294
364
  ]).as("joint_publishers")
295
365
  .then(space)
296
- .then(str("P").maybe())
366
+ .then(str("P").as("project_marker").maybe())
297
367
  .then(digits.as("number"))
298
368
  .then((dot.or(dash)).then(absent(R("year_digits"))).then(digits.as("part")).maybe())
299
369
  .then(dot)
@@ -577,6 +647,7 @@ function buildRules() {
577
647
  .or(ref(rules, "conformance_identifier"))
578
648
  .or(ref(rules, "joint_development_ieee_format"))
579
649
  .or(ref(rules, "joint_development_iso_format"))
650
+ .or(ref(rules, "joint_development_iso_iec_edition"))
580
651
  .or(ref(rules, "joint_development_embedded_stage"))
581
652
  .or(ref(rules, "iec_ieee_copublished"))
582
653
  .or(ref(rules, "number_first_identifier"))
@@ -121,6 +121,10 @@ class IeeeBuilder {
121
121
  attributes["parts"] = codeObj.parts;
122
122
  if (codeObj?.originalSeparator !== undefined)
123
123
  attributes["separator"] = codeObj.originalSeparator;
124
+ // P = project (the document is a draft): the marker is
125
+ // identity-bearing, so stripping the letter must not strip the
126
+ // fact (docs/IEEE-DRAFT-STAGES.md §1.1).
127
+ attributes["project_marker"] = true;
124
128
  }
125
129
  }
126
130
  delete attributes["__code"];
@@ -298,11 +302,23 @@ class IeeeBuilder {
298
302
  return attributes;
299
303
  }
300
304
  determineStageAbbr(typeValue, parsed) {
305
+ // An explicit joint stage on the draft ("=DFDIS.3") IS the stage —
306
+ // it outranks the ordinal ladder, which classifies IEEE-internal
307
+ // drafts only (docs/IEEE-DRAFT-STAGES.md §1.3).
301
308
  if (parsed["draft"] !== undefined) {
302
309
  let draftData = parsed["draft"];
303
310
  if (Array.isArray(draftData)) {
304
311
  draftData = mergeParsedArray(draftData);
305
312
  }
313
+ if (isObj(draftData) && draftData["draft_iso_stage"] !== undefined) {
314
+ const stage = extractValue(draftData["draft_iso_stage"]) ?? "";
315
+ // The doubled-D alias ("DFDIS") carries its own draft marker;
316
+ // the stage is what remains.
317
+ const rest = stage.slice(1);
318
+ return stage.startsWith("D") && ["PWI", "NP", "WD", "CD", "CDV", "DIS", "FDIS"].includes(rest)
319
+ ? rest
320
+ : stage;
321
+ }
306
322
  if (isObj(draftData) && draftData["draft_version"] !== undefined) {
307
323
  const dv = draftData["draft_version"];
308
324
  const version = Array.isArray(dv) ? dv.map((v) => extractValue(v) ?? "").join("") : extractValue(dv);
@@ -343,6 +359,8 @@ class IeeeBuilder {
343
359
  }
344
360
  let version;
345
361
  let revision;
362
+ let isoStage;
363
+ let isoIteration;
346
364
  let month;
347
365
  let year;
348
366
  let day;
@@ -355,6 +373,8 @@ class IeeeBuilder {
355
373
  version = version.replace(/^-/, "");
356
374
  }
357
375
  revision = extractValue(draftData["revision"]);
376
+ isoStage = extractValue(draftData["draft_iso_stage"]);
377
+ isoIteration = extractValue(draftData["draft_iso_iteration"]);
358
378
  month = extractValue(draftData["month"]);
359
379
  year = extractValue(draftData["year"]);
360
380
  day = extractValue(draftData["day"]);
@@ -370,10 +390,12 @@ class IeeeBuilder {
370
390
  else {
371
391
  version = extractValue(draftData);
372
392
  }
373
- if (version !== undefined) {
393
+ if (version !== undefined || (isObj(draftData) && draftData["draft_iso_stage"] !== undefined)) {
374
394
  const draftObj = new IeeeDraft({
375
395
  version,
376
396
  revision,
397
+ iso_stage: isoStage,
398
+ iso_iteration: isoIteration,
377
399
  month,
378
400
  year,
379
401
  day,
@@ -601,7 +623,14 @@ class IeeeBuilder {
601
623
  codeParts.push(extractValue(parsed["part"]) ?? "");
602
624
  let codeStr = extractValue(parsed["number"]);
603
625
  if (codeStr !== undefined && codeParts.length > 0) {
604
- codeStr += `.${codeParts.join(".")}`;
626
+ // The ISO/IEC label's part may be dash-joined ("8802-9") - keep the
627
+ // printed separator.
628
+ const sep = parsed["iec_label_dash"] !== undefined ? "-" : ".";
629
+ codeStr += `${sep}${codeParts.join(sep)}`;
630
+ }
631
+ // P = project (a draft): identity-bearing, preserved as spelled.
632
+ if (parsed["project_marker"] !== undefined && codeStr !== undefined) {
633
+ codeStr = `P${codeStr}`;
605
634
  }
606
635
  if (codeStr !== undefined) {
607
636
  const codeObj = IeeeCode.parse(codeStr);
@@ -639,6 +668,37 @@ class IeeeBuilder {
639
668
  if (draftVer !== "")
640
669
  attributes["ieee_draft"] = `D${draftVer}`;
641
670
  }
671
+ // The "(E)" edition marker of an ISO/IEC label rides on the joint
672
+ // reference as parenthetical_content - either from the trailing
673
+ // parenthetical slot (the double-label form) or the mid-rule slot
674
+ // before an amendment tail.
675
+ const parenthetical = parsed["parameters"];
676
+ if (parenthetical !== undefined && typeof parenthetical === "object" && !Array.isArray(parenthetical)) {
677
+ const content = parenthetical["parenthetical_content"];
678
+ if (content !== undefined)
679
+ attributes["parenthetical_content"] = extractValue(content);
680
+ }
681
+ if (attributes["parenthetical_content"] === undefined && parsed["edition_marker"] !== undefined) {
682
+ attributes["parenthetical_content"] = extractValue(parsed["edition_marker"]);
683
+ }
684
+ // The joint stage-draft clause (docs/IEEE-DRAFT-STAGES.md §1.3):
685
+ // variant 1's compound tail composes onto the IEEE ordinal
686
+ // ("D5=DIS.3"); variant 1b is the ordinal-less stage draft whose
687
+ // date rides inside the designator ("D=CDV:2020"). The doubled-D
688
+ // alias spelling echoes as spelled.
689
+ if (parsed["draft_iso_stage"] !== undefined) {
690
+ const stageText = `${extractValue(parsed["draft_stage_d"]) ?? ""}${extractValue(parsed["draft_iso_stage"]) ?? ""}`;
691
+ let jointDraft = parsed["draft_version"] !== undefined
692
+ ? `D${(extractValue(parsed["draft_version"]) ?? "").replace(/^D/, "")}=${stageText}`
693
+ : `D=${stageText}`;
694
+ if (parsed["draft_iso_iteration"] !== undefined) {
695
+ jointDraft += `.${extractValue(parsed["draft_iso_iteration"])}`;
696
+ }
697
+ if (parsed["draft_stage_year"] !== undefined) {
698
+ jointDraft += `:${extractValue(parsed["draft_stage_year"])}`;
699
+ }
700
+ attributes["ieee_draft"] = jointDraft;
701
+ }
642
702
  // The catalogue-PRINTED joint form (dash-year / ", Month YYYY" /
643
703
  // date-trailing-the-draft / date-less parenthetical): a Standard
644
704
  // carrying publisher/copublisher renders it as printed; the colon-year
@@ -654,8 +714,11 @@ class IeeeBuilder {
654
714
  typeof parsed["parameters"] === "object" && !Array.isArray(parsed["parameters"]) &&
655
715
  parsed["parameters"]["parenthetical_content"] !== undefined))) {
656
716
  const sep = parsed["part_dash"] !== undefined ? "-" : ".";
657
- const printedCode = [extractValue(parsed["number"]), extractValue(parsed["part"])]
717
+ let printedCode = [extractValue(parsed["number"]), extractValue(parsed["part"])]
658
718
  .filter((x) => x !== undefined).join(sep);
719
+ // P = project: the marker is identity, preserved as spelled.
720
+ if (parsed["project_marker"] !== undefined)
721
+ printedCode = `P${printedCode}`;
659
722
  const printedAttrs = {
660
723
  publisher: attributes["publisher"],
661
724
  copublisher: attributes["copublisher"],
@@ -1022,7 +1085,16 @@ function copublishedStructured(copublishedNumber) {
1022
1085
  }
1023
1086
  // --- parse orchestration -------------------------------------------------------
1024
1087
  function ieeeParseSingle(input) {
1025
- const cleaned = preprocessIeee(applyUpdateCodes(input));
1088
+ const normalized = applyUpdateCodes(input);
1089
+ // UpdateCodes may introduce the "; " double-label separator (the
1090
+ // ISO/IEC and IEEE labels of one document), but the dispatch ran on
1091
+ // the raw input - re-dispatch so the dual construction sees it.
1092
+ if (normalized !== input && normalized.includes("; ")) {
1093
+ const re = preParse(normalized);
1094
+ if (re.dispatch === "dual_semicolon")
1095
+ return buildDual(re.parts);
1096
+ }
1097
+ const cleaned = preprocessIeee(normalized);
1026
1098
  const tree = parseGrammar(ieeeGrammar, cleaned);
1027
1099
  const builder = new IeeeBuilder();
1028
1100
  builder.originalInput = input;
@@ -43,6 +43,8 @@ export declare class IeeeCode {
43
43
  export declare class IeeeDraft {
44
44
  readonly version: string;
45
45
  readonly revision: string | undefined;
46
+ readonly isoStage: string | undefined;
47
+ readonly isoIteration: string | undefined;
46
48
  readonly year: string | undefined;
47
49
  readonly month: string | undefined;
48
50
  readonly day: string | undefined;
@@ -126,6 +126,8 @@ const DRAFT_MONTH_NAMES = {
126
126
  export class IeeeDraft {
127
127
  version;
128
128
  revision;
129
+ isoStage;
130
+ isoIteration;
129
131
  year;
130
132
  month;
131
133
  day;
@@ -137,6 +139,8 @@ export class IeeeDraft {
137
139
  this.commaBeforeMonthSet = "comma_before_month" in attrs;
138
140
  this.version = String(attrs["version"] ?? "");
139
141
  this.revision = attrs["revision"];
142
+ this.isoStage = attrs["iso_stage"];
143
+ this.isoIteration = attrs["iso_iteration"];
140
144
  this.year = attrs["year"];
141
145
  this.originalMonth = attrs["month"];
142
146
  this.month = attrs["month"] !== undefined
@@ -155,24 +159,45 @@ export class IeeeDraft {
155
159
  body = body.slice(1);
156
160
  else if (body.startsWith("D") && body.length > 1)
157
161
  body = body.slice(1);
162
+ // The compound stage suffix (docs/IEEE-DRAFT-STAGES.md §1.3):
163
+ // "=DIS.3" is canonical - D (draft) = DIS (the stage), iteration 3.
164
+ // "=DDIS.3", "=DDIS3", "=DDIS-3" (the stage echoed with its own D)
165
+ // are accepted aliases. Captures [stage, iteration].
166
+ const compound = /=D?(PWI|NP|WD|CDV|FDIS|DIS|CD)(?:[.-]?(\d+))?$/;
167
+ let isoStage;
168
+ let isoIteration;
169
+ const cm = compound.exec(body);
170
+ if (cm !== null) {
171
+ isoStage = cm[1];
172
+ isoIteration = cm[2];
173
+ body = body.slice(0, cm.index);
174
+ }
158
175
  const monthAlt = Object.keys(DRAFT_MONTH_NAMES)
159
176
  .sort((a, b) => b.length - a.length)
160
177
  .map((m) => `${m.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.?`)
161
178
  .join("|");
162
179
  const suffix = new RegExp(`(?:(, | )(${monthAlt})(?: (\\d{1,2}))?(?:, | )| )((?:19|20)\\d{2}[a-z]{0,2})$`);
163
180
  const m = suffix.exec(body);
164
- if (m === null)
165
- return new IeeeDraft({ version: body });
181
+ if (m === null) {
182
+ return new IeeeDraft({ version: body, iso_stage: isoStage, iso_iteration: isoIteration });
183
+ }
166
184
  return new IeeeDraft({
167
185
  version: body.slice(0, m.index),
168
186
  month: m[2],
169
187
  day: m[3],
170
188
  year: m[4],
171
189
  comma_before_month: m[1] === ", ",
190
+ iso_stage: isoStage,
191
+ iso_iteration: isoIteration,
172
192
  });
173
193
  }
174
194
  render() {
175
195
  let result = `/D${this.version}`;
196
+ if (this.isoStage !== undefined)
197
+ result += `=${this.isoStage}`;
198
+ if (this.isoStage !== undefined && this.isoIteration !== undefined) {
199
+ result += `.${this.isoIteration}`;
200
+ }
176
201
  if (this.revision !== undefined)
177
202
  result += `.${this.revision}`;
178
203
  if (this.year !== undefined) {
@@ -432,10 +457,14 @@ export function renderIeeeBase(id) {
432
457
  const iecIeeOnly = id.publisher === "IEC" && JSON.stringify(id.copublisher) === '["IEEE"]';
433
458
  if (codeObj !== undefined) {
434
459
  let result = codeObj.render();
435
- // A draft-carrying ISO-led joint reference prints P-less.
436
- if (id.publisher === "ISO" && draftObj !== undefined)
437
- result = result.replace(/^P/, "");
438
- if (stageObj?.projectStatus === true && shouldRenderType && !result.startsWith("P")) {
460
+ // Prepend P if this is a project AND code doesn't already have P.
461
+ // A recorded project marker (the source spelled the P on a
462
+ // non-IEEE-led publisher) prints regardless of the publisher.
463
+ if ((stageObj?.projectStatus === true && shouldRenderType ||
464
+ id.constructor.polymorphicName ===
465
+ "pubid:ieee:project-draft-identifier" &&
466
+ id["project_marker"] === true) &&
467
+ !result.startsWith("P")) {
439
468
  result = `P${result}`;
440
469
  }
441
470
  if (id.year !== undefined && draftObj === undefined && id.edition === undefined && id.month === undefined) {
@@ -560,6 +589,12 @@ class IeeeUrnGenerator extends BaseUrnGenerator {
560
589
  return SPECIAL_URN_OF(this.identifier);
561
590
  }
562
591
  publisherComponent() {
592
+ // The IEC/IEEE co-published pair is definitional to the class and
593
+ // was previously lost ("urn:ieee:ieee" — no number, no draft).
594
+ if (this.identifier.constructor.polymorphicName ===
595
+ "pubid:ieee:iec-ieee-copublished") {
596
+ return "iec-ieee";
597
+ }
563
598
  // Supplements delegate #publisher/#copublisher to their base (Ruby
564
599
  // SupplementIdentifier); DualPublished derives publisher from BOTH
565
600
  // members; MultiNumbered derives it from its primary.
@@ -589,12 +624,35 @@ class IeeeUrnGenerator extends BaseUrnGenerator {
589
624
  return pub;
590
625
  }
591
626
  typeComponent() {
627
+ // A joint stage draft carries the stage in its draft clause and the
628
+ // project P inside the code — a separate type segment would
629
+ // duplicate both.
630
+ const jointDraft = this.identifier["ieee_draft"];
631
+ if (jointDraft !== undefined && jointDraft.startsWith("D="))
632
+ return undefined;
592
633
  const type = this.identifier.type;
593
634
  if (type === undefined || type.trim() === "" || type === "Std")
594
635
  return undefined;
595
636
  return type.toLowerCase().replaceAll(" ", ".");
596
637
  }
597
638
  codeComponent() {
639
+ const id = this.identifier;
640
+ // The co-published code is number+parts+separators (no year — its
641
+ // own segment), e.g. "61886-1".
642
+ if (id.constructor.polymorphicName ===
643
+ "pubid:ieee:iec-ieee-copublished") {
644
+ const self = id;
645
+ const number = self["number"];
646
+ if (number === undefined || number === "")
647
+ return undefined;
648
+ const parts = self["parts"] ?? [];
649
+ const separators = self["separators"] ?? [];
650
+ let code = number;
651
+ separators.forEach((sep, i) => {
652
+ code += `${sep}${parts[i] ?? ""}`;
653
+ });
654
+ return code;
655
+ }
598
656
  // Wrappers delegate code_obj to the identifier they wrap (Ruby
599
657
  // AdoptedStandard/CsaDualPublished/MultiNumberedIdentifier).
600
658
  const self = this.identifier;
@@ -615,6 +673,17 @@ class IeeeUrnGenerator extends BaseUrnGenerator {
615
673
  return root === id ? undefined : root.yearForUrn();
616
674
  }
617
675
  draftComponent() {
676
+ // The joint stage draft's stage rides in its ieee_draft clause
677
+ // ("D=CDV[:2020]"), emitted directly as the draft segment.
678
+ const jointDraft = this.identifier["ieee_draft"];
679
+ if (jointDraft !== undefined && jointDraft.startsWith("D="))
680
+ return `draft.${jointDraft}`;
681
+ const draftInfo = this.identifier["draft_info"];
682
+ if (draftInfo !== undefined &&
683
+ this.identifier.constructor.polymorphicName ===
684
+ "pubid:ieee:iec-ieee-copublished") {
685
+ return draftInfo === "" ? undefined : `draft.${draftInfo}`;
686
+ }
618
687
  const draftObj = this.identifier.draftObject();
619
688
  return draftObj === undefined ? undefined : `draft.${draftObj.render()}`;
620
689
  }
@@ -738,7 +807,13 @@ export const SupplementBaseDefs = {
738
807
  base: { type: IeeeIdentifier },
739
808
  };
740
809
  export const StandardClass = ieeeClass({ kind: "standard" }, { codeColumns: true });
741
- export const ProjectDraftIdentifierClass = ieeeClass({ kind: "project-draft-identifier" }, { codeColumns: true });
810
+ export const ProjectDraftIdentifierClass = ieeeClass({ kind: "project-draft-identifier" }, {
811
+ codeColumns: true,
812
+ extraDefs: {
813
+ project_marker: { type: "boolean", default: false },
814
+ },
815
+ extraMappings: keyValue({ wire: "project_marker", to: "project_marker" }),
816
+ });
742
817
  export const SiStandardClass = ieeeClass({ kind: "si-standard" }, {
743
818
  codeColumns: true,
744
819
  specialUrn: "si",
@@ -940,8 +1015,9 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
940
1015
  lead_party: { type: "string" },
941
1016
  iso_stage: { type: "string" },
942
1017
  ieee_draft: { type: "string" },
1018
+ parenthetical_content: { type: "string" },
943
1019
  },
944
- extraMappings: keyValue({ wire: "publishers", to: "publishers" }, { wire: "lead_party", to: "lead_party" }, { wire: "iso_stage", to: "iso_stage" }, { wire: "ieee_draft", to: "ieee_draft" }),
1020
+ extraMappings: keyValue({ wire: "publishers", to: "publishers" }, { wire: "lead_party", to: "lead_party" }, { wire: "iso_stage", to: "iso_stage" }, { wire: "ieee_draft", to: "ieee_draft" }, { wire: "parenthetical_content", to: "parenthetical_content" }),
945
1021
  wireDelete: { keys: ["publisher", "copublisher"] },
946
1022
  render: (id) => {
947
1023
  const publishers = id["publishers"];
@@ -951,6 +1027,9 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
951
1027
  const codeObj = id.codeObj;
952
1028
  // ISO-led renders the ISO format ALWAYS (the stage-less published
953
1029
  // form "ISO/IEC/IEEE 12207.2:2020" prints colon-year, no stage).
1030
+ // IEEE semantics: P = project (a draft); no P = a standard. The
1031
+ // P-state is identity-bearing and prints as spelled - it is never
1032
+ // added or stripped here.
954
1033
  if (leadParty === "ISO") {
955
1034
  const parts = [];
956
1035
  if (publishers !== undefined && publishers.length > 0)
@@ -961,21 +1040,31 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
961
1040
  else if (isoStage !== undefined) {
962
1041
  parts.push(isoStage);
963
1042
  }
964
- let codeStr = codeObj?.render().replace(/^P/, "") ?? "";
1043
+ const codeStr = codeObj?.render() ?? "";
965
1044
  if (codeStr !== "")
966
1045
  parts.push(codeStr);
967
1046
  let result = parts.join(" ");
968
1047
  if (id.year !== undefined)
969
1048
  result += `:${id.year}`;
1049
+ // The joint stage-draft clause ("D=WD.5") rides after the year in
1050
+ // the ISO-led print; only the bare language/edition marker ("(E)")
1051
+ // prints - a relationship narrative is metadata, not identity.
1052
+ const jointDraft = id["ieee_draft"];
1053
+ if (jointDraft !== undefined && jointDraft.startsWith("D="))
1054
+ result += `/${jointDraft}`;
1055
+ const marker = id["parenthetical_content"];
1056
+ if (marker !== undefined && /^[A-Z](?:\s*[/&]\s*[A-Z])*$/.test(marker)) {
1057
+ result += ` (${marker})`;
1058
+ }
970
1059
  return result;
971
1060
  }
972
1061
  // IEEE format
973
1062
  const parts = [];
974
1063
  if (publishers !== undefined && publishers.length > 0)
975
1064
  parts.push(publishers.join("/"));
976
- let codeStr = codeObj?.render().replace(/^P/, "") ?? "";
977
- if (id.typed_stage?.projectStatus === true || id.type === "P")
978
- codeStr = `P${codeStr}`;
1065
+ // The P-state prints as spelled (P = project draft; no P =
1066
+ // standard). Never added or stripped.
1067
+ let codeStr = codeObj?.render() ?? "";
979
1068
  if (ieeeDraft !== undefined)
980
1069
  codeStr += `/${ieeeDraft}`;
981
1070
  else if (id.typed_stage?.ieeeDraftEquivalent !== undefined) {
@@ -110,15 +110,21 @@ export const UPDATE_CODES = [
110
110
  { plain: "ISO/IEC/IEEE/P16085_DIS, March 2020", replacement: "ISO/IEC/IEEE DIS P16085-2020-03" },
111
111
  { plain: "ANSI/IEEE Std: Outdoor Apparatus Bushings", replacement: "ANSI/IEEE Std 21-1976-11" },
112
112
  { plain: "Draft National Electrical Safety Code, January 2016", replacement: "IEEE PC2-2016-01" },
113
- { plain: "International Standard ISO/IEC 8802-9: 1996(E) ANSI/IEEE Std 802.9, 1996 Edition", replacement: "ISO/IEC/IEEE Std 802.9-1996" },
114
- { plain: "ISO/IEC13210: 1994 (E) ANSI/IEEE Std 1003.3-1991", replacement: "ISO/IEC/IEEE 13210-1994" },
113
+ { plain: "International Standard ISO/IEC 8802-9: 1996(E) ANSI/IEEE Std 802.9, 1996 Edition", replacement: "ISO/IEC 8802-9:1996(E); ANSI/IEEE Std 802.9, 1996 Edition" },
114
+ { plain: "ISO/IEC13210: 1994 (E) ANSI/IEEE Std 1003.3-1991", replacement: "ISO/IEC 13210:1994 (E); ANSI/IEEE Std 1003.3-1991" },
115
115
  { plain: "J-STD-016-1995", replacement: "IEEE 016-1995" },
116
116
  { plain: "Nuclear EQ Sourcebook and Supplement", replacement: "IEEE 7438946" },
117
117
  { plain: "PC37.30.2/D043 Rev 18, May 2015", replacement: "IEEE PC37.30-2/D-043/R-18-2015" },
118
118
  { plain: "IEEE/IEC P62582 CD2 proposal, May 2017", replacement: "IEEE/IEC CD2 P62582-2017-05" },
119
- { plain: "ISO/IEC/IEEE P16326:201x WD.4a, July 2017", replacement: "ISO/IEC/IEEE P16326/D-4a-2017-07" },
119
+ { plain: "ISO/IEC/IEEE P16326:201x WD.4a, July 2017", replacement: "ISO/IEC/IEEE P16326:2017/D=WD.4a" },
120
120
  { plain: "ISO/IEC/IEEE CD.1 P21839, October 2017", replacement: "ISO/IEC/IEEE CD1 P21839-2017-10" },
121
- { plain: "ISO/IEC/IEEE P16326:201x WD5, December 2017", replacement: "ISO/IEC/IEEE P16326/D-5-2017-12" },
121
+ { plain: "ISO/IEC/IEEE P16326:201x WD5, December 2017", replacement: "ISO/IEC/IEEE P16326:2017/D=WD.5" },
122
+ // The joint-publisher underscore drafts ("ISO /IEC/IEEE P24774_D1,
123
+ // February 2020") are the slash spellings ("/D1") of rows the ledger
124
+ // already pins as project drafts; the underscore form parsed through
125
+ // the printed-joint family and flipped the classification (pubid#430).
126
+ { pattern: "\\b(ISO ?\\/ ?IEC ?\\/ ?IEEE|IEC ?\\/ ?IEEE) (P\\d+)_D(\\d+), ([A-Z][a-z]+.?) (\\d{4})\\b", replacement: "\\1 \\2/D\\3, \\4, \\5" },
127
+ { pattern: "\\b(ISO ?\\/ ?IEC ?\\/ ?IEEE|IEC ?\\/ ?IEEE) (P\\d+)_D(\\d+)\\b", replacement: "\\1 \\2/D\\3" },
122
128
  { plain: "IEEE/ISO/IEC P42010.CD1-V1.0, April 2020", replacement: "IEEE/ISO/IEC CD1 P42010-2020-04" },
123
129
  { plain: "Unapproved Draft Std ISO/IEC FDIS 15288:2007(E) IEEE P15288/D3,", replacement: "ISO/IEC/IEEE FDIS Std P15288/D-3-2007" },
124
130
  { plain: "IS0/IEC/IEEE 8802-11:2012/Amd.5:2015(E) (Adoption of IEEE Std 802.11af-2014)", replacement: "ISO/IEC/IEEE 802.11/Amd5-2012" },
@@ -47,12 +47,15 @@ function buildRules() {
47
47
  .or(ref(rules, "annex_identifier"))
48
48
  .or(ref(rules, "plus_supplement_identifier"))
49
49
  .or(ref(rules, "trailing_supplement_identifier"))
50
+ .or(ref(rules, "cs_identifier"))
50
51
  .or(ref(rules, "bulletin_identifier"))
51
52
  .or(ref(rules, "base")));
52
53
  // Publisher - always "OIML"
53
54
  rule("publisher", () => str("OIML").as("publisher").then(ref(rules, "space")));
54
- // Document type - single letter
55
- rule("doc_type", () => match("[BDEGRSVX]").as("type").then(ref(rules, "space")));
55
+ // Document type - single letter. Strict family set: OIML publishes
56
+ // R D B G E V S documents (the estate grammar's family letters);
57
+ // any other letter is a rejection, not a flavor.
58
+ rule("doc_type", () => match("[BDEGRSV]").as("type").then(ref(rules, "space")));
56
59
  // Bulletin locator - structured YYYY[-II[-SS]]
57
60
  rule("bulletin_date", () => ref(rules, "space")
58
61
  .then(ref(rules, "year_digits").as("year"))
@@ -108,9 +111,14 @@ function buildRules() {
108
111
  rule("lang_single", () => match("[EFRXDSCAU]"));
109
112
  rule("lang_multi_oiml", () => str("PO").or(str("PT")).or(str("PE")).or(str("SR")));
110
113
  rule("lang_multi", () => match("[a-z]").repeat(2, 2));
114
+ // Full-word language markers as OIML prints them ("(Fra)", "(Eng)"),
115
+ // three or more letters, any case. Kept verbatim on the identifier;
116
+ // the URN lowercases.
117
+ rule("lang_word", () => match("[A-Za-z]").repeat(3));
111
118
  rule("language_code", () => ref(rules, "lang_single")
112
119
  .then(ref(rules, "slash"), ref(rules, "lang_single"))
113
120
  .or(ref(rules, "lang_multi_oiml"))
121
+ .or(ref(rules, "lang_word"))
114
122
  .or(ref(rules, "lang_single"))
115
123
  .or(ref(rules, "lang_multi"))
116
124
  .as("language"));
@@ -137,7 +145,25 @@ function buildRules() {
137
145
  rule("trailing_supplement_identifier", () => ref(rules, "base_without_language")
138
146
  .as("base")
139
147
  .then(str(" "), str("Amendment").or(str("Errata")).as("trailing_marker"))
140
- .then(ref(rules, "language_portion").maybe().as("language")));
148
+ .then(str(" ").then(ref(rules, "digits").as("number")).maybe(), ref(rules, "language_portion").maybe().as("language")));
149
+ // OIML-CS certification-system documents. Two head spellings
150
+ // ("OIML-CS" / "OIML CS") and two family-number separators
151
+ // ("PD-05" / "PD 05"), an "Edition N" instead of a year, and an
152
+ // optional parenthesized trailing amendment - "(Amendment 1)".
153
+ rule("cs_identifier", () => str("OIML")
154
+ .as("publisher")
155
+ .then(ref(rules, "dash")
156
+ .then(str("CS"))
157
+ .or(str(" ").then(str("CS")))
158
+ .as("cs_series"))
159
+ .then(ref(rules, "space"))
160
+ .then(str("PD").or(str("OD")).or(str("CID")).as("cs_family"))
161
+ .then(ref(rules, "dash").or(str(" ")).as("cs_separator"))
162
+ .then(ref(rules, "digits").as("number"))
163
+ .then(str(" "), str("Edition"), str(" "), ref(rules, "digits").as("edition"))
164
+ .then(str(" ")
165
+ .then(ref(rules, "lparen"), str("Amendment"), str(" "), ref(rules, "digits").as("cs_amendment"), ref(rules, "rparen"))
166
+ .maybe()));
141
167
  rule("plus_supplement_identifier", () => ref(rules, "base_without_language")
142
168
  .as("base")
143
169
  .then(str("+"), str("Amendment").or(str("Errata")).as("plus_marker"))
@@ -1,6 +1,12 @@
1
1
  import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
2
2
  import { oimlGrammar } from "./grammar.js";
3
- import { buildOimlIdentifier } from "./model.js";
3
+ import { buildOimlIdentifier, OimlDualPublished } from "./model.js";
4
+ import { grammarImplementation } from "../index.js";
5
+ // The co-publisher prefix (only ISO confirmed so far; the gem routes via
6
+ // Pubid.parse's prefix table).
7
+ function flavorOf(input) {
8
+ return input.toUpperCase().startsWith("ISO ") ? "iso" : "iso";
9
+ }
4
10
  /**
5
11
  * Grammar-backed OIML on the unified model: the model classes are
6
12
  * BaseIdentifier subclasses, so they satisfy the Identifier adapter
@@ -9,6 +15,29 @@ import { buildOimlIdentifier } from "./model.js";
9
15
  export function oimlGrammarImplementation() {
10
16
  return {
11
17
  parse(input) {
18
+ // The co-published form carries both identifiers joined by "|":
19
+ // "ISO 4064-1:2024|OIML R 49-1:2024". The first side is another
20
+ // flavor's identifier (routed by prefix, the Ruby Pubid.parse
21
+ // counterpart); the second is the OIML side.
22
+ if (input.includes("|") && !input.startsWith("|") && !input.endsWith("|")) {
23
+ // Each side may be either flavor ("ISO …|OIML R …" and the
24
+ // reversed print): the OIML side parses through this grammar, the
25
+ // co-publisher's through its own (routed by prefix).
26
+ const parts = input.split("|");
27
+ const members = parts.map((part) => {
28
+ const s = part.trim();
29
+ if (s.toUpperCase().startsWith("OIML")) {
30
+ return buildOimlIdentifier(parseGrammar(oimlGrammar, s));
31
+ }
32
+ const impl = grammarImplementation(flavorOf(s));
33
+ if (impl === undefined)
34
+ throw new ParseFailed(`OIML: no flavor for ${s}`, 0);
35
+ return impl.parse(s);
36
+ });
37
+ if (members.length === 2 && members[0] !== undefined && members[1] !== undefined) {
38
+ return new OimlDualPublished({ first: members[0], second: members[1] });
39
+ }
40
+ }
12
41
  return buildOimlIdentifier(parseGrammar(oimlGrammar, input));
13
42
  },
14
43
  };
@@ -1,4 +1,23 @@
1
1
  import type { Tree } from "../../grammar/engine.js";
2
2
  import { BaseIdentifier } from "../../model/identifier.js";
3
+ import type { AttributeTable } from "../../model/attribute.js";
4
+ declare abstract class OimlBase extends BaseIdentifier {
5
+ readonly publisher: string | undefined;
6
+ readonly language: string | undefined;
7
+ readonly parsed_format: string | undefined;
8
+ effectiveFormat(): "long" | "short";
9
+ }
3
10
  /** ---- builder (lib/pubid/oiml/builder.rb) ---- */
11
+ export declare class OimlDualPublished extends OimlBase {
12
+ static polymorphicName: string;
13
+ static get attributes(): AttributeTable;
14
+ static get mappings(): never[];
15
+ readonly first: BaseIdentifier;
16
+ readonly second: BaseIdentifier;
17
+ constructor(attrs?: Record<string, unknown>);
18
+ render(): string;
19
+ toUrn(): string;
20
+ root(): BaseIdentifier;
21
+ }
4
22
  export declare function buildOimlIdentifier(tree: Tree): BaseIdentifier;
23
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { ParseFailed } from "../../grammar/engine.js";
2
- import { BaseIdentifier, registerType } from "../../model/identifier.js";
2
+ import { BaseIdentifier, registerType, resolveType } from "../../model/identifier.js";
3
3
  import { extendAttributes, keyValue } from "../../model/attribute.js";
4
4
  import { BaseUrnGenerator } from "../../model/urn-generator.js";
5
5
  const KIND_BY_TYPE = {
@@ -13,6 +13,7 @@ const KIND_BY_TYPE = {
13
13
  };
14
14
  const TYPE_STRINGS = {
15
15
  "basic-publication": "B",
16
+ "certification-system": "CS",
16
17
  document: "D",
17
18
  "expert-report": "E",
18
19
  guide: "G",
@@ -108,6 +109,29 @@ class OimlSingle extends OimlBase {
108
109
  return result;
109
110
  }
110
111
  }
112
+ /** ---- OIML-CS certification-system documents ---- */
113
+ const CS_ATTRS = {
114
+ ...SINGLE_ATTRS,
115
+ family: { type: "string" },
116
+ amendment: { type: "string" },
117
+ space_separator: { type: "boolean", default: false },
118
+ };
119
+ const CS_MAPPINGS = keyValue(...Object.keys(CS_ATTRS).map((name) => ({ wire: name, to: name })));
120
+ // "OIML-CS PD-05 Edition 6 (Amendment 1)": the number keeps its printed
121
+ // zero padding; the print states an edition instead of a year.
122
+ class OimlCertificationSystem extends OimlSingle {
123
+ static polymorphicName = "pubid:oiml:certification-system";
124
+ static attributes = extendAttributes(BaseIdentifier, CS_ATTRS);
125
+ static mappings = CS_MAPPINGS;
126
+ render() {
127
+ let result = `${this.publisher}-CS ${this.family}` +
128
+ `${this.space_separator ? " " : "-"}${this.number} Edition ${this.edition}`;
129
+ if (this.amendment)
130
+ result += ` (Amendment ${this.amendment})`;
131
+ return result;
132
+ }
133
+ }
134
+ registerType(OimlCertificationSystem);
111
135
  function oimlSingleClass(kind) {
112
136
  const isBulletin = kind === "bulletin";
113
137
  class OimlSingleIdentifier extends OimlSingle {
@@ -146,12 +170,13 @@ const SUPP_ATTRS = {
146
170
  parsed_format: { type: "string", default: "short" },
147
171
  base: { type: OimlSingle },
148
172
  supp_year: { type: "string" },
173
+ number: { type: "string" },
149
174
  trailing: { type: "boolean", default: false },
150
175
  joined: { type: "boolean", default: false },
151
176
  letter: { type: "string" },
152
177
  year_on_base: { type: "boolean", default: false },
153
178
  };
154
- const SUPP_MAPPINGS = keyValue({ wire: "language", to: "language" }, { wire: "parsed_format", to: "parsed_format" }, { wire: "base", to: "base" }, { wire: "year", to: "supp_year" }, { wire: "trailing", to: "trailing" }, { wire: "joined", to: "joined" }, { wire: "letter", to: "letter" }, { wire: "year_on_base", to: "year_on_base" });
179
+ const SUPP_MAPPINGS = keyValue({ wire: "language", to: "language" }, { wire: "parsed_format", to: "parsed_format" }, { wire: "base", to: "base" }, { wire: "year", to: "supp_year" }, { wire: "number", to: "number" }, { wire: "trailing", to: "trailing" }, { wire: "joined", to: "joined" }, { wire: "letter", to: "letter" }, { wire: "year_on_base", to: "year_on_base" });
155
180
  /** ---- supplements (Amendment / Errata) and Annex ---- */
156
181
  class OimlSupplement extends OimlBase {
157
182
  supplementType() {
@@ -178,6 +203,8 @@ class OimlSupplement extends OimlBase {
178
203
  }
179
204
  if (this.trailing) {
180
205
  let result = `${stripLanguage(this.base.render())} ${this.supplementType()}`;
206
+ if (this.number)
207
+ result += ` ${this.number}`;
181
208
  if (this.language)
182
209
  result += ` (${this.language})`;
183
210
  return result;
@@ -247,6 +274,7 @@ for (const kind of [
247
274
  ]) {
248
275
  KIND_CLASSES[kind] = oimlSingleClass(kind);
249
276
  }
277
+ KIND_CLASSES["certification-system"] = OimlCertificationSystem;
250
278
  KIND_CLASSES["amendment"] = oimlSupplementClass("amendment");
251
279
  KIND_CLASSES["errata"] = oimlSupplementClass("errata");
252
280
  KIND_CLASSES["annex"] = oimlSupplementClass("annex");
@@ -270,6 +298,13 @@ class OimlUrnGenerator extends BaseUrnGenerator {
270
298
  parts.push(id.language.toLowerCase());
271
299
  return parts.join(":");
272
300
  }
301
+ // Certification-system documents: the family-number pair is the
302
+ // document identity ("cs:pd-05"); the printed edition and trailing
303
+ // amendment are print states, not URN segments.
304
+ if (kind === "certification-system") {
305
+ const cs = id;
306
+ return ["urn", "oiml", "cs", `${(cs.family ?? "").toLowerCase()}-${cs.number ?? ""}`].join(":");
307
+ }
273
308
  const isSupp = kind === "amendment" || kind === "errata" || kind === "annex";
274
309
  const single = (isSupp ? id.base : id);
275
310
  const parts = ["urn", "oiml"];
@@ -280,8 +315,12 @@ class OimlUrnGenerator extends BaseUrnGenerator {
280
315
  const code = single.composedCode();
281
316
  if (code)
282
317
  parts.push(code);
318
+ // The trailing-word form keeps the publication year on the base
319
+ // ("R 138:2009 Amendment 1"); the supplement itself carries only the
320
+ // ordinal.
283
321
  const year = id.year ??
284
- id.supp_year;
322
+ id.supp_year ??
323
+ (isSupp ? single.year : undefined);
285
324
  if (year)
286
325
  parts.push(year);
287
326
  const stage = id.stage;
@@ -292,6 +331,12 @@ class OimlUrnGenerator extends BaseUrnGenerator {
292
331
  parts.push(iteration);
293
332
  if (id.language)
294
333
  parts.push(id.language.toLowerCase());
334
+ // Ruby's supplement branch appends the ordinal after the language.
335
+ if (isSupp) {
336
+ const ordinal = id.number;
337
+ if (ordinal)
338
+ parts.push(ordinal);
339
+ }
295
340
  return parts.join(":");
296
341
  }
297
342
  }
@@ -299,15 +344,97 @@ for (const klass of Object.values(KIND_CLASSES)) {
299
344
  klass.urnGenerator = OimlUrnGenerator;
300
345
  }
301
346
  /** ---- builder (lib/pubid/oiml/builder.rb) ---- */
347
+ // OIML co-publishes some documents jointly with another SDO (ISO so
348
+ // far); the printed reference carries both identifiers joined by "|":
349
+ // "ISO 4064-1:2024|OIML R 49-1:2024". The URN is the OIML side's.
350
+ export class OimlDualPublished extends OimlBase {
351
+ static polymorphicName = "pubid:oiml:dual-published";
352
+ static get attributes() {
353
+ return extendAttributes(OimlBase, {
354
+ // "string" keeps objects passing through the scalar coercion
355
+ // untouched (the ieee AdoptedStandard precedent for cross-flavor
356
+ // members); the constructor coerces bare hashes via the registry.
357
+ first: { type: "string" },
358
+ second: { type: "string" },
359
+ });
360
+ }
361
+ static get mappings() {
362
+ return [
363
+ { wire: "first", to: "first" },
364
+ { wire: "second", to: "second" },
365
+ ];
366
+ }
367
+ constructor(attrs = {}) {
368
+ super(attrs);
369
+ // The members are cross-flavor (the co-publisher's side arrives as a
370
+ // bare hash with _type); coerce through the registry - the abstract
371
+ // BaseIdentifier attribute type cannot dispatch polymorphically.
372
+ const coerceMember = (v) => {
373
+ if (v instanceof BaseIdentifier)
374
+ return v;
375
+ if (typeof v === "object" && v !== null && "_type" in v) {
376
+ const klass = resolveType(v["_type"]);
377
+ if (klass !== undefined) {
378
+ return klass.fromHash(v);
379
+ }
380
+ }
381
+ return undefined;
382
+ };
383
+ const self = this;
384
+ const f = coerceMember(self["first"]);
385
+ const s = coerceMember(self["second"]);
386
+ if (f !== undefined)
387
+ self["first"] = f;
388
+ if (s !== undefined)
389
+ self["second"] = s;
390
+ }
391
+ render() {
392
+ return `${this.first.toHuman()}|${this.second.toHuman()}`;
393
+ }
394
+ toUrn() {
395
+ // The URN is the OIML side's, whichever position it prints in.
396
+ const oimlSide = this.second instanceof OimlBase ? this.second : this.first;
397
+ return oimlSide.toUrn();
398
+ }
399
+ root() {
400
+ return this.second instanceof OimlBase ? this.second : this.first;
401
+ }
402
+ }
403
+ registerType(OimlDualPublished);
302
404
  export function buildOimlIdentifier(tree) {
303
405
  if (!isObj(tree))
304
406
  throw new ParseFailed("OIML: unexpected parse tree", 0);
407
+ if (tree["cs_series"] !== undefined)
408
+ return buildCs(tree);
305
409
  if (tree["amd_marker"] !== undefined)
306
410
  return buildShortAmendment(tree);
307
411
  if (tree["base"] !== undefined)
308
412
  return buildSupplement(tree);
309
413
  return buildBaseDocument(tree);
310
414
  }
415
+ // OIML-CS documents: family (PD/OD/CID), zero-padded number, the
416
+ // printed "Edition N", and an optional parenthesized trailing
417
+ // amendment. The family-number separator is a spelling flag.
418
+ function buildCs(tree) {
419
+ const attrs = {
420
+ publisher: str(tree["publisher"]) ?? "OIML",
421
+ parsed_format: "short",
422
+ };
423
+ const family = str(tree["cs_family"]);
424
+ if (family !== undefined)
425
+ attrs["family"] = family;
426
+ const number = str(tree["number"]);
427
+ if (number !== undefined)
428
+ attrs["number"] = number;
429
+ const edition = str(tree["edition"]);
430
+ if (edition !== undefined)
431
+ attrs["edition"] = edition;
432
+ const amendment = str(tree["cs_amendment"]);
433
+ if (amendment !== undefined)
434
+ attrs["amendment"] = amendment;
435
+ attrs["space_separator"] = tree["cs_separator"] === " ";
436
+ return new KIND_CLASSES["certification-system"](attrs);
437
+ }
311
438
  function buildShortAmendment(tree) {
312
439
  const baseCode = isObj(tree["base_code"]) ? tree["base_code"] : undefined;
313
440
  const base = buildBaseDocument({
@@ -359,6 +486,9 @@ function buildSupplement(tree) {
359
486
  const language = extractLanguage(tree["language"]);
360
487
  if (language !== undefined)
361
488
  attrs["language"] = language;
489
+ const ordinal = str(tree["number"]);
490
+ if (ordinal !== undefined)
491
+ attrs["number"] = ordinal;
362
492
  if (marker !== undefined)
363
493
  attrs["trailing"] = true;
364
494
  if (plusMarker !== undefined)
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export type { FlavorImplementation, Identifier, ImplementationRegistry } from ".
6
6
  export { PendingRegistry } from "./conformance/pending.js";
7
7
  export { checkCase, runFlavor, runCorpus } from "./conformance/runner.js";
8
8
  export { corpusModeImplementation, CorpusBackedIdentifier } from "./implementations/corpus-mode.js";
9
+ export { grammarImplementation } from "./flavors/index.js";
package/dist/index.js CHANGED
@@ -4,3 +4,6 @@ export { buildArtifact, loadBundledCorpus, parseArtifact, } from "./corpus/artif
4
4
  export { PendingRegistry } from "./conformance/pending.js";
5
5
  export { checkCase, runFlavor, runCorpus } from "./conformance/runner.js";
6
6
  export { corpusModeImplementation, CorpusBackedIdentifier } from "./implementations/corpus-mode.js";
7
+ // Root re-export so consumers needn't deep-import flavor modules
8
+ // (pubid/pubid-ts#63 - the estate serving codec).
9
+ export { grammarImplementation } from "./flavors/index.js";
@@ -39,6 +39,7 @@ export type UrnGeneratorCtor = new (identifier: BaseIdentifier) => {
39
39
  };
40
40
  /** Register a concrete identifier class under its polymorphic _type (the *_TYPE_MAP role). */
41
41
  export declare function registerType(klass: IdentifierStatic): void;
42
+ export declare function resolveType(type: string): IdentifierStatic | undefined;
42
43
  export declare abstract class BaseIdentifier {
43
44
  static polymorphicName: string;
44
45
  /** The human renderer body — the one method every flavor writes. */
@@ -25,7 +25,7 @@ const TYPE_REGISTRY = new Map();
25
25
  export function registerType(klass) {
26
26
  TYPE_REGISTRY.set(klass.polymorphicName, klass);
27
27
  }
28
- function resolveType(type) {
28
+ export function resolveType(type) {
29
29
  return TYPE_REGISTRY.get(type);
30
30
  }
31
31
  function isScalarType(t) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",