@pubid/pubid 0.1.5 → 0.1.7
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/README.md +22 -6
- package/artifact/corpus.json.gz +0 -0
- package/dist/conformance/implementation.d.ts +3 -0
- package/dist/flavors/bipm/index.js +39 -0
- package/dist/flavors/iec/index.js +80 -0
- package/dist/flavors/ieee/index.js +39 -3
- package/dist/flavors/ieee/model.js +3 -4
- package/dist/flavors/index.js +27 -0
- 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/flavors/jcgm/index.js +61 -0
- package/dist/flavors/nist/grammar.js +9 -2
- package/dist/flavors/nist/index.js +8 -4
- package/dist/flavors/nist/model.js +10 -3
- package/dist/flavors/oiml/implementation.js +16 -0
- 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.d.ts +5 -0
- package/dist/urn/parsers.js +566 -0
- package/dist/urn/types.d.ts +12 -0
- package/dist/urn/types.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,12 +87,28 @@ The conformance gate enforces corpus mode over the whole testsuite on
|
|
|
87
87
|
every CI run: 42/42 flavors, canonical hash, human form, URN, alias
|
|
88
88
|
normalization, negative rejection and deserialize idempotency.
|
|
89
89
|
|
|
90
|
-
Grammar
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
## Grammar mode: open-universe runtime parsing
|
|
91
|
+
|
|
92
|
+
All 42 flavors are grammar-backed and ship the wave plan end to end
|
|
93
|
+
(core model → parser engine → builder → normalizer → renderer → urn →
|
|
94
|
+
per-flavor grammars). Grammar implementations parse real-world
|
|
95
|
+
identifiers far beyond the published corpus — novel spellings, unseen
|
|
96
|
+
numbers, live catalogues — and the conformance gate still enforces
|
|
97
|
+
parity with the corpus floor on every run, so a grammar can never
|
|
98
|
+
regress below what corpus mode set:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { grammarImplementation } from "@pubid/pubid";
|
|
102
|
+
|
|
103
|
+
const oiml = grammarImplementation("oiml")!;
|
|
104
|
+
oiml.parse("OIML R 60-1:2021"); // any published identifier
|
|
105
|
+
oiml.parse(oiml.parse("OIML R 117-1").toUrn()!); // URNs ingest back
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`grammarImplementation` is exported from the package root. URN ingestion
|
|
109
|
+
ships per flavor where the Ruby reference implements a URN parser
|
|
110
|
+
(oiml today; the remaining flavors throw a clear "not yet supported"
|
|
111
|
+
error naming this issue until their ports land).
|
|
96
112
|
|
|
97
113
|
## Development
|
|
98
114
|
|
package/artifact/corpus.json.gz
CHANGED
|
Binary file
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
export interface FlavorImplementation {
|
|
8
8
|
/** Parse a canonical human form; must throw on a bad input. */
|
|
9
9
|
parse(input: string): Identifier;
|
|
10
|
+
/** Inverse of toUrn for flavors whose reference implements a URN
|
|
11
|
+
* parser; when absent, "urn:" inputs throw a not-supported error. */
|
|
12
|
+
parseUrn?(urn: string): Identifier;
|
|
10
13
|
}
|
|
11
14
|
export interface Identifier {
|
|
12
15
|
/** The canonical serialized form - compared byte-exactly against the corpus identifier record. */
|
|
@@ -422,6 +422,45 @@ class BipmBuilder {
|
|
|
422
422
|
export function bipmGrammarImplementation() {
|
|
423
423
|
const builder = new BipmBuilder();
|
|
424
424
|
return {
|
|
425
|
+
// Inverse of BipmUrnGenerator (lib/pubid/bipm/urn_parser.rb): rebuild
|
|
426
|
+
// the identifier DIRECTLY from the URN fields — the printed forms carry
|
|
427
|
+
// surface detail (meeting ordinals, French wording) the URN omits.
|
|
428
|
+
parseUrn(urn) {
|
|
429
|
+
const body = urn.replace(/^urn:bipm:/, "");
|
|
430
|
+
// Keep trailing empty segments (gem splits with -1).
|
|
431
|
+
const parts = body.split(":");
|
|
432
|
+
const presence = (v) => v === undefined || v === "" ? undefined : v;
|
|
433
|
+
if (parts[0] === "metrologia") {
|
|
434
|
+
return new BipmMetrologiaArticle({
|
|
435
|
+
number: presence(parts[1]),
|
|
436
|
+
issue: parts[2],
|
|
437
|
+
article: parts[3],
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (parts[0] === "si-brochure") {
|
|
441
|
+
return new BipmSiBrochure({
|
|
442
|
+
number: presence(parts[2]),
|
|
443
|
+
language: (parts[1] ?? "").toUpperCase(),
|
|
444
|
+
edition: parts[2],
|
|
445
|
+
version: parts[3],
|
|
446
|
+
years: parts[4],
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const group = (parts[0] ?? "").toUpperCase();
|
|
450
|
+
if (parts[1] === "meeting") {
|
|
451
|
+
return new BipmMeeting({
|
|
452
|
+
group,
|
|
453
|
+
number: parts[2] ?? "",
|
|
454
|
+
year: parts[3] ? Number(parts[3]) : undefined,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return new BipmCommitteeDocument({
|
|
458
|
+
group,
|
|
459
|
+
type_code: presence(parts[1])?.toUpperCase(),
|
|
460
|
+
number: presence(parts[2]),
|
|
461
|
+
year: parts[3] ? Number(parts[3]) : undefined,
|
|
462
|
+
});
|
|
463
|
+
},
|
|
425
464
|
parse(input) {
|
|
426
465
|
const normalized = UPDATE_CODES[input] ?? input;
|
|
427
466
|
const tree = parseGrammar(bipmGrammar, normalized);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
|
|
2
|
+
import { TYPED_STAGES } from "./model.js";
|
|
2
3
|
import { Language, PubidDate } from "../../model/component.js";
|
|
3
4
|
import { iecGrammar, preprocessIec } from "./grammar.js";
|
|
4
5
|
import { locateStage, IEC_CLASSES, } from "./model.js";
|
|
@@ -493,6 +494,27 @@ for (const klass of [
|
|
|
493
494
|
export function iecGrammarImplementation() {
|
|
494
495
|
const builder = new IecBuilder();
|
|
495
496
|
return {
|
|
497
|
+
// Inverse of the IEC UrnGenerator (lib/pubid/iec/urn_parser.rb):
|
|
498
|
+
// reassemble the positional URN fields into a text code, parse it, then
|
|
499
|
+
// attach languages and the all-parts wrap.
|
|
500
|
+
parseUrn(urn) {
|
|
501
|
+
const parsed = urnToCode(urn);
|
|
502
|
+
if (!parsed)
|
|
503
|
+
throw new Error(`Invalid IEC URN: ${urn}`);
|
|
504
|
+
const [code, lang, allParts] = parsed;
|
|
505
|
+
// The reference parses the unprefixed rebuild and its grammar
|
|
506
|
+
// defaults the IEC publisher; the ts grammar needs it explicit.
|
|
507
|
+
const id = this.parse(code);
|
|
508
|
+
if (lang && lang !== "" && "languages" in id) {
|
|
509
|
+
id.languages = lang
|
|
510
|
+
.split("-")
|
|
511
|
+
.map((c) => new Language({ code: c }));
|
|
512
|
+
}
|
|
513
|
+
if (allParts) {
|
|
514
|
+
id.all_parts = true;
|
|
515
|
+
}
|
|
516
|
+
return id;
|
|
517
|
+
},
|
|
496
518
|
parse(input) {
|
|
497
519
|
let tree;
|
|
498
520
|
try {
|
|
@@ -505,3 +527,61 @@ export function iecGrammarImplementation() {
|
|
|
505
527
|
},
|
|
506
528
|
};
|
|
507
529
|
}
|
|
530
|
+
// Port of Relaton::Iec.urn_to_code + the slot helpers
|
|
531
|
+
// (lib/pubid/iec/urn_parser.rb): reassemble the positional URN fields into
|
|
532
|
+
// a text code. Returns [code, language, allParts].
|
|
533
|
+
function urnToCode(urn) {
|
|
534
|
+
const fields = urn.toUpperCase().split(":");
|
|
535
|
+
if (fields.length < 5)
|
|
536
|
+
return undefined;
|
|
537
|
+
const [head, num, date, type, deliv, lang] = fields.slice(3, 9);
|
|
538
|
+
let allParts = false;
|
|
539
|
+
let code = head.replaceAll("-", "/");
|
|
540
|
+
const typeCode = typeSlotToCode(type);
|
|
541
|
+
if (typeCode !== "")
|
|
542
|
+
code += ` ${typeCode}`;
|
|
543
|
+
code += ` ${num}`;
|
|
544
|
+
if (date !== undefined && date !== "")
|
|
545
|
+
code += `:${date}`;
|
|
546
|
+
code += adjunctToCode(fields.slice(9));
|
|
547
|
+
if ((deliv ?? "").toLowerCase() === "ser") {
|
|
548
|
+
allParts = true;
|
|
549
|
+
}
|
|
550
|
+
else if (deliv !== undefined) {
|
|
551
|
+
const edition = editionSlotToCode(deliv);
|
|
552
|
+
if (edition)
|
|
553
|
+
code += ` ${edition}`;
|
|
554
|
+
else if (deliv !== "")
|
|
555
|
+
code += ` ${deliv}`;
|
|
556
|
+
}
|
|
557
|
+
return [code, lang?.toLowerCase(), allParts];
|
|
558
|
+
}
|
|
559
|
+
// The type slot holds a legacy type token ("TS"), a stage
|
|
560
|
+
// ("STAGE-10.20"), or both ("TS-STAGE-50.00"); a stage is written back as
|
|
561
|
+
// the abbreviation resolved from the typed-stage registry.
|
|
562
|
+
function typeSlotToCode(type) {
|
|
563
|
+
if (type === undefined || type === "")
|
|
564
|
+
return "";
|
|
565
|
+
const match = type.match(/^([A-Z]+-)?STAGE-([\d.]+)$/);
|
|
566
|
+
if (!match)
|
|
567
|
+
return type;
|
|
568
|
+
const stageType = (match[1] ?? "IS-").replace("-", "").toLowerCase();
|
|
569
|
+
const stage = TYPED_STAGES.find((t) => t.typeCode === stageType && (t.harmonized ?? "") === match[2]);
|
|
570
|
+
return stage ? stage.abbr : "";
|
|
571
|
+
}
|
|
572
|
+
// "ED-7" in the deliverable slot is an edition, not a deliverable code.
|
|
573
|
+
function editionSlotToCode(deliv) {
|
|
574
|
+
const match = deliv?.match(/^ED-(.+)$/);
|
|
575
|
+
return match ? `ED${match[1]}` : undefined;
|
|
576
|
+
}
|
|
577
|
+
// Adjuncts are (relation, type, number, date) quartets; a "PLUS" relation
|
|
578
|
+
// yields "+", otherwise "/".
|
|
579
|
+
function adjunctToCode(fields) {
|
|
580
|
+
if (!fields.length)
|
|
581
|
+
return "";
|
|
582
|
+
const [rel = "", type = "", num = "", date = ""] = fields.slice(0, 4);
|
|
583
|
+
let code = (rel === "" ? "/" : "+") + type + num;
|
|
584
|
+
if (date !== "")
|
|
585
|
+
code += `:${date}`;
|
|
586
|
+
return code + adjunctToCode(fields.slice(4));
|
|
587
|
+
}
|
|
@@ -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");
|
package/dist/flavors/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lookupUrnParser, parseUrnFor } from "../urn/parsers.js";
|
|
1
2
|
import { oimlGrammarImplementation } from "./oiml/implementation.js";
|
|
2
3
|
import { xsfGrammarImplementation } from "./xsf/index.js";
|
|
3
4
|
import { doiGrammarImplementation } from "./doi/index.js";
|
|
@@ -47,6 +48,32 @@ import { ialaGrammarImplementation } from "./iala/index.js";
|
|
|
47
48
|
* flavor NOT listed falls back to corpus mode.
|
|
48
49
|
*/
|
|
49
50
|
export function grammarImplementation(flavor) {
|
|
51
|
+
const impl = grammarImplementationSwitch(flavor);
|
|
52
|
+
if (impl === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
// "urn:" inputs route to the flavor's URN parser where the reference
|
|
55
|
+
// implements one (impl.parseUrn or the registry in src/urn/parsers.ts
|
|
56
|
+
// mirroring lib/pubid/<flavor>/urn_parser.rb); flavors without one whose
|
|
57
|
+
// own grammar accepts URN spellings keep parsing them natively
|
|
58
|
+
// (pubid/pubid-ts#1).
|
|
59
|
+
const urnDef = lookupUrnParser(flavor);
|
|
60
|
+
if (impl.parseUrn === undefined && urnDef === undefined)
|
|
61
|
+
return impl;
|
|
62
|
+
return {
|
|
63
|
+
parse(input) {
|
|
64
|
+
if (/^urn:/i.test(input)) {
|
|
65
|
+
if (impl.parseUrn)
|
|
66
|
+
return impl.parseUrn(input);
|
|
67
|
+
return parseUrnFor(urnDef, input, impl.parse);
|
|
68
|
+
}
|
|
69
|
+
return impl.parse(input);
|
|
70
|
+
},
|
|
71
|
+
...(impl.parseUrn
|
|
72
|
+
? { parseUrn: impl.parseUrn }
|
|
73
|
+
: { parseUrn: (urn) => parseUrnFor(urnDef, urn, impl.parse) }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function grammarImplementationSwitch(flavor) {
|
|
50
77
|
switch (flavor) {
|
|
51
78
|
case "oiml":
|
|
52
79
|
return oimlGrammarImplementation();
|
|
@@ -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,
|