@pubid/pubid 0.1.21 → 0.1.23

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.
Binary file
@@ -2,6 +2,7 @@ import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
2
2
  import { PubidDate, Language, Iteration } from "../../model/component.js";
3
3
  import { isoGrammar } from "./grammar.js";
4
4
  import { IsoBundledIdentifier, IsoDirectives, IsoDirectivesSupplement, IsoEdition, IsoPublisher, IsoSupplementIdentifier, IsoTcDocument, isoClassForType, locateStage, } from "./model.js";
5
+ import { ISO_TYPED_STAGES } from "./stages.generated.js";
5
6
  /**
6
7
  * Port of lib/pubid/iso/{builder,normalizer}.rb. Class dispatch follows
7
8
  * the Ruby Builder: joint → Combined (unreachable by the corpus),
@@ -246,7 +247,14 @@ class IsoBuilder {
246
247
  }
247
248
  }
248
249
  const state = typeStateFromToken(s(tree["type_with_stage"]), "is");
249
- const typeKey = state?.entry.typeCode ?? "is";
250
+ // The explicit URN type token pins the class: a stage's abbreviation
251
+ // can overwrite type_with_stage with an IS-only stage ("WDA" for
252
+ // 90.93), and the document must stay a TR/TS (the Ruby builder reads
253
+ // the same document_type hint).
254
+ const documentType = s(tree["document_type"]);
255
+ const typeKey = documentType !== undefined
256
+ ? documentType.toLowerCase()
257
+ : state?.entry.typeCode ?? "is";
250
258
  if (typeKey === "dir-sup")
251
259
  return this.buildDirectivesSupplement(tree, state);
252
260
  if (typeKey === "dir")
@@ -334,8 +342,264 @@ function parseHuman(input) {
334
342
  }
335
343
  return new IsoBuilder().build(tree);
336
344
  }
345
+ // --- URN ingestion (lib/pubid/iso/urn_parser.rb) ----------------------------
346
+ // RFC 5141-bis typed-stage abbreviations → stage codes. "DIS"/"ADIS" are
347
+ // ISO ladder stages the reference maps; everything else mirrors the gem's
348
+ // TYPED_STAGE_REVERSE_MAP verbatim.
349
+ const URN_STAGE_ABBRS = {
350
+ WD: "wd", WDS: "wds", CD: "cd", CDV: "cdv", DIS: "dis", FDIS: "fdis",
351
+ PDAM: "pdam", DAM: "dam", FDAM: "fdamd", DCOR: "dcor", FDCOR: "fdcor",
352
+ CDTS: "cdts", DTS: "dts", FDTS: "fdts",
353
+ };
354
+ const URN_TYPE_TOKENS = new Set([
355
+ "tr", "ts", "pas", "guide", "dir", "dir-sup", "iwa-sup", "isp", "iwa", "r", "data",
356
+ ]);
357
+ const URN_SUPP_TYPES = new Set(["amd", "cor", "sup", "add"]);
358
+ // The URN type tokens are the gem's type keys; the ts leaf for ISO/R is
359
+ // keyed "rec".
360
+ const URN_TYPE_KEYS = { r: "rec" };
361
+ function urnStageByHarmonized(code) {
362
+ const candidates = ISO_TYPED_STAGES.filter((s) => s.harmonized?.includes(code));
363
+ // The gem prefers the IS family when a code is shared across families
364
+ // (pubid#570 — the registry order is load-order dependent there).
365
+ return candidates.find((s) => s.typeCode === "is") ?? candidates[0];
366
+ }
367
+ // When the URN gives a harmonized code and no type token, the document is
368
+ // an IS: prefer the non-published IS stages (PRF over the published IS for
369
+ // 60.00), then the stage covering the most codes (WD over WDS).
370
+ function urnIsStageByHarmonized(code) {
371
+ const candidates = ISO_TYPED_STAGES.filter((s) => s.typeCode === "is" && s.harmonized?.includes(code));
372
+ if (candidates.length === 0)
373
+ return undefined;
374
+ const nonPublished = candidates.filter((s) => s.stageCode !== "published");
375
+ const pool = nonPublished.length > 0 ? nonPublished : candidates;
376
+ let best = pool[0];
377
+ for (const candidate of pool) {
378
+ if ((candidate.harmonized?.length ?? 0) > (best.harmonized?.length ?? 0))
379
+ best = candidate;
380
+ }
381
+ return best;
382
+ }
383
+ // The URN carries parts as "10164:-5-1-1" or "9001-1"; split into
384
+ // number / part / subpart (a subpart keeps its dashes).
385
+ function parseUrnNumberPart(raw) {
386
+ if (raw === undefined)
387
+ return [undefined, undefined, undefined];
388
+ if (/^\w+:-/.test(raw)) {
389
+ const segments = raw.split(":-");
390
+ const tail = segments.slice(1).join("-").split("-");
391
+ return [
392
+ segments[0],
393
+ tail[0],
394
+ tail.length > 1 ? tail.slice(1).join("-") : undefined,
395
+ ];
396
+ }
397
+ if (raw.includes("-")) {
398
+ const segments = raw.split("-");
399
+ return [
400
+ segments[0],
401
+ segments[1],
402
+ segments.length > 2 ? segments.slice(2).join("-") : undefined,
403
+ ];
404
+ }
405
+ return [raw, undefined, undefined];
406
+ }
407
+ function parseIsoUrn(urn) {
408
+ if (!urn.startsWith("urn:iso:std:"))
409
+ throw new Error(`Invalid ISO URN: ${urn}`);
410
+ let parts = urn.slice("urn:iso:std:".length).split(":");
411
+ // A trailing "ser" marks an all-parts identifier.
412
+ let allParts = false;
413
+ if (parts[parts.length - 1] === "ser") {
414
+ parts = parts.slice(0, -1);
415
+ allParts = true;
416
+ }
417
+ const publishers = (parts.shift() ?? "").split("-").map((p) => p.toUpperCase());
418
+ let typeCode;
419
+ if (parts[0] !== undefined && URN_TYPE_TOKENS.has(parts[0].toLowerCase())) {
420
+ typeCode = parts.shift().toLowerCase();
421
+ }
422
+ let [number, part, subpart] = parseUrnNumberPart(parts.shift());
423
+ let year;
424
+ if (/^\d{4}$/.test(parts[0] ?? ""))
425
+ year = parts.shift();
426
+ // URN-style part notation can trail the year (":10164:2019:-22").
427
+ if ((parts[0] ?? "").startsWith("-")) {
428
+ const partStr = parts.shift();
429
+ [number, part, subpart] = parseUrnNumberPart(`${number}${partStr}`);
430
+ }
431
+ if (year === undefined && /^\d{4}$/.test(parts[0] ?? ""))
432
+ year = parts.shift();
433
+ let edition;
434
+ if ((parts[0] ?? "").startsWith("ed-"))
435
+ edition = parts.shift().slice(3);
436
+ let languages;
437
+ if (parts[0] !== undefined && !/^\d+$/.test(parts[0]) &&
438
+ !/^(amd|cor|sup|add|v\d+|stage-|ed-)/i.test(parts[0]) &&
439
+ !(parts[0].toUpperCase() in URN_STAGE_ABBRS) &&
440
+ !/^[A-Za-z]+\.\d+$/.test(parts[0])) {
441
+ languages = parts.shift().split(",").map((l) => l.toUpperCase()).join("/");
442
+ }
443
+ // Stage: either "stage-XX.XX[.vN]" (harmonized) or a typed abbreviation
444
+ // ("WD", "CD.2").
445
+ let stageCode;
446
+ let stageFromAbbr;
447
+ let stageIteration;
448
+ if ((parts[0] ?? "").startsWith("stage-")) {
449
+ const stageStr = parts.shift();
450
+ stageCode = stageStr.slice(6).replace(/\.v\d+$/i, "");
451
+ const iteration = /\.v(\d+)$/i.exec(stageStr);
452
+ if (iteration !== null)
453
+ stageIteration = iteration[1];
454
+ }
455
+ else if ((parts[0] !== undefined && parts[0].toUpperCase() in URN_STAGE_ABBRS) ||
456
+ (parts[0] !== undefined && /^[A-Za-z]+\.\d+$/.test(parts[0]) &&
457
+ (parts[0].toUpperCase().split(".")[0] ?? "") in URN_STAGE_ABBRS)) {
458
+ const stageAbbr = parts[0].toUpperCase();
459
+ // A supplement-family stage directly before its own supplement type
460
+ // token ("...:FDAM:amd:1:v1") names the SUPPLEMENT's stage: consuming
461
+ // it as the base's stage builds the base as a base-less supplement,
462
+ // which cannot render (the gem guards the same way).
463
+ const family = locateStage(stageAbbr.split(".")[0] ?? "")?.typeCode;
464
+ const supplementFollows = (family !== undefined &&
465
+ ["amd", "cor", "suppl", "add"].includes(family)) &&
466
+ parts.slice(1).some((token) => URN_SUPP_TYPES.has(token.toLowerCase()) &&
467
+ (URN_TYPE_KEYS[token.toLowerCase()] ?? token.toLowerCase()) === family);
468
+ if (!supplementFollows) {
469
+ parts.shift();
470
+ if (stageAbbr.includes(".")) {
471
+ const abbrPart = stageAbbr.split(".")[0];
472
+ const iterationPart = stageAbbr.split(".")[1];
473
+ stageCode = URN_STAGE_ABBRS[abbrPart];
474
+ stageFromAbbr = abbrPart;
475
+ stageIteration = iterationPart;
476
+ }
477
+ else {
478
+ stageCode = URN_STAGE_ABBRS[stageAbbr];
479
+ stageFromAbbr = stageAbbr;
480
+ }
481
+ }
482
+ }
483
+ // Supplements (amd, cor, sup, add), left-to-right: each wraps the previous.
484
+ const supplements = [];
485
+ while (parts.length > 0) {
486
+ const supp = {};
487
+ if ((parts[0] ?? "").startsWith("stage-")) {
488
+ supp.stage = parts.shift().slice(6).replace(/\.v\d+$/i, "");
489
+ }
490
+ else if (parts[0] !== undefined && parts[0].toUpperCase() in URN_STAGE_ABBRS) {
491
+ supp.stage = URN_STAGE_ABBRS[parts.shift().toUpperCase()];
492
+ }
493
+ if (parts[0] !== undefined && URN_SUPP_TYPES.has(parts[0].toLowerCase())) {
494
+ supp.type = parts.shift().toLowerCase();
495
+ }
496
+ if (/^\d+$/.test(parts[0] ?? "")) {
497
+ if (/^\d{4}$/.test(parts[0]))
498
+ supp.year = parts.shift();
499
+ else
500
+ supp.number = parts.shift();
501
+ }
502
+ if ((parts[0] ?? "").startsWith("v")) {
503
+ const version = parts.shift().slice(1);
504
+ supp.number = version.split(".")[0];
505
+ }
506
+ if (supp.year === undefined && /^\d{4}$/.test(parts[0] ?? ""))
507
+ supp.year = parts.shift();
508
+ if (parts[0] !== undefined && !/^\d+$/.test(parts[0]) &&
509
+ !/^(amd|cor|sup|add|v\d+|stage-)/i.test(parts[0])) {
510
+ supp.languages = parts.shift().split(",").map((l) => l.toUpperCase()).join("/");
511
+ }
512
+ supplements.push(supp);
513
+ }
514
+ // Builder tree, mirroring the gem's build_identifier.
515
+ const base = {};
516
+ if (publishers[0] !== undefined && publishers[0] !== "") {
517
+ base["publisher"] = publishers[0];
518
+ if (publishers.length > 1) {
519
+ base["copublishers"] = publishers.slice(1).map((c) => ({ copublisher: c }));
520
+ }
521
+ }
522
+ if (part !== undefined || subpart !== undefined) {
523
+ base["number_with_part"] = [number, part, subpart].filter((p) => p !== undefined).join("-");
524
+ }
525
+ else if (number !== undefined) {
526
+ base["number"] = number;
527
+ }
528
+ if (year !== undefined)
529
+ base["date"] = year;
530
+ if (edition !== undefined)
531
+ base["edition"] = edition;
532
+ if (typeCode !== undefined && typeCode !== "is") {
533
+ base["type_with_stage"] = typeCode.toUpperCase();
534
+ // Pin the document class against the stage block below overwriting
535
+ // type_with_stage (the Ruby builder reads the same hint).
536
+ base["document_type"] = (URN_TYPE_KEYS[typeCode] ?? typeCode).toUpperCase();
537
+ }
538
+ if (stageCode !== undefined) {
539
+ let stage;
540
+ if (stageFromAbbr !== undefined) {
541
+ stage = locateStage(stageFromAbbr) ??
542
+ ISO_TYPED_STAGES.find((s) => s.stageCode === stageCode);
543
+ }
544
+ else if (typeCode === undefined || typeCode === "is") {
545
+ stage = urnIsStageByHarmonized(stageCode);
546
+ // FCD is a legacy spelling of the DIS stage in URN form.
547
+ if (stageCode === "40.00" && stage?.stageCode === "fcd") {
548
+ stage = ISO_TYPED_STAGES.find((s) => s.stageCode === "dis" && s.harmonized?.includes("40.00")) ?? stage;
549
+ }
550
+ }
551
+ stage ??= urnStageByHarmonized(stageCode);
552
+ if (stage !== undefined)
553
+ base["type_with_stage"] = stage.abbr[0] ?? "";
554
+ else
555
+ base["stage"] = stageCode;
556
+ if (stageIteration !== undefined)
557
+ base["stage_iteration"] = stageIteration;
558
+ }
559
+ if (languages !== undefined)
560
+ base["languages"] = languages;
561
+ let tree = base;
562
+ for (const supp of supplements) {
563
+ // A bare language token with no supplement identity names the BASE
564
+ // document (the gem merges it the same way — a wrapper would drop it).
565
+ if (supp.type === undefined && supp.stage === undefined &&
566
+ supp.number === undefined && supp.year === undefined &&
567
+ supp.languages !== undefined) {
568
+ base["languages"] = supp.languages;
569
+ continue;
570
+ }
571
+ const suppTree = {};
572
+ if (supp.stage !== undefined) {
573
+ const stage = ISO_TYPED_STAGES.find((s) => s.stageCode === supp.stage);
574
+ suppTree["type_with_stage"] = stage !== undefined
575
+ ? stage.abbr[0] ?? ""
576
+ : supp.stage.toUpperCase();
577
+ }
578
+ if (supp.type !== undefined && suppTree["type_with_stage"] === undefined) {
579
+ suppTree["type_with_stage"] = supp.type.toUpperCase();
580
+ }
581
+ if (supp.number !== undefined)
582
+ suppTree["number"] = supp.number;
583
+ if (supp.year !== undefined)
584
+ suppTree["date"] = supp.year;
585
+ if (supp.languages !== undefined)
586
+ suppTree["languages"] = supp.languages;
587
+ tree = { base: tree, ...suppTree };
588
+ }
589
+ const identifier = new IsoBuilder().build(tree);
590
+ if (allParts) {
591
+ identifier.all_parts = true;
592
+ }
593
+ return identifier;
594
+ }
337
595
  export function isoGrammarImplementation() {
338
596
  return {
597
+ // Inverse of the ISO UrnGenerator (lib/pubid/iso/urn_parser.rb): the
598
+ // reference rebuilds a builder hash straight from the positional URN
599
+ // fields, so the port does the same against the shared IsoBuilder.
600
+ parseUrn(urn) {
601
+ return parseIsoUrn(urn);
602
+ },
339
603
  parse(input) {
340
604
  return parseHuman(input);
341
605
  },
@@ -61,7 +61,7 @@ export declare abstract class IsoIdentifier extends BaseIdentifier {
61
61
  protected stageString(): string;
62
62
  /** Render with an EXPLICIT long-format flag (the supplement
63
63
  * renderer propagates its own context to the base). */
