@pubid/pubid 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/artifact/corpus.json.gz +0 -0
  2. package/dist/flavors/iec/index.js +12 -5
  3. package/dist/flavors/ieee/index.js +321 -113
  4. package/dist/flavors/ieee/model.d.ts +18 -0
  5. package/dist/flavors/ieee/model.js +62 -8
  6. package/dist/flavors/ieee/preprocessor.js +3 -0
  7. package/dist/flavors/ieee/update_codes.js +1 -1
  8. package/dist/flavors/iho/index.js +198 -35
  9. package/dist/flavors/jis/index.js +74 -2
  10. package/dist/flavors/nist/grammar.js +44 -16
  11. package/dist/flavors/nist/index.js +17 -2
  12. package/dist/model/identifier.d.ts +9 -0
  13. package/dist/model/identifier.js +50 -0
  14. package/dist/urn/parsers.js +57 -20
  15. package/package.json +2 -2
  16. package/vendor/parg/adobe.json +1 -1
  17. package/vendor/parg/aiee.json +1 -1
  18. package/vendor/parg/amca.json +1 -1
  19. package/vendor/parg/ansi.json +1 -1
  20. package/vendor/parg/api.json +1 -1
  21. package/vendor/parg/ashrae.json +1 -1
  22. package/vendor/parg/asme.json +1 -1
  23. package/vendor/parg/astm.json +1 -1
  24. package/vendor/parg/bipm.json +1 -1
  25. package/vendor/parg/bsi.json +1 -1
  26. package/vendor/parg/calconnect.json +1 -1
  27. package/vendor/parg/ccsds.json +1 -1
  28. package/vendor/parg/cen_cenelec.json +1 -1
  29. package/vendor/parg/cie.json +1 -1
  30. package/vendor/parg/csa.json +1 -1
  31. package/vendor/parg/doi.json +1 -1
  32. package/vendor/parg/easc.json +1 -1
  33. package/vendor/parg/ecma.json +1 -1
  34. package/vendor/parg/etsi.json +1 -1
  35. package/vendor/parg/evs.json +1 -1
  36. package/vendor/parg/gb.json +1 -1
  37. package/vendor/parg/gost.json +1 -1
  38. package/vendor/parg/iala.json +1 -1
  39. package/vendor/parg/iana.json +1 -1
  40. package/vendor/parg/idf.json +1 -1
  41. package/vendor/parg/iec.json +1 -1
  42. package/vendor/parg/ieee.json +1 -29571
  43. package/vendor/parg/ietf.json +1 -1
  44. package/vendor/parg/iho.json +1 -0
  45. package/vendor/parg/ire.json +1 -1
  46. package/vendor/parg/isbn.json +1 -1
  47. package/vendor/parg/iso.json +1 -8510
  48. package/vendor/parg/itu.json +1 -1
  49. package/vendor/parg/jcgm.json +1 -1
  50. package/vendor/parg/jis.json +1 -1
  51. package/vendor/parg/nesc.json +1 -1
  52. package/vendor/parg/nist.json +1 -9588
  53. package/vendor/parg/oasis.json +1 -1
  54. package/vendor/parg/ogc.json +1 -1
  55. package/vendor/parg/oiml.json +1 -1
  56. package/vendor/parg/omg.json +1 -1
  57. package/vendor/parg/parg.json +1 -1
  58. package/vendor/parg/plateau.json +1 -1
  59. package/vendor/parg/sae.json +1 -1
  60. package/vendor/parg/tgpp.json +1 -1
  61. package/vendor/parg/un.json +1 -1
  62. package/vendor/parg/w3c.json +1 -1
  63. package/vendor/parg/xsf.json +1 -1
@@ -36,9 +36,27 @@ export declare class IeeeCode {
36
36
  readonly number: string;
37
37
  readonly parts: string[];
38
38
  readonly originalSeparator: string | undefined;
39
+ /** The separator BEFORE each part, when the source mixed them
40
+ * ("7.4-3-2" — dot after the number, dashes before the subparts).
41
+ * Runtime-only: the serialized columns keep the single
42
+ * original_separator, and only a fresh parse knows the per-part
43
+ * spelling (the reference's Components::Code#original_separators). */
44
+ readonly originalSeparators: string[] | undefined;
39
45
  constructor(attrs: Record<string, unknown>);
40
46
  static parse(codeStr: string): IeeeCode | undefined;
47
+ /** The printed face. The reference's Components::Code#to_s keeps the
48
+ * separator each part was printed with ("7.4-3-2"), and the corpus
49
+ * canonically pins the printed spelling (the project-draft rows freeze
50
+ * "IEEE P7-4.3.2/D2" with the dashed spelling as the input alias), so
51
+ * this face renders per-part like the Ruby reference. */
41
52
  render(): string;
53
+ /** The canonical face: one uniform separator across every part
54
+ * boundary (the first distinct one the source printed) — the face the
55
+ * URN segment, the joint IEEE/ISO faces and the reassembled PSI code
56
+ * render, so spellings that differ only in part separators share one
57
+ * identity. */
58
+ canonicalRender(): string;
59
+ private renderCode;
42
60
  }
