@pubid/pubid 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/artifact/corpus.json.gz +0 -0
- package/dist/flavors/bipm/index.js +132 -28
- package/dist/flavors/ecma/index.js +35 -1
- package/dist/flavors/iec/grammar.js +39 -2
- package/dist/flavors/iec/index.js +110 -0
- package/dist/flavors/iec/model.js +29 -4
- package/dist/flavors/iho/index.js +126 -5
- package/dist/flavors/iso/grammar.d.ts +5 -0
- package/dist/flavors/iso/grammar.js +68 -1
- package/dist/flavors/iso/index.js +65 -1
- package/dist/flavors/iso/model.js +7 -1
- package/dist/flavors/iso/stages.generated.js +1 -1
- package/dist/flavors/nist/model.js +18 -2
- package/dist/flavors/nist/preprocessor.js +11 -0
- package/dist/flavors/plateau/index.js +21 -2
- package/dist/pubid-urn/schemas/iho.js +3 -0
- package/dist/pubid-urn/schemas/plateau.js +1 -0
- package/package.json +2 -2
- package/vendor/parg/bipm.json +1 -1
- package/vendor/parg/iec.json +1 -1
- package/vendor/parg/iso.json +1 -1
|
@@ -43,7 +43,28 @@ function buildRules() {
|
|
|
43
43
|
rule("part_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("part_edition")));
|
|
44
44
|
rule("part_year_rule", () => space.then(str("(").then(year4.as("part_year"), str(")"))));
|
|
45
45
|
rule("part_dated", () => rules["part_edition_rule"].then(rules["part_year_rule"].maybe()));
|
|
46
|
-
|
|
46
|
+
// --- S-4 loose-leaf sections and specifications (issue #691) ---
|
|
47
|
+
// The spec number's vocabulary: the S-4 sections run 100-600 (Part C
|
|
48
|
+
// 100-500), so the leaf number is 3 digits with hundreds digit 1-6 —
|
|
49
|
+
// the same shape the bare citation branch leans on to keep the B
|
|
50
|
+
// Bibliographic series (B-1..B-13 style) distinct from B-4xx specs.
|
|
51
|
+
const specShape = match("[1-6]").then(match("[0-9]").repeat(2, 2));
|
|
52
|
+
// The citation letter is the spec's part (A/B/C); the builder binds
|
|
53
|
+
// it into `part` and rejects a conflicting Part-rule capture.
|
|
54
|
+
rule("spec_citation_body", () => str("A").or(str("B"), str("C")).as("spec_part")
|
|
55
|
+
.then(dash, specShape.as("spec"))
|
|
56
|
+
.then(dot.then(digits.as("spec_sub")).maybe())
|
|
57
|
+
.then(str("(").then(match("[A-Z]").repeat(1, Infinity).as("spec_variant"), str(")")).maybe()));
|
|
58
|
+
// The compact anchor form: the citation follows the code after one
|
|
59
|
+
// space ("IHO S-4 B-401").
|
|
60
|
+
rule("spec_citation", () => space.then(rules["spec_citation_body"]));
|
|
61
|
+
// The spelled section long form: "Part B, Section 400" — the spelled
|
|
62
|
+
// number becomes the spec (the section-level citation), and a
|
|
63
|
+
// trailing citation (", Section 400, B-401") makes it a cross-check
|
|
64
|
+
// only.
|
|
65
|
+
rule("part_section_tail", () => str(",").then(space, str("Section"), space, specShape.as("spec_section"))
|
|
66
|
+
.then(str(",").then(space, rules["spec_citation_body"]).maybe()));
|
|
67
|
+
rule("part", () => space.then(str("Part")).then(space, rules["part_value"], rules["part_dated"].maybe(), rules["part_section_tail"].maybe()));
|
|
47
68
|
rule("supplement_edition_rule", () => space.then(digits.then(dot, digits, dot, digits).as("supplement_edition")));
|
|
48
69
|
rule("supplement_year_rule", () => space.then(str("(").then(year4.as("supplement_year"), str(")"))));
|
|
49
70
|
rule("supplement_dated", () => rules["supplement_edition_rule"].then(rules["supplement_year_rule"].maybe()));
|
|
@@ -79,8 +100,16 @@ function buildRules() {
|
|
|
79
100
|
rule("identifier", () => str("IHO")
|
|
80
101
|
.then(space)
|
|
81
102
|
.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()));
|
|
83
|
-
|
|
103
|
+
.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["spec_citation"].maybe(), rules["annex"].maybe(), rules["appendix"].maybe(), rules["serial"].maybe(), rules["year_qualifier"].maybe()));
|
|
104
|
+
// The bare citation branch (the entry's FIRST alternative): "B-401" /
|
|
105
|
+
// "IHO B-401" — implicit S-4 anchor (the builder synthesizes series S
|
|
106
|
+
// number 4). B only: bare C-x stays the Circular Letter series, and
|
|
107
|
+
// the 3-digit [1-6]xx shape keeps the B Bibliographic series (B-12)
|
|
108
|
+
// on the main branch.
|
|
109
|
+
rule("bare_spec_citation", () => str("IHO").then(space).maybe()
|
|
110
|
+
.then(str("B").as("spec_part"), dash, specShape.as("spec"), dot.then(digits.as("spec_sub")).maybe(), str("(").then(match("[A-Z]").repeat(1, Infinity).as("spec_variant"), str(")")).maybe()));
|
|
111
|
+
rule("identifier_entry", () => rules["bare_spec_citation"].or(rules["identifier"]));
|
|
112
|
+
rule("root", () => rules["identifier_entry"]);
|
|
84
113
|
return rules;
|
|
85
114
|
}
|
|
86
115
|
export const ihoGrammar = { rules: buildRules(), root: "root" };
|
|
@@ -115,8 +144,15 @@ function ihoClass(kind) {
|
|
|
115
144
|
volume_edition: { type: "string" },
|
|
116
145
|
volume_year: { type: "string" },
|
|
117
146
|
issue: { type: "string" },
|
|
147
|
+
// The S-4 loose-leaf spec citation (issue #691): spec is the leaf
|
|
148
|
+
// number, the dotted sub and the parenthesized variant its
|
|
149
|
+
// children. The citation letter rides `part`; the spelled section
|
|
150
|
+
// is resolve-only (never an atom).
|
|
151
|
+
spec: { type: "string" },
|
|
152
|
+
spec_sub: { type: "string" },
|
|
153
|
+
spec_variant: { type: "string" },
|
|
118
154
|
});
|
|
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" });
|
|
155
|
+
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" }, { wire: "spec", to: "spec" }, { wire: "spec_sub", to: "spec_sub" }, { wire: "spec_variant", to: "spec_variant" });
|
|
120
156
|
render() {
|
|
121
157
|
let result = `IHO ${letter}-${this.number}`;
|
|
122
158
|
if (this.edition !== undefined)
|
|
@@ -128,7 +164,17 @@ function ihoClass(kind) {
|
|
|
128
164
|
if (this.supplement_year !== undefined)
|
|
129
165
|
result += ` (${this.supplement_year})`;
|
|
130
166
|
}
|
|
131
|
-
if (this.
|
|
167
|
+
if (this.spec !== undefined) {
|
|
168
|
+
// The S-4 citation face replaces the Part face (same
|
|
169
|
+
// containment slot: the citation letter IS the part):
|
|
170
|
+
// "B-401", "B-122.1", "B-521(I)".
|
|
171
|
+
result += ` ${this.part}-${this.spec}`;
|
|
172
|
+
if (this.spec_sub !== undefined)
|
|
173
|
+
result += `.${this.spec_sub}`;
|
|
174
|
+
if (this.spec_variant !== undefined)
|
|
175
|
+
result += `(${this.spec_variant})`;
|
|
176
|
+
}
|
|
177
|
+
else if (this.part !== undefined) {
|
|
132
178
|
result += ` Part ${this.part}`;
|
|
133
179
|
if (this.part_edition !== undefined)
|
|
134
180
|
result += ` ${this.part_edition}`;
|
|
@@ -192,6 +238,17 @@ function ihoClass(kind) {
|
|
|
192
238
|
if (id.part_year !== undefined)
|
|
193
239
|
parts.push(`year-${id.part_year}`);
|
|
194
240
|
}
|
|
241
|
+
// The S-4 spec citation: a child of its part (the citation letter
|
|
242
|
+
// rides the part- atom), the dotted sub and the parenthesized
|
|
243
|
+
// variant its own children. The spelled SECTION is NOT emitted —
|
|
244
|
+
// derivable from the spec's hundreds digit.
|
|
245
|
+
if (id.spec !== undefined) {
|
|
246
|
+
parts.push(`spec-${id.spec}`);
|
|
247
|
+
if (id.spec_sub !== undefined)
|
|
248
|
+
parts.push(`specsub-${id.spec_sub}`);
|
|
249
|
+
if (id.spec_variant !== undefined)
|
|
250
|
+
parts.push(`specvar-${id.spec_variant}`);
|
|
251
|
+
}
|
|
195
252
|
if (id.annex !== undefined) {
|
|
196
253
|
parts.push(`annex-${id.annex}`);
|
|
197
254
|
if (id.annex_edition !== undefined)
|
|
@@ -235,6 +292,18 @@ const KIND_CLASSES = {
|
|
|
235
292
|
};
|
|
236
293
|
const LETTER_TO_CLASS = new Map(Object.keys(KIND_LETTERS).map((kind) => [KIND_LETTERS[kind], KIND_CLASSES[kind]]));
|
|
237
294
|
class IhoBuilder extends BaseBuilder {
|
|
295
|
+
// The S-4 anchor: the loose-leaf publication every spec citation
|
|
296
|
+
// resolves through (bare "B-401" carries it implicitly).
|
|
297
|
+
static S4_TYPE = "S";
|
|
298
|
+
static S4_NUMBER = "4";
|
|
299
|
+
// A citation letter is one of S-4's lettered Parts; the section
|
|
300
|
+
// vocabulary (100-600, Part C 100-500) is theirs.
|
|
301
|
+
static SPEC_LETTERS = ["A", "B", "C"];
|
|
302
|
+
// Components an S-4 spec citation cannot carry: the citation is a
|
|
303
|
+
// leaf of the Part, not a wrappable publication.
|
|
304
|
+
static SPEC_FOREIGN_COMPONENTS = [
|
|
305
|
+
"supplement", "annex", "appendix", "appendix_subnumber", "volume", "issue",
|
|
306
|
+
];
|
|
238
307
|
selectClass(data) {
|
|
239
308
|
const kind = LETTER_TO_CLASS.get(String(data["type"]));
|
|
240
309
|
if (kind === undefined) {
|
|
@@ -264,6 +333,58 @@ class IhoBuilder extends BaseBuilder {
|
|
|
264
333
|
delete flat[yearKey];
|
|
265
334
|
}
|
|
266
335
|
}
|
|
336
|
+
// The S-4 spec citation resolution (ruby resolve_spec_citation!):
|
|
337
|
+
// the citation letter folds into `part` (a Part-rule capture must
|
|
338
|
+
// agree), and the long form's spelled section becomes the spec
|
|
339
|
+
// alone — beside a citation it only cross-checks the citation's
|
|
340
|
+
// derived section ("Part B, Section 400, B-401" stores spec 401;
|
|
341
|
+
// "Part B, Section 500, B-401" is rejected).
|
|
342
|
+
const letter = flat["spec_part"];
|
|
343
|
+
const spelled = flat["spec_section"];
|
|
344
|
+
delete flat["spec_part"];
|
|
345
|
+
delete flat["spec_section"];
|
|
346
|
+
if (letter !== undefined) {
|
|
347
|
+
if (flat["part"] === undefined || flat["part"] === letter) {
|
|
348
|
+
flat["part"] = letter;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
throw new ParseFailed(`IHO: the citation letter ${String(letter)} conflicts with part ${String(flat["part"])}`, 0);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (spelled !== undefined) {
|
|
355
|
+
if (flat["spec"] === undefined) {
|
|
356
|
+
flat["spec"] = spelled;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const section = `${String(flat["spec"])[0]}00`;
|
|
360
|
+
if (spelled !== section) {
|
|
361
|
+
throw new ParseFailed(`IHO: the spelled section ${String(spelled)} disagrees with the citation ${String(flat["part"])}-${String(flat["spec"])}`, 0);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (flat["spec"] !== undefined) {
|
|
366
|
+
if (!IhoBuilder.SPEC_LETTERS.includes(String(flat["part"]))) {
|
|
367
|
+
throw new ParseFailed(`IHO: an S-4 spec citation needs a lettered Part (A/B/C), not ${JSON.stringify(flat["part"])}`, 0);
|
|
368
|
+
}
|
|
369
|
+
if (flat["part_edition"] !== undefined || flat["part_year"] !== undefined) {
|
|
370
|
+
throw new ParseFailed("IHO: a spec citation rides the S-4 edition, not a part edition", 0);
|
|
371
|
+
}
|
|
372
|
+
const foreign = IhoBuilder.SPEC_FOREIGN_COMPONENTS.filter((k) => flat[k] !== undefined);
|
|
373
|
+
if (foreign.length > 0) {
|
|
374
|
+
throw new ParseFailed(`IHO: an S-4 spec citation is a leaf of its Part and cannot carry ${foreign.join(", ")}`, 0);
|
|
375
|
+
}
|
|
376
|
+
// The series letter: a bare citation branch parse has no series
|
|
377
|
+
// capture, and its anchor is implicitly the S-4 publication (the
|
|
378
|
+
// number binds with it). An explicit series must BE the anchor.
|
|
379
|
+
if (flat["type"] === undefined) {
|
|
380
|
+
flat["type"] = IhoBuilder.S4_TYPE;
|
|
381
|
+
flat["number"] = IhoBuilder.S4_NUMBER;
|
|
382
|
+
}
|
|
383
|
+
else if (flat["type"] !== IhoBuilder.S4_TYPE ||
|
|
384
|
+
flat["number"] !== IhoBuilder.S4_NUMBER) {
|
|
385
|
+
throw new ParseFailed(`IHO: an S-4 spec citation requires the S-4 anchor: ${String(flat["type"])}-${String(flat["number"])} cannot carry spec ${String(flat["spec"])}`, 0);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
267
388
|
return super.build(flat);
|
|
268
389
|
}
|
|
269
390
|
cast(key, value) {
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { Grammar } from "../../grammar/engine.js";
|
|
2
|
+
/** The DAM-family ballot stage codes (grammar table
|
|
3
|
+
* typed_stages_ballots: PDAM, DAM, DAmd, FDAM, FDAmd, FPDAM — exactly
|
|
4
|
+
* these entries' abbrs); the TYPE-first ballot branch stays scoped to
|
|
5
|
+
* the ballot family. */
|
|
6
|
+
export declare const BALLOT_STAGE_CODES: string[];
|
|
2
7
|
export declare const isoGrammar: Grammar;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { match, ref, str } from "../../grammar/engine.js";
|
|
2
2
|
import { stageTokens } from "./model.js";
|
|
3
|
+
import { ISO_TYPED_STAGES } from "./stages.generated.js";
|
|
3
4
|
/**
|
|
4
5
|
* Port of lib/pubid/iso/parser.rb. The identifier alternation order is
|
|
5
6
|
* load-bearing (PEG ordered choice): nsb_iso | tc_document |
|
|
@@ -9,6 +10,11 @@ import { stageTokens } from "./model.js";
|
|
|
9
10
|
* supplement tables exactly like the Ruby TYPED_STAGES constants.
|
|
10
11
|
*/
|
|
11
12
|
const SUPPLEMENT_TYPE_KEYS = ["amd", "cor", "suppl", "ext", "add"];
|
|
13
|
+
/** The DAM-family ballot stage codes (grammar table
|
|
14
|
+
* typed_stages_ballots: PDAM, DAM, DAmd, FDAM, FDAmd, FPDAM — exactly
|
|
15
|
+
* these entries' abbrs); the TYPE-first ballot branch stays scoped to
|
|
16
|
+
* the ballot family. */
|
|
17
|
+
export const BALLOT_STAGE_CODES = ["pdam", "damd", "fdamd", "fpdam"];
|
|
12
18
|
const BASE_STAGE_TOKENS = stageTokens().filter((t) => {
|
|
13
19
|
// Supplement tokens are excluded from the base table by type key, but
|
|
14
20
|
// stageTokens() is type-agnostic here; the split is done via the
|
|
@@ -208,7 +214,68 @@ function buildRules() {
|
|
|
208
214
|
.as("base")
|
|
209
215
|
.then(space.maybe(), str("|"), space.maybe(), rules["idf_identifier"])
|
|
210
216
|
.as("joint_identifier"));
|
|
211
|
-
|
|
217
|
+
// Historic TYPE-first ballot faces (decode-to-modern, owner ruling
|
|
218
|
+
// 2026-11-12): the DAM-family token in TYPE position — "ISO/IEC FDAM 1
|
|
219
|
+
// 14496-12:2004", "ISO/PDAM 19973-2" — and the space-attached ballot
|
|
220
|
+
// face "ISO 15877-5:2009 FDAM 1". Both branches sit LAST in the
|
|
221
|
+
// identifier alternation, so they are purely additive: every face an
|
|
222
|
+
// earlier branch parses keeps its exact parse. The ballot token
|
|
223
|
+
// captures as ballot_type_with_stage; a number on the TYPE token is
|
|
224
|
+
// space-bounded (ballot_number) so "PDAM 19973-2" keeps 19973 in the
|
|
225
|
+
// base; an on-base stamp's number/date capture as ballot_stamp_* (the
|
|
226
|
+
// stamp token itself matches uncaptured — the ballot's stage comes
|
|
227
|
+
// from the TYPE token and the decode keeps no spelling). The builder
|
|
228
|
+
// folds the captures into ONE amendment layer over the whole base, the
|
|
229
|
+
// number taken from the TYPE token, the stamp, or the implied 1.
|
|
230
|
+
const ballotStageTokens = ISO_TYPED_STAGES
|
|
231
|
+
.filter((st) => BALLOT_STAGE_CODES.includes(st.code))
|
|
232
|
+
.flatMap((st) => st.abbr);
|
|
233
|
+
const ballotTypeAlt = literalAlternation(ballotStageTokens).as("ballot_type_with_stage");
|
|
234
|
+
rule("ballot_type_with_stage", () => ballotTypeAlt);
|
|
235
|
+
// An on-base stamp riding a ballot face ("/Amd 9:2001", "/Amd.1",
|
|
236
|
+
// "/Am1:2006"): it carries the amendment's number and ballot date
|
|
237
|
+
// while its own token letters are decode-away (matched uncaptured).
|
|
238
|
+
// The stamp's date captures under its own ballot_stamp_* names —
|
|
239
|
+
// reusing the base date capture would collide with second_part's at
|
|
240
|
+
// the sequence merge and the stamp's year would eat the base's.
|
|
241
|
+
rule("ballot_stamp", () => str("/")
|
|
242
|
+
.then(literalAlternation(supplementTypeTokens))
|
|
243
|
+
.then(space
|
|
244
|
+
.maybe()
|
|
245
|
+
.then(digit.repeat(1, 5).as("ballot_stamp_number"))
|
|
246
|
+
.then(str(":")
|
|
247
|
+
.or(dash)
|
|
248
|
+
.then(yearDigits
|
|
249
|
+
.then(dash.then(monthDigits).maybe(), dash.then(dayDigits).maybe())
|
|
250
|
+
.as("ballot_stamp_date"))
|
|
251
|
+
.or(str(":")
|
|
252
|
+
.or(dash)
|
|
253
|
+
.then(str("--").or(str("—")).as("ballot_stamp_undated_marker")))
|
|
254
|
+
.maybe())
|
|
255
|
+
.or(str(".").then(digits.as("ballot_stamp_dot_number")))));
|
|
256
|
+
// "ISO/IEC FDAM 1 14496-12:2004", "ISO/PDAM 19973-2": the ballot
|
|
257
|
+
// token first, then the base kept whole. The TYPE-token number is
|
|
258
|
+
// space-bounded on both sides — mandatory trailing space — so
|
|
259
|
+
// "PDAM 19973-2" cannot read 19973 as the ballot number: with no
|
|
260
|
+
// second space the group backtracks and 19973 parses in second_part.
|
|
261
|
+
rule("ballot_supplement_identifier", () => rules["prefix_with_copublishers"]
|
|
262
|
+
.then(space.maybe(), str("/").maybe(), ballotTypeAlt, space.then(digit.repeat(1, Infinity).as("ballot_number")).then(space).maybe(), space.maybe(), rules["type_with_stage"].maybe(), space.maybe(), rules["second_part"], rules["ballot_stamp"].maybe()));
|
|
263
|
+
// The space-attached ballot face: "ISO 15877-5:2009 FDAM 1" — the
|
|
264
|
+
// base designation first, then the ballot token attached by a space.
|
|
265
|
+
// The base front is inlined (not identifier_copublishers_no_third):
|
|
266
|
+
// that rule ends in third_part_edition, whose own optional space
|
|
267
|
+
// would consume the separator the ballot token needs. No base
|
|
268
|
+
// edition is served here — none of the stored or owner-cited ballot
|
|
269
|
+
// faces carries one.
|
|
270
|
+
rule("ballot_space_supplement_identifier", () => rules["guide_prefix"]
|
|
271
|
+
.as("type_with_stage_fr")
|
|
272
|
+
.then(space)
|
|
273
|
+
.maybe()
|
|
274
|
+
.then(rules["prefix_with_copublishers"].maybe(), space.maybe(), str("/").maybe(), rules["type_with_stage"].maybe(), space.maybe(), rules["second_part"], space, ballotTypeAlt, space.then(digit.repeat(1, Infinity).as("ballot_number")).maybe()));
|
|
275
|
+
rule("identifier", () => rules["bare_tc_document"].or(rules["nsb_iso_identifier"], rules["tc_document"], rules["directives_identifiers"], rules["jcgm_identifier"], rules["iso_r_supplement_identifier"], rules["iso_r_identifier"], rules["supplement_supplement_supplement_identifier"], rules["supplement_supplement_identifier"], rules["supplement_identifier"], rules["joint_identifier"], rules["identifier_copublishers"],
|
|
276
|
+
// The historic ballot faces go last — purely additive branches
|
|
277
|
+
// (an input an earlier member parses keeps that parse).
|
|
278
|
+
rules["ballot_supplement_identifier"], rules["ballot_space_supplement_identifier"]));
|
|
212
279
|
rule("root", () => rules["identifier"]);
|
|
213
280
|
return rules;
|
|
214
281
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
|
|
2
2
|
import { PubidDate, Language, Iteration } from "../../model/component.js";
|
|
3
|
-
import { isoGrammar } from "./grammar.js";
|
|
3
|
+
import { isoGrammar, BALLOT_STAGE_CODES } from "./grammar.js";
|
|
4
4
|
import { IsoBundledIdentifier, IsoDirectives, IsoDirectivesSupplement, IsoEdition, IsoPublisher, IsoSupplementIdentifier, IsoTcDocument, isoClassForType, locateStage, } from "./model.js";
|
|
5
5
|
import { ISO_TYPED_STAGES } from "./stages.generated.js";
|
|
6
6
|
/**
|
|
@@ -232,6 +232,15 @@ class IsoBuilder {
|
|
|
232
232
|
}
|
|
233
233
|
buildStandard(treeIn) {
|
|
234
234
|
const tree = { ...treeIn };
|
|
235
|
+
// Historic TYPE-first ballot face ("ISO/IEC FDAM 1 14496-12:2004",
|
|
236
|
+
// "ISO/PDAM 19973-2", "ISO 15877-5:2009 FDAM 1"): the ballot_* keys
|
|
237
|
+
// are one amendment layer over the whole base, never a second
|
|
238
|
+
// supplement layer (the stored twins keep one level), so the layer
|
|
239
|
+
// is assembled here, before the supplement branch could read the
|
|
240
|
+
// ballot token as an outer wrapper.
|
|
241
|
+
if (tree["ballot_type_with_stage"] !== undefined) {
|
|
242
|
+
return this.buildBallotSupplement(tree);
|
|
243
|
+
}
|
|
235
244
|
// Legacy ISO/R: publisher ISO + type "R".
|
|
236
245
|
if (tree["iso_r_prefix"] !== undefined) {
|
|
237
246
|
tree["publisher"] = "ISO";
|
|
@@ -280,8 +289,63 @@ class IsoBuilder {
|
|
|
280
289
|
attrs["base"] = this.build(tree["base"]);
|
|
281
290
|
}
|
|
282
291
|
const identifier = new klass(attrs);
|
|
292
|
+
// The owner's no-number rule, applied on every construction path
|
|
293
|
+
// that routes through this builder (parse, URN, bundle members): a
|
|
294
|
+
// DAM-family ballot-stage amendment token with no number anywhere
|
|
295
|
+
// names Amendment 1 ("ISO 19973-2/PDAM", "ISO/IEC PDAM 12207"). The
|
|
296
|
+
// number is identity-bearing, so the default lands at build time and
|
|
297
|
+
// the wire hash serializes it — parse and fromHash(toHash) agree
|
|
298
|
+
// because both carry the filled number. The ballot stages are
|
|
299
|
+
// matched by the typed stage's code (the per-typed-stage key) —
|
|
300
|
+
// PDAM's stageCode is "cd", the harmonized range it lives in.
|
|
301
|
+
if (typeKey === "amd" && attrs["number"] === undefined &&
|
|
302
|
+
BALLOT_STAGE_CODES.includes(attrs["typedStageState"]?.entry.code ?? "")) {
|
|
303
|
+
identifier["number"] = "1";
|
|
304
|
+
}
|
|
283
305
|
return identifier;
|
|
284
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Historic TYPE-first ballot face (owner ruling 2026-11-12, Option B
|
|
309
|
+
* decode-to-modern): "[P|F]?DAM [N] BASE [stamp]" decodes to ONE
|
|
310
|
+
* amendment over the WHOLE base, rendered as the canonical on-base
|
|
311
|
+
* slash face. The stage comes from the ballot token; the number from
|
|
312
|
+
* the TYPE token, then the on-base stamp, then the owner's no-number
|
|
313
|
+
* rule (a ballot face with no number anywhere names Amendment 1); the
|
|
314
|
+
* stamp's date is the ballot supplement's date. The base keeps every
|
|
315
|
+
* component it carried: a legacy slash-part stays a part of the base
|
|
316
|
+
* number ("ISO/IEC FPDAM 9075/1" → base "9075-1"), and the base's own
|
|
317
|
+
* type token stays the base's ("ISO/IEC PDAM TR 9573-11" → base
|
|
318
|
+
* "TR 9573-11") — nothing migrates into the supplement slot. The
|
|
319
|
+
* stamp's token letters (Amd/Am/…) are decode-away: the ballot's
|
|
320
|
+
* stage is the TYPE token's.
|
|
321
|
+
*/
|
|
322
|
+
buildBallotSupplement(tree) {
|
|
323
|
+
const fields = {
|
|
324
|
+
type_with_stage: s(tree["ballot_type_with_stage"]),
|
|
325
|
+
};
|
|
326
|
+
const number = s(tree["ballot_number"]) ??
|
|
327
|
+
s(tree["ballot_stamp_number"]) ??
|
|
328
|
+
s(tree["ballot_stamp_dot_number"]) ??
|
|
329
|
+
"1";
|
|
330
|
+
fields["number_with_part"] = number;
|
|
331
|
+
// The on-base stamp: its date (or undated marker) is the ballot
|
|
332
|
+
// supplement's; its number feeds the precedence above; its token
|
|
333
|
+
// letters were matched uncaptured and decode away.
|
|
334
|
+
const stampDate = s(tree["ballot_stamp_date"]);
|
|
335
|
+
if (stampDate !== undefined) {
|
|
336
|
+
const [year, month, day] = stampDate.split("-");
|
|
337
|
+
fields["date"] = new PubidDate({ year, month, day });
|
|
338
|
+
}
|
|
339
|
+
else if (tree["ballot_stamp_undated_marker"] !== undefined) {
|
|
340
|
+
fields["date"] = new PubidDate({ undated: true });
|
|
341
|
+
}
|
|
342
|
+
const rest = {};
|
|
343
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
344
|
+
if (!key.startsWith("ballot_"))
|
|
345
|
+
rest[key] = value;
|
|
346
|
+
}
|
|
347
|
+
return this.build({ ...fields, base: rest });
|
|
348
|
+
}
|
|
285
349
|
buildDirectives(tree, state) {
|
|
286
350
|
const attrs = this.baseAttrs(tree);
|
|
287
351
|
const publisher = this.publisherOf(tree);
|
|
@@ -102,8 +102,14 @@ export class IsoIdentifier extends BaseIdentifier {
|
|
|
102
102
|
const ts = this.typedStageState;
|
|
103
103
|
if (ts === undefined)
|
|
104
104
|
return false;
|
|
105
|
-
if (ts.entry.longAbbr !== undefined && ts.originalAbbr
|
|
105
|
+
if (ts.entry.longAbbr !== undefined && ts.originalAbbr !== undefined &&
|
|
106
|
+
(ts.originalAbbr.startsWith(ts.entry.longAbbr) ||
|
|
107
|
+
// The shorter long-family spellings ("Am", the alias of "Amd"
|
|
108
|
+
// the owner ruled renders canonical) are prefixes of the long
|
|
109
|
+
// abbr; the short family ("AMD") matches neither direction.
|
|
110
|
+
ts.entry.longAbbr.startsWith(ts.originalAbbr))) {
|
|
106
111
|
return true;
|
|
112
|
+
}
|
|
107
113
|
if (ts.entry.longAbbr !== undefined && ts.originalAbbr?.includes("Directives"))
|
|
108
114
|
return true;
|
|
109
115
|
return false;
|
|
@@ -32,7 +32,7 @@ export const ISO_TYPED_STAGES = [
|
|
|
32
32
|
code: "prf_amd", stageCode: "prf", typeCode: "amd", abbr: ["PRF Amd"], harmonized: ["50.00"]
|
|
33
33
|
}),
|
|
34
34
|
TS("amd", {
|
|
35
|
-
code: "published", stageCode: "published", typeCode: "amd", abbr: ["Amd", "AMD", "Amd."], shortAbbr: "AMD", longAbbr: "Amd", harmonized: ["60.00", "60.60"]
|
|
35
|
+
code: "published", stageCode: "published", typeCode: "amd", abbr: ["Amd", "AMD", "Amd.", "Am"], shortAbbr: "AMD", longAbbr: "Amd", harmonized: ["60.00", "60.60"]
|
|
36
36
|
}),
|
|
37
37
|
TS("cor", {
|
|
38
38
|
code: "pwi_cor", stageCode: "proposal", typeCode: "cor", abbr: ["PWI Cor"], harmonized: ["00.00", "00.20", "00.60", "00.92", "00.93", "00.98", "00.99"]
|
|
@@ -709,8 +709,24 @@ function nistClass(spec) {
|
|
|
709
709
|
return `${this.publisher ?? "NIST"} ${this.date_year ?? ""}-${this.date_month ?? ""}-${this.date_day ?? ""} ${this.dated_seq ?? ""}`;
|
|
710
710
|
}
|
|
711
711
|
if (spec.style === "fips") {
|
|
712
|
-
|
|
713
|
-
|
|
712
|
+
// The official published face is "FIPS PUB <number>": FIPS is
|
|
713
|
+
// government-published and "PUB" (Federal Information Processing
|
|
714
|
+
// Standards PUBlication) is part of the official designation
|
|
715
|
+
// (owner ruling 2026-11-12). A spelled NIST publisher renders the
|
|
716
|
+
// official face; the historical NBS-prefixed rows keep
|
|
717
|
+
// "NBS FIPS <number>", and the bare publisher-less spelling keeps
|
|
718
|
+
// its short "FIPS <number>" face. The lead reads
|
|
719
|
+
// shortRenderPublisher() (the printed-publisher view), so a
|
|
720
|
+
// series-default publisher filled without being spelled (#704's
|
|
721
|
+
// ruby model) can never flip the bare face; the tail rides the
|
|
722
|
+
// lead.
|
|
723
|
+
const pub = this.shortRenderPublisher();
|
|
724
|
+
const series = this.seriesCode() ?? "";
|
|
725
|
+
let result = pub === "NIST"
|
|
726
|
+
? "FIPS PUB"
|
|
727
|
+
: pub !== undefined && pub !== ""
|
|
728
|
+
? `${pub} ${series}`
|
|
729
|
+
: series;
|
|
714
730
|
let numberPart = this.number ?? "";
|
|
715
731
|
const fy = /^(\d{1,3})-(\d{4})$/.exec(numberPart);
|
|
716
732
|
if (fy !== null) {
|
|
@@ -137,6 +137,17 @@ export function preprocessNist(input) {
|
|
|
137
137
|
cleaned = cleaned.replace(/^(NBS|NIST)(IR|FIPS|GCR|HB|MONO|MP|NCSTAR|NSRDS)/i, "$1 $2");
|
|
138
138
|
cleaned = cleaned.replace(/\b(ir|sp|tn|hb|fips|ams|vts)\b/i, (m) => m.toUpperCase());
|
|
139
139
|
cleaned = cleaned.replace(/\bLC\b(?!IRC)/g, "LCIRC");
|
|
140
|
+
// normalize_fips_pub_alias: the FIPS series' official printed face is
|
|
141
|
+
// "FIPS PUB <number>" — FIPS is government-published and "PUB" (Federal
|
|
142
|
+
// Information Processing Standards PUBlication) is part of the official
|
|
143
|
+
// designation — and the RENDERER emits that face directly (owner ruling
|
|
144
|
+
// 2026-11-12, mirroring the gem). The grammar reads the
|
|
145
|
+
// publisher-bearing spelling only through this fold (pubid#697): the
|
|
146
|
+
// printed spellings — "FIPS PUB 205" and the publisher-prefixed
|
|
147
|
+
// "NIST FIPS PUB 205" (pubid#702) — fold onto the one model "NIST FIPS
|
|
148
|
+
// <number>" the grammar already builds (same hash, same URN, same MR
|
|
149
|
+
// string). Anchored at the head; bare "FIPS" is untouched.
|
|
150
|
+
cleaned = cleaned.replace(/^(?:NIST )?FIPS PUB\b/i, "NIST FIPS");
|
|
140
151
|
// normalize_lcirc_supplement_contexts
|
|
141
152
|
cleaned = cleaned.replace(/\bNBS LCIRC\b(?=.*\b(?:supp?|sup\+|r\d+\/)\d)/, "NBS.LCIRC");
|
|
142
153
|
cleaned = cleaned.replace(/\bNBS\.LCIRC\.(\d+r\d+\/\d{4})/, "NBS LCIRC $1");
|
|
@@ -20,11 +20,17 @@ function buildRules() {
|
|
|
20
20
|
rule("edition_part", () => str("第").then(digits.then(str("."), digits).as("edition"), str("版")));
|
|
21
21
|
rule("edition_latin", () => digits.then(str("."), digits).as("edition"));
|
|
22
22
|
rule("edition", () => space.then(rules["edition_part"].or(rules["edition_latin"])));
|
|
23
|
+
// MLIT's volume label after the edition: 本編 (main volume) or
|
|
24
|
+
// 本編+付録 (main volume with appendix). ASCII or full-width (U+3000)
|
|
25
|
+
// space; ASCII "+" or full-width "+" (U+FF0B), which the builder
|
|
26
|
+
// normalizes to "+".
|
|
27
|
+
rule("volume_sep", () => str(" ").or(str(" ")));
|
|
28
|
+
rule("volume", () => rules["volume_sep"].then(str("本編").then(str("+").or(str("+")).then(str("付録")).maybe()).as("volume")));
|
|
23
29
|
rule("annex_supplement", () => space.then(str("Annex")).then(space, match("[A-Z]").as("annex_letter")));
|
|
24
30
|
rule("handbook", () => str("PLATEAU")
|
|
25
31
|
.then(space, rules["doc_type"], space, rules["number"])
|
|
26
32
|
.then(rules["annex"].maybe())
|
|
27
|
-
.then(rules["edition"].maybe()));
|
|
33
|
+
.then(rules["edition"].then(rules["volume"].maybe()).maybe()));
|
|
28
34
|
rule("identifier", () => rules["handbook"]
|
|
29
35
|
.then(rules["annex_supplement"])
|
|
30
36
|
.as("base")
|
|
@@ -42,14 +48,21 @@ function plateauClass(kind) {
|
|
|
42
48
|
number: { type: "integer" },
|
|
43
49
|
annex: { type: "integer" },
|
|
44
50
|
edition: { type: "string" },
|
|
51
|
+
// The volume label MLIT prints after the edition: "本編" (the main
|
|
52
|
+
// volume alone) or "本編+付録" (main volume and appendix, one
|
|
53
|
+
// PDF). A nil volume means the publication has no volume label:
|
|
54
|
+
// "#00 第5.0版" does not subsume "#00 第5.0版 本編+付録".
|
|
55
|
+
volume: { type: "string" },
|
|
45
56
|
});
|
|
46
|
-
static mappings = keyValue({ wire: "number", to: "number" }, { wire: "edition", to: "edition" }, { wire: "annex", to: "annex" });
|
|
57
|
+
static mappings = keyValue({ wire: "number", to: "number" }, { wire: "edition", to: "edition" }, { wire: "annex", to: "annex" }, { wire: "volume", to: "volume" });
|
|
47
58
|
render() {
|
|
48
59
|
let result = `PLATEAU ${isHandbook ? "Handbook" : "Technical Report"} #${pad2(this.number)}`;
|
|
49
60
|
if (this.annex !== undefined)
|
|
50
61
|
result += `-${this.annex}`;
|
|
51
62
|
if (isHandbook && this.edition !== undefined)
|
|
52
63
|
result += ` 第${this.edition}版`;
|
|
64
|
+
if (this.volume !== undefined)
|
|
65
|
+
result += ` ${this.volume}`;
|
|
53
66
|
return result;
|
|
54
67
|
}
|
|
55
68
|
}
|
|
@@ -91,6 +104,12 @@ export function plateauGrammarImplementation() {
|
|
|
91
104
|
if (data["edition"] !== undefined && data["edition"] !== null && kind === "handbook") {
|
|
92
105
|
attrs["edition"] = String(data["edition"]);
|
|
93
106
|
}
|
|
107
|
+
// The grammar reads a volume label only after an edition, on the
|
|
108
|
+
// Handbook; the builder normalizes the ASCII "+" to the
|
|
109
|
+
// full-width "+" (MLIT prints full-width).
|
|
110
|
+
if (data["volume"] !== undefined && data["volume"] !== null && kind === "handbook") {
|
|
111
|
+
attrs["volume"] = String(data["volume"]).replaceAll("+", "+");
|
|
112
|
+
}
|
|
94
113
|
return new KIND_CLASSES[kind](attrs);
|
|
95
114
|
},
|
|
96
115
|
};
|
|
@@ -15,6 +15,9 @@ export const ihoSchema = define("iho", (s) => {
|
|
|
15
15
|
s.atom("part", "part");
|
|
16
16
|
s.atom("part_edition", "parted");
|
|
17
17
|
s.atom("part_year", "party");
|
|
18
|
+
s.atom("spec", "spec");
|
|
19
|
+
s.atom("spec_sub", "specsub");
|
|
20
|
+
s.atom("spec_variant", "specvar");
|
|
18
21
|
s.atom("annex", "annex");
|
|
19
22
|
s.atom("annex_edition", "anned");
|
|
20
23
|
s.atom("annex_year", "annyear");
|
|
@@ -10,6 +10,7 @@ export const plateauSchema = define("plateau", (s) => {
|
|
|
10
10
|
s.atom("publisher", "pub");
|
|
11
11
|
s.atom("edition", "ed");
|
|
12
12
|
s.atom("annex", "annex", "int");
|
|
13
|
+
s.atom("volume", "vol");
|
|
13
14
|
s.type("pubid:plateau:annex", "annex", {
|
|
14
15
|
core: [],
|
|
15
16
|
atoms: [["letter", "letter"]],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pubid/pubid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "TypeScript PubID identifiers: resolution, normalization and canonical hashes for every identifier in the pubid-testsuite corpus (41 standards bodies), with a conformance runner mirroring the Ruby reference",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"doc:api": "node scripts/api-reference.mjs"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"parsanol": "0.10.
|
|
22
|
+
"parsanol": "0.10.46",
|
|
23
23
|
"yaml": "^2.5.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|