@pubid/pubid 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/gb/index.d.ts +8 -1
- package/dist/flavors/gb/index.js +55 -7
- package/dist/flavors/iec/index.js +80 -0
- package/dist/flavors/ieee/grammar.js +77 -6
- package/dist/flavors/ieee/index.js +76 -4
- package/dist/flavors/ieee/model.d.ts +2 -0
- package/dist/flavors/ieee/model.js +101 -12
- package/dist/flavors/ieee/update_codes.js +10 -4
- package/dist/flavors/index.js +27 -0
- 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/grammar.js +29 -3
- package/dist/flavors/oiml/implementation.js +46 -1
- package/dist/flavors/oiml/model.d.ts +19 -0
- package/dist/flavors/oiml/model.js +133 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/model/identifier.d.ts +1 -0
- package/dist/model/identifier.js +1 -1
- 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);
|
|
@@ -18,7 +18,14 @@ export declare class GbIdentifier extends BaseIdentifier {
|
|
|
18
18
|
readonly number: string;
|
|
19
19
|
readonly part: string | undefined;
|
|
20
20
|
readonly date: PubidDate | undefined;
|
|
21
|
-
readonly all_parts: boolean | undefined;
|
|
22
21
|
render(): string;
|
|
23
22
|
}
|
|
23
|
+
export declare class GbAllPartsIdentifier extends BaseIdentifier {
|
|
24
|
+
static polymorphicName: string;
|
|
25
|
+
static get attributes(): import("../../model/attribute.js").AttributeTable;
|
|
26
|
+
static get mappings(): never[];
|
|
27
|
+
readonly identifiers: GbIdentifier[];
|
|
28
|
+
render(): string;
|
|
29
|
+
toUrn(): string;
|
|
30
|
+
}
|
|
24
31
|
export declare function gbGrammarImplementation(): FlavorImplementation;
|
package/dist/flavors/gb/index.js
CHANGED
|
@@ -53,9 +53,17 @@ export class GbIdentifier extends BaseIdentifier {
|
|
|
53
53
|
mandate: { type: "string" },
|
|
54
54
|
number: { type: "string" },
|
|
55
55
|
part: { type: "string" },
|
|
56
|
-
all_parts: { type: "boolean", default: false },
|
|
57
56
|
});
|
|
58
|
-
static mappings = keyValue({
|
|
57
|
+
static mappings = keyValue({
|
|
58
|
+
wire: "publisher",
|
|
59
|
+
to: "publisher",
|
|
60
|
+
// The flat wire form is the Publisher's body scalar; rebuild the
|
|
61
|
+
// component on deserialize (lutaml coerces it on the Ruby side).
|
|
62
|
+
fromWire: (hash) => {
|
|
63
|
+
const value = hash["publisher"];
|
|
64
|
+
return typeof value === "string" ? new Publisher({ body: value }) : value;
|
|
65
|
+
},
|
|
66
|
+
}, { wire: "mandate", to: "mandate" }, { wire: "number", to: "number" }, { wire: "part", to: "part" }, { wire: "date", to: "date" });
|
|
59
67
|
// GB's flat-scalar entry: the Publisher serializes as its body scalar.
|
|
60
68
|
static flatScalarComponents = { publisher: "publisher" };
|
|
61
69
|
static flatScalarFields = { publisher: "body" };
|
|
@@ -68,17 +76,56 @@ export class GbIdentifier extends BaseIdentifier {
|
|
|
68
76
|
numberPortion += `.${this.part}`;
|
|
69
77
|
if (this.date !== undefined && this.date.year !== undefined)
|
|
70
78
|
numberPortion += `-${this.date.year}`;
|
|
71
|
-
|
|
72
|
-
if (this.all_parts)
|
|
73
|
-
result += " (all parts)";
|
|
74
|
-
return result;
|
|
79
|
+
return `${code} ${numberPortion}`;
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
registerType(GbIdentifier);
|
|
83
|
+
// The document a member names: without its part and its edition (the
|
|
84
|
+
// attributes an all-parts reference ignores).
|
|
85
|
+
function gbDocumentOf(member) {
|
|
86
|
+
const r = member;
|
|
87
|
+
return new GbIdentifier({
|
|
88
|
+
publisher: r["publisher"],
|
|
89
|
+
mandate: r["mandate"],
|
|
90
|
+
number: r["number"],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// "GB/T 5606 (all parts)" names every part of one document: the wrapper
|
|
94
|
+
// holds the member identifiers (gem #433 - the mark is a class, not a
|
|
95
|
+
// flag), renders the document plus the suffix, and URNs as the document.
|
|
96
|
+
export class GbAllPartsIdentifier extends BaseIdentifier {
|
|
97
|
+
static polymorphicName = "pubid:gb:all-parts";
|
|
98
|
+
static get attributes() {
|
|
99
|
+
return extendAttributes(BaseIdentifier, {
|
|
100
|
+
identifiers: { type: GbIdentifier, collection: true },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
static get mappings() {
|
|
104
|
+
return [{ wire: "identifiers", to: "identifiers" }];
|
|
105
|
+
}
|
|
106
|
+
render() {
|
|
107
|
+
const first = this.identifiers[0];
|
|
108
|
+
return `${first === undefined ? "" : gbDocumentOf(first).render()} (all parts)`;
|
|
109
|
+
}
|
|
110
|
+
toUrn() {
|
|
111
|
+
const first = this.identifiers[0];
|
|
112
|
+
return gbDocumentOf(first).toUrn();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
registerType(GbAllPartsIdentifier);
|
|
78
116
|
class GbBuilder extends BaseBuilder {
|
|
79
117
|
defaultIdentifierClass() {
|
|
80
118
|
return GbIdentifier;
|
|
81
119
|
}
|
|
120
|
+
build(tree) {
|
|
121
|
+
const id = super.build(tree);
|
|
122
|
+
if (tree["all_parts"] === undefined)
|
|
123
|
+
return id;
|
|
124
|
+
// "(all parts)" wraps the document it parsed.
|
|
125
|
+
return new GbAllPartsIdentifier({
|
|
126
|
+
identifiers: [id],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
82
129
|
cast(key, value) {
|
|
83
130
|
if (key === "publisher_code") {
|
|
84
131
|
// Split an inline /T or /Z mandate out of the code.
|
|
@@ -92,7 +139,8 @@ class GbBuilder extends BaseBuilder {
|
|
|
92
139
|
return value === undefined || value === null ? null : { date: new PubidDate({ year: String(value) }) };
|
|
93
140
|
}
|
|
94
141
|
if (key === "all_parts") {
|
|
95
|
-
|
|
142
|
+
// Presence marker only: the wrapper carries the members.
|
|
143
|
+
return value;
|
|
96
144
|
}
|
|
97
145
|
return value;
|
|
98
146
|
}
|
|
@@ -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
|
+
}
|
|
@@ -42,6 +42,10 @@ function buildRules() {
|
|
|
42
42
|
const dashMaybe = () => dash.maybe();
|
|
43
43
|
const monthName = () => literalAlternation(MONTHS);
|
|
44
44
|
const absent = (p) => p.absent();
|
|
45
|
+
// The ISO/IEC draft-stage vocabulary of the D= notation
|
|
46
|
+
// (docs/IEEE-DRAFT-STAGES.md §1.2). Ordered so the longest token wins
|
|
47
|
+
// ("FDIS" over "DIS").
|
|
48
|
+
const stageVocab = () => literalAlternation(["FDIS", "CDV", "PWI", "DIS", "WD", "NP", "CD"]);
|
|
45
49
|
rule("year_digits", () => str("19").or(str("20")).then(digit.repeat(2, 2)).then(lower.repeat(0, 2)).then(absent(digits)));
|
|
46
50
|
rule("month_numeric", () => str("0").then(match("[1-9]")).or(str("1").then(match("[0-2]"))));
|
|
47
51
|
rule("date_with_month_text", () => monthName().as("month").then(space).then(R("year_digits").as("year")));
|
|
@@ -128,6 +132,16 @@ function buildRules() {
|
|
|
128
132
|
rule("draft", () => (ref(rules, "draft_prefix")
|
|
129
133
|
.then(ref(rules, "draft_version").repeat(1, 2))
|
|
130
134
|
.then(dot.then(digits.as("revision")).maybe())
|
|
135
|
+
.then(
|
|
136
|
+
// The compound both-systems form (docs/IEEE-DRAFT-STAGES.md
|
|
137
|
+
// §1.3): "=DDIS.3" - IEEE draft ordinal = draft of the ISO/IEC
|
|
138
|
+
// stage, with its iteration. The glued "=DDIS3" / "=DDIS-3"
|
|
139
|
+
// spellings are accepted aliases of "=DDIS.3".
|
|
140
|
+
str("=")
|
|
141
|
+
.then(str("D").then(stageVocab().as("draft_iso_stage"))
|
|
142
|
+
.or(stageVocab().as("draft_iso_stage")))
|
|
143
|
+
.then((dot.or(dash)).maybe().then(digits.as("draft_iso_iteration")).maybe())
|
|
144
|
+
.maybe())
|
|
131
145
|
.then(ref(rules, "draft_date").maybe())).as("draft"));
|
|
132
146
|
rule("revision_suffix", () => slash.then(str("R")).then(dash).then(match("[0-9A-Za-z]").repeat(1, Infinity).as("revision")));
|
|
133
147
|
rule("edition", () => (comma.then(R("year_digits").as("edition_year")).then(str(" Edition"))).or(space.or(dash)
|
|
@@ -230,13 +244,38 @@ function buildRules() {
|
|
|
230
244
|
.then(match("[^\\n]").repeat(1, Infinity).as("content")));
|
|
231
245
|
rule("joint_development_ieee_format", () => literalAlternation(["ISO/IEC/IEEE", "ISO/IEEE", "IEC/IEEE", "IEEE/CSA"]).as("joint_publishers")
|
|
232
246
|
.then(space)
|
|
233
|
-
|
|
247
|
+
// P = project (the document is a draft): identity-bearing, so it
|
|
248
|
+
// is captured and preserved, never silently consumed.
|
|
249
|
+
.then(str("P").as("project_marker"))
|
|
234
250
|
.then(digits.as("number"))
|
|
235
251
|
.then((dot.or(dash)).then(digits.as("part")).maybe())
|
|
236
252
|
.then(slash.then(digits).then(dot.then(digits).maybe())
|
|
237
253
|
.then(dash.then(digits.as("draft_version")).maybe()).maybe())
|
|
238
|
-
.then(
|
|
239
|
-
|
|
254
|
+
.then(
|
|
255
|
+
// Variant 1b: the ordinal-less stage draft "D=CDV[:2020]" -
|
|
256
|
+
// D (draft) = CDV (the IEC stage it drafts). The year rides in
|
|
257
|
+
// the draft clause (a distinct key, so the builder keeps the
|
|
258
|
+
// date inside the designator).
|
|
259
|
+
slash.then(str("D")).then(str("="))
|
|
260
|
+
.then(stageVocab().as("draft_iso_stage"))
|
|
261
|
+
.then(str(":").then(R("year_digits").as("draft_stage_year")).maybe())
|
|
262
|
+
.or(
|
|
263
|
+
// Variant 1: /D8 notation (original), with the compound
|
|
264
|
+
// both-systems suffix "=DDIS.3" (docs/IEEE-DRAFT-STAGES.md
|
|
265
|
+
// §1.3). The doubled-D spelling is consumed and echoed: the
|
|
266
|
+
// joint form prints the stage as spelled.
|
|
267
|
+
slash.then(str("D")).then(digits.as("draft_version"))
|
|
268
|
+
.then(str("=")
|
|
269
|
+
.then(str("D").as("draft_stage_d").then(stageVocab().as("draft_iso_stage"))
|
|
270
|
+
.or(stageVocab().as("draft_iso_stage")))
|
|
271
|
+
.then((dot.or(dash)).maybe().then(digits.as("draft_iso_iteration")).maybe())
|
|
272
|
+
.maybe()))
|
|
273
|
+
.or(
|
|
274
|
+
// Variant 2: , CDV1 notation (comma before stage code) —
|
|
275
|
+
// the stage draft of the named ISO/IEC stage, its iteration
|
|
276
|
+
comma.then(literalAlternation(["CDV", "FDIS", "CD", "DIS"]).as("iec_stage"))
|
|
277
|
+
.then(digits.maybe().as("stage_iteration")))
|
|
278
|
+
.maybe())
|
|
240
279
|
.then(ref(rules, "edition").maybe())
|
|
241
280
|
.then((dash.then(R("year_digits").as("year"))).or(comma.maybe().then(space).then(monthName().as("month")).then(spaceMaybe())
|
|
242
281
|
.then(R("year_digits").as("year"))).maybe())
|
|
@@ -270,14 +309,27 @@ function buildRules() {
|
|
|
270
309
|
.then(isoStage().then(stdNoise()).then(space)
|
|
271
310
|
.or(str("").as("iso_published").then(spaceMaybe())))
|
|
272
311
|
.or(stagedOnly.as("joint_publishers").then(space).then(isoStage()).then(stdNoise()).then(space)))
|
|
273
|
-
.then(str("P").maybe())
|
|
312
|
+
.then(str("P").as("project_marker").maybe())
|
|
274
313
|
.then(digits.as("number"))
|
|
275
314
|
.then((dot.then(absent(R("year_digits"))).then(digits.as("part")))
|
|
276
315
|
.or(dash.then(str("").as("part_dash")).then(absent(R("year_digits")))
|
|
277
316
|
.then(digits.as("part")))
|
|
278
317
|
.maybe())
|
|
279
318
|
.then(dateClause())
|
|
280
|
-
.then(
|
|
319
|
+
.then(
|
|
320
|
+
// The ISO/IEC edition marker may sit between the year and an
|
|
321
|
+
// amendment/draft tail ("8802.11:2012 (E)/Amd 1-2014").
|
|
322
|
+
(spaceMaybe().then(str("("))
|
|
323
|
+
.then(match("[^)]").repeat(1, Infinity).as("edition_marker"))
|
|
324
|
+
.then(str(")"))).maybe())
|
|
325
|
+
.then(
|
|
326
|
+
// The stage-draft clause of the ISO-led print: "/D=WD.5" - D
|
|
327
|
+
// (draft) = the ISO/IEC stage it drafts, with its iteration
|
|
328
|
+
// (docs/IEEE-DRAFT-STAGES.md §1.2).
|
|
329
|
+
(slash.then(str("D")).then(str("="))
|
|
330
|
+
.then(stageVocab().as("draft_iso_stage"))
|
|
331
|
+
.then(dot.then(digits.then(match("[A-Za-z]").repeat(0, 1)).as("draft_iso_iteration")).maybe()))
|
|
332
|
+
.or(slash.then(str("D")).then(dashMaybe())
|
|
281
333
|
.then(match("[0-9.]").repeat(1, Infinity).as("draft_version"))
|
|
282
334
|
.then(((comma.or(space)).then(monthName().as("draft_month")).then(space)
|
|
283
335
|
.then(R("year_digits").as("draft_year"))).or(comma.then(R("year_digits").as("draft_year"))).maybe())).maybe())
|
|
@@ -288,12 +340,30 @@ function buildRules() {
|
|
|
288
340
|
.then(ref(rules, "revision_suffix").maybe())
|
|
289
341
|
.then(ref(rules, "parenthetical").maybe());
|
|
290
342
|
});
|
|
343
|
+
// The stage-less ISO/IEC label with a colon year and the ISO/IEC
|
|
344
|
+
// edition marker: "ISO/IEC 13210:1994 (E)" - the ISO/IEC portion of a
|
|
345
|
+
// double-labeled standard. The (E) parenthetical is REQUIRED: without it
|
|
346
|
+
// this spelling is the ISO flavor's own form and must not be stolen.
|
|
347
|
+
rule("joint_development_iso_iec_edition", () => str("ISO/IEC").as("joint_publishers")
|
|
348
|
+
.then(space)
|
|
349
|
+
.then(str("").as("iso_published"))
|
|
350
|
+
.then(str("P").as("project_marker").maybe())
|
|
351
|
+
.then(digits.as("number"))
|
|
352
|
+
// The part may be dot- or dash-joined ("8802-9"); a dash-year is
|
|
353
|
+
// not a part. A distinct key: the iso-format route's part_dash
|
|
354
|
+
// prints as a dot in the joint code; only the label keeps its dash.
|
|
355
|
+
.then((dot.then(digits.as("part")))
|
|
356
|
+
.or(dash.then(str("").as("iec_label_dash")).then(absent(R("year_digits"))).then(digits.as("part")))
|
|
357
|
+
.maybe())
|
|
358
|
+
.then(str(":").then(spaceMaybe()).then(R("year_digits").as("year")))
|
|
359
|
+
.then(spaceMaybe())
|
|
360
|
+
.then(ref(rules, "parenthetical")));
|
|
291
361
|
rule("joint_development_embedded_stage", () => literalAlternation([
|
|
292
362
|
"ISO/IEC/IEEE", "IEEE/ISO/IEC", "IEEE/IEC/ISO", "ISO/IEEE",
|
|
293
363
|
"IEC/IEEE", "IEEE/IEC", "ISO/IEC", "IEEE",
|
|
294
364
|
]).as("joint_publishers")
|
|
295
365
|
.then(space)
|
|
296
|
-
.then(str("P").maybe())
|
|
366
|
+
.then(str("P").as("project_marker").maybe())
|
|
297
367
|
.then(digits.as("number"))
|
|
298
368
|
.then((dot.or(dash)).then(absent(R("year_digits"))).then(digits.as("part")).maybe())
|
|
299
369
|
.then(dot)
|
|
@@ -577,6 +647,7 @@ function buildRules() {
|
|
|
577
647
|
.or(ref(rules, "conformance_identifier"))
|
|
578
648
|
.or(ref(rules, "joint_development_ieee_format"))
|
|
579
649
|
.or(ref(rules, "joint_development_iso_format"))
|
|
650
|
+
.or(ref(rules, "joint_development_iso_iec_edition"))
|
|
580
651
|
.or(ref(rules, "joint_development_embedded_stage"))
|
|
581
652
|
.or(ref(rules, "iec_ieee_copublished"))
|
|
582
653
|
.or(ref(rules, "number_first_identifier"))
|
|
@@ -121,6 +121,10 @@ class IeeeBuilder {
|
|
|
121
121
|
attributes["parts"] = codeObj.parts;
|
|
122
122
|
if (codeObj?.originalSeparator !== undefined)
|
|
123
123
|
attributes["separator"] = codeObj.originalSeparator;
|
|
124
|
+
// P = project (the document is a draft): the marker is
|
|
125
|
+
// identity-bearing, so stripping the letter must not strip the
|
|
126
|
+
// fact (docs/IEEE-DRAFT-STAGES.md §1.1).
|
|
127
|
+
attributes["project_marker"] = true;
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
delete attributes["__code"];
|
|
@@ -298,11 +302,23 @@ class IeeeBuilder {
|
|
|
298
302
|
return attributes;
|
|
299
303
|
}
|
|
300
304
|
determineStageAbbr(typeValue, parsed) {
|
|
305
|
+
// An explicit joint stage on the draft ("=DFDIS.3") IS the stage —
|
|
306
|
+
// it outranks the ordinal ladder, which classifies IEEE-internal
|
|
307
|
+
// drafts only (docs/IEEE-DRAFT-STAGES.md §1.3).
|
|
301
308
|
if (parsed["draft"] !== undefined) {
|
|
302
309
|
let draftData = parsed["draft"];
|
|
303
310
|
if (Array.isArray(draftData)) {
|
|
304
311
|
draftData = mergeParsedArray(draftData);
|
|
305
312
|
}
|
|
313
|
+
if (isObj(draftData) && draftData["draft_iso_stage"] !== undefined) {
|
|
314
|
+
const stage = extractValue(draftData["draft_iso_stage"]) ?? "";
|
|
315
|
+
// The doubled-D alias ("DFDIS") carries its own draft marker;
|
|
316
|
+
// the stage is what remains.
|
|
317
|
+
const rest = stage.slice(1);
|
|
318
|
+
return stage.startsWith("D") && ["PWI", "NP", "WD", "CD", "CDV", "DIS", "FDIS"].includes(rest)
|
|
319
|
+
? rest
|
|
320
|
+
: stage;
|
|
321
|
+
}
|
|
306
322
|
if (isObj(draftData) && draftData["draft_version"] !== undefined) {
|
|
307
323
|
const dv = draftData["draft_version"];
|
|
308
324
|
const version = Array.isArray(dv) ? dv.map((v) => extractValue(v) ?? "").join("") : extractValue(dv);
|
|
@@ -343,6 +359,8 @@ class IeeeBuilder {
|
|
|
343
359
|
}
|
|
344
360
|
let version;
|
|
345
361
|
let revision;
|
|
362
|
+
let isoStage;
|
|
363
|
+
let isoIteration;
|
|
346
364
|
let month;
|
|
347
365
|
let year;
|
|
348
366
|
let day;
|
|
@@ -355,6 +373,8 @@ class IeeeBuilder {
|
|
|
355
373
|
version = version.replace(/^-/, "");
|
|
356
374
|
}
|
|
357
375
|
revision = extractValue(draftData["revision"]);
|
|
376
|
+
isoStage = extractValue(draftData["draft_iso_stage"]);
|
|
377
|
+
isoIteration = extractValue(draftData["draft_iso_iteration"]);
|
|
358
378
|
month = extractValue(draftData["month"]);
|
|
359
379
|
year = extractValue(draftData["year"]);
|
|
360
380
|
day = extractValue(draftData["day"]);
|
|
@@ -370,10 +390,12 @@ class IeeeBuilder {
|
|
|
370
390
|
else {
|
|
371
391
|
version = extractValue(draftData);
|
|
372
392
|
}
|
|
373
|
-
if (version !== undefined) {
|
|
393
|
+
if (version !== undefined || (isObj(draftData) && draftData["draft_iso_stage"] !== undefined)) {
|
|
374
394
|
const draftObj = new IeeeDraft({
|
|
375
395
|
version,
|
|
376
396
|
revision,
|
|
397
|
+
iso_stage: isoStage,
|
|
398
|
+
iso_iteration: isoIteration,
|
|
377
399
|
month,
|
|
378
400
|
year,
|
|
379
401
|
day,
|
|
@@ -601,7 +623,14 @@ class IeeeBuilder {
|
|
|
601
623
|
codeParts.push(extractValue(parsed["part"]) ?? "");
|
|
602
624
|
let codeStr = extractValue(parsed["number"]);
|
|
603
625
|
if (codeStr !== undefined && codeParts.length > 0) {
|
|
604
|
-
|
|
626
|
+
// The ISO/IEC label's part may be dash-joined ("8802-9") - keep the
|
|
627
|
+
// printed separator.
|
|
628
|
+
const sep = parsed["iec_label_dash"] !== undefined ? "-" : ".";
|
|
629
|
+
codeStr += `${sep}${codeParts.join(sep)}`;
|
|
630
|
+
}
|
|
631
|
+
// P = project (a draft): identity-bearing, preserved as spelled.
|
|
632
|
+
if (parsed["project_marker"] !== undefined && codeStr !== undefined) {
|
|
633
|
+
codeStr = `P${codeStr}`;
|
|
605
634
|
}
|
|
606
635
|
if (codeStr !== undefined) {
|
|
607
636
|
const codeObj = IeeeCode.parse(codeStr);
|
|
@@ -639,6 +668,37 @@ class IeeeBuilder {
|
|
|
639
668
|
if (draftVer !== "")
|
|
640
669
|
attributes["ieee_draft"] = `D${draftVer}`;
|
|
641
670
|
}
|
|
671
|
+
// The "(E)" edition marker of an ISO/IEC label rides on the joint
|
|
672
|
+
// reference as parenthetical_content - either from the trailing
|
|
673
|
+
// parenthetical slot (the double-label form) or the mid-rule slot
|
|
674
|
+
// before an amendment tail.
|
|
675
|
+
const parenthetical = parsed["parameters"];
|
|
676
|
+
if (parenthetical !== undefined && typeof parenthetical === "object" && !Array.isArray(parenthetical)) {
|
|
677
|
+
const content = parenthetical["parenthetical_content"];
|
|
678
|
+
if (content !== undefined)
|
|
679
|
+
attributes["parenthetical_content"] = extractValue(content);
|
|
680
|
+
}
|
|
681
|
+
if (attributes["parenthetical_content"] === undefined && parsed["edition_marker"] !== undefined) {
|
|
682
|
+
attributes["parenthetical_content"] = extractValue(parsed["edition_marker"]);
|
|
683
|
+
}
|
|
684
|
+
// The joint stage-draft clause (docs/IEEE-DRAFT-STAGES.md §1.3):
|
|
685
|
+
// variant 1's compound tail composes onto the IEEE ordinal
|
|
686
|
+
// ("D5=DIS.3"); variant 1b is the ordinal-less stage draft whose
|
|
687
|
+
// date rides inside the designator ("D=CDV:2020"). The doubled-D
|
|
688
|
+
// alias spelling echoes as spelled.
|
|
689
|
+
if (parsed["draft_iso_stage"] !== undefined) {
|
|
690
|
+
const stageText = `${extractValue(parsed["draft_stage_d"]) ?? ""}${extractValue(parsed["draft_iso_stage"]) ?? ""}`;
|
|
691
|
+
let jointDraft = parsed["draft_version"] !== undefined
|
|
692
|
+
? `D${(extractValue(parsed["draft_version"]) ?? "").replace(/^D/, "")}=${stageText}`
|
|
693
|
+
: `D=${stageText}`;
|
|
694
|
+
if (parsed["draft_iso_iteration"] !== undefined) {
|
|
695
|
+
jointDraft += `.${extractValue(parsed["draft_iso_iteration"])}`;
|
|
696
|
+
}
|
|
697
|
+
if (parsed["draft_stage_year"] !== undefined) {
|
|
698
|
+
jointDraft += `:${extractValue(parsed["draft_stage_year"])}`;
|
|
699
|
+
}
|
|
700
|
+
attributes["ieee_draft"] = jointDraft;
|
|
701
|
+
}
|
|
642
702
|
// The catalogue-PRINTED joint form (dash-year / ", Month YYYY" /
|
|
643
703
|
// date-trailing-the-draft / date-less parenthetical): a Standard
|
|
644
704
|
// carrying publisher/copublisher renders it as printed; the colon-year
|
|
@@ -654,8 +714,11 @@ class IeeeBuilder {
|
|
|
654
714
|
typeof parsed["parameters"] === "object" && !Array.isArray(parsed["parameters"]) &&
|
|
655
715
|
parsed["parameters"]["parenthetical_content"] !== undefined))) {
|
|
656
716
|
const sep = parsed["part_dash"] !== undefined ? "-" : ".";
|
|
657
|
-
|
|
717
|
+
let printedCode = [extractValue(parsed["number"]), extractValue(parsed["part"])]
|
|
658
718
|
.filter((x) => x !== undefined).join(sep);
|
|
719
|
+
// P = project: the marker is identity, preserved as spelled.
|
|
720
|
+
if (parsed["project_marker"] !== undefined)
|
|
721
|
+
printedCode = `P${printedCode}`;
|
|
659
722
|
const printedAttrs = {
|
|
660
723
|
publisher: attributes["publisher"],
|
|
661
724
|
copublisher: attributes["copublisher"],
|
|
@@ -1022,7 +1085,16 @@ function copublishedStructured(copublishedNumber) {
|
|
|
1022
1085
|
}
|
|
1023
1086
|
// --- parse orchestration -------------------------------------------------------
|
|
1024
1087
|
function ieeeParseSingle(input) {
|
|
1025
|
-
const
|
|
1088
|
+
const normalized = applyUpdateCodes(input);
|
|
1089
|
+
// UpdateCodes may introduce the "; " double-label separator (the
|
|
1090
|
+
// ISO/IEC and IEEE labels of one document), but the dispatch ran on
|
|
1091
|
+
// the raw input - re-dispatch so the dual construction sees it.
|
|
1092
|
+
if (normalized !== input && normalized.includes("; ")) {
|
|
1093
|
+
const re = preParse(normalized);
|
|
1094
|
+
if (re.dispatch === "dual_semicolon")
|
|
1095
|
+
return buildDual(re.parts);
|
|
1096
|
+
}
|
|
1097
|
+
const cleaned = preprocessIeee(normalized);
|
|
1026
1098
|
const tree = parseGrammar(ieeeGrammar, cleaned);
|
|
1027
1099
|
const builder = new IeeeBuilder();
|
|
1028
1100
|
builder.originalInput = input;
|
|
@@ -43,6 +43,8 @@ export declare class IeeeCode {
|
|
|
43
43
|
export declare class IeeeDraft {
|
|
44
44
|
readonly version: string;
|
|
45
45
|
readonly revision: string | undefined;
|
|
46
|
+
readonly isoStage: string | undefined;
|
|
47
|
+
readonly isoIteration: string | undefined;
|
|
46
48
|
readonly year: string | undefined;
|
|
47
49
|
readonly month: string | undefined;
|
|
48
50
|
readonly day: string | undefined;
|