64
- renderWithFormat(formatLong: boolean): string;
64
+ renderWithFormat(formatLong: boolean, withLanguages?: boolean): string;
65
65
  /** Ruby SupplementRenderer renders the base with the wrapper's
66
66
  * detected context (stage_format_long propagates down the chain;
67
67
  * with_date being non-nil pins it against re-detection — a nested
@@ -119,7 +119,7 @@ export class IsoIdentifier extends BaseIdentifier {
119
119
  }
120
120
  /** Render with an EXPLICIT long-format flag (the supplement
121
121
  * renderer propagates its own context to the base). */
122
- renderWithFormat(formatLong) {
122
+ renderWithFormat(formatLong, withLanguages = true) {
123
123
  let result = "";
124
124
  if (this.typeKey() === "iwa") {
125
125
  const stage = this.typedStageState === undefined ? "" : stageAbbr(this.typedStageState.entry, formatLong);
@@ -141,7 +141,8 @@ export class IsoIdentifier extends BaseIdentifier {
141
141
  const num = this.numberPortion(true);
142
142
  if (num !== "")
143
143
  result += result === "" ? num : ` ${num}`;
144
- result += this.languagePortion(this.languageSingle());
144
+ if (withLanguages)
145
+ result += this.languagePortion(this.languageSingle());
145
146
  if (this.all_parts === true)
146
147
  result += " (all parts)";
147
148
  return result;
@@ -152,7 +153,11 @@ export class IsoIdentifier extends BaseIdentifier {
152
153
  * amendment renders "AMD" under a "CD Cor" top but "Amd" under a
153
154
  * "Cor." top). */
154
155
  renderAsSupplementBase(long) {
155
- return this.renderWithFormat(long);
156
+ // The reference re-renders a nested base from the four propagated
157
+ // options alone, and that rebuilt context defaults to
158
+ // with_language_code :none — only the outermost identifier renders
159
+ // its own language portion.
160
+ return this.renderWithFormat(long, false);
156
161
  }
157
162
  stageStringFor(formatLong) {
158
163
  const ts = this.typedStageState;
@@ -618,12 +623,15 @@ export class IsoSupplementIdentifier extends IsoSingleIdentifier {
618
623
  });
619
624
  static mappings = SUPPLEMENT_MAPPINGS;
620
625
  render() {
621
- return this.renderSupplement(this.stageFormatLong());
626
+ return this.renderSupplement(this.stageFormatLong(), false);
622
627
  }
623
628
  renderAsSupplementBase(long) {
624
- return this.renderSupplement(long);
629
+ return this.renderSupplement(long, true);
625
630
  }
626
- renderSupplement(long) {
631
+ // The reference renders a nested base from a rebuilt context whose
632
+ // with_language_code defaults to :none, so only the outermost
633
+ // identifier of a chain renders its language portion.
634
+ renderSupplement(long, asBase) {
627
635
  const base = this.base;
628
636
  const baseStr = base === undefined ? "" : base.renderAsSupplementBase(long);
629
637
  const ts = this.typedStageState;
@@ -635,7 +643,8 @@ export class IsoSupplementIdentifier extends IsoSingleIdentifier {
635
643
  result += " ";
636
644
  result += num;
637
645
  }
638
- result += this.languagePortion(this.languageSingle());
646
+ if (!asBase)
647
+ result += this.languagePortion(this.languageSingle());
639
648
  return result;
640
649
  }
641
650
  }
@@ -43,14 +43,19 @@ function buildRules() {
43
43
  rule("numbered_corrigendum_identifier", () => rules["base"]
44
44
  .as("base")
45
45
  .then(str("/"), str("Cor").as("type_with_stage"))
46
- .then(space, digits.as("number"))
46
+ .then(supplementSeparator(), digits.as("number"))
47
47
  .then(datePortion.maybe()));
48
48
  rule("amendment_identifier", () => rules["base"]
49
49
  .as("base")
50
50
  .then(str("/"), str("Amd").as("type_with_stage"))
51
- .then(space, digits.as("number"))
51
+ .then(supplementSeparator(), digits.as("number"))
52
52
  .then(datePortion.maybe()));
53
53
  rule("named_number", () => (str("GUM").or(str("VIM").then(str("-").then(digits).maybe()))).as("number"));
54
+ // "Amd 1" and the dotted "Amd.1:2026" BIPM prints are both accepted on
55
+ // input; rendering stays "Amd 1" (the gem's jcgm parser, pubid#574).
56
+ function supplementSeparator() {
57
+ return space.or(str(".").then(space.maybe()));
58
+ }
54
59
  rule("named_guide_identifier", () => rules["publisher"].then(space, rules["named_number"]));
55
60
  rule("identifier", () => rules["meeting_identifier"]
56
61
  .or(rules["amendment_identifier"])
@@ -688,7 +688,10 @@ function nistClass(spec) {
688
688
  }
689
689
  renderMr() {
690
690
  if (spec.style === "fips") {
691
- let result = this.seriesCode() ?? "";
691
+ // The MR form keeps a parsed publisher ("NIST.FIPS.197-upd1" is the
692
+ // DOI suffix NIST publishes); a bare FIPS invents none.
693
+ const series = this.seriesCode() ?? "";
694
+ let result = this.publisher !== undefined ? `${this.publisher}.${series}` : series;
692
695
  if (this.number !== undefined)
693
696
  result += `.${this.number}`;
694
697
  if (this.part instanceof NistPart)
@@ -650,6 +650,31 @@ export const URN_PARSERS = {
650
650
  return parse(text);
651
651
  },
652
652
  },
653
+ // lib/pubid/easc/urn_parser.rb — "urn:easc:<series>[:v]:<number>[:<year>]";
654
+ // the human face renders the Cyrillic series label
655
+ // ("ПМГ 02-2008", "ПМГ В 31-2001", "РМГ 151-2025").
656
+ easc: {
657
+ prefix: "urn:easc:",
658
+ reconstruct(body, parse) {
659
+ const parts = body.split(":");
660
+ const series = parts.shift() ?? "";
661
+ if (series === "") {
662
+ throw new Error(`EASC URN missing series: ${JSON.stringify(body)}`);
663
+ }
664
+ const label = series.toUpperCase() === "PMG" ? "ПМГ" : "РМГ";
665
+ let rest = parts;
666
+ let separator = " ";
667
+ if (/^v$/i.test(rest[0] ?? "")) {
668
+ separator = " В ";
669
+ rest = rest.slice(1);
670
+ }
671
+ const number = rest[0] ?? "";
672
+ let text = `${label}${separator}${number}`;
673
+ if (rest[1] !== undefined)
674
+ text += `-${rest[1]}`;
675
+ return parse(text);
676
+ },
677
+ },
653
678
  // lib/pubid/ieee/urn_parser.rb — "IEEE Std <code>[-<year>]". The fixture
654
679
  // pins the reference's own parse failures for narrative/artifact rows.
655
680
  ieee: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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",
@@ -1 +1 @@
1
- {"bindings":null,"version":"0.1.0","grammar":"PubidJcgm","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Entity":{"atom":36}},{"Entity":{"atom":3}},{"Str":{"pattern":"JCGM"}},{"Named":{"name":"publisher","atom":2}},{"Entity":{"atom":5}},{"Str":{"pattern":" "}},{"Entity":{"atom":9}},{"Entity":{"atom":8}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":7,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"number","atom":6}},{"Entity":{"atom":16}},{"Str":{"pattern":"st"}},{"Str":{"pattern":"nd"}},{"Str":{"pattern":"rd"}},{"Str":{"pattern":"th"}},{"Alternative":{"atoms":[12,13,14,15]}},{"Entity":{"atom":5}},{"Str":{"pattern":"Meeting"}},{"Named":{"name":"type_with_stage","atom":18}},{"Entity":{"atom":5}},{"Str":{"pattern":"("}},{"Entity":{"atom":31}},{"Entity":{"atom":26}},{"Str":{"pattern":"19"}},{"Str":{"pattern":"20"}},{"Alternative":{"atoms":[24,25]}},{"Entity":{"atom":8}},{"Repetition":{"atom":27,"min":2,"max":null,"tag":"Repetition"}},{"Entity":{"atom":8}},{"Lookahead":{"atom":29,"positive":false}},{"Sequence":{"atoms":[23,28,30]}},{"Named":{"name":"date","atom":22}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[20,21,32,33]}},{"Repetition":{"atom":34,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[1,4,10,11,17,19,35]}},{"Entity":{"atom":101}},{"Entity":{"atom":86}},{"Entity":{"atom":3}},{"Entity":{"atom":5}},{"Entity":{"atom":51}},{"Entity":{"atom":47}},{"Str":{"pattern":"GUM-"}},{"Entity":{"atom":8}},{"Repetition":{"atom":44,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"gum_number","atom":45}},{"Sequence":{"atoms":[43,46]}},{"Entity":{"atom":50}},{"Entity":{"atom":9}},{"Named":{"name":"number","atom":49}},{"Alternative":{"atoms":[42,48]}},{"Entity":{"atom":72}},{"Entity":{"atom":54}},{"Str":{"pattern":":"}},{"Entity":{"atom":67}},{"Entity":{"atom":31}},{"Entity":{"atom":58}},{"Str":{"pattern":"-"}},{"Entity":{"atom":61}},{"Entity":{"atom":8}},{"Repetition":{"atom":60,"min":2,"max":null,"tag":"Repetition"}},{"Entity":{"atom":58}},{"Entity":{"atom":65}},{"Entity":{"atom":8}},{"Repetition":{"atom":64,"min":2,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[56,57,59,62,63]}},{"Named":{"name":"date","atom":66}},{"Entity":{"atom":70}},{"Entity":{"atom":31}},{"Named":{"name":"date","atom":69}},{"Alternative":{"atoms":[55,68]}},{"Sequence":{"atoms":[53,71]}},{"Repetition":{"atom":52,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":84}},{"Str":{"pattern":"("}},{"Str":{"pattern":"E/F"}},{"Str":{"pattern":"F/E"}},{"Alternative":{"atoms":[76,77]}},{"Entity":{"atom":80}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Alternative":{"atoms":[78,79]}},{"Named":{"name":"languages","atom":81}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[75,82,83]}},{"Repetition":{"atom":74,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[39,40,41,73,85]}},{"Named":{"name":"base","atom":38}},{"Entity":{"atom":89}},{"Str":{"pattern":"/"}},{"Str":{"pattern":"Amd"}},{"Named":{"name":"type_with_stage","atom":90}},{"Entity":{"atom":5}},{"Entity":{"atom":9}},{"Named":{"name":"number","atom":93}},{"Entity":{"atom":54}},{"Entity":{"atom":67}},{"Entity":{"atom":70}},{"Alternative":{"atoms":[96,97]}},{"Sequence":{"atoms":[95,98]}},{"Repetition":{"atom":99,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[87,88,91,92,94,100]}},{"Entity":{"atom":108}},{"Entity":{"atom":86}},{"Named":{"name":"base","atom":103}},{"Entity":{"atom":5}},{"Str":{"pattern":"Corrigendum"}},{"Named":{"name":"type_with_stage","atom":106}},{"Sequence":{"atoms":[104,105,107]}},{"Entity":{"atom":124}},{"Entity":{"atom":86}},{"Named":{"name":"base","atom":110}},{"Entity":{"atom":89}},{"Str":{"pattern":"Cor"}},{"Named":{"name":"type_with_stage","atom":113}},{"Entity":{"atom":5}},{"Entity":{"atom":9}},{"Named":{"name":"number","atom":116}},{"Entity":{"atom":54}},{"Entity":{"atom":67}},{"Entity":{"atom":70}},{"Alternative":{"atoms":[119,120]}},{"Sequence":{"atoms":[118,121]}},{"Repetition":{"atom":122,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[111,112,114,115,117,123]}},{"Entity":{"atom":86}},{"Entity":{"atom":139}},{"Entity":{"atom":3}},{"Entity":{"atom":5}},{"Entity":{"atom":138}},{"Str":{"pattern":"GUM"}},{"Str":{"pattern":"VIM"}},{"Entity":{"atom":58}},{"Entity":{"atom":9}},{"Sequence":{"atoms":[132,133]}},{"Repetition":{"atom":134,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[131,135]}},{"Alternative":{"atoms":[130,136]}},{"Named":{"name":"number","atom":137}},{"Sequence":{"atoms":[127,128,129]}},{"Alternative":{"atoms":[0,37,102,109,125,126]}}],"root":140},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule identifier: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 3 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 3 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 4 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 4 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 5 and 6 are order-dependent (shared first bytes; ordered choice is decisive)"]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"JCGM 200:2008(F)","expect":{}},{"entry":"identifier","kind":"accept","input":"JCGM 100:2008","expect":{}},{"entry":"identifier","kind":"accept","input":"JCGM 1st Meeting (2004)","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}}],"docs":{},"source":"# Port of lib/pubid/jcgm/parser.rb: JCGM base, meeting, amendment and\n# corrigendum identifiers, and the named GUM/VIM guides.\n\ngrammar PubidJcgm version \"0.1.0\" {\n century = \"19\" / \"20\"\n year_digits = century 2*digit !digit\n month_digits = 2*digit\n day_digits = 2*digit\n digits = 1*digit\n publisher = \"JCGM\" as publisher\n ordinal_suffix = \"st\" / \"nd\" / \"rd\" / \"th\"\n gum_number = \"GUM-\" (1*digit) as gum_number\n standard_number = digits as number\n number_portion = gum_number / standard_number\n full_date = (year_digits dash month_digits dash day_digits) as date\n year_only = year_digits as date\n date_portion = colon (full_date / year_only)\n language_portion = \"(\" ((\"E/F\" / \"F/E\") / upper) as languages \")\"\n base = publisher space number_portion [date_portion] [language_portion]\n meeting_identifier = publisher space digits as number ordinal_suffix space \"Meeting\" as type_with_stage [(space \"(\" year_digits as date \")\")]\n amendment_identifier = (base as base) slash \"Amd\" as type_with_stage space digits as number [(colon (full_date / year_only))]\n corrigendum_identifier = (base as base) space \"Corrigendum\" as type_with_stage\n numbered_corrigendum_identifier = (base as base) slash \"Cor\" as type_with_stage space digits as number [(colon (full_date / year_only))]\n named_number = (\"GUM\" / (\"VIM\" [(dash digits)])) as number\n named_guide_identifier = publisher space named_number\n identifier = meeting_identifier / amendment_identifier / corrigendum_identifier / numbered_corrigendum_identifier / base / named_guide_identifier\n upper = %x41-5A\n digit = %x30-39\n slash = \"/\"\n dash = \"-\"\n colon = \":\"\n space = \" \"\n entry identifier: identifier\n\n test {\n accept \"JCGM 200:2008(F)\"\n accept \"JCGM 100:2008\"\n accept \"JCGM 1st Meeting (2004)\"\n reject \"nonsense\"\n }\n}\n","source_hash":"c4a5cc929b1d59a996d52826fb4199659b3eaee6c645bc5064ce46804be96b59","compiler_version":"1.3.93","checksum":"sha256:b1816fde60067cea27889e934e7bc6064a1618c5f61fdcfb574318bbbb75d496"}
1
+ {"bindings":null,"version":"0.1.0","grammar":"PubidJcgm","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Entity":{"atom":36}},{"Entity":{"atom":3}},{"Str":{"pattern":"JCGM"}},{"Named":{"name":"publisher","atom":2}},{"Entity":{"atom":5}},{"Str":{"pattern":" "}},{"Entity":{"atom":9}},{"Entity":{"atom":8}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":7,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"number","atom":6}},{"Entity":{"atom":16}},{"Str":{"pattern":"st"}},{"Str":{"pattern":"nd"}},{"Str":{"pattern":"rd"}},{"Str":{"pattern":"th"}},{"Alternative":{"atoms":[12,13,14,15]}},{"Entity":{"atom":5}},{"Str":{"pattern":"Meeting"}},{"Named":{"name":"type_with_stage","atom":18}},{"Entity":{"atom":5}},{"Str":{"pattern":"("}},{"Entity":{"atom":31}},{"Entity":{"atom":26}},{"Str":{"pattern":"19"}},{"Str":{"pattern":"20"}},{"Alternative":{"atoms":[24,25]}},{"Entity":{"atom":8}},{"Repetition":{"atom":27,"min":2,"max":null,"tag":"Repetition"}},{"Entity":{"atom":8}},{"Lookahead":{"atom":29,"positive":false}},{"Sequence":{"atoms":[23,28,30]}},{"Named":{"name":"date","atom":22}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[20,21,32,33]}},{"Repetition":{"atom":34,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[1,4,10,11,17,19,35]}},{"Entity":{"atom":106}},{"Entity":{"atom":86}},{"Entity":{"atom":3}},{"Entity":{"atom":5}},{"Entity":{"atom":51}},{"Entity":{"atom":47}},{"Str":{"pattern":"GUM-"}},{"Entity":{"atom":8}},{"Repetition":{"atom":44,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"gum_number","atom":45}},{"Sequence":{"atoms":[43,46]}},{"Entity":{"atom":50}},{"Entity":{"atom":9}},{"Named":{"name":"number","atom":49}},{"Alternative":{"atoms":[42,48]}},{"Entity":{"atom":72}},{"Entity":{"atom":54}},{"Str":{"pattern":":"}},{"Entity":{"atom":67}},{"Entity":{"atom":31}},{"Entity":{"atom":58}},{"Str":{"pattern":"-"}},{"Entity":{"atom":61}},{"Entity":{"atom":8}},{"Repetition":{"atom":60,"min":2,"max":null,"tag":"Repetition"}},{"Entity":{"atom":58}},{"Entity":{"atom":65}},{"Entity":{"atom":8}},{"Repetition":{"atom":64,"min":2,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[56,57,59,62,63]}},{"Named":{"name":"date","atom":66}},{"Entity":{"atom":70}},{"Entity":{"atom":31}},{"Named":{"name":"date","atom":69}},{"Alternative":{"atoms":[55,68]}},{"Sequence":{"atoms":[53,71]}},{"Repetition":{"atom":52,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":84}},{"Str":{"pattern":"("}},{"Str":{"pattern":"E/F"}},{"Str":{"pattern":"F/E"}},{"Alternative":{"atoms":[76,77]}},{"Entity":{"atom":80}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Alternative":{"atoms":[78,79]}},{"Named":{"name":"languages","atom":81}},{"Str":{"pattern":")"}},{"Sequence":{"atoms":[75,82,83]}},{"Repetition":{"atom":74,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[39,40,41,73,85]}},{"Named":{"name":"base","atom":38}},{"Entity":{"atom":89}},{"Str":{"pattern":"/"}},{"Str":{"pattern":"Amd"}},{"Named":{"name":"type_with_stage","atom":90}},{"Entity":{"atom":5}},{"Str":{"pattern":"."}},{"Entity":{"atom":5}},{"Repetition":{"atom":94,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[93,95]}},{"Alternative":{"atoms":[92,96]}},{"Entity":{"atom":9}},{"Named":{"name":"number","atom":98}},{"Entity":{"atom":54}},{"Entity":{"atom":67}},{"Entity":{"atom":70}},{"Alternative":{"atoms":[101,102]}},{"Sequence":{"atoms":[100,103]}},{"Repetition":{"atom":104,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[87,88,91,97,99,105]}},{"Entity":{"atom":113}},{"Entity":{"atom":86}},{"Named":{"name":"base","atom":108}},{"Entity":{"atom":5}},{"Str":{"pattern":"Corrigendum"}},{"Named":{"name":"type_with_stage","atom":111}},{"Sequence":{"atoms":[109,110,112]}},{"Entity":{"atom":134}},{"Entity":{"atom":86}},{"Named":{"name":"base","atom":115}},{"Entity":{"atom":89}},{"Str":{"pattern":"Cor"}},{"Named":{"name":"type_with_stage","atom":118}},{"Entity":{"atom":5}},{"Str":{"pattern":"."}},{"Entity":{"atom":5}},{"Repetition":{"atom":122,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[121,123]}},{"Alternative":{"atoms":[120,124]}},{"Entity":{"atom":9}},{"Named":{"name":"number","atom":126}},{"Entity":{"atom":54}},{"Entity":{"atom":67}},{"Entity":{"atom":70}},{"Alternative":{"atoms":[129,130]}},{"Sequence":{"atoms":[128,131]}},{"Repetition":{"atom":132,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[116,117,119,125,127,133]}},{"Entity":{"atom":86}},{"Entity":{"atom":149}},{"Entity":{"atom":3}},{"Entity":{"atom":5}},{"Entity":{"atom":148}},{"Str":{"pattern":"GUM"}},{"Str":{"pattern":"VIM"}},{"Entity":{"atom":58}},{"Entity":{"atom":9}},{"Sequence":{"atoms":[142,143]}},{"Repetition":{"atom":144,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[141,145]}},{"Alternative":{"atoms":[140,146]}},{"Named":{"name":"number","atom":147}},{"Sequence":{"atoms":[137,138,139]}},{"Alternative":{"atoms":[0,37,107,114,135,136]}}],"root":150},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule identifier: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 1 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 2 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 3 and 4 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 3 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 3 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 4 and 5 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 4 and 6 are order-dependent (shared first bytes; ordered choice is decisive)","rule identifier: branches 5 and 6 are order-dependent (shared first bytes; ordered choice is decisive)"]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"JCGM 200:2008(F)","expect":{}},{"entry":"identifier","kind":"accept","input":"JCGM 100:2008","expect":{}},{"entry":"identifier","kind":"accept","input":"JCGM 1st Meeting (2004)","expect":{}},{"entry":"identifier","kind":"accept","input":"JCGM 100:2008/Amd.1:2026","expect":{}},{"entry":"identifier","kind":"accept","input":"JCGM 101:2008/Cor.1:2009","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}}],"docs":{},"source":"# Port of lib/pubid/jcgm/parser.rb: JCGM base, meeting, amendment and\n# corrigendum identifiers, and the named GUM/VIM guides.\n\ngrammar PubidJcgm version \"0.1.0\" {\n century = \"19\" / \"20\"\n year_digits = century 2*digit !digit\n month_digits = 2*digit\n day_digits = 2*digit\n digits = 1*digit\n publisher = \"JCGM\" as publisher\n ordinal_suffix = \"st\" / \"nd\" / \"rd\" / \"th\"\n gum_number = \"GUM-\" (1*digit) as gum_number\n standard_number = digits as number\n number_portion = gum_number / standard_number\n full_date = (year_digits dash month_digits dash day_digits) as date\n year_only = year_digits as date\n date_portion = colon (full_date / year_only)\n language_portion = \"(\" ((\"E/F\" / \"F/E\") / upper) as languages \")\"\n base = publisher space number_portion [date_portion] [language_portion]\n meeting_identifier = publisher space digits as number ordinal_suffix space \"Meeting\" as type_with_stage [(space \"(\" year_digits as date \")\")]\n amendment_identifier = (base as base) slash \"Amd\" as type_with_stage (space / (\".\" [space])) digits as number [(colon (full_date / year_only))]\n corrigendum_identifier = (base as base) space \"Corrigendum\" as type_with_stage\n numbered_corrigendum_identifier = (base as base) slash \"Cor\" as type_with_stage (space / (\".\" [space])) digits as number [(colon (full_date / year_only))]\n named_number = (\"GUM\" / (\"VIM\" [(dash digits)])) as number\n named_guide_identifier = publisher space named_number\n identifier = meeting_identifier / amendment_identifier / corrigendum_identifier / numbered_corrigendum_identifier / base / named_guide_identifier\n upper = %x41-5A\n digit = %x30-39\n slash = \"/\"\n dash = \"-\"\n colon = \":\"\n space = \" \"\n entry identifier: identifier\n\n test {\n accept \"JCGM 200:2008(F)\"\n accept \"JCGM 100:2008\"\n accept \"JCGM 1st Meeting (2004)\"\n accept \"JCGM 100:2008/Amd.1:2026\"\n accept \"JCGM 101:2008/Cor.1:2009\"\n reject \"nonsense\"\n }\n}\n","source_hash":"d380467460b56523c7a36149a52e36904cd94a76406359f763847cfd4b177e8b","compiler_version":"1.3.93","checksum":"sha256:bf00524a2d18b2ab81764b53960b410509b53e924ced7ec438ab2494fcd0fc7e"}