43
61
  export declare const DRAFT_MONTH_NAMES: Record<string, string>;
44
62
  export declare class IeeeDraft {
@@ -64,11 +64,20 @@ export class IeeeCode {
64
64
  number;
65
65
  parts;
66
66
  originalSeparator;
67
+ /** The separator BEFORE each part, when the source mixed them
68
+ * ("7.4-3-2" — dot after the number, dashes before the subparts).
69
+ * Runtime-only: the serialized columns keep the single
70
+ * original_separator, and only a fresh parse knows the per-part
71
+ * spelling (the reference's Components::Code#original_separators). */
72
+ originalSeparators;
67
73
  constructor(attrs) {
68
74
  this.prefix = attrs["prefix"];
69
75
  this.number = String(attrs["number"] ?? "");
70
76
  this.parts = Array.isArray(attrs["parts"]) ? attrs["parts"] : [];
71
77
  this.originalSeparator = attrs["original_separator"];
78
+ this.originalSeparators = Array.isArray(attrs["original_separators"])
79
+ ? attrs["original_separators"]
80
+ : undefined;
72
81
  }
73
82
  static parse(codeStr) {
74
83
  if (codeStr === "")
@@ -83,14 +92,37 @@ export class IeeeCode {
83
92
  separator = "-";
84
93
  const components = remainder.split(/[.-]/);
85
94
  const number = components.shift() ?? "";
95
+ // The separators AS PRINTED, one per part boundary — a code may mix
96
+ // them ("7.4-3-2": dot after the number, dashes before the
97
+ // subparts), and to_s re-joins with these instead of the uniform
98
+ // original_separator when they were captured.
99
+ const separators = remainder.match(/[.-]/g) ?? [];
100
+ const perPart = separators.length === components.length ? separators : undefined;
86
101
  return new IeeeCode({
87
102
  prefix,
88
103
  number,
89
104
  parts: components,
90
105
  original_separator: separator,
106
+ original_separators: perPart,
91
107
  });
92
108
  }
109
+ /** The printed face. The reference's Components::Code#to_s keeps the
110
+ * separator each part was printed with ("7.4-3-2"), and the corpus
111
+ * canonically pins the printed spelling (the project-draft rows freeze
112
+ * "IEEE P7-4.3.2/D2" with the dashed spelling as the input alias), so
113
+ * this face renders per-part like the Ruby reference. */
93
114
  render() {
115
+ return this.renderCode(true);
116
+ }
117
+ /** The canonical face: one uniform separator across every part
118
+ * boundary (the first distinct one the source printed) — the face the
119
+ * URN segment, the joint IEEE/ISO faces and the reassembled PSI code
120
+ * render, so spellings that differ only in part separators share one
121
+ * identity. */
122
+ canonicalRender() {
123
+ return this.renderCode(false);
124
+ }
125
+ renderCode(perPart) {
94
126
  let result = "";
95
127
  if (this.prefix !== undefined)
96
128
  result += this.prefix;
@@ -111,8 +143,14 @@ export class IeeeCode {
111
143
  separator = ".";
112
144
  }
113
145
  }
114
- for (const part of this.parts)
115
- result += separator + part;
146
+ for (const [idx, part] of this.parts.entries()) {
147
+ // A fresh parse of mixed-separator codes ("7.4-3-2") carries the
148
+ // separator each part was printed with; the canonical face (and
149
+ // an unknown per-part spelling — the serialized column shape)
150
+ // falls back to the uniform original separator.
151
+ const sep = (perPart && this.originalSeparators?.[idx]) || separator;
152
+ result += sep + part;
153
+ }
116
154
  }
117
155
  return result;
118
156
  }
@@ -764,7 +802,10 @@ class IeeeUrnGenerator extends BaseUrnGenerator {
764
802
  ? ["ieee_identifier", "primary_identifier", "first_identifier", "base"]
765
803
  : ["ieee_identifier", "primary_identifier", "first_identifier"];
766
804
  const wrapped = fallbacks.map((k) => self[k]).find((v) => v instanceof IeeeIdentifier);
767
- return (wrapped ?? this.identifier).codeObj?.render();
805
+ // The URN is spelling-stable: codes printed with mixed part
806
+ // separators ("7.4-3-2") share one identity with their uniform
807
+ // spelling, so the segment renders canonically, not as printed.
808
+ return (wrapped ?? this.identifier).codeObj?.canonicalRender();
768
809
  }
769
810
  yearComponent() {
770
811
  const id = this.identifier;
@@ -858,12 +899,19 @@ function ieeeClass(spec, opts = {}) {
858
899
  return (opts.render ?? renderIeeeBase)(this);
859
900
  }
860
901
  get codeObj() {
902
+ // The parse path carries the freshly parsed Code (with its per-part
903
+ // separator spelling — the reference's `code:` string →
904
+ // Components::Code.parse in base #initialize); only the from_hash
905
+ // rebuild falls back to the split columns (uniform separator).
906
+ const self = this;
907
+ const runtime = self["__code_obj"];
908
+ if (runtime instanceof IeeeCode)
909
+ return runtime;
861
910
  // Only CodeNumber leaves carry code columns; on a supplement the
862
911
  // `number` attribute is the ORDINAL (Ruby SupplementIdentifier#code
863
912
  // delegates to base, code_obj stays nil).
864
913
  if (opts.codeColumns !== true)
865
914
  return undefined;
866
- const self = this;
867
915
  const number = self["number"];
868
916
  if (typeof number !== "string" || number === "")
869
917
  return undefined;
@@ -927,7 +975,9 @@ export const SiStandardClass = ieeeClass({ kind: "si-standard" }, {
927
975
  const isPsi = (id.typed_stage?.abbr ?? []).includes("PSI");
928
976
  parts.push(isPsi ? "PSI" : "SI");
929
977
  const codeObj = id.codeObj;
930
- let codePart = codeObj?.render() ?? "";
978
+ // Canonical code: the PSI code is reassembled from parse-tree
979
+ // captures (synthetic separators), so the face spells uniformly.
980
+ let codePart = codeObj?.canonicalRender() ?? "";
931
981
  const draftObj = id.draftObject();
932
982
  if (draftObj !== undefined)
933
983
  codePart += `/D${draftObj.version}`;
@@ -1166,7 +1216,10 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1166
1216
  parts.push(publishers.join("/"));
1167
1217
  if (stageWord !== "")
1168
1218
  parts.push(stageWord);
1169
- let codeStr = codeObj?.render() ?? "";
1219
+ // Canonical (uniform-separator) code: a joint document parses
1220
+ // from both the dotted IEEE face and the dashed IEC face, and
1221
+ // both spellings must render — and URN — identically.
1222
+ let codeStr = codeObj?.canonicalRender() ?? "";
1170
1223
  // A P-prefixed joint project prints its parts dot-joined on the
1171
1224
  // ISO face ("ISO/IEEE DIS P15500.1"), whatever separator the
1172
1225
  // parse captured ("IEC/IEEE FDIS P60214-2-2018(E)" aliases in).
@@ -1214,8 +1267,9 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1214
1267
  if (draftStatus !== undefined)
1215
1268
  parts.push(draftStatus);
1216
1269
  // The P-state prints as spelled (P = project draft; no P =
1217
- // standard). Never added or stripped.
1218
- let codeStr = codeObj?.render() ?? "";
1270
+ // standard). Never added or stripped. Canonical code for the same
1271
+ // both-faces reason as the ISO face above.
1272
+ let codeStr = codeObj?.canonicalRender() ?? "";
1219
1273
  let designatorCarriesYear = false;
1220
1274
  if (ieeeDraft !== undefined && ieeeDraft.startsWith("D=") && id.year !== undefined) {
1221
1275
  // The ordinal-less stage draft's canonical face (the
@@ -503,6 +503,9 @@ export function preprocessIeee(input) {
503
503
  cleaned = cleaned.replaceAll("Edn.", "Edition");
504
504
  cleaned = cleaned.replaceAll("ASHARE", "ASHRAE");
505
505
  cleaned = cleaned.replace(/^PC(\d)/, "P$1");
506
+ // A two-digit draft designator spelled without its D ("IEEE P1052/08,
507
+ // September 2018"): the corpus face is the D ordinal ("…/D08").
508
+ cleaned = cleaned.replace(/^IEEE\s+(P[\d.-]+)\/(\d{2}),\s+([A-Za-z]+)\s+(\d{4})$/, "IEEE $1/D$2, $3 $4");
506
509
  cleaned = cleaned.replace(/^(IEEE\s+C?\d[\d.]*\/D\d+)([A-Z][a-z]+\s+\d{4})/, "$1, $2");
507
510
  cleaned = cleaned.replace(/^(ANSI\/IEEE Std):\s+.*$/, "$1");
508
511
  cleaned = cleaned.replace(/^(IEEE\s+[\d.]+)\s+(IEC\s+\d+[-\d]*\s+.*edition\s+\d{4}-\d{2})$/i, "$1; $2");
@@ -112,7 +112,7 @@ export const UPDATE_CODES = [
112
112
  { plain: "ISO/IEC13210: 1994 (E) ANSI/IEEE Std 1003.3-1991", replacement: "ISO/IEC 13210:1994 (E); ANSI/IEEE Std 1003.3-1991" },
113
113
  { plain: "J-STD-016-1995", replacement: "IEEE 016-1995" },
114
114
  { plain: "Nuclear EQ Sourcebook and Supplement", replacement: "IEEE 7438946" },
115
- { plain: "PC37.30.2/D043 Rev 18, May 2015", replacement: "IEEE PC37.30-2/D-043/R-18-2015" },
115
+ { plain: "PC37.30.2/D043 Rev 18, May 2015", replacement: "IEEE PC37.30.2/D-043/R-18-2015" },
116
116
  { plain: "IEEE/IEC P62582 CD2 proposal, May 2017", replacement: "IEEE/IEC CD2 P62582-2017-05" },
117
117
  { plain: "ISO/IEC/IEEE P16326:201x WD.4a, July 2017", replacement: "ISO/IEC/IEEE P16326:2017/D=WD.4a" },
118
118
  { plain: "ISO/IEC/IEEE CD.1 P21839, October 2017", replacement: "ISO/IEC/IEEE CD1 P21839-2017-10" },
@@ -20,54 +20,150 @@ function buildRules() {
20
20
  const dash = str("-");
21
21
  const dot = str(".");
22
22
  const digits = match("[0-9]").repeat(1, Infinity);
23
- rule("series", () => str("S").or(str("P"), str("M"), str("B"), str("C")).as("type"));
24
- rule("number_suffix", () => str(":").then(digits).or(str("/").then(digits)).or(dash.then(digits)).or(match("[A-Z]")));
23
+ const letters = match("[A-Za-z]").repeat(0, Infinity);
24
+ const seriesLetter = str("S").or(str("P"), str("M"), str("B"), str("C"));
25
+ const year4 = str("19").or(str("20")).then(match("[0-9]").repeat(2));
26
+ rule("series", () => seriesLetter.as("type"));
27
+ rule("number_suffix", () => str(":").then(digits));
25
28
  rule("number", () => digits.then(rules["number_suffix"].maybe()).as("number"));
29
+ // The compact part face, hyphenated onto the code: S-5-B (lettered),
30
+ // S-100-1 (numbered) and S-158-S-102 (series reference).
31
+ rule("compact_part", () => dash.then(seriesLetter
32
+ .then(dash, digits)
33
+ .or(digits.then(letters), match("[A-Z]"))
34
+ .as("part")));
35
+ // LEGACY glued lettered bases (S-5A): parse-only alias, rendered as
36
+ // "Part A".
37
+ rule("glued", () => match("[A-Z]").as("part"));
38
+ rule("edition", () => space.then(digits.then(dot, digits, dot, digits).as("edition")));
39
+ rule("v_edition", () => space.then(str("v").then(digits.then(str(".").then(digits).maybe()).as("edition_shorthand"))));
40
+ rule("year_qualifier", () => space.then(str("(").then(year4.as("year"), str(")"))));
41
+ rule("part_series_ref", () => seriesLetter.then(dash, digits));
42
+ rule("part_value", () => rules["part_series_ref"].or(digits.then(letters), match("[A-Z]")).as("part"));
43
+ rule("part_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("part_edition")));
44
+ rule("part_year_rule", () => space.then(str("(").then(year4.as("part_year"), str(")"))));
45
+ rule("part_dated", () => rules["part_edition_rule"].then(rules["part_year_rule"].maybe()));
46
+ rule("part", () => space.then(str("Part")).then(space, rules["part_value"], rules["part_dated"].maybe()));
47
+ rule("supplement_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("supplement_edition")));
48
+ rule("supplement_year_rule", () => space.then(str("(").then(year4.as("supplement_year"), str(")"))));
49
+ rule("supplement_dated", () => rules["supplement_edition_rule"].then(rules["supplement_year_rule"].maybe()));
50
+ rule("supplement", () => space
51
+ .then(str("Suppl").or(str("Sup")))
52
+ .then(space, digits.as("supplement"), rules["supplement_dated"].maybe()));
53
+ rule("annex_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("annex_edition")));
54
+ rule("annex_year_rule", () => space.then(str("(").then(year4.as("annex_year"), str(")"))));
55
+ rule("annex_dated", () => rules["annex_edition_rule"].then(rules["annex_year_rule"].maybe()));
56
+ rule("annex", () => space.then(str("Annex")).then(space, match("[A-Z]").as("annex"), rules["annex_dated"].maybe()));
57
+ rule("appendix_value", () => match("[A-Z]").or(digits).as("appendix"));
58
+ rule("appendix_subnumber", () => dash.then(digits.as("appendix_subnumber")));
59
+ rule("appendix_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("appendix_edition")));
60
+ rule("appendix_year_rule", () => space.then(str("(").then(year4.as("appendix_year"), str(")"))));
61
+ rule("appendix_dated", () => rules["appendix_edition_rule"].then(rules["appendix_year_rule"].maybe()));
26
62
  rule("appendix", () => space
27
- .then(str("Appendix").or(str("Ap.")))
28
- .then(space, match("[A-Z]").then(dash, digits).or(digits).or(match("[A-Z]")).as("appendix")));
29
- rule("part", () => space.then(str("Part")).then(space, digits.then(match("[a-zA-Z]").repeat(0, Infinity)).as("part").or(match("[A-Z]").as("part"))));
30
- rule("annex", () => space.then(str("Annex")).then(space, match("[A-Z]").as("annex")));
31
- rule("supplement", () => space.then(str("Suppl")).then(space, digits.as("supplement")));
32
- rule("version", () => space.then(digits.then(dot, digits, dot, digits).as("version")));
63
+ .then(str("Appendix").or(str("App"), str("Ap.")))
64
+ .then(space, rules["appendix_value"], rules["appendix_subnumber"].maybe(), rules["appendix_dated"].maybe()));
65
+ rule("volume_issue", () => space
66
+ .then(str("Issue"))
67
+ .then(space, digits.as("issue"))
68
+ .or(space.then(str("("), digits.as("issue"), str(")"))));
69
+ rule("volume_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("volume_edition")));
70
+ rule("volume_year_rule", () => space.then(str("(").then(year4.as("volume_year"), str(")"))));
71
+ rule("volume_dated", () => rules["volume_edition_rule"].then(rules["volume_year_rule"].maybe()));
72
+ rule("volume", () => space.then(str("Vol.")).then(space, digits.as("volume"), rules["volume_issue"].maybe()));
73
+ // The volume's short face — the slash chain ("IHO P-1/21").
74
+ rule("volume_short", () => str("/").then(digits.as("volume"), rules["volume_dated"].maybe()));
75
+ // LEGACY bare year ("IHO P-5 2024"): the yearbook year IS a volume.
76
+ rule("volume_bare_year", () => space.then(year4.as("volume")));
77
+ rule("serial", () => rules["volume"].or(rules["volume_bare_year"]).then(rules["volume_dated"].maybe()));
78
+ rule("dated", () => rules["edition"].or(rules["v_edition"]).then(rules["year_qualifier"].maybe()));
33
79
  rule("identifier", () => str("IHO")
34
80
  .then(space)
35
81
  .maybe()
36
- .then(rules["series"], dash, rules["number"])
37
- .then(rules["appendix"].maybe())
38
- .then(rules["part"].maybe())
39
- .then(rules["annex"].maybe())
40
- .then(rules["supplement"].maybe())
41
- .then(rules["version"].maybe()));
82
+ .then(rules["series"], dash, rules["number"], rules["compact_part"].maybe(), rules["glued"].maybe(), rules["volume_short"].maybe(), rules["dated"].maybe(), rules["supplement"].maybe(), rules["part"].maybe(), rules["annex"].maybe(), rules["appendix"].maybe(), rules["serial"].maybe(), rules["year_qualifier"].maybe()));
42
83
  rule("root", () => rules["identifier"]);
43
84
  return rules;
44
85
  }
45
86
  export const ihoGrammar = { rules: buildRules(), root: "root" };
87
+ function padEditionShorthand(value) {
88
+ const segments = value.split(".");
89
+ while (segments.length < 3)
90
+ segments.push("0");
91
+ return segments.join(".");
92
+ }
46
93
  function ihoClass(kind) {
47
94
  const letter = KIND_LETTERS[kind];
48
95
  class IhoKindIdentifier extends BaseIdentifier {
49
96
  static polymorphicName = `pubid:iho:${kind}`;
50
97
  static attributes = extendAttributes(BaseIdentifier, {
51
98
  number: { type: "string" },
52
- appendix: { type: "string" },
53
99
  part: { type: "string" },
100
+ part_edition: { type: "string" },
101
+ part_year: { type: "string" },
54
102
  annex: { type: "string" },
103
+ annex_edition: { type: "string" },
104
+ annex_year: { type: "string" },
105
+ appendix: { type: "string" },
106
+ appendix_subnumber: { type: "string" },
107
+ appendix_edition: { type: "string" },
108
+ appendix_year: { type: "string" },
55
109
  supplement: { type: "string" },
56
- version: { type: "string" },
110
+ supplement_edition: { type: "string" },
111
+ supplement_year: { type: "string" },
112
+ edition: { type: "string" },
113
+ year: { type: "string" },
114
+ volume: { type: "string" },
115
+ volume_edition: { type: "string" },
116
+ volume_year: { type: "string" },
117
+ issue: { type: "string" },
57
118
  });
58
- static mappings = keyValue({ wire: "number", to: "number" }, { wire: "appendix", to: "appendix" }, { wire: "part", to: "part" }, { wire: "annex", to: "annex" }, { wire: "supplement", to: "supplement" }, { wire: "version", to: "version" });
119
+ static mappings = keyValue({ wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "part_edition", to: "part_edition" }, { wire: "part_year", to: "part_year" }, { wire: "annex", to: "annex" }, { wire: "annex_edition", to: "annex_edition" }, { wire: "annex_year", to: "annex_year" }, { wire: "appendix", to: "appendix" }, { wire: "appendix_subnumber", to: "appendix_subnumber" }, { wire: "appendix_edition", to: "appendix_edition" }, { wire: "appendix_year", to: "appendix_year" }, { wire: "supplement", to: "supplement" }, { wire: "supplement_edition", to: "supplement_edition" }, { wire: "supplement_year", to: "supplement_year" }, { wire: "edition", to: "edition" }, { wire: "year", to: "year" }, { wire: "volume", to: "volume" }, { wire: "volume_edition", to: "volume_edition" }, { wire: "volume_year", to: "volume_year" }, { wire: "issue", to: "issue" });
59
120
  render() {
60
121
  let result = `IHO ${letter}-${this.number}`;
61
- if (this.appendix !== undefined)
62
- result += ` Ap. ${this.appendix}`;
63
- if (this.part !== undefined)
122
+ if (this.edition !== undefined)
123
+ result += ` ${this.edition}`;
124
+ if (this.supplement !== undefined) {
125
+ result += ` Sup ${this.supplement}`;
126
+ if (this.supplement_edition !== undefined)
127
+ result += ` ${this.supplement_edition}`;
128
+ if (this.supplement_year !== undefined)
129
+ result += ` (${this.supplement_year})`;
130
+ }
131
+ if (this.part !== undefined) {
64
132
  result += ` Part ${this.part}`;
65
- if (this.annex !== undefined)
133
+ if (this.part_edition !== undefined)
134
+ result += ` ${this.part_edition}`;
135
+ if (this.part_year !== undefined)
136
+ result += ` (${this.part_year})`;
137
+ }
138
+ if (this.annex !== undefined) {
66
139
  result += ` Annex ${this.annex}`;
67
- if (this.supplement !== undefined)
68
- result += ` Suppl ${this.supplement}`;
69
- if (this.version !== undefined)
70
- result += ` ${this.version}`;
140
+ if (this.annex_edition !== undefined)
141
+ result += ` ${this.annex_edition}`;
142
+ if (this.annex_year !== undefined)
143
+ result += ` (${this.annex_year})`;
144
+ }
145
+ if (this.appendix !== undefined) {
146
+ result += ` Appendix ${this.appendix}`;
147
+ if (this.appendix_subnumber !== undefined)
148
+ result += `-${this.appendix_subnumber}`;
149
+ if (this.appendix_edition !== undefined)
150
+ result += ` ${this.appendix_edition}`;
151
+ if (this.appendix_year !== undefined)
152
+ result += ` (${this.appendix_year})`;
153
+ }
154
+ if (this.volume !== undefined) {
155
+ result += ` Vol. ${this.volume}`;
156
+ if (this.issue !== undefined)
157
+ result += ` (${this.issue})`;
158
+ if (this.volume_edition !== undefined)
159
+ result += ` ${this.volume_edition}`;
160
+ if (this.volume_year !== undefined)
161
+ result += ` (${this.volume_year})`;
162
+ }
163
+ // The base year qualifier prints last, parenthesized ("IHO
164
+ // S-100 1.0.0 Part A (2024)").
165
+ if (this.year !== undefined)
166
+ result += ` (${this.year})`;
71
167
  return result;
72
168
  }
73
169
  }
@@ -75,16 +171,54 @@ function ihoClass(kind) {
75
171
  generate() {
76
172
  const id = this.identifier;
77
173
  const parts = ["urn", "iho", letter.toLowerCase(), id.number];
78
- if (id.appendix !== undefined)
79
- parts.push(`ap.${id.appendix}`);
80
- if (id.part !== undefined)
81
- parts.push(`part.${id.part}`);
82
- if (id.annex !== undefined)
83
- parts.push(`annex.${id.annex}`);
84
- if (id.supplement !== undefined)
85
- parts.push(`suppl.${id.supplement}`);
86
- if (id.version !== undefined)
87
- parts.push(id.version);
174
+ // Each component is a marker-prefixed segment; its own edition
175
+ // (ed-) and year (year-) follow it directly. The base edition
176
+ // and year come first (they attach to the base node).
177
+ if (id.edition !== undefined)
178
+ parts.push(`ed-${id.edition}`);
179
+ if (id.year !== undefined)
180
+ parts.push(`year-${id.year}`);
181
+ if (id.supplement !== undefined) {
182
+ parts.push(`sup-${id.supplement}`);
183
+ if (id.supplement_edition !== undefined)
184
+ parts.push(`ed-${id.supplement_edition}`);
185
+ if (id.supplement_year !== undefined)
186
+ parts.push(`year-${id.supplement_year}`);
187
+ }
188
+ if (id.part !== undefined) {
189
+ parts.push(`part-${id.part}`);
190
+ if (id.part_edition !== undefined)
191
+ parts.push(`ed-${id.part_edition}`);
192
+ if (id.part_year !== undefined)
193
+ parts.push(`year-${id.part_year}`);
194
+ }
195
+ if (id.annex !== undefined) {
196
+ parts.push(`annex-${id.annex}`);
197
+ if (id.annex_edition !== undefined)
198
+ parts.push(`ed-${id.annex_edition}`);
199
+ if (id.annex_year !== undefined)
200
+ parts.push(`year-${id.annex_year}`);
201
+ }
202
+ if (id.appendix !== undefined) {
203
+ parts.push(`appendix-${id.appendix}`);
204
+ // The subnumber is a bare child segment directly after the
205
+ // appendix ("appendix-D:2").
206
+ if (id.appendix_subnumber !== undefined)
207
+ parts.push(id.appendix_subnumber);
208
+ if (id.appendix_edition !== undefined)
209
+ parts.push(`ed-${id.appendix_edition}`);
210
+ if (id.appendix_year !== undefined)
211
+ parts.push(`year-${id.appendix_year}`);
212
+ }
213
+ if (id.volume !== undefined) {
214
+ parts.push(`vol-${id.volume}`);
215
+ if (id.issue !== undefined)
216
+ parts.push(`issue-${id.issue}`);
217
+ if (id.volume_edition !== undefined)
218
+ parts.push(`ed-${id.volume_edition}`);
219
+ if (id.volume_year !== undefined)
220
+ parts.push(`year-${id.volume_year}`);
221
+ }
88
222
  return parts.join(":");
89
223
  }
90
224
  }
@@ -111,6 +245,35 @@ class IhoBuilder extends BaseBuilder {
111
245
  defaultIdentifierClass() {
112
246
  return KIND_CLASSES["standard"];
113
247
  }
248
+ build(data) {
249
+ const flat = Array.isArray(data)
250
+ ? Object.assign({}, ...data)
251
+ : { ...data };
252
+ // A component year without its component edition attaches to the
253
+ // nearest preceding edition node — the base ("IHO S-100 1.0.0
254
+ // Part A (2024)" binds the base year, never part_year). With the
255
+ // component's own edition present it stays component-scoped
256
+ // ("IHO S-98 Part A 1.0.0 (2024)" keeps part_year).
257
+ for (const component of ["part", "supplement", "annex", "appendix", "volume"]) {
258
+ const yearKey = `${component}_year`;
259
+ if (flat[yearKey] !== undefined &&
260
+ flat[`${component}_edition`] === undefined &&
261
+ flat["edition"] !== undefined) {
262
+ if (flat["year"] === undefined)
263
+ flat["year"] = flat[yearKey];
264
+ delete flat[yearKey];
265
+ }
266
+ }
267
+ return super.build(flat);
268
+ }
269
+ cast(key, value) {
270
+ if (key === "type")
271
+ return null; // the kind is encoded in _type
272
+ if (key === "edition_shorthand") {
273
+ return { edition: padEditionShorthand(String(value)) };
274
+ }
275
+ return value;
276
+ }
114
277
  }
115
278
  export function ihoGrammarImplementation() {
116
279
  const builder = new IhoBuilder();
@@ -11,6 +11,10 @@ import { BaseUrnGenerator } from "../../model/urn-generator.js";
11
11
  * code ("B 0205-1") lives in the series/number/parts columns and is
12
12
  * composed on demand. URN: urn:jis:<code>[:year][:lang]["all"]? with a
13
13
  * supplement notation appended lowercased.
14
+ *
15
+ * WES (welding-engineering-standard, pubid#638) rides the same flavor:
16
+ * "WES <number>[-parts][:year[R]]…" with no series letter and no JIS
17
+ * prefix — the code itself carries the "WES" root.
14
18
  */
15
19
  function buildRules() {
16
20
  const rules = {};
@@ -55,7 +59,19 @@ function buildRules() {
55
59
  .then(rules["all_parts"].maybe())
56
60
  .then(rules["supplement"].maybe())
57
61
  .then(rules["symbol_clause"].maybe()));
58
- rule("root", () => rules["identifier"]);
62
+ // WES (Japan Welding Engineering Society) rides the JIS flavor with the
63
+ // same tail but no prefix and no series letter; the root tries it first,
64
+ // as the reference grammar does.
65
+ rule("wes_identifier", () => str("WES").as("wes")
66
+ .then(space)
67
+ .then(rules["number"])
68
+ .then(rules["parts"])
69
+ .then(rules["year"].maybe())
70
+ .then(rules["language"].maybe())
71
+ .then(rules["all_parts"].maybe())
72
+ .then(rules["supplement"].maybe())
73
+ .then(rules["symbol_clause"].maybe()));
74
+ rule("root", () => rules["wes_identifier"].or(rules["identifier"]));
59
75
  return rules;
60
76
  }
61
77
  export const jisGrammar = { rules: buildRules(), root: "root" };
@@ -151,6 +167,45 @@ const KIND_CLASSES = {
151
167
  corrigendum: jisClass("corrigendum"),
152
168
  explanation: jisClass("explanation"),
153
169
  };
170
+ // WES 7700-1:2019 — JSA sells Welding Engineering Standards next to JIS,
171
+ // so they parse through the JIS flavor, but the id carries no series
172
+ // letter and no JIS prefix ("WES" is the root of the designation).
173
+ function wesClass() {
174
+ class JisWesIdentifier extends JisIdentifier {
175
+ static polymorphicName = "pubid:jis:welding-engineering-standard";
176
+ static attributes = extendAttributes(BaseIdentifier, {
177
+ number: { type: "string" },
178
+ parts: { type: "string", collection: true },
179
+ year: { type: "integer" },
180
+ language: { type: "string" },
181
+ all_parts: { type: "boolean", default: false },
182
+ reaffirmed: { type: "boolean" },
183
+ symbol: { type: "string" },
184
+ });
185
+ static mappings = keyValue({ wire: "number", to: "number" }, { wire: "parts", to: "parts" }, { wire: "year", to: "year" }, { wire: "language", to: "language" }, { wire: "all_parts", to: "all_parts" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "symbol", to: "symbol" });
186
+ code() {
187
+ if (this.base !== undefined)
188
+ return this.base.code();
189
+ let result = `WES ${this.number}`;
190
+ if (this.parts !== undefined)
191
+ result += this.parts.map((p) => `-${p}`).join("");
192
+ return result;
193
+ }
194
+ render() {
195
+ let result = this.code() ?? "";
196
+ if (this.year !== undefined)
197
+ result += `:${this.yearWithReaffirmation()}`;
198
+ if (this.language !== undefined)
199
+ result += `(${this.language})`;
200
+ if (this.all_parts === true)
201
+ result += "(規格群)";
202
+ return result + this.symbolSuffix();
203
+ }
204
+ }
205
+ registerType(JisWesIdentifier);
206
+ return JisWesIdentifier;
207
+ }
208
+ const WES_CLASS = wesClass();
154
209
  class JisUrnGenerator extends BaseUrnGenerator {
155
210
  generate() {
156
211
  const id = this.identifier;
@@ -171,7 +226,7 @@ class JisUrnGenerator extends BaseUrnGenerator {
171
226
  return parts.join(":");
172
227
  }
173
228
  }
174
- for (const klass of Object.values(KIND_CLASSES)) {
229
+ for (const klass of [...Object.values(KIND_CLASSES), WES_CLASS]) {
175
230
  klass.urnGenerator = JisUrnGenerator;
176
231
  }
177
232
  function isObj(v) {
@@ -213,6 +268,8 @@ class JisBuilder {
213
268
  return out;
214
269
  }
215
270
  buildSingle(tree) {
271
+ if (tree["wes"] !== undefined)
272
+ return this.buildWes(tree);
216
273
  const type = tree["type"] === undefined || tree["type"] === null ? undefined : String(tree["type"]);
217
274
  const kind = type === "TR" ? "technical-report" : type === "TS" ? "technical-specification" : "japanese-industrial-standard";
218
275
  const attrs = {
@@ -230,6 +287,21 @@ class JisBuilder {
230
287
  attrs["reaffirmed"] = true;
231
288
  return new KIND_CLASSES[kind](attrs);
232
289
  }
290
+ buildWes(tree) {
291
+ const attrs = {
292
+ number: String(tree["number"]),
293
+ parts: extractParts(tree["parts"]),
294
+ };
295
+ if (tree["year"] !== undefined && tree["year"] !== null)
296
+ attrs["year"] = Number(tree["year"]);
297
+ if (tree["language"] !== undefined && tree["language"] !== null)
298
+ attrs["language"] = String(tree["language"]);
299
+ if (tree["all_parts"] !== undefined)
300
+ attrs["all_parts"] = true;
301
+ if (tree["reaffirmed"] !== undefined && tree["reaffirmed"] !== null)
302
+ attrs["reaffirmed"] = true;
303
+ return new WES_CLASS(attrs);
304
+ }
233
305
  buildSupplement(tree, kind, key, prefix) {
234
306
  const supp = isObj(tree[key]) ? tree[key] : {};
235
307
  const base = this.buildSingle(this.baseData(tree, key));