@pubid/pubid 0.1.4 → 0.1.6

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.
@@ -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" },
@@ -1,3 +1,4 @@
1
+ import { lookupUrnParser, parseUrnFor } from "../urn/parsers.js";
1
2
  import { oimlGrammarImplementation } from "./oiml/implementation.js";
2
3
  import { xsfGrammarImplementation } from "./xsf/index.js";
3
4
  import { doiGrammarImplementation } from "./doi/index.js";
@@ -47,6 +48,32 @@ import { ialaGrammarImplementation } from "./iala/index.js";
47
48
  * flavor NOT listed falls back to corpus mode.
48
49
  */
49
50
  export function grammarImplementation(flavor) {
51
+ const impl = grammarImplementationSwitch(flavor);
52
+ if (impl === undefined)
53
+ return undefined;
54
+ // "urn:" inputs route to the flavor's URN parser where the reference
55
+ // implements one (impl.parseUrn or the registry in src/urn/parsers.ts
56
+ // mirroring lib/pubid/<flavor>/urn_parser.rb); flavors without one whose
57
+ // own grammar accepts URN spellings keep parsing them natively
58
+ // (pubid/pubid-ts#1).
59
+ const urnDef = lookupUrnParser(flavor);
60
+ if (impl.parseUrn === undefined && urnDef === undefined)
61
+ return impl;
62
+ return {
63
+ parse(input) {
64
+ if (/^urn:/i.test(input)) {
65
+ if (impl.parseUrn)
66
+ return impl.parseUrn(input);
67
+ return parseUrnFor(urnDef, input, impl.parse);
68
+ }
69
+ return impl.parse(input);
70
+ },
71
+ ...(impl.parseUrn
72
+ ? { parseUrn: impl.parseUrn }
73
+ : { parseUrn: (urn) => parseUrnFor(urnDef, urn, impl.parse) }),
74
+ };
75
+ }
76
+ function grammarImplementationSwitch(flavor) {
50
77
  switch (flavor) {
51
78
  case "oiml":
52
79
  return oimlGrammarImplementation();
@@ -288,6 +288,27 @@ class JcgmBuilder extends BaseBuilder {
288
288
  export function jcgmGrammarImplementation() {
289
289
  const builder = new JcgmBuilder();
290
290
  return {
291
+ // Inverse of JcgmUrnGenerator (lib/pubid/jcgm/urn_parser.rb): rebuild
292
+ // identifiers DIRECTLY from the URN segments — re-parsing through the
293
+ // grammar flattened segments and lost languages/supplements.
294
+ parseUrn(urn) {
295
+ const parts = urn.replace(/^urn:jcgm:/, "").split(":");
296
+ if (parts[0] === "meeting") {
297
+ const number = toMeetingNumber(parts[1]);
298
+ const date = urnDate(parts[2]);
299
+ return new (KIND_CLASSES.meeting)({ number, date });
300
+ }
301
+ const markerAt = parts.findIndex((seg) => seg === "corrigendum" || seg === "amendment");
302
+ if (markerAt === -1) {
303
+ return buildUrnDocument(parts);
304
+ }
305
+ const kind = (parts[markerAt] === "amendment" ? "amendment" : "corrigendum");
306
+ const base = buildUrnDocument(parts.slice(0, markerAt));
307
+ const tail = parts.slice(markerAt + 1);
308
+ const number = tail[0] === undefined || tail[0] === "" ? undefined : tail[0];
309
+ const date = urnDate(tail[1]);
310
+ return new (KIND_CLASSES[kind])({ base, number, date });
311
+ },
291
312
  parse(input) {
292
313
  const tree = parseGrammar(jcgmGrammar, input);
293
314
  if (typeof tree !== "object" || tree === null || Array.isArray(tree)) {
@@ -297,4 +318,44 @@ export function jcgmGrammarImplementation() {
297
318
  },
298
319
  };
299
320
  }
321
+ // lib/pubid/jcgm/urn_parser.rb build_document: Guide vs GumGuide by the
322
+ // "gum." number prefix; the year and language segments are matched by
323
+ // pattern, not position.
324
+ function buildUrnDocument(parts) {
325
+ const number = parts[0];
326
+ if (number === undefined || number === "") {
327
+ throw new Error("JCGM URN has no document number");
328
+ }
329
+ const gum = number.startsWith("gum.");
330
+ const rest = parts.slice(1);
331
+ const year = rest.find((seg) => /^(19|20)\d{2}$/.test(seg));
332
+ const langSegment = rest.find((seg) => /^[a-z]{2}(,[a-z]{2})*$/.test(seg));
333
+ const langLetters = {
334
+ ru: "R", fr: "F", en: "E", ar: "A", es: "S", de: "D",
335
+ };
336
+ const languages = langSegment
337
+ ? langSegment.split(",").map((code) => new Language({ code, originalCode: langLetters[code] ?? code }))
338
+ : [];
339
+ const cls = gum ? KIND_CLASSES["gum-guide"] : KIND_CLASSES.guide;
340
+ const cleanNumber = gum ? number.slice("gum.".length) : number;
341
+ return new cls({
342
+ number: cleanNumber,
343
+ date: year ? new PubidDate({ year }) : undefined,
344
+ languages,
345
+ });
346
+ }
347
+ // nil for an absent year; a component for a well-formed 19xx/20xx year;
348
+ // anything else rejects (no re-parse validates it any more).
349
+ function urnDate(segment) {
350
+ if (segment === undefined || segment === "")
351
+ return undefined;
352
+ if (!/^(19|20)\d{2}$/.test(segment)) {
353
+ throw new Error(`Invalid year in JCGM URN: ${JSON.stringify(segment)}`);
354
+ }
355
+ return new PubidDate({ year: segment });
356
+ }
357
+ // "011" reads back as "11" (Identifiers::Meeting.ordinal normalization).
358
+ function toMeetingNumber(segment) {
359
+ return String(Number(segment));
360
+ }
300
361
  export { isObj };
@@ -271,12 +271,19 @@ function buildRules() {
271
271
  .as("series")
272
272
  .then(space.or(dot))
273
273
  .as("circ_series")
274
- .then(str("supp")
274
+ .then(
275
+ // Bare supplement marker to the whole series, no base number
276
+ // ("NBS.CIRC.sup" — testsuite#5 C4).
277
+ ((str("supp").or(str("sup"))).then(monthAbbrev().then(digits).as("supplement_month_year").or(digits.as("supp_number").then(slash, digits.as("supp_year")).as("supplement_slash_year"), str("").as("supplement_empty")).maybe())).or(
278
+ // Date-range supplement (no base document).
279
+ str("supp")
275
280
  .then(monthAbbrev().as("supp_month_start"), digits.as("supp_year_start"), dash, monthAbbrev().as("supp_month_end"), digits.as("supp_year_end"))
276
281
  .as("supplement_date_range")
277
282
  .or((str("supp").or(str("sup")))
278
283
  .then(match("[0-9]").repeat(4, 4).as("supp_year_start"), dash, match("[0-9]").repeat(4, 4).as("supp_year_end"))
279
- .as("supplement_date_range"), (digits.as("base_number").then(str("e"), digits.as("edition_number")).or(digits
284
+ .as("supplement_date_range"))).or(
285
+ // Base identifier + supplement.
286
+ (digits.as("base_number").then(str("e"), digits.as("edition_number")).or(digits
280
287
  .as("base_number")
281
288
  .then(lowerLetter.as("revision_letter"), digits.as("revision_number")), digits.as("base_number").then(upperLetter.as("letter_suffix")), digits.as("simple_number")))
282
289
  .as("base_portion")
@@ -803,6 +803,7 @@ class NistBuilder {
803
803
  h["supplement_month_year"] !== undefined ||
804
804
  h["supplement_year"] !== undefined ||
805
805
  h["supplement"] !== undefined ||
806
+ h["supplement_empty"] !== undefined ||
806
807
  h["base_portion"] !== undefined) {
807
808
  return this.buildCircularSupplement(h);
808
809
  }
@@ -1094,9 +1095,12 @@ class NistBuilder {
1094
1095
  return this.buildCircularSupplementWrapper(h);
1095
1096
  }
1096
1097
  const seriesValue = isObj(h["circ_series"]) ? s(h["circ_series"]["series"]) : s(h["series"]);
1098
+ // The dotted series literal ("NBS.CIRC") names its publisher in the
1099
+ // first token; the builder default would render "NIST" instead.
1100
+ const seriesPublisher = seriesValue?.startsWith("NBS") ? "NBS" : undefined;
1097
1101
  if (isObj(h["supplement_date_range"])) {
1098
1102
  const range = h["supplement_date_range"];
1099
- const identifier = this.build({ series: seriesValue }, "short");
1103
+ const identifier = this.build({ series: seriesValue, publisher: seriesPublisher }, "short");
1100
1104
  const ms = s(range["supp_month_start"]);
1101
1105
  const ys = s(range["supp_year_start"]);
1102
1106
  const me = s(range["supp_month_end"]);
@@ -1109,7 +1113,7 @@ class NistBuilder {
1109
1113
  });
1110
1114
  return identifier;
1111
1115
  }
1112
- const base = this.buildCircularSupplementBase(h, seriesValue);
1116
+ const base = this.buildCircularSupplementBase(h, seriesValue, seriesPublisher);
1113
1117
  const self = base;
1114
1118
  const raw = h["supplement_month_year"] !== undefined
1115
1119
  ? s(h["supplement_month_year"])
@@ -1119,11 +1123,11 @@ class NistBuilder {
1119
1123
  self["supplement"] = NistSupplement.fromRaw(raw);
1120
1124
  return base;
1121
1125
  }
1122
- buildCircularSupplementBase(h, seriesValue) {
1126
+ buildCircularSupplementBase(h, seriesValue, seriesPublisher) {
1123
1127
  const basePortion = h["base_portion"];
1124
1128
  if (!isObj(basePortion)) {
1125
1129
  return this.build({
1126
- publisher: s(h["publisher"]),
1130
+ publisher: s(h["publisher"]) ?? seriesPublisher,
1127
1131
  series: seriesValue ?? s(h["series"]),
1128
1132
  first_number: s(h["first_number"]),
1129
1133
  }, s(h["parsed_format"]) ?? "short");
@@ -516,11 +516,18 @@ export class NistIdentifier extends BaseIdentifier {
516
516
  else if (this.volume !== undefined && this.issue_number !== undefined) {
517
517
  result += `${this.volume.render()}n${this.issue_number.number}`;
518
518
  }
519
- if (this.edition !== undefined)
520
- result += this.edition.render();
519
+ // With a number the edition glues per the NIST spec ("800-53r5");
520
+ // series-only editions take a dot separator ("NBS.CIRC.e2" — the
521
+ // attested raw spelling; testsuite#5 C4/C5).
522
+ if (this.edition !== undefined) {
523
+ result += this.number !== undefined ? this.edition.render() : `.${this.edition.render()}`;
524
+ }
521
525
  if (this.version_component !== undefined)
522
526
  result += this.version_component.render();
523
- result += this.supplementShort();
527
+ // A series-only supplement takes the dot itself ("NBS.CIRC.sup").
528
+ if (this.supplement !== undefined) {
529
+ result += this.number !== undefined ? this.supplementShort() : `.${this.supplementShort()}`;
530
+ }
524
531
  if (this.update_component !== undefined)
525
532
  result += this.update_component.render("mr");
526
533
  if (this.stage !== undefined) {
@@ -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
@@ -8,7 +14,46 @@ import { buildOimlIdentifier } from "./model.js";
8
14
  */
9
15
  export function oimlGrammarImplementation() {
10
16
  return {
17
+ // Inverse of OimlUrnGenerator (lib/pubid/oiml/urn_parser.rb): strip the
18
+ // "urn:oiml:" namespace, rebuild the human head "OIML <TYPE> <locator>",
19
+ // and re-parse. Mirrors the reference exactly — the year and language
20
+ // URN segments are not carried back (the reference parser reads only
21
+ // the type and the number-part locator).
22
+ parseUrn(urn) {
23
+ const body = urn.replace(/^urn:oiml:/i, "");
24
+ const parts = body.split(":");
25
+ const typeToken = parts[0] ?? "";
26
+ const number = parts[1];
27
+ const displayType = typeToken.toLowerCase() === "bulletin" ? "Bulletin" : typeToken.toUpperCase();
28
+ let text = `OIML ${displayType}`;
29
+ if (number !== undefined && number !== "")
30
+ text += ` ${number}`;
31
+ return this.parse(text);
32
+ },
11
33
  parse(input) {
34
+ // The co-published form carries both identifiers joined by "|":
35
+ // "ISO 4064-1:2024|OIML R 49-1:2024". The first side is another
36
+ // flavor's identifier (routed by prefix, the Ruby Pubid.parse
37
+ // counterpart); the second is the OIML side.
38
+ if (input.includes("|") && !input.startsWith("|") && !input.endsWith("|")) {
39
+ // Each side may be either flavor ("ISO …|OIML R …" and the
40
+ // reversed print): the OIML side parses through this grammar, the
41
+ // co-publisher's through its own (routed by prefix).
42
+ const parts = input.split("|");
43
+ const members = parts.map((part) => {
44
+ const s = part.trim();
45
+ if (s.toUpperCase().startsWith("OIML")) {
46
+ return buildOimlIdentifier(parseGrammar(oimlGrammar, s));
47
+ }
48
+ const impl = grammarImplementation(flavorOf(s));
49
+ if (impl === undefined)
50
+ throw new ParseFailed(`OIML: no flavor for ${s}`, 0);
51
+ return impl.parse(s);
52
+ });
53
+ if (members.length === 2 && members[0] !== undefined && members[1] !== undefined) {
54
+ return new OimlDualPublished({ first: members[0], second: members[1] });
55
+ }
56
+ }
12
57
  return buildOimlIdentifier(parseGrammar(oimlGrammar, input));
13
58
  },
14
59
  };
@@ -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 {};