@pubid/pubid 0.1.6 → 0.1.8
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/iec/index.js +15 -3
- package/dist/flavors/iec/model.d.ts +1 -1
- package/dist/flavors/iec/model.js +12 -2
- package/dist/flavors/ieee/index.js +39 -3
- package/dist/flavors/ieee/model.js +3 -4
- package/dist/flavors/itu/grammar.js +71 -9
- package/dist/flavors/itu/index.js +23 -3
- package/dist/flavors/itu/model.d.ts +8 -0
- package/dist/flavors/itu/model.js +45 -7
- package/dist/pg/runtime.d.ts +85 -0
- package/dist/pg/runtime.js +170 -0
- package/dist/pg/suite.d.ts +20 -0
- package/dist/pg/suite.js +90 -0
- package/dist/urn/parsers.js +5 -1
- package/package.json +1 -1
package/artifact/corpus.json.gz
CHANGED
|
Binary file
|
|
@@ -77,10 +77,11 @@ class IecBuilder {
|
|
|
77
77
|
Object.assign(attrs, numberComponents(tree["number_with_part"]));
|
|
78
78
|
// Letters-prefixed numbers (IECEE "CAB", "AD") only take digit-first
|
|
79
79
|
// parts — "CAB-G01" rejects (Ruby parity; digit numbers like 61158-X
|
|
80
|
-
// keep letter parts).
|
|
80
|
+
// keep letter parts). "DIR" is the exception: the Directives number
|
|
81
|
+
// is its own grammar branch and takes letter parts ("DIR-IEC-SUP").
|
|
81
82
|
const number = attrs["number"];
|
|
82
83
|
const part = attrs["part"];
|
|
83
|
-
if (number !== undefined && /^[A-Z]{2,4}$/.test(number) &&
|
|
84
|
+
if (number !== undefined && /^[A-Z]{2,4}$/.test(number) && number !== "DIR" &&
|
|
84
85
|
part !== undefined && /^[A-Za-z]/.test(part)) {
|
|
85
86
|
throw new ParseFailed(`IEC: letter part ${part} after letters number ${number}`, 0);
|
|
86
87
|
}
|
|
@@ -501,7 +502,18 @@ export function iecGrammarImplementation() {
|
|
|
501
502
|
const parsed = urnToCode(urn);
|
|
502
503
|
if (!parsed)
|
|
503
504
|
throw new Error(`Invalid IEC URN: ${urn}`);
|
|
504
|
-
|
|
505
|
+
let [code, lang, allParts] = parsed;
|
|
506
|
+
// The "ser" slot names every part of the document: the reference
|
|
507
|
+
// wraps via to_all_parts, which strips the part and the date (a
|
|
508
|
+
// series names the document across editions, "IEC 60034 (all
|
|
509
|
+
// parts)"). The part rides dash-joined on the number, the date in
|
|
510
|
+
// its own slot.
|
|
511
|
+
if (allParts) {
|
|
512
|
+
code = code
|
|
513
|
+
.replace(/:[^:]*$/, "")
|
|
514
|
+
.replace(/\s(19|20)\d\d(-\d\d)?$/, "")
|
|
515
|
+
.replace(/-[^\s:-]+$/, "");
|
|
516
|
+
}
|
|
505
517
|
// The reference parses the unprefixed rebuild and its grammar
|
|
506
518
|
// defaults the IEC publisher; the ts grammar needs it explicit.
|
|
507
519
|
const id = this.parse(code);
|
|
@@ -6,7 +6,7 @@ import { Language, PubidDate } from "../../model/component.js";
|
|
|
6
6
|
* runtime attribute driving type derivation and the URN stage slot; the
|
|
7
7
|
* wire hash carries only the stage CODE for non-published drafts.
|
|
8
8
|
*/
|
|
9
|
-
export type IecTypeCode = "is" | "tr" | "ts" | "pas" | "srd" | "od" | "guide" | "ish" | "frag" | "trf" | "tec" | "wp" | "sttr" | "amd" | "cor";
|
|
9
|
+
export type IecTypeCode = "is" | "tr" | "ts" | "pas" | "srd" | "od" | "guide" | "ish" | "frag" | "trf" | "tec" | "wp" | "sttr" | "amd" | "cor" | "cs" | "ca";
|
|
10
10
|
export interface TypedStage {
|
|
11
11
|
code: string;
|
|
12
12
|
stageCode: string;
|
|
@@ -15,7 +15,15 @@ export const TYPED_STAGES = [
|
|
|
15
15
|
TS("trf", "published", "trf", "TRF"),
|
|
16
16
|
TS("amd", "published", "amd", "AMD"),
|
|
17
17
|
TS("cor", "published", "cor", "COR"),
|
|
18
|
-
|
|
18
|
+
// IEC CS (Component Specifications) and IEC CA (Conformity Assessment):
|
|
19
|
+
// the abbreviation leads the code and the publisher renders as its
|
|
20
|
+
// default ("IEC CS 033200-TW0001", "IEC CA 01") — gem
|
|
21
|
+
// ComponentSpecification/ConformityAssessment.
|
|
22
|
+
TS("cs", "published", "cs", "CS"),
|
|
23
|
+
TS("ca", "published", "ca", "CA"),
|
|
24
|
+
// PNW owns harmonized 10.20 (gem: NP stages are 10.00/10.60/… — the
|
|
25
|
+
// URN "stage-10.20" slot resolves to PNW, not NP).
|
|
26
|
+
TS("np", "np", "is", "NP"),
|
|
19
27
|
TS("pnw", "pnw", "is", "PNW", "10.20"),
|
|
20
28
|
TS("pwi", "pwi", "is", "PWI", "00.00"),
|
|
21
29
|
TS("anw", "anw", "is", "ANW"),
|
|
@@ -57,6 +65,8 @@ const TYPE_NAMES = {
|
|
|
57
65
|
ts: "technical-specification",
|
|
58
66
|
pas: "publicly-available-specification",
|
|
59
67
|
srd: "systems-reference-document",
|
|
68
|
+
cs: "component-specification",
|
|
69
|
+
ca: "conformity-assessment",
|
|
60
70
|
od: "operational-document",
|
|
61
71
|
guide: "guide",
|
|
62
72
|
ish: "interpretation-sheet",
|
|
@@ -414,7 +424,7 @@ function iecFragmentClass() {
|
|
|
414
424
|
registerType(IecFragmentIdentifier);
|
|
415
425
|
return IecFragmentIdentifier;
|
|
416
426
|
}
|
|
417
|
-
const SINGLE_KINDS = ["is", "tr", "ts", "pas", "srd", "od", "guide"];
|
|
427
|
+
const SINGLE_KINDS = ["is", "tr", "ts", "pas", "srd", "od", "guide", "cs", "ca"];
|
|
418
428
|
const KIND_CLASSES = {};
|
|
419
429
|
for (const kind of SINGLE_KINDS) {
|
|
420
430
|
KIND_CLASSES[kind] = iecSingleClass(kind);
|
|
@@ -895,12 +895,21 @@ class IeeeBuilder {
|
|
|
895
895
|
});
|
|
896
896
|
}
|
|
897
897
|
buildIecIeeCopublished(parsed) {
|
|
898
|
-
const
|
|
898
|
+
const contentRaw = extractValue(parsed["content"]);
|
|
899
899
|
let copublishedNumber;
|
|
900
900
|
let draftInfo;
|
|
901
901
|
let iecYear;
|
|
902
902
|
let dateInfo;
|
|
903
|
-
|
|
903
|
+
let relationships;
|
|
904
|
+
if (contentRaw !== undefined) {
|
|
905
|
+
// A bare (unparenthesised) revision narrative printed after the code
|
|
906
|
+
// is the same relationship prose as the parenthetical form (C3
|
|
907
|
+
// ruling) — rewrite it into the parenthetical shape so both
|
|
908
|
+
// spellings collapse onto one canonical.
|
|
909
|
+
let content = contentRaw;
|
|
910
|
+
const bare = content.match(/^(.+?) (Revis(?:ion|on) (?:of|to) IEEE Std .+)$/);
|
|
911
|
+
if (bare !== null)
|
|
912
|
+
content = `${bare[1]} (${bare[2]})`;
|
|
904
913
|
if (content.includes("IEC:")) {
|
|
905
914
|
copublishedNumber = content.split(" IEC:")[0]?.trim();
|
|
906
915
|
}
|
|
@@ -923,10 +932,35 @@ class IeeeBuilder {
|
|
|
923
932
|
if (iecPart !== undefined)
|
|
924
933
|
iecYear = iecPart.split(" ")[0];
|
|
925
934
|
}
|
|
935
|
+
// The parenthetical tail is classified, not swallowed (C3 ruling):
|
|
936
|
+
// a "(Revision of IEEE Std ...)" narrative is a relationship, not
|
|
937
|
+
// identity — it lands in `relationships` so the URN carries it as
|
|
938
|
+
// the rel. segment and the canonical human drops it; an "(MM/DD)"
|
|
939
|
+
// print date is non-identity and dropped; anything else keeps the
|
|
940
|
+
// historical date_info rendering.
|
|
926
941
|
if (content.includes(" (")) {
|
|
927
942
|
const datePart = content.split(" (")[1];
|
|
928
943
|
if (datePart !== undefined && datePart.includes(")")) {
|
|
929
|
-
|
|
944
|
+
const tail = datePart.split(")")[0];
|
|
945
|
+
const revision = tail !== undefined
|
|
946
|
+
? tail.match(/^Revis(?:ion|on) (?:of|to) IEEE Std (.+)$/)
|
|
947
|
+
: null;
|
|
948
|
+
if (tail !== undefined && revision !== null) {
|
|
949
|
+
let related;
|
|
950
|
+
try {
|
|
951
|
+
related = ieeeParseSingle(revision[1]);
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
related = { toHuman: () => revision[1] };
|
|
955
|
+
}
|
|
956
|
+
relationships = [new IeeeRelationship("revision_of", [related], [])];
|
|
957
|
+
}
|
|
958
|
+
else if (tail !== undefined && /^\d{2}\/\d{2}$/.test(tail)) {
|
|
959
|
+
// print date — non-identity, dropped
|
|
960
|
+
}
|
|
961
|
+
else if (tail !== undefined) {
|
|
962
|
+
dateInfo = tail;
|
|
963
|
+
}
|
|
930
964
|
}
|
|
931
965
|
}
|
|
932
966
|
}
|
|
@@ -937,6 +971,8 @@ class IeeeBuilder {
|
|
|
937
971
|
attrs["iec_year"] = iecYear;
|
|
938
972
|
if (dateInfo !== undefined)
|
|
939
973
|
attrs["date_info"] = dateInfo;
|
|
974
|
+
if (relationships !== undefined)
|
|
975
|
+
attrs["relationships"] = relationships;
|
|
940
976
|
Object.assign(attrs, copublishedStructured(copublishedNumber));
|
|
941
977
|
return new IecIeeeCopublishedClass(attrs);
|
|
942
978
|
}
|
|
@@ -474,10 +474,9 @@ export function renderIeeeBase(id) {
|
|
|
474
474
|
result += `Rev${id.revision}`;
|
|
475
475
|
if (draftObj !== undefined) {
|
|
476
476
|
let printed = draftObj.render();
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
else if (id.publisher === "IEEE" && (id.draft_status ?? "") === "") {
|
|
477
|
+
// The draft designator carries its date on every lead
|
|
478
|
+
// (docs/IEEE-DRAFT-STAGES.md §3) — mirrors the ruby render fix.
|
|
479
|
+
if (id.publisher === "IEEE" && (id.draft_status ?? "") === "") {
|
|
481
480
|
// The long comma form for dated project drafts; the
|
|
482
481
|
// unapproved-draft keeps its pinned single-comma form.
|
|
483
482
|
printed = printed.replace(/, ([A-Z][a-z]+) (\d{4})$/, ", $1, $2");
|
|
@@ -61,7 +61,16 @@ function buildRules() {
|
|
|
61
61
|
rule("attachment", () => space.then(str("attachment").as("attachment")));
|
|
62
62
|
// "ITU-T Q.120-Q.139 (11/1988)" — one document covering a range.
|
|
63
63
|
rule("range_end", () => dash.then(letter.repeat(1, 3).then(dot, digits).as("range_end")));
|
|
64
|
-
rule("
|
|
64
|
+
rule("date_part", () => space.then(str("("), digits.as("month").then(str("/")).maybe(), digit.repeat(4, 4).as("year"), str(")")));
|
|
65
|
+
// A "-YYYYMM" approval date — the "200307" of "T-REC-T.4-200307-I" and
|
|
66
|
+
// "ITU-T T.4-200307". ITU's own edition suffix is short ("-5"), so six
|
|
67
|
+
// digits that read as a plausible year (19xx/20xx) and month (01-12) are
|
|
68
|
+
// the date, never a part. A six-digit run that fails either test
|
|
69
|
+
// ("-200313", "-180001") is still a part, as it was before.
|
|
70
|
+
rule("yyyymm_year", () => str("19").or(str("20")).then(digit.repeat(2, 2)));
|
|
71
|
+
rule("yyyymm_month", () => str("0").then(match("[1-9]")).or(str("1").then(match("[0-2]"))));
|
|
72
|
+
rule("yyyymm_shape", () => rules["yyyymm_year"].then(rules["yyyymm_month"], digit.absent()));
|
|
73
|
+
rule("part", () => dash.then(rules["yyyymm_shape"].absent(), digits.as("part")));
|
|
65
74
|
rule("parts", () => rules["part"].repeat(0, Infinity).as("parts"));
|
|
66
75
|
// Imp code first: the standard code needs digits, which "Imp712" lacks.
|
|
67
76
|
rule("imp_code", () => str("Imp")
|
|
@@ -70,7 +79,21 @@ function buildRules() {
|
|
|
70
79
|
rule("standard_code", () => rules["number"]
|
|
71
80
|
.then(rules["series_suffix"].maybe(), rules["subseries"].maybe(), rules["parts"], rules["qualifier_glued"].maybe()));
|
|
72
81
|
rule("code", () => rules["imp_code"].or(rules["standard_code"]));
|
|
73
|
-
|
|
82
|
+
// The status letter that trails the date in a publication id — "I" (in
|
|
83
|
+
// force) or "S" (superseded). It names the state of the edition, not the
|
|
84
|
+
// edition, so it is parsed and dropped. "S" is also the Spanish language
|
|
85
|
+
// suffix, so it is a status ONLY in the full "T-REC-…" id, where ITU
|
|
86
|
+
// always writes one; after an "ITU-T …-YYYYMM" print form only "I" is,
|
|
87
|
+
// and "-S" stays the language ("ITU-T Z.100-199911-S").
|
|
88
|
+
rule("id_status", () => dash.then(match("[IS]"), match("[A-Za-z0-9]").absent()));
|
|
89
|
+
rule("print_id_status", () => dash.then(str("I"), match("[A-Za-z0-9]").absent()));
|
|
90
|
+
rule("yyyymm_date", () => dash.then(rules["yyyymm_year"].as("year"), rules["yyyymm_month"].as("month"), digit.absent()));
|
|
91
|
+
rule("id_date", () => rules["yyyymm_date"].then(rules["print_id_status"].maybe()));
|
|
92
|
+
// Either date spelling of a Recommendation.
|
|
93
|
+
rule("document_date", () => rules["date_part"].or(rules["id_date"]));
|
|
94
|
+
// "ITU-T REC T.4", "ITU-T REC-T.4" — the redundant type word of ITU's
|
|
95
|
+
// own URLs. Not captured: a Recommendation is the default type.
|
|
96
|
+
rule("rec_word", () => str("REC").then(space.or(dash)));
|
|
74
97
|
// "(V14)" plus the bare spellings "V2", "v10", "v.1" — all
|
|
75
98
|
// normalise to the parenthesised form on render.
|
|
76
99
|
rule("version_part", () => space.then(str("(V")
|
|
@@ -103,9 +126,9 @@ function buildRules() {
|
|
|
103
126
|
.then(digits.then(dot.then(digits).repeat(0, Infinity)).as("supplement_number")));
|
|
104
127
|
rule("supplement_date", () => space.then(str("("), digits.as("supplement_month").then(str("/")).maybe(), digit.repeat(4, 4).as("supplement_year"), str(")")));
|
|
105
128
|
rule("base_with_series", () => rules["itu_prefix"]
|
|
106
|
-
.then(rules["sector"], space, rules["series"], dot, rules["code"], rules["range_end"].maybe(), rules["code_suffixes"], rules["combined_suffixes"].maybe(), rules["series_word"].maybe(), rules["attachment"].maybe(), rules["version_part"].maybe(), rules["
|
|
129
|
+
.then(rules["sector"], space, rules["rec_word"].maybe(), rules["series"], dot, rules["code"], rules["range_end"].maybe(), rules["code_suffixes"], rules["combined_suffixes"].maybe(), rules["series_word"].maybe(), rules["attachment"].maybe(), rules["version_part"].maybe(), rules["document_date"].maybe()));
|
|
107
130
|
rule("base_without_series", () => rules["itu_prefix"]
|
|
108
|
-
.then(rules["sector"], space, rules["code"], rules["code_suffixes"], rules["attachment"].maybe(), rules["version_part"].maybe(), rules["
|
|
131
|
+
.then(rules["sector"], space, rules["code"], rules["code_suffixes"], rules["attachment"].maybe(), rules["version_part"].maybe(), rules["document_date"].maybe()));
|
|
109
132
|
// Series-code documents ("EMC-5", "SEC-QKD"): 2+ letter mnemonic,
|
|
110
133
|
// dash-joined number. The OB guard keeps "ITU-T OB-1" a clean parse
|
|
111
134
|
// failure rather than an escaped ArgumentError.
|
|
@@ -165,16 +188,39 @@ function buildRules() {
|
|
|
165
188
|
rule("supplement_identifier", () => rules["chained_supplement"]
|
|
166
189
|
.or(rules["slash_chained_supplement"], rules["supplement_with_base"], rules["supplement_series_only"]));
|
|
167
190
|
rule("with_series", () => rules["itu_prefix"]
|
|
168
|
-
.then(rules["sector"], space, rules["series"], dot, rules["code"], rules["range_end"].maybe(), rules["code_suffixes"], rules["combined_suffixes"].maybe(), rules["series_word"].maybe(), rules["attachment"].maybe(), rules["version_part"].maybe(), rules["
|
|
191
|
+
.then(rules["sector"], space, rules["rec_word"].maybe(), rules["series"], dot, rules["code"], rules["range_end"].maybe(), rules["code_suffixes"], rules["combined_suffixes"].maybe(), rules["series_word"].maybe(), rules["attachment"].maybe(), rules["version_part"].maybe(), rules["document_date"].maybe(), rules["language"].maybe()));
|
|
169
192
|
rule("without_series", () => rules["itu_prefix"]
|
|
170
|
-
.then(rules["sector"], space, rules["code"], rules["code_suffixes"], rules["attachment"].maybe(), rules["version_part"].maybe(), rules["
|
|
193
|
+
.then(rules["sector"], space, rules["code"], rules["code_suffixes"], rules["attachment"].maybe(), rules["version_part"].maybe(), rules["document_date"].maybe(), rules["language"].maybe()));
|
|
171
194
|
rule("ob_series", () => str("OB").as("series"));
|
|
172
195
|
rule("ob_dot_body", () => dot.then(rules["number"]));
|
|
173
196
|
rule("ob_no_body", () => space.then(str("No."), space, rules["number"]));
|
|
197
|
+
// "ITU OB 1000" — metanorma-itu's docidentifier ("Annex to ITU OB %").
|
|
198
|
+
// Accepted as an input spelling only; it renders "ITU OB No. 1000", the
|
|
199
|
+
// form ITU's own bulletin site uses.
|
|
200
|
+
rule("ob_bare_body", () => space.then(rules["number"]));
|
|
201
|
+
// The date as a bulletin prints it — "ITU-T OB.1096 - 15.III.2016": day,
|
|
202
|
+
// Roman month, year. The months are tried longest first, because PEG
|
|
203
|
+
// takes the first alternative that matches and "I" would otherwise win
|
|
204
|
+
// on "III"; "XIII" matches "XII", then fails on the required dot.
|
|
205
|
+
rule("roman_month", () => str("XII")
|
|
206
|
+
.or(str("XI"))
|
|
207
|
+
.or(str("X"))
|
|
208
|
+
.or(str("IX"))
|
|
209
|
+
.or(str("VIII"))
|
|
210
|
+
.or(str("VII"))
|
|
211
|
+
.or(str("VI"))
|
|
212
|
+
.or(str("V"))
|
|
213
|
+
.or(str("IV"))
|
|
214
|
+
.or(str("III"))
|
|
215
|
+
.or(str("II"))
|
|
216
|
+
.or(str("I")));
|
|
217
|
+
rule("ob_roman_date", () => str(" - ")
|
|
218
|
+
.then(digit.repeat(2, 2).as("day"), dot, rules["roman_month"].as("roman_month"), dot, digit.repeat(4, 4).as("year")));
|
|
219
|
+
rule("ob_date", () => rules["date_part"].or(rules["ob_roman_date"]));
|
|
174
220
|
rule("ob_with_sector", () => rules["itu_prefix"]
|
|
175
|
-
.then(rules["sector"].then(space).maybe(), rules["ob_series"], rules["ob_dot_body"].or(rules["ob_no_body"]), rules["
|
|
221
|
+
.then(rules["sector"].then(space).maybe(), rules["ob_series"], rules["ob_dot_body"].or(rules["ob_no_body"]).or(rules["ob_bare_body"]), rules["ob_date"].maybe(), rules["language"].maybe()));
|
|
176
222
|
rule("ob_long_form", () => rules["itu_prefix"]
|
|
177
|
-
.then(rules["sector"].then(space).maybe(), str("Operational Bulletin").as("_op_bull"), space, str("No."), space, rules["number"], rules["
|
|
223
|
+
.then(rules["sector"].then(space).maybe(), str("Operational Bulletin").as("_op_bull"), space, str("No."), space, rules["number"], rules["ob_date"].maybe(), rules["language"].maybe()));
|
|
178
224
|
rule("special_publication", () => rules["ob_with_sector"].or(rules["ob_long_form"]));
|
|
179
225
|
rule("annex_to_identifier", () => str("Annex to").then(space, rules["special_publication"].as("annex_to")));
|
|
180
226
|
rule("handbook", () => rules["itu_prefix"]
|
|
@@ -191,8 +237,24 @@ function buildRules() {
|
|
|
191
237
|
rule("common_text_twin", () => space
|
|
192
238
|
.then(str("|"), space, match("[^|]").repeat(1, Infinity))
|
|
193
239
|
.as("common_text_twin"));
|
|
240
|
+
// ITU's publication id — "T-REC-T.4-200307-I",
|
|
241
|
+
// "R-REC-BO.1130-5-202602-I": <sector>-REC-<number>[-<edition>]-<YYYYMM>
|
|
242
|
+
// [-<status>], the name ITU gives each edition in its URLs and PDF
|
|
243
|
+
// files. It builds the plain Recommendation it names and renders in the
|
|
244
|
+
// print form ("ITU-T T.4 (07/2003)"). The date is required: without it
|
|
245
|
+
// the string names no edition. No other rule starts with a bare sector
|
|
246
|
+
// letter, so the slot is free.
|
|
247
|
+
rule("publication_id", () => rules["sector"]
|
|
248
|
+
.then(dash, str("REC"), dash, rules["series"], dot, rules["code"], rules["yyyymm_date"], rules["id_status"].maybe(), rules["language"].maybe()));
|
|
249
|
+
// The Radio Regulations — "ITU-R RR", "ITU-R RR (2020)", and the URL
|
|
250
|
+
// spelling "ITU-R RR-2020". Always ITU-R. The trailing any.absent? is
|
|
251
|
+
// load-bearing: PEG ordered choice never re-enters the alternation once
|
|
252
|
+
// an alternative succeeds, so a partial match on "ITU-R RR.1" must fail
|
|
253
|
+
// here and fall through to with_series.
|
|
254
|
+
rule("radio_regulations", () => rules["itu_prefix"]
|
|
255
|
+
.then(str("R").as("sector"), space, str("RR").as("radio_regulations"), rules["date_part"].or(dash.then(digit.repeat(4, 4).as("year"))).maybe(), rules["language"].maybe(), match(".").absent()));
|
|
194
256
|
rule("identifier", () => rules["annex_to_identifier"]
|
|
195
|
-
.or(rules["special_publication"], rules["supplement_identifier"], rules["annex_identifier"], rules["appendix_identifier"], rules["report_identifier"], rules["handbook"], rules["numeric_question"], rules["letter_question"], rules["with_series"], rules["contribution"], rules["series_code_identifier"], rules["without_series"]));
|
|
257
|
+
.or(rules["special_publication"], rules["supplement_identifier"], rules["annex_identifier"], rules["appendix_identifier"], rules["report_identifier"], rules["handbook"], rules["numeric_question"], rules["letter_question"], rules["radio_regulations"], rules["with_series"], rules["contribution"], rules["series_code_identifier"], rules["without_series"], rules["publication_id"]));
|
|
196
258
|
rule("root", () => rules["identifier"]
|
|
197
259
|
.then(rules["common_text_twin"])
|
|
198
260
|
.or(rules["identifier"]));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
|
|
2
2
|
import { PubidDate } from "../../model/component.js";
|
|
3
3
|
import { ituGrammar, normalizeWhitespaceItu } from "./grammar.js";
|
|
4
|
-
import { ITU_CLASSES, ITU_LANGUAGES, ItuCode, ItuDesignation, } from "./model.js";
|
|
4
|
+
import { ITU_CLASSES, ITU_LANGUAGES, ItuCode, ItuDesignation, ItuSpecialPublication, } from "./model.js";
|
|
5
5
|
/**
|
|
6
6
|
* Port of lib/pubid/itu/builder.rb — the exact dispatch order:
|
|
7
7
|
* annex_to → annex_number → appendix_number → OB/_op_bull →
|
|
@@ -43,6 +43,9 @@ class ItuBuilder {
|
|
|
43
43
|
if (tree["appendix_number"] !== undefined) {
|
|
44
44
|
return this.withTwin(this.buildAppendixOfRecommendation(tree), twin);
|
|
45
45
|
}
|
|
46
|
+
if (tree["radio_regulations"] !== undefined) {
|
|
47
|
+
return this.withTwin(this.buildRadioRegulations(tree), twin);
|
|
48
|
+
}
|
|
46
49
|
if ((s(tree["series"]) === "OB" && tree["series_dash"] === undefined) || tree["_op_bull"] !== undefined) {
|
|
47
50
|
return this.withTwin(this.buildSpecialPublication(tree), twin);
|
|
48
51
|
}
|
|
@@ -98,17 +101,34 @@ class ItuBuilder {
|
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
103
|
buildDate(tree) {
|
|
101
|
-
|
|
104
|
+
// The Roman month of a bulletin date ("15.III.2016") is stored as the
|
|
105
|
+
// two-digit month every other ITU date uses; the day marks the spelling.
|
|
106
|
+
const roman = s(tree["roman_month"]);
|
|
107
|
+
const month = roman !== undefined
|
|
108
|
+
? (ItuSpecialPublication.ROMAN_MONTHS.indexOf(roman) + 1).toString().padStart(2, "0")
|
|
109
|
+
: s(tree["month"]);
|
|
110
|
+
return new PubidDate({ year: s(tree["year"]), month, day: s(tree["day"]) });
|
|
102
111
|
}
|
|
103
|
-
/** OB
|
|
112
|
+
/** OB keeps the sector of the TSB spelling ("ITU-T OB.1096") — it is a
|
|
113
|
+
* spelling, not identity (ItuSpecialPublication ignores it in ==). */
|
|
104
114
|
buildSpecialPublication(tree) {
|
|
105
115
|
return new ITU_CLASSES["special_publication"]({
|
|
116
|
+
sector: s(tree["sector"]),
|
|
106
117
|
series: "OB",
|
|
107
118
|
code: tree["number"] !== undefined ? this.buildCode(tree) : undefined,
|
|
108
119
|
date: tree["year"] !== undefined ? this.buildDate(tree) : undefined,
|
|
109
120
|
language: normLanguage(tree["language"]),
|
|
110
121
|
});
|
|
111
122
|
}
|
|
123
|
+
/** "ITU-R RR (2020)" — the series carries the whole designation. */
|
|
124
|
+
buildRadioRegulations(tree) {
|
|
125
|
+
return new ITU_CLASSES["radio_regulations"]({
|
|
126
|
+
sector: s(tree["sector"]),
|
|
127
|
+
series: "RR",
|
|
128
|
+
date: tree["year"] !== undefined ? this.buildDate(tree) : undefined,
|
|
129
|
+
language: normLanguage(tree["language"]),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
112
132
|
buildAnnexTo(inner) {
|
|
113
133
|
const data = flatten(inner);
|
|
114
134
|
const base = this.buildSpecialPublication(data);
|
|
@@ -78,6 +78,13 @@ export declare class ItuHandbook extends ItuIdentifier {
|
|
|
78
78
|
renderBase(): string;
|
|
79
79
|
}
|
|
80
80
|
export declare class ItuSpecialPublication extends ItuIdentifier {
|
|
81
|
+
static polymorphicName: string;
|
|
82
|
+
static mappings: FieldMapping[];
|
|
83
|
+
static ROMAN_MONTHS: string[];
|
|
84
|
+
renderBase(): string;
|
|
85
|
+
private renderObDate;
|
|
86
|
+
}
|
|
87
|
+
export declare class ItuRadioRegulations extends ItuIdentifier {
|
|
81
88
|
static polymorphicName: string;
|
|
82
89
|
static mappings: FieldMapping[];
|
|
83
90
|
renderBase(): string;
|
|
@@ -169,6 +176,7 @@ export declare const ITU_CLASSES: {
|
|
|
169
176
|
readonly report: typeof ItuReport;
|
|
170
177
|
readonly handbook: typeof ItuHandbook;
|
|
171
178
|
readonly special_publication: typeof ItuSpecialPublication;
|
|
179
|
+
readonly radio_regulations: typeof ItuRadioRegulations;
|
|
172
180
|
readonly contribution: typeof ItuContribution;
|
|
173
181
|
readonly question: typeof ItuQuestion;
|
|
174
182
|
readonly combined: typeof ItuCombinedIdentifier;
|
|
@@ -218,6 +218,11 @@ const SUPPLEMENT_MAPPINGS = keyValue({ wire: "sector", to: "sector", toWire: (m)
|
|
|
218
218
|
function urnDateSegment(id) {
|
|
219
219
|
if (!(id.date instanceof PubidDate))
|
|
220
220
|
return undefined;
|
|
221
|
+
// Only an Operational Bulletin's printed date carries a day
|
|
222
|
+
// ("15.III.2016"); it is in `==`, so it must reach the URN too.
|
|
223
|
+
if (id.date.day !== undefined && id.date.month !== undefined) {
|
|
224
|
+
return `${id.date.day}/${id.date.month}/${id.date.year}`;
|
|
225
|
+
}
|
|
221
226
|
return id.date.month !== undefined ? `${id.date.month}/${id.date.year}` : id.date.year;
|
|
222
227
|
}
|
|
223
228
|
export class ItuUrnGenerator extends BaseUrnGenerator {
|
|
@@ -245,7 +250,14 @@ export class ItuUrnGenerator extends BaseUrnGenerator {
|
|
|
245
250
|
}
|
|
246
251
|
generateBaseUrn(id) {
|
|
247
252
|
const parts = ["urn", "itu"];
|
|
248
|
-
|
|
253
|
+
// An Operational Bulletin is cross-bureau: its sector is a spelling,
|
|
254
|
+
// not identity (see ItuSpecialPublication), so it stays out of the URN.
|
|
255
|
+
if (id.sector !== undefined && !(id instanceof ItuSpecialPublication)) {
|
|
256
|
+
parts.push(id.sector.toLowerCase());
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
parts.push("itu");
|
|
260
|
+
}
|
|
249
261
|
// Reports number independently of Recommendations — the marker must
|
|
250
262
|
// reach the URN or the two same-numbered documents collide.
|
|
251
263
|
if (id instanceof ItuReport)
|
|
@@ -338,18 +350,43 @@ attachUrn(ItuHandbook);
|
|
|
338
350
|
export class ItuSpecialPublication extends ItuIdentifier {
|
|
339
351
|
static polymorphicName = "pubid:itu:special-publication";
|
|
340
352
|
static mappings = STANDARD_MAPPINGS;
|
|
353
|
+
static ROMAN_MONTHS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
|
|
341
354
|
renderBase() {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
355
|
+
const number = this.code?.number ?? "";
|
|
356
|
+
const result = this.sector !== undefined
|
|
357
|
+
? `ITU-${this.sector} ${this.series}.${number}`
|
|
358
|
+
: `ITU ${this.series} No. ${number}`;
|
|
359
|
+
return result + this.renderObDate();
|
|
360
|
+
}
|
|
361
|
+
renderObDate() {
|
|
362
|
+
if (!(this.date instanceof PubidDate))
|
|
363
|
+
return "";
|
|
364
|
+
if (this.date.day !== undefined && this.date.month !== undefined) {
|
|
365
|
+
const roman = ItuSpecialPublication.ROMAN_MONTHS[Number(this.date.month) - 1] ?? "";
|
|
366
|
+
return ` - ${this.date.day.padStart(2, "0")}.${roman}.${this.date.year}`;
|
|
347
367
|
}
|
|
348
|
-
|
|
368
|
+
if (this.date.month !== undefined) {
|
|
369
|
+
return ` (${this.date.month.padStart(2, "0")}/${this.date.year})`;
|
|
370
|
+
}
|
|
371
|
+
return ` (${this.date.year})`;
|
|
349
372
|
}
|
|
350
373
|
}
|
|
351
374
|
registerType(ItuSpecialPublication);
|
|
352
375
|
attachUrn(ItuSpecialPublication);
|
|
376
|
+
// The ITU Radio Regulations — the treaty text revised by each World
|
|
377
|
+
// Radiocommunication Conference (hand-off itu-relaton-query-forms).
|
|
378
|
+
// It has no document number: "RR" is the whole designation, stored as
|
|
379
|
+
// the series; "ITU-R RR-2020" (URL spelling) normalizes to
|
|
380
|
+
// "ITU-R RR (2020)".
|
|
381
|
+
export class ItuRadioRegulations extends ItuIdentifier {
|
|
382
|
+
static polymorphicName = "pubid:itu:radio-regulations";
|
|
383
|
+
static mappings = STANDARD_MAPPINGS;
|
|
384
|
+
renderBase() {
|
|
385
|
+
return `ITU-${this.sector} ${this.series}${this.dateSuffix()}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
registerType(ItuRadioRegulations);
|
|
389
|
+
attachUrn(ItuRadioRegulations);
|
|
353
390
|
export class ItuContribution extends ItuIdentifier {
|
|
354
391
|
static polymorphicName = "pubid:itu:contribution";
|
|
355
392
|
static mappings = STANDARD_MAPPINGS;
|
|
@@ -585,6 +622,7 @@ export const ITU_CLASSES = {
|
|
|
585
622
|
report: ItuReport,
|
|
586
623
|
handbook: ItuHandbook,
|
|
587
624
|
special_publication: ItuSpecialPublication,
|
|
625
|
+
radio_regulations: ItuRadioRegulations,
|
|
588
626
|
contribution: ItuContribution,
|
|
589
627
|
question: ItuQuestion,
|
|
590
628
|
combined: ItuCombinedIdentifier,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
type PgArtifactJs = any;
|
|
2
|
+
/** A field requirement from the binding-requirements schema. */
|
|
3
|
+
export interface FieldRequirement {
|
|
4
|
+
type: string;
|
|
5
|
+
card: string;
|
|
6
|
+
preprocess?: string;
|
|
7
|
+
}
|
|
8
|
+
/** The binding-requirements schema record for one entry. */
|
|
9
|
+
export interface EntrySchema {
|
|
10
|
+
root: string;
|
|
11
|
+
fields: Record<string, FieldRequirement>;
|
|
12
|
+
examples: {
|
|
13
|
+
input: string;
|
|
14
|
+
captures: Record<string, unknown>;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
export type Schema = Record<string, EntrySchema>;
|
|
18
|
+
/**
|
|
19
|
+
* A loaded, checksum-verified PG artifact with its schema.
|
|
20
|
+
* Construction throws when the checksum does not verify.
|
|
21
|
+
*/
|
|
22
|
+
export declare class PgRuntime {
|
|
23
|
+
readonly artifact: PgArtifactJs;
|
|
24
|
+
readonly schema: Schema;
|
|
25
|
+
readonly renderSpec: Record<string, RenderSegment[]>;
|
|
26
|
+
readonly entry: string;
|
|
27
|
+
readonly defaultEntry: string | null;
|
|
28
|
+
readonly envelope: Record<string, unknown>;
|
|
29
|
+
constructor(artifactJson: string, entry?: string);
|
|
30
|
+
/** Load an artifact JSON file and verify its checksum. */
|
|
31
|
+
static fromFile(path: string, entry?: string): PgRuntime;
|
|
32
|
+
/** The default artifacts directory (the pubid-grammar checkout). */
|
|
33
|
+
static artifactsDir(): string;
|
|
34
|
+
static load(grammar: string, entry?: string): PgRuntime;
|
|
35
|
+
/** Parse with the native wasm engine; returns the parsanol-shape tree. */
|
|
36
|
+
parseShape(input: string): unknown;
|
|
37
|
+
/** Apply the entry's bindings to a parsanol-shape tree. */
|
|
38
|
+
applyBindings(shape: unknown): Record<string, unknown>;
|
|
39
|
+
/** Parse and bind in one step. */
|
|
40
|
+
parseAndBind(input: string): Record<string, unknown> | null;
|
|
41
|
+
/**
|
|
42
|
+
* Schema-driven materialization: the schema (from the artifact, not
|
|
43
|
+
* hand-written model code) declares the fields a data model must
|
|
44
|
+
* implement; the bound captures populate a structurally-typed object.
|
|
45
|
+
* Card `1*`/`*` fields become arrays; `0..1` fields become nullable.
|
|
46
|
+
*/
|
|
47
|
+
materialize(input: string): Materialized | null;
|
|
48
|
+
/** Render the identifier string from a bound map (F6). */
|
|
49
|
+
render(bound: Record<string, unknown>, variant?: string): string;
|
|
50
|
+
/** Parse, bind, and render in one step. */
|
|
51
|
+
renderString(input: string, variant?: string): string | null;
|
|
52
|
+
/** Evaluate a named derive spec against a bound map (F6). */
|
|
53
|
+
derive(name: string, bound: Record<string, unknown>): string;
|
|
54
|
+
/** Parse, bind, and evaluate a derive spec in one step. */
|
|
55
|
+
deriveString(input: string, name: string): string | null;
|
|
56
|
+
/** Run the artifact's embedded tests; empty list means green. */
|
|
57
|
+
runTests(): string[];
|
|
58
|
+
}
|
|
59
|
+
/** F6 v1 derive evaluator: {field} interpolates from the bound map. */
|
|
60
|
+
export declare function deriveTemplate(derive: Record<string, string>, name: string, bound: Record<string, unknown>): string;
|
|
61
|
+
export type RenderSegment = {
|
|
62
|
+
type: "field";
|
|
63
|
+
field: string;
|
|
64
|
+
} | {
|
|
65
|
+
type: "literal";
|
|
66
|
+
text: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: "cond";
|
|
69
|
+
field: string;
|
|
70
|
+
then: RenderSegment[];
|
|
71
|
+
};
|
|
72
|
+
/** F6 v1 generic renderer: field / literal / cond-presence segments. */
|
|
73
|
+
export declare function renderSegments(segments: RenderSegment[] | null, bound: Record<string, unknown>): string;
|
|
74
|
+
/** A schema-materialized identifier: typed fields plus provenance. */
|
|
75
|
+
export interface Materialized {
|
|
76
|
+
entry: string;
|
|
77
|
+
attributes: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Schema-driven materialization independent of any wasm handle —
|
|
81
|
+
* the same function the model layer will build on (T2 codegen emits the
|
|
82
|
+
* static view of exactly these fields).
|
|
83
|
+
*/
|
|
84
|
+
export declare function materializeFromSchema(schema: EntrySchema, bound: Record<string, unknown>): Materialized;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//! PG artifact runtime for TypeScript (TODO 12 / T1): checksum-verified
|
|
2
|
+
//! artifact load, native wasm parse, bindings application, and
|
|
3
|
+
//! schema-driven materialization. The engine is the vendored
|
|
4
|
+
//! parsanol wasm package (PgArtifactJs, C9) — no grammar code lives in
|
|
5
|
+
//! this repo; the artifact is the contract.
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
/** Walk up from this module to the package root (which holds package.json). */
|
|
11
|
+
function packageRoot() {
|
|
12
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
for (; !existsSync(join(dir, "package.json")); dir = dirname(dir)) {
|
|
14
|
+
if (dir === dirname(dir))
|
|
15
|
+
throw new Error("package.json not found above pg runtime");
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
// The vendored wasm glue is CommonJS and lives outside the compiled tree,
|
|
20
|
+
// so resolve it against the package root at runtime.
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
23
|
+
const wasm = require(join(packageRoot(), "vendor", "parsanol-wasm", "parsanol.js"));
|
|
24
|
+
/**
|
|
25
|
+
* A loaded, checksum-verified PG artifact with its schema.
|
|
26
|
+
* Construction throws when the checksum does not verify.
|
|
27
|
+
*/
|
|
28
|
+
export class PgRuntime {
|
|
29
|
+
artifact;
|
|
30
|
+
schema;
|
|
31
|
+
renderSpec;
|
|
32
|
+
entry;
|
|
33
|
+
defaultEntry;
|
|
34
|
+
envelope;
|
|
35
|
+
constructor(artifactJson, entry) {
|
|
36
|
+
this.envelope = JSON.parse(artifactJson);
|
|
37
|
+
this.artifact = new wasm.PgArtifactJs(artifactJson);
|
|
38
|
+
this.schema = JSON.parse(this.artifact.schema());
|
|
39
|
+
this.renderSpec =
|
|
40
|
+
this.envelope["render"] ?? {};
|
|
41
|
+
// The compiler bakes default_entry: JSON key order is not preserved
|
|
42
|
+
// across engines, so the sorted entry list cannot rederive it.
|
|
43
|
+
this.defaultEntry =
|
|
44
|
+
JSON.parse(artifactJson)["default_entry"] ?? null;
|
|
45
|
+
this.entry = entry ?? this.defaultEntry ?? this.artifact.entryNames()[0];
|
|
46
|
+
}
|
|
47
|
+
/** Load an artifact JSON file and verify its checksum. */
|
|
48
|
+
static fromFile(path, entry) {
|
|
49
|
+
return new PgRuntime(readFileSync(path, "utf8"), entry);
|
|
50
|
+
}
|
|
51
|
+
/** The default artifacts directory (the pubid-grammar checkout). */
|
|
52
|
+
static artifactsDir() {
|
|
53
|
+
const env = process.env.PG_ARTIFACT_DIR;
|
|
54
|
+
if (env)
|
|
55
|
+
return env;
|
|
56
|
+
// pubid-grammar is a sibling checkout in the pubid workspace.
|
|
57
|
+
return join(packageRoot(), "..", "pubid-grammar", "artifacts");
|
|
58
|
+
}
|
|
59
|
+
static load(grammar, entry) {
|
|
60
|
+
return PgRuntime.fromFile(join(PgRuntime.artifactsDir(), `${grammar}.json`), entry);
|
|
61
|
+
}
|
|
62
|
+
/** Parse with the native wasm engine; returns the parsanol-shape tree. */
|
|
63
|
+
parseShape(input) {
|
|
64
|
+
return JSON.parse(this.artifact.parseShape(this.entry, input));
|
|
65
|
+
}
|
|
66
|
+
/** Apply the entry's bindings to a parsanol-shape tree. */
|
|
67
|
+
applyBindings(shape) {
|
|
68
|
+
return JSON.parse(this.artifact.applyBindings(this.entry, JSON.stringify(shape)));
|
|
69
|
+
}
|
|
70
|
+
/** Parse and bind in one step. */
|
|
71
|
+
parseAndBind(input) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(this.artifact.parseAndBind(this.entry, input));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Schema-driven materialization: the schema (from the artifact, not
|
|
81
|
+
* hand-written model code) declares the fields a data model must
|
|
82
|
+
* implement; the bound captures populate a structurally-typed object.
|
|
83
|
+
* Card `1*`/`*` fields become arrays; `0..1` fields become nullable.
|
|
84
|
+
*/
|
|
85
|
+
materialize(input) {
|
|
86
|
+
const bound = this.parseAndBind(input);
|
|
87
|
+
if (bound === null)
|
|
88
|
+
return null;
|
|
89
|
+
const schema = this.schema[this.entry];
|
|
90
|
+
if (!schema)
|
|
91
|
+
throw new Error(`artifact has no schema for entry ${this.entry}`);
|
|
92
|
+
return materializeFromSchema(schema, bound);
|
|
93
|
+
}
|
|
94
|
+
/** Render the identifier string from a bound map (F6). */
|
|
95
|
+
render(bound, variant = "default") {
|
|
96
|
+
return renderSegments((this.renderSpec[variant] ?? null), bound);
|
|
97
|
+
}
|
|
98
|
+
/** Parse, bind, and render in one step. */
|
|
99
|
+
renderString(input, variant = "default") {
|
|
100
|
+
const bound = this.parseAndBind(input);
|
|
101
|
+
return bound === null ? null : this.render(bound, variant);
|
|
102
|
+
}
|
|
103
|
+
/** Evaluate a named derive spec against a bound map (F6). */
|
|
104
|
+
derive(name, bound) {
|
|
105
|
+
return deriveTemplate((this.envelope["derive"] ?? {}), name, bound);
|
|
106
|
+
}
|
|
107
|
+
/** Parse, bind, and evaluate a derive spec in one step. */
|
|
108
|
+
deriveString(input, name) {
|
|
109
|
+
const bound = this.parseAndBind(input);
|
|
110
|
+
return bound === null ? null : this.derive(name, bound);
|
|
111
|
+
}
|
|
112
|
+
/** Run the artifact's embedded tests; empty list means green. */
|
|
113
|
+
runTests() {
|
|
114
|
+
return JSON.parse(this.artifact.runTests());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** F6 v1 derive evaluator: {field} interpolates from the bound map. */
|
|
118
|
+
export function deriveTemplate(derive, name, bound) {
|
|
119
|
+
const template = derive[name];
|
|
120
|
+
if (template === undefined)
|
|
121
|
+
throw new Error(`derive spec ${name} not declared`);
|
|
122
|
+
return template.replace(/\{(\w+)\}/g, (_, field) => {
|
|
123
|
+
const value = bound[field];
|
|
124
|
+
return value === undefined || value === null ? "" : String(value);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** F6 v1 generic renderer: field / literal / cond-presence segments. */
|
|
128
|
+
export function renderSegments(segments, bound) {
|
|
129
|
+
let out = "";
|
|
130
|
+
for (const segment of segments ?? []) {
|
|
131
|
+
if (segment.type === "field") {
|
|
132
|
+
const value = bound[segment.field];
|
|
133
|
+
out += value === undefined || value === null ? "" : String(value);
|
|
134
|
+
}
|
|
135
|
+
else if (segment.type === "literal") {
|
|
136
|
+
out += segment.text;
|
|
137
|
+
}
|
|
138
|
+
else if (segment.type === "cond") {
|
|
139
|
+
const value = bound[segment.field];
|
|
140
|
+
if (value !== undefined && value !== null)
|
|
141
|
+
out += renderSegments(segment.then, bound);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Schema-driven materialization independent of any wasm handle —
|
|
148
|
+
* the same function the model layer will build on (T2 codegen emits the
|
|
149
|
+
* static view of exactly these fields).
|
|
150
|
+
*/
|
|
151
|
+
export function materializeFromSchema(schema, bound) {
|
|
152
|
+
const attributes = {};
|
|
153
|
+
for (const [path, requirements] of Object.entries(schema.fields)) {
|
|
154
|
+
const leaf = path.includes("[]")
|
|
155
|
+
? (path.split("].").pop() ?? path)
|
|
156
|
+
: path;
|
|
157
|
+
const value = bound[leaf];
|
|
158
|
+
if (requirements.card.endsWith("*")) {
|
|
159
|
+
const list = Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
160
|
+
attributes[leaf] = list;
|
|
161
|
+
}
|
|
162
|
+
else if (requirements.card === "0..1") {
|
|
163
|
+
attributes[leaf] = value === undefined ? null : value;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
attributes[leaf] = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { entry: schema.root, attributes };
|
|
170
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PgRuntime } from "./runtime.js";
|
|
2
|
+
export type TestKind = "accept" | "reject" | "example";
|
|
3
|
+
export interface SuiteTest {
|
|
4
|
+
kind: TestKind;
|
|
5
|
+
input: string;
|
|
6
|
+
expect?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface Suite {
|
|
9
|
+
name: string;
|
|
10
|
+
entry?: string;
|
|
11
|
+
tests: SuiteTest[];
|
|
12
|
+
}
|
|
13
|
+
/** Parse one *.pgtest file (same grammar as Parsanol::PG::Suite#read_file). */
|
|
14
|
+
export declare function readSuite(text: string, name: string): Suite;
|
|
15
|
+
/** Load every *.pgtest file in a directory. */
|
|
16
|
+
export declare function loadSuites(dir: string): Suite[];
|
|
17
|
+
/** Run a suite against a runtime; returns failure descriptions. */
|
|
18
|
+
export declare function runSuite(runtime: PgRuntime, suite: Suite): string[];
|
|
19
|
+
/** The default suites directory (the pubid-grammar checkout). */
|
|
20
|
+
export declare function suitesDir(): string;
|
package/dist/pg/suite.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//! T3: external *.pgtest suites run through the wasm runtime. The suite
|
|
2
|
+
//! syntax matches the Ruby loader (Parsanol::PG::Suite): a
|
|
3
|
+
//! `suite <name> [for <entry>] { ... }` header with accept / reject /
|
|
4
|
+
//! example lines. Green means Ruby and TypeScript agree on the contract.
|
|
5
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { PgRuntime } from "./runtime.js";
|
|
8
|
+
const LINE = /^\s*(accept|reject|example)\s+"((?:[^"\\]|\\.)*)"(?:\s*\{([^}]*)\})?\s*$/;
|
|
9
|
+
const HEADER = /suite\s+(\w+)(?:\s+for\s+entry\s+(\w+))?\s*\{/;
|
|
10
|
+
/** Parse one *.pgtest file (same grammar as Parsanol::PG::Suite#read_file). */
|
|
11
|
+
export function readSuite(text, name) {
|
|
12
|
+
const body = text.replace(/^#[^\n]*\n/gm, "");
|
|
13
|
+
const header = HEADER.exec(body);
|
|
14
|
+
if (!header) {
|
|
15
|
+
throw new Error(`${name}: expected 'suite <name> [for <entry>] { ... }'`);
|
|
16
|
+
}
|
|
17
|
+
const tests = [];
|
|
18
|
+
for (const line of body.slice((header.index ?? 0) + header[0].length).split("\n")) {
|
|
19
|
+
if (line.trim() === "}")
|
|
20
|
+
break;
|
|
21
|
+
const match = LINE.exec(line);
|
|
22
|
+
if (!match)
|
|
23
|
+
continue;
|
|
24
|
+
const kind = match[1];
|
|
25
|
+
const rawInput = match[2] ?? "";
|
|
26
|
+
const input = JSON.parse(`"${rawInput}"`);
|
|
27
|
+
const expect = {};
|
|
28
|
+
if (match[3]) {
|
|
29
|
+
for (const pair of match[3].split(",")) {
|
|
30
|
+
const kv = /(\w+)\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(pair);
|
|
31
|
+
const key = kv?.[1];
|
|
32
|
+
const raw = kv?.[2];
|
|
33
|
+
if (key && raw !== undefined)
|
|
34
|
+
expect[key] = JSON.parse(`"${raw}"`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
tests.push(kind === "example" ? { kind, input, expect } : { kind, input });
|
|
38
|
+
}
|
|
39
|
+
const entry = header[2];
|
|
40
|
+
return entry === undefined
|
|
41
|
+
? { name: header[1] ?? "suite", tests }
|
|
42
|
+
: { name: header[1] ?? "suite", entry, tests };
|
|
43
|
+
}
|
|
44
|
+
/** Load every *.pgtest file in a directory. */
|
|
45
|
+
export function loadSuites(dir) {
|
|
46
|
+
return readdirSync(dir)
|
|
47
|
+
.filter((file) => file.endsWith(".pgtest"))
|
|
48
|
+
.map((file) => readSuite(readFileSync(join(dir, file), "utf8"), file.replace(/\.pgtest$/, "")));
|
|
49
|
+
}
|
|
50
|
+
/** Run a suite against a runtime; returns failure descriptions. */
|
|
51
|
+
export function runSuite(runtime, suite) {
|
|
52
|
+
const failures = [];
|
|
53
|
+
const entry = suite.entry ?? runtime.entry;
|
|
54
|
+
for (const test of suite.tests) {
|
|
55
|
+
// The wasm runtime throws on parse failure; that is the reject signal.
|
|
56
|
+
let bound = null;
|
|
57
|
+
try {
|
|
58
|
+
bound = runtime.artifact.parseAndBind(entry, test.input);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
bound = null;
|
|
62
|
+
}
|
|
63
|
+
if (test.kind === "reject") {
|
|
64
|
+
if (bound !== null) {
|
|
65
|
+
failures.push(`test ${JSON.stringify(test.input)}: expected the input to be rejected`);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (bound === null) {
|
|
70
|
+
failures.push(`test ${JSON.stringify(test.input)}: expected the input to parse`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (test.kind === "example") {
|
|
74
|
+
const parsed = JSON.parse(bound);
|
|
75
|
+
for (const [key, value] of Object.entries(test.expect ?? {})) {
|
|
76
|
+
if (parsed[key] !== value) {
|
|
77
|
+
failures.push(`test ${JSON.stringify(test.input)}: expected captures ${key}: ${JSON.stringify(value)}, got ${JSON.stringify(parsed[key])}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return failures;
|
|
83
|
+
}
|
|
84
|
+
/** The default suites directory (the pubid-grammar checkout). */
|
|
85
|
+
export function suitesDir() {
|
|
86
|
+
const env = process.env.PG_SUITES_DIR;
|
|
87
|
+
if (env)
|
|
88
|
+
return env;
|
|
89
|
+
return join(PgRuntime.artifactsDir(), "..", "suites");
|
|
90
|
+
}
|
package/dist/urn/parsers.js
CHANGED
|
@@ -534,7 +534,11 @@ export const URN_PARSERS = {
|
|
|
534
534
|
const revMatch = stripped.match(/^(.*)\.(r\d+)$/i);
|
|
535
535
|
const code = revMatch ? revMatch[1] : stripped;
|
|
536
536
|
const revision = revMatch ? revMatch[2] : undefined;
|
|
537
|
-
|
|
537
|
+
// The NBS-era series carry the NBS imprint in their canonical form
|
|
538
|
+
// ("NBS CSM 1") — the rebuild names the publisher the document
|
|
539
|
+
// carries, or the flavor parse rejects its own URN's rebuild.
|
|
540
|
+
const publisher = typeToken.toLowerCase() === "csm" ? "NBS" : "NIST";
|
|
541
|
+
let text = `${publisher} ${label} ${code}`;
|
|
538
542
|
if (revision)
|
|
539
543
|
text += revision;
|
|
540
544
|
return parse(text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pubid/pubid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|