@pubid/pubid 0.1.8 → 0.1.9

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.
Files changed (76) hide show
  1. package/artifact/corpus.json.gz +0 -0
  2. package/dist/flavors/cen_cenelec/grammar.js +10 -4
  3. package/dist/flavors/cen_cenelec/index.js +20 -1
  4. package/dist/flavors/cen_cenelec/model.d.ts +1 -0
  5. package/dist/flavors/cen_cenelec/model.js +4 -1
  6. package/dist/flavors/evs/grammar.js +53 -7
  7. package/dist/flavors/evs/index.js +74 -7
  8. package/dist/flavors/evs/model.d.ts +37 -1
  9. package/dist/flavors/evs/model.js +173 -11
  10. package/dist/flavors/ieee/artifact_tree.d.ts +2 -0
  11. package/dist/flavors/ieee/artifact_tree.js +36 -0
  12. package/dist/flavors/ieee/grammar.js +99 -19
  13. package/dist/flavors/ieee/index.js +312 -18
  14. package/dist/flavors/ieee/model.d.ts +5 -0
  15. package/dist/flavors/ieee/model.js +130 -20
  16. package/dist/flavors/ieee/preprocessor.js +3 -6
  17. package/dist/flavors/nist/grammar.js +18 -9
  18. package/dist/flavors/nist/index.js +16 -7
  19. package/dist/flavors/nist/preprocessor.js +9 -1
  20. package/dist/parg/runtime.d.ts +25 -0
  21. package/dist/parg/runtime.js +76 -0
  22. package/dist/parg/suite.d.ts +4 -0
  23. package/dist/parg/suite.js +14 -0
  24. package/dist/urn/parsers.js +7 -0
  25. package/package.json +6 -4
  26. package/vendor/parg/adobe.json +1 -0
  27. package/vendor/parg/aiee.json +1 -0
  28. package/vendor/parg/amca.json +1 -0
  29. package/vendor/parg/ansi.json +1 -0
  30. package/vendor/parg/api.json +1 -0
  31. package/vendor/parg/ashrae.json +1 -0
  32. package/vendor/parg/asme.json +1 -0
  33. package/vendor/parg/astm.json +1 -0
  34. package/vendor/parg/bipm.json +1 -0
  35. package/vendor/parg/bsi.json +1 -0
  36. package/vendor/parg/calconnect.json +1 -0
  37. package/vendor/parg/ccsds.json +1 -0
  38. package/vendor/parg/cen_cenelec.json +1 -0
  39. package/vendor/parg/cie.json +1 -0
  40. package/vendor/parg/csa.json +1 -0
  41. package/vendor/parg/doi.json +1 -0
  42. package/vendor/parg/easc.json +1 -0
  43. package/vendor/parg/ecma.json +1 -0
  44. package/vendor/parg/etsi.json +1 -0
  45. package/vendor/parg/evs.json +1 -0
  46. package/vendor/parg/gb.json +1 -0
  47. package/vendor/parg/gost.json +1 -0
  48. package/vendor/parg/iala.json +1 -0
  49. package/vendor/parg/iana.json +1 -0
  50. package/vendor/parg/idf.json +1 -0
  51. package/vendor/parg/iec.json +1 -0
  52. package/vendor/parg/ieee.json +1 -0
  53. package/vendor/parg/ietf.json +1 -0
  54. package/vendor/parg/ire.json +1 -0
  55. package/vendor/parg/isbn.json +1 -0
  56. package/vendor/parg/iso.json +1 -0
  57. package/vendor/parg/itu.json +1 -0
  58. package/vendor/parg/jcgm.json +1 -0
  59. package/vendor/parg/jis.json +1 -0
  60. package/vendor/parg/nesc.json +1 -0
  61. package/vendor/parg/nist.json +1 -0
  62. package/vendor/parg/oasis.json +1 -0
  63. package/vendor/parg/ogc.json +1 -0
  64. package/vendor/parg/oiml.json +1 -0
  65. package/vendor/parg/omg.json +1 -0
  66. package/vendor/parg/parg.json +1 -0
  67. package/vendor/parg/plateau.json +1 -0
  68. package/vendor/parg/sae.json +1 -0
  69. package/vendor/parg/tgpp.json +1 -0
  70. package/vendor/parg/un.json +1 -0
  71. package/vendor/parg/w3c.json +1 -0
  72. package/vendor/parg/xsf.json +1 -0
  73. package/dist/pg/runtime.d.ts +0 -85
  74. package/dist/pg/runtime.js +0 -170
  75. package/dist/pg/suite.d.ts +0 -20
  76. package/dist/pg/suite.js +0 -90
@@ -117,7 +117,16 @@ export class IeeeCode {
117
117
  return result;
118
118
  }
119
119
  }
120
- const DRAFT_MONTH_NAMES = {
120
+ // Canonical month spelling (mirrors the gem's CANONICAL_ABBREVIATIONS):
121
+ // full names normalize onto the crawl abbreviation, and "September"
122
+ // keeps its IEEE "Sept" form — the 3-letter "Sep" folds onto it, so
123
+ // every spelling of September renders and hashes alike.
124
+ const DRAFT_MONTH_CANONICAL = {
125
+ January: "Jan", February: "Feb", March: "Mar", April: "Apr",
126
+ June: "Jun", July: "Jul", August: "Aug", September: "Sept",
127
+ October: "Oct", November: "Nov", December: "Dec", Sep: "Sept",
128
+ };
129
+ export const DRAFT_MONTH_NAMES = {
121
130
  January: "1", February: "2", March: "3", April: "4", May: "5", June: "6",
122
131
  July: "7", August: "8", September: "9", October: "10", November: "11",
123
132
  December: "12", Jan: "1", Feb: "2", Mar: "3", Apr: "4", Jun: "6",
@@ -139,12 +148,27 @@ export class IeeeDraft {
139
148
  this.commaBeforeMonthSet = "comma_before_month" in attrs;
140
149
  this.version = String(attrs["version"] ?? "");
141
150
  this.revision = attrs["revision"];
142
- this.isoStage = attrs["iso_stage"];
143
151
  this.isoIteration = attrs["iso_iteration"];
152
+ // The grammar's ordered alternation captures the stage WITH its
153
+ // draft-marker D when the spelling doubles it ("=DDIS"): the marker
154
+ // is notation, the stage is the bare ISO/IEC token (the reference's
155
+ // Draft constructor strips it).
156
+ let stage = attrs["iso_stage"];
157
+ if (stage !== undefined && stage.startsWith("D") &&
158
+ ["PWI", "NP", "WD", "CD", "CDV", "DIS", "FDIS"].includes(stage.slice(1))) {
159
+ stage = stage.slice(1);
160
+ }
161
+ this.isoStage = stage;
144
162
  this.year = attrs["year"];
145
- this.originalMonth = attrs["month"];
146
- this.month = attrs["month"] !== undefined
147
- ? DRAFT_MONTH_NAMES[String(attrs["month"])] ?? String(attrs["month"])
163
+ // Canonical month spelling: the crawl abbreviation ("September" →
164
+ // "Sept"); the 3-letter "Sep" folds onto the IEEE "Sept" form, so
165
+ // every spelling of September renders — and hashes — alike.
166
+ const rawMonth = attrs["month"];
167
+ this.originalMonth = rawMonth !== undefined
168
+ ? DRAFT_MONTH_CANONICAL[rawMonth.replace(/\.$/, "")] ?? rawMonth
169
+ : undefined;
170
+ this.month = rawMonth !== undefined
171
+ ? DRAFT_MONTH_NAMES[String(this.originalMonth)] ?? String(this.originalMonth)
148
172
  : undefined;
149
173
  this.day = attrs["day"];
150
174
  this.commaBeforeMonth = attrs["comma_before_month"] === true;
@@ -363,10 +387,17 @@ export class IeeeIdentifier extends BaseIdentifier {
363
387
  draft_status: { type: "string" },
364
388
  draft: { type: "string" },
365
389
  revision: { type: "string" },
390
+ rev_sep: { type: "string" },
391
+ rev_bare: { type: "boolean" },
366
392
  edition: { type: "string" },
367
393
  month: { type: "string" },
368
394
  day: { type: "string" },
369
395
  redline: { type: "boolean", default: false },
396
+ // Provenance (gem#520): the month arrived glued to the identity year
397
+ // on the code ("ANSI/IEEE 21-1976-11") rather than as a trailing
398
+ // print date. The render keys on the month being numeric; this
399
+ // records how it arrived.
400
+ month_glued: { type: "boolean", default: false },
370
401
  edition_month: { type: "string" },
371
402
  space_before_draft: { type: "boolean", default: false },
372
403
  typed_stage: { type: IeeeTypedStage },
@@ -384,7 +415,7 @@ export class IeeeIdentifier extends BaseIdentifier {
384
415
  interpretation: { type: "boolean", default: false },
385
416
  relationships: { type: "string", collection: true },
386
417
  });
387
- static mappings = keyValue({ wire: "publisher", to: "publisher" }, { wire: "copublisher", to: "copublisher" }, { wire: "year", to: "year" }, { wire: "type", to: "type" }, { wire: "draft_status", to: "draft_status" }, draftWire, { wire: "revision", to: "revision" }, { wire: "edition", to: "edition" }, { wire: "month", to: "month" }, { wire: "day", to: "day" }, { wire: "redline", to: "redline" }, { wire: "edition_month", to: "edition_month" }, { wire: "space_before_draft", to: "space_before_draft" }, typedStageWire, { wire: "parenthetical_content", to: "parenthetical_content" }, { wire: "nickname", to: "nickname" }, { wire: "adoption", to: "adoption" }, { wire: "amendment_to", to: "amendment_to" }, { wire: "note", to: "note" }, { wire: "iso_identifier", to: "iso_identifier" }, { wire: "revision_of", to: "revision_of" }, { wire: "ashrae_number", to: "ashrae_number" }, { wire: "ashrae_year", to: "ashrae_year" }, { wire: "crossref", to: "crossref" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "interpretation", to: "interpretation" }, {
418
+ static mappings = keyValue({ wire: "publisher", to: "publisher" }, { wire: "copublisher", to: "copublisher" }, { wire: "year", to: "year" }, { wire: "type", to: "type" }, { wire: "draft_status", to: "draft_status" }, draftWire, { wire: "revision", to: "revision" }, { wire: "rev_sep", to: "rev_sep" }, { wire: "rev_bare", to: "rev_bare" }, { wire: "edition", to: "edition" }, { wire: "month", to: "month" }, { wire: "day", to: "day" }, { wire: "redline", to: "redline" }, { wire: "month_glued", to: "month_glued" }, { wire: "edition_month", to: "edition_month" }, { wire: "space_before_draft", to: "space_before_draft" }, typedStageWire, { wire: "parenthetical_content", to: "parenthetical_content" }, { wire: "nickname", to: "nickname" }, { wire: "adoption", to: "adoption" }, { wire: "amendment_to", to: "amendment_to" }, { wire: "note", to: "note" }, { wire: "iso_identifier", to: "iso_identifier" }, { wire: "revision_of", to: "revision_of" }, { wire: "ashrae_number", to: "ashrae_number" }, { wire: "ashrae_year", to: "ashrae_year" }, { wire: "crossref", to: "crossref" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "interpretation", to: "interpretation" }, {
388
419
  wire: "relationships",
389
420
  to: "relationships",
390
421
  toWire: (model) => {
@@ -446,6 +477,12 @@ export function renderIeeeBase(id) {
446
477
  let typeStr = id.type;
447
478
  if (typeStr.startsWith("P"))
448
479
  typeStr = typeStr.slice(1);
480
+ // A draft carries the word Draft alone in every face (the C2
481
+ // ruling): Std/Standard never renders on a draft - a status-less
482
+ // draft ("IEEE Draft Std P802.3/D2.0") renders "IEEE Draft
483
+ // P802.3/D2.0".
484
+ if (id.draft !== undefined && id.draft_status === undefined)
485
+ typeStr = "Draft";
449
486
  if (typeStr.trim() !== "")
450
487
  parts.push(typeStr);
451
488
  }
@@ -455,6 +492,8 @@ export function renderIeeeBase(id) {
455
492
  // dates (catalogue metadata, not identity); wider sets and
456
493
  // space-separated dates print as spelled.
457
494
  const iecIeeOnly = id.publisher === "IEC" && JSON.stringify(id.copublisher) === '["IEEE"]';
495
+ // Short format = numeric month (glued face); the comma face is a month name.
496
+ const gluedMonth = /^\d{1,2}$/.test(id.month ?? "");
458
497
  if (codeObj !== undefined) {
459
498
  let result = codeObj.render();
460
499
  // Prepend P if this is a project AND code doesn't already have P.
@@ -467,11 +506,29 @@ export function renderIeeeBase(id) {
467
506
  !result.startsWith("P")) {
468
507
  result = `P${result}`;
469
508
  }
470
- if (id.year !== undefined && draftObj === undefined && id.edition === undefined && id.month === undefined) {
509
+ // A base year dash-attaches to the code when nothing else carries
510
+ // it: no edition, no identity-month date - and either no draft at
511
+ // all, or a draft that carries its OWN date (the draft+corrigendum
512
+ // family: "PC37.09-1999/D7.2, Nov 2006").
513
+ // A numeric identity month is the SHORT format: the year stays on
514
+ // the code and the month dash-joins after it, whatever the input
515
+ // spelling ("IEEE P15026.1, 05 2014" renders "P15026.1-2014-05").
516
+ const yearOnCode = id.year !== undefined && id.edition === undefined &&
517
+ (id.month === undefined || gluedMonth) &&
518
+ (draftObj === undefined || draftObj.year !== undefined);
519
+ if (yearOnCode) {
471
520
  result += `-${id.year}`;
472
521
  }
522
+ if (gluedMonth && id.month !== undefined) {
523
+ result += `-${id.month}`;
524
+ }
525
+ // The lettered revision keeps its printed separator ("P802.11-REVmc"
526
+ // renders "P802.11-Revmc"); the bare word with no id still prints
527
+ // "Rev" (pubid ruling 2026-10-14).
473
528
  if (id.revision !== undefined)
474
- result += `Rev${id.revision}`;
529
+ result += `${id.rev_sep ?? ""}Rev${id.revision}`;
530
+ if (id.rev_bare === true)
531
+ result += `${id.rev_sep ?? ""}Rev`;
475
532
  if (draftObj !== undefined) {
476
533
  let printed = draftObj.render();
477
534
  // The draft designator carries its date on every lead
@@ -510,13 +567,21 @@ export function renderIeeeBase(id) {
510
567
  parts.push(editionStr);
511
568
  }
512
569
  let result = parts.join(" ");
513
- if (id.month !== undefined && !iecIeeOnly) {
570
+ if (id.month !== undefined && !gluedMonth && !iecIeeOnly) {
514
571
  result += `, ${id.month}`;
515
572
  if (id.day !== undefined)
516
573
  result += ` ${id.day}`;
517
574
  if (id.year !== undefined && id.edition === undefined)
518
575
  result += ` ${id.year}`;
519
576
  }
577
+ else if (id.year !== undefined && draftObj !== undefined && id.edition === undefined &&
578
+ draftObj?.year === undefined && !iecIeeOnly) {
579
+ // A year-only date trailing the draft ("ANSI N42.34/D9a, 2015")
580
+ // keeps its comma form instead of being silently dropped; a year
581
+ // whose draft carries its own date dash-attached to the code above
582
+ // and never repeats.
583
+ result += `, ${id.year}`;
584
+ }
520
585
  const parentheticals = [];
521
586
  if (id.reaffirmed !== undefined && id.reaffirmed.trim() !== "") {
522
587
  parentheticals.push(`(R${id.reaffirmed})`);
@@ -559,6 +624,13 @@ class IeeeUrnGenerator extends BaseUrnGenerator {
559
624
  push("redline");
560
625
  if (special === undefined && id.interpretation === true)
561
626
  push("int");
627
+ // A NESC preprint is a draft-stage document distinct from the
628
+ // published edition it precedes — without this component it would
629
+ // share the edition's URN (urn:ieee:ieee:C2:2012).
630
+ if (id.constructor.polymorphicName ===
631
+ "pubid:ieee:nesc-preprint") {
632
+ push("preprint");
633
+ }
562
634
  if (id.ashrae_number !== undefined) {
563
635
  let ashrae = `ashrae.${id.ashrae_number}`;
564
636
  if (id.ashrae_year !== undefined)
@@ -836,8 +908,14 @@ export const SiStandardClass = ieeeClass({ kind: "si-standard" }, {
836
908
  result += ` ${part}`;
837
909
  }
838
910
  }
839
- if (id.month !== undefined && id.year !== undefined)
840
- result += `, ${id.month} ${id.year}`;
911
+ if (id.month !== undefined && id.year !== undefined) {
912
+ // Short format (numeric month) renders glued; the comma marks a
913
+ // month name (the long format).
914
+ if (/^\d{1,2}$/.test(id.month))
915
+ result += `-${id.year}-${id.month}`;
916
+ else
917
+ result += `, ${id.month} ${id.year}`;
918
+ }
841
919
  else if (id.year !== undefined)
842
920
  result += `-${id.year}`;
843
921
  return result;
@@ -1030,16 +1108,28 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1030
1108
  // P-state is identity-bearing and prints as spelled - it is never
1031
1109
  // added or stripped here.
1032
1110
  if (leadParty === "ISO") {
1111
+ // A D= designator row ("P26511/D8=DDIS.3") prints the designator
1112
+ // after the code, not the stage as a word; only genuine stage
1113
+ // rows print the stage position.
1114
+ const designatorDraft = id["ieee_draft"];
1115
+ const printsDesignator = designatorDraft !== undefined && /^D\d*=/.test(designatorDraft);
1033
1116
  const parts = [];
1034
1117
  if (publishers !== undefined && publishers.length > 0)
1035
1118
  parts.push(publishers.join("/"));
1036
- if (id.typed_stage !== undefined) {
1037
- parts.push(id.typed_stage.record.iso_stage_equivalent ?? id.typed_stage.render());
1038
- }
1039
- else if (isoStage !== undefined) {
1040
- parts.push(isoStage);
1119
+ if (!printsDesignator) {
1120
+ if (id.typed_stage !== undefined) {
1121
+ parts.push(id.typed_stage.record.iso_stage_equivalent ?? id.typed_stage.render());
1122
+ }
1123
+ else if (isoStage !== undefined) {
1124
+ parts.push(isoStage);
1125
+ }
1041
1126
  }
1042
- const codeStr = codeObj?.render() ?? "";
1127
+ let codeStr = codeObj?.render() ?? "";
1128
+ // A P-prefixed joint project prints its parts dot-joined on the
1129
+ // ISO face ("ISO/IEEE DIS P15500.1"), whatever separator the
1130
+ // parse captured ("IEC/IEEE FDIS P60214-2-2018(E)" aliases in).
1131
+ if (codeStr.startsWith("P"))
1132
+ codeStr = codeStr.replaceAll("-", ".");
1043
1133
  if (codeStr !== "")
1044
1134
  parts.push(codeStr);
1045
1135
  let result = parts.join(" ");
@@ -1049,12 +1139,16 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1049
1139
  // the ISO-led print; only the bare language/edition marker ("(E)")
1050
1140
  // prints - a relationship narrative is metadata, not identity.
1051
1141
  const jointDraft = id["ieee_draft"];
1052
- if (jointDraft !== undefined && jointDraft.startsWith("D="))
1142
+ if (jointDraft !== undefined && /^D\d*=/.test(jointDraft))
1053
1143
  result += `/${jointDraft}`;
1054
1144
  const marker = id["parenthetical_content"];
1055
1145
  if (marker !== undefined && /^[A-Z](?:\s*[/&]\s*[A-Z])*$/.test(marker)) {
1056
1146
  result += ` (${marker})`;
1057
1147
  }
1148
+ // The joint ISO-print redline is a distinct document (pubid#215):
1149
+ // "15289:2015(E) - Redline".
1150
+ if (id.redline === true)
1151
+ result += " - Redline";
1058
1152
  return result;
1059
1153
  }
1060
1154
  // IEEE format
@@ -1074,6 +1168,8 @@ export const JointDevelopmentClass = ieeeClass({ kind: "joint-development" }, {
1074
1168
  let result = parts.join(" ");
1075
1169
  if (id.year !== undefined)
1076
1170
  result += `-${id.year}`;
1171
+ if (id.redline === true)
1172
+ result += " - Redline";
1077
1173
  return result;
1078
1174
  },
1079
1175
  });
@@ -1215,11 +1311,25 @@ export const NescDraftClass = ieeeClass({ kind: "nesc-draft" }, {
1215
1311
  extraMappings: NescMappings,
1216
1312
  render: (id) => {
1217
1313
  const parts = ["Draft National Electrical Safety Code"];
1218
- if (id.month !== undefined && id.year !== undefined)
1219
- parts.push(`, ${id.month} ${id.year}`);
1314
+ if (id.month !== undefined && id.year !== undefined) {
1315
+ if (/^\d{1,2}$/.test(id.month))
1316
+ parts.push(` ${id.number}-${id.year}-${id.month}`);
1317
+ else
1318
+ parts.push(`, ${id.month} ${id.year}`);
1319
+ }
1220
1320
  return parts.join("");
1221
1321
  },
1222
1322
  });
1323
+ // The NESC's own term for its pre-edition drafts: a draft STAGE of the
1324
+ // C2 code, not a variant. Canonical spelling is dotted and prefixed;
1325
+ // the dash, catalogue slash-tail and verbose proposal spellings are
1326
+ // aliases.
1327
+ export const NescPreprintClass = ieeeClass({ kind: "nesc-preprint" }, {
1328
+ codeColumns: true,
1329
+ extraDefs: NescDefs,
1330
+ extraMappings: NescMappings,
1331
+ render: (id) => `IEEE Std ${["C2", id.year, "Preprint"].filter((p) => p !== undefined).join(".")}`,
1332
+ });
1223
1333
  function nescNamePortion(id) {
1224
1334
  let name = "National Electrical Safety Code";
1225
1335
  if (id["registered"] === true)
@@ -259,9 +259,9 @@ function normalizeRevisionNotation(input) {
259
259
  cleaned = cleaned.replace(/(\/D-?[0-9A-Za-z.]+)\s+[Rr][Ee][Vv]\s*(\d+)/, "$1/R-$2");
260
260
  cleaned = cleaned.replace(/[-/_.]?\s?[Rr][Ee][Vv][-\s]?(\d+)(\/D-?[0-9A-Za-z.]+)/, "$2/R-$1");
261
261
  cleaned = cleaned.replace(/(\d)[-._]?\s?[Rr][Ee][Vv]\s*(\d+)\s*$/, "$1/R-$2");
262
- // Lettered inline revisions stripped.
263
- cleaned = cleaned.replace(/[-/_.]?\s?[Rr][Ee][Vv][-\s]?[A-Za-z0-9]+(?=\/D-?[0-9A-Za-z])/, "");
264
- cleaned = cleaned.replace(/(\d)[Rr][Ee][Vv][A-Za-z0-9]+\s*$/, "$1");
262
+ // Lettered inline revisions are IDENTITY (pubid ruling 2026-10-14):
263
+ // the grammar captures them natively with their separator; the former
264
+ // strip rewrite here dropped the letters and is gone.
265
265
  return cleaned;
266
266
  }
267
267
  function normalizeJointStageSpellings(input) {
@@ -271,7 +271,6 @@ function normalizeJointStageSpellings(input) {
271
271
  const num = "(P?\\d+(?:[.-]\\d+)?)";
272
272
  const tail = "(,? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]* \\d{4}|$|-\\d{4}(?:-\\d\\d)?)";
273
273
  cleaned = cleaned.replace(/(\d)_D(\d)/g, "$1/D$2");
274
- cleaned = cleaned.replace(/,(?=[A-Za-z])/, ", ");
275
274
  cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/(D[\\d.]+)/ ?${stage}\\b`), "$1$4 $2/$3");
276
275
  cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/ ${stage}${tail}`), "$1$3 $2$4");
277
276
  cleaned = cleaned.replace(new RegExp(`^${pubs}${num}/${stage} (\\d{4})\\b`), "$1$3 $2-$4");
@@ -287,7 +286,6 @@ function normalizeJointStageSpellings(input) {
287
286
  // The Parser.parse cleaning pipeline, in Ruby order.
288
287
  export function preprocessIeee(input) {
289
288
  let cleaned = input.replace(/\.pdf$/i, "");
290
- cleaned = cleaned.replace(/_(FDIS|CDV|CD|DIS|WD|PWI|NP)/g, "/$1");
291
289
  cleaned = cleaned.replace(/\s+/g, " ");
292
290
  cleaned = cleaned.replace(/\b(ISO\/IEC\/IEEE|IEEE\/ISO\/IEC|IEEE\/IEC\/ISO|ISO\/IEEE|IEC\/IEEE|IEEE\/IEC|ISO\/IEC)\/ ?(FDIS|FCD|CDV|DIS\d?|CD\d?|WD|PWI|NP)\b/g, "$1 $2");
293
291
  cleaned = normalizeRevisionNotation(cleaned);
@@ -363,7 +361,6 @@ export function preprocessIeee(input) {
363
361
  if (/^(\d+[-.]\d+)\/D\d+/.test(cleaned)) {
364
362
  cleaned = `IEEE P${cleaned}`;
365
363
  }
366
- cleaned = cleaned.replace(/\/Preprint\b/g, "");
367
364
  cleaned = cleaned.replaceAll("Proposed Revision of", "Revision of");
368
365
  cleaned = cleaned.replace(/\bammended\b/gi, "amended");
369
366
  cleaned = cleaned.replace(/(\/INT|\/Cor\s+\d+-\d{4})\./g, "$1");
@@ -70,8 +70,14 @@ function buildRules() {
70
70
  rule("simple_series", () => literalAlternation(SIMPLE_SERIES).as("series"));
71
71
  rule("number_suffix", () => str("U")
72
72
  .then(lowerLetter)
73
- .or(letter.then(literalAlternation([
74
- "ec", "ndex", "nsert", "rrata", "raft", "pp", "s", "t",
73
+ .or(
74
+ // An "r" directly followed by a digit is the revision marker
75
+ // (pubid#170), not a suffix — leave it for the edition rules.
76
+ str("r")
77
+ .then(digit)
78
+ .absent()
79
+ .then(letter, literalAlternation([
80
+ "ec", "ndex", "ndx", "nsert", "rrata", "raft", "pp", "s", "t",
75
81
  "hi", "iet", "ort", "r", "p",
76
82
  ]).absent(), digits.maybe())));
77
83
  rule("digits_with_suffix", () => digits.then(ref(rules, "number_suffix").then(digit.absent()).maybe()));
@@ -105,20 +111,23 @@ function buildRules() {
105
111
  .absent()
106
112
  .then(str("draft").absent(), (digits
107
113
  .then(literalAlternation(["supp", "sup"]), digit.absent(), letter.absent())
108
- .or(digits.then(str("r"), digits, str("U"), lowerLetter), digits.then(str("r"), digits, letter), digits.as("number_only").then(str("r"), digits.as("edition_id")), digits.then(str("_"), digits, dash, digits, upperLetter.maybe()), lowerLetter.then(dash, digits), digits.then(str("pt"), digits, dash.absent()), digits.then(upperLetter), digits
114
+ .or(digits.then(str("r"), digits, str("U"), lowerLetter), digits.then(str("r"), digits, letter), digits.as("number_only").then(str("r"), digits.then(dot.then(digits).maybe()).as("edition_id")), digits.then(str("_"), digits, dash, digits, upperLetter.maybe()), lowerLetter.then(dash, digits), digits.then(str("pt"), digits, dash.absent()), digits.then(upperLetter), digits
109
115
  .as("number_only")
110
116
  .then(str("r"), letter.as("letter"))
111
- .as("revision_letter"), str("r").then(letter).as("revision_letter_suffix"), str("r").then(digits.as("edition_id")).as("revision_simple"), str("NCNR"), str("PERMIS"), str("BFRL"), upperLetter.repeat(1, 3), rules["digits_with_suffix"]
117
+ .as("revision_letter"), str("r").then(letter).as("revision_letter_suffix"), str("r")
118
+ .then(digits.then(dot.then(digits).maybe()).as("edition_id"))
119
+ .as("revision_simple"), str("NCNR"), str("PERMIS"), str("BFRL"), upperLetter.repeat(1, 3), rules["digits_with_suffix"]
112
120
  .then(dash.then(monthAbbrev(), digits, slash).absent()), lowerLetter.then(digit.absent()))).as("second_number")));
121
+ const dottedDigits = () => digits.then(dot.then(digits).maybe());
113
122
  const editionInner = () => space
114
123
  .maybe()
115
- .then(str("e"), digits.as("edition_id"))
124
+ .then(str("e"), dottedDigits().as("edition_id"))
116
125
  .as("edition_e")
117
126
  .or(space
118
- .then(str("r"), digits.as("edition_id"), letter.as("edition_letter"))
119
- .as("edition_r_with_space_letter"), space.then(str("r"), digits.as("edition_id")).as("edition_r_with_space"), str("r")
120
- .then(digits.as("edition_id"), letter.as("edition_letter"))
121
- .as("edition_r_no_space_letter"), str("r").then(digits.as("edition_id")).as("edition_r_no_space"), space
127
+ .then(str("r"), dottedDigits().as("edition_id"), letter.as("edition_letter"))
128
+ .as("edition_r_with_space_letter"), space.then(str("r"), dottedDigits().as("edition_id")).as("edition_r_with_space"), str("r")
129
+ .then(dottedDigits().as("edition_id"), letter.as("edition_letter"))
130
+ .as("edition_r_no_space_letter"), str("r").then(dottedDigits().as("edition_id")).as("edition_r_no_space"), space
122
131
  .maybe()
123
132
  .then(str("rev"), space.maybe(), digits.as("edition_id"))
124
133
  .as("edition_rev"), dash.then(match("[1-9]").as("edition_id"), digit.absent()).as("edition_historical"), dash
@@ -1,5 +1,13 @@
1
- import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
2
- import { nistGrammar } from "./grammar.js";
1
+ import { PubidRuntime, toBuilderTree } from "../../parg/runtime.js";
2
+ // The grammar runtime is the baked PARG artifact (pubid-grammar
3
+ // artifacts/nist.json via the parsanol engine) — the same artifact the
4
+ // gem runs, so grammar changes land once and this port cannot drift.
5
+ // Only the preprocessor, builder and model are ported code.
6
+ let cachedNistRuntime;
7
+ function cachedRuntime(input) {
8
+ cachedNistRuntime ??= PubidRuntime.load("nist");
9
+ return cachedNistRuntime.parseShape(input);
10
+ }
3
11
  import { preprocessNist } from "./preprocessor.js";
4
12
  import { NIST_CLASSES, NistCode, NistEdition, NistPart, NistStage, NistSupplement, NistTranslation, NistUpdate, NistVersion, NistVolume, SP_SUBSERIES, seriesPolicy, } from "./model.js";
5
13
  /**
@@ -1238,11 +1246,12 @@ export function nistGrammarImplementation() {
1238
1246
  return {
1239
1247
  parse(input) {
1240
1248
  const { cleaned, format } = preprocessNist(input);
1241
- const tree = parseGrammar(nistGrammar, cleaned);
1242
- if (typeof tree !== "object" || tree === null) {
1243
- throw new ParseFailed("NIST: unexpected parse tree", 0);
1244
- }
1245
- return builder.build(tree, format);
1249
+ const base = cleaned.replace(/\s*\(all parts\)\s*$/, "");
1250
+ const tree = toBuilderTree(cachedRuntime(base));
1251
+ const marked = base !== cleaned
1252
+ ? { ...tree, all_parts: true }
1253
+ : tree;
1254
+ return builder.build(marked, format);
1246
1255
  },
1247
1256
  };
1248
1257
  }
@@ -192,8 +192,16 @@ export function preprocessNist(input) {
192
192
  cleaned = cleaned.replace(/(\d)(v\d+[a-z]-[a-z])/g, "$1 $2");
193
193
  cleaned = cleaned.replace(/(\d)(v\d+[A-Z])/g, "$1 $2");
194
194
  cleaned = cleaned.replace(/(v\d+)([A-Z])-([A-Z])/g, (_m, v, a, b) => `${v}${a}-${b.toLowerCase()}`);
195
- // normalize_edition_year_suffix
195
+ // normalize_edition_year_suffix — a dotted minor after the dot stays
196
+ // attached (pubid#170): "2006ed.1" → "e2006.1", never "e20061".
197
+ cleaned = cleaned.replace(/(\d{4})ed\.(\d+)/g, "e$1.$2");
196
198
  cleaned = cleaned.replace(/(\d{4})ed\./g, "e$1");
199
+ // normalize_glued_dotted_edition — a glued dotted e-edition needs the
200
+ // space the spaced form has (pubid#170): "800-53e2.1" → "800-53 e2.1".
201
+ // The minor is 1-2 digits; a 4-digit tail is the additional-text year of
202
+ // "NBS CIRC 11e2.1915", which keeps its existing tree. Dot-less "800-53e2"
203
+ // keeps its number-bound tree.
204
+ cleaned = cleaned.replace(/(\d)e(\d+\.\d{1,2})(?!\d)/g, "$1 e$2");
197
205
  // normalize_revision_with_letter
198
206
  cleaned = cleaned.replace(/(\d+)(r\d{1,2})([a-z])(?=-|[A-Z]|$)/g, (_m, n, r, l) => `${n}${r}${l.toUpperCase()}`);
199
207
  cleaned = cleaned.replace(/(\d+)(r\d{1,2})(?![a-zA-Z])(?=[A-Z]|-(?=[A-Z])|\/(?:upd|errata|insert))/g, "$1 $2");
@@ -0,0 +1,25 @@
1
+ import { PargRuntime as EngineRuntime } from "parsanol";
2
+ export type { FieldRequirement, EntrySchema, Schema } from "parsanol";
3
+ export { PargRuntime } from "parsanol";
4
+ /** The artifacts directory: PARG_ARTIFACT_DIR, else the sibling
5
+ * pubid-grammar checkout of the pubid workspace, else the vendored copy
6
+ * shipped in the published package (refreshed by `npm run artifact`). */
7
+ export declare function artifactsDir(): string;
8
+ /** The pubid flavor runtime: loads baked artifacts from the workspace. */
9
+ export declare class PubidRuntime extends EngineRuntime {
10
+ static artifactsDir(): string;
11
+ static load(grammar: string, entry?: string): EngineRuntime;
12
+ }
13
+ /**
14
+ * The artifact emits capture leaves ({value, line, column, offset,
15
+ * length}) and a top-level sequence of capture hashes. This mirrors the
16
+ * gem's Pubid::Parg::Backend.to_builder_hash: scalarize leaves, fold the
17
+ * top-level list into one hash (last key wins — parslet's sequence fold).
18
+ */
19
+ export declare function toBuilderTree(shape: unknown): unknown;
20
+ /**
21
+ * Strip the shape metadata an artifact parse carries (offset/line/column
22
+ * wrappers) down to the plain string/array capture tree the builders
23
+ * consume — the same tree shape the hand-written grammars produced.
24
+ */
25
+ export declare function normalizeShape(node: unknown): unknown;
@@ -0,0 +1,76 @@
1
+ //! The PARG artifact runtime for pubid: a thin adapter over the
2
+ //! published `parsanol` npm package (the wasm engine), adding the
3
+ //! pubid-workspace artifacts directory convention. No grammar code and
4
+ //! no vendored engine copy live in this repo; the baked artifact is
5
+ //! the contract and the engine ships from the registry.
6
+ import { existsSync } from "node:fs";
7
+ import { fileURLToPath } from "node:url";
8
+ import { dirname, join } from "node:path";
9
+ import { PargRuntime as EngineRuntime } from "parsanol";
10
+ export { PargRuntime } from "parsanol";
11
+ /** Walk up from this module to the package root (which holds package.json). */
12
+ function packageRoot() {
13
+ let dir = dirname(fileURLToPath(import.meta.url));
14
+ for (; !existsSync(join(dir, "package.json")); dir = dirname(dir)) {
15
+ if (dir === dirname(dir))
16
+ throw new Error("package.json not found above the parg runtime");
17
+ }
18
+ return dir;
19
+ }
20
+ /** The artifacts directory: PARG_ARTIFACT_DIR, else the sibling
21
+ * pubid-grammar checkout of the pubid workspace, else the vendored copy
22
+ * shipped in the published package (refreshed by `npm run artifact`). */
23
+ export function artifactsDir() {
24
+ const env = process.env.PARG_ARTIFACT_DIR;
25
+ if (env)
26
+ return env;
27
+ const sibling = join(packageRoot(), "..", "pubid-grammar", "artifacts");
28
+ if (existsSync(sibling))
29
+ return sibling;
30
+ return join(packageRoot(), "vendor", "parg");
31
+ }
32
+ /** The pubid flavor runtime: loads baked artifacts from the workspace. */
33
+ export class PubidRuntime extends EngineRuntime {
34
+ static artifactsDir() {
35
+ return artifactsDir();
36
+ }
37
+ static load(grammar, entry) {
38
+ return EngineRuntime.fromFile(join(artifactsDir(), `${grammar}.json`), entry);
39
+ }
40
+ }
41
+ /**
42
+ * The artifact emits capture leaves ({value, line, column, offset,
43
+ * length}) and a top-level sequence of capture hashes. This mirrors the
44
+ * gem's Pubid::Parg::Backend.to_builder_hash: scalarize leaves, fold the
45
+ * top-level list into one hash (last key wins — parslet's sequence fold).
46
+ */
47
+ export function toBuilderTree(shape) {
48
+ const normalized = normalizeShape(shape);
49
+ if (Array.isArray(normalized) &&
50
+ normalized.length > 0 &&
51
+ normalized.every((n) => typeof n === "object" && n !== null && !Array.isArray(n))) {
52
+ return Object.assign({}, ...normalized);
53
+ }
54
+ return normalized;
55
+ }
56
+ /**
57
+ * Strip the shape metadata an artifact parse carries (offset/line/column
58
+ * wrappers) down to the plain string/array capture tree the builders
59
+ * consume — the same tree shape the hand-written grammars produced.
60
+ */
61
+ export function normalizeShape(node) {
62
+ if (Array.isArray(node))
63
+ return node.map(normalizeShape);
64
+ if (typeof node === "object" && node !== null) {
65
+ const rec = node;
66
+ if (typeof rec["value"] === "string" && Object.keys(rec).length >= 1 &&
67
+ "offset" in rec) {
68
+ return rec["value"];
69
+ }
70
+ const out = {};
71
+ for (const [k, v] of Object.entries(rec))
72
+ out[k] = normalizeShape(v);
73
+ return out;
74
+ }
75
+ return node;
76
+ }
@@ -0,0 +1,4 @@
1
+ export { loadSuites, readSuite, runSuite, type Suite, type SuiteTest, type TestKind, } from "parsanol/suite";
2
+ export { PubidRuntime } from "./runtime.js";
3
+ /** The suites directory: PARG_SUITES_DIR, else beside the artifacts. */
4
+ export declare function suitesDir(): string;
@@ -0,0 +1,14 @@
1
+ //! pgtest suite runner: re-exports the engine suite machinery from the
2
+ //! published `parsanol` package; only the suites directory convention
3
+ //! (sibling pubid-grammar checkout) is pubid-specific.
4
+ export { loadSuites, readSuite, runSuite, } from "parsanol/suite";
5
+ export { PubidRuntime } from "./runtime.js";
6
+ import { join } from "node:path";
7
+ import { artifactsDir } from "./runtime.js";
8
+ /** The suites directory: PARG_SUITES_DIR, else beside the artifacts. */
9
+ export function suitesDir() {
10
+ const env = process.env.PARG_SUITES_DIR;
11
+ if (env)
12
+ return env;
13
+ return join(artifactsDir(), "..", "suites");
14
+ }
@@ -552,6 +552,13 @@ export const URN_PARSERS = {
552
552
  const parts = splitParts(body);
553
553
  const [, code, year] = parts;
554
554
  let text = `IEEE Std ${code}`;
555
+ // A "-YYYY-MM" tail is a dated reference (year + numeric month),
556
+ // mirroring the gem's urn parser; it re-parses through the
557
+ // trailing-month-year clause so the render prints "21, 11 1976"
558
+ // like the reference.
559
+ const dated = /^(.*)-(\d{4})-(\d{2})$/.exec(text);
560
+ if (dated)
561
+ return parse(`${dated[1]}, ${dated[3]} ${dated[2]}`);
555
562
  if (year)
556
563
  text += `-${year}`;
557
564
  return parse(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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",
@@ -17,15 +17,17 @@
17
17
  "artifact": "tsc -p tsconfig.json && node scripts/build-artifact.mjs"
18
18
  },
19
19
  "dependencies": {
20
+ "parsanol": "^0.10.3",
20
21
  "yaml": "^2.5.0"
21
22
  },
22
23
  "devDependencies": {
23
- "typescript": "^5.6.0",
24
- "@types/node": "^20.14.0"
24
+ "@types/node": "^20.14.0",
25
+ "typescript": "^5.6.0"
25
26
  },
26
27
  "files": [
27
28
  "dist",
28
- "artifact"
29
+ "artifact",
30
+ "vendor/parg"
29
31
  ],
30
32
  "keywords": [
31
33
  "pubid",
@@ -0,0 +1 @@
1
+ {"bindings":null,"version":"0.1.0","grammar":"PubidAdobe","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Entity":{"atom":48}},{"Entity":{"atom":30}},{"Entity":{"atom":9}},{"Str":{"pattern":"Adobe"}},{"Entity":{"atom":5}},{"Str":{"pattern":" "}},{"Str":{"pattern":"TN"}},{"Entity":{"atom":5}},{"Repetition":{"atom":7,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[3,4,6,8]}},{"Entity":{"atom":24}},{"Str":{"pattern":"Adobe"}},{"Entity":{"atom":5}},{"Str":{"pattern":"Technical"}},{"Entity":{"atom":5}},{"Str":{"pattern":"Note"}},{"Entity":{"atom":5}},{"Repetition":{"atom":16,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"#"}},{"Repetition":{"atom":18,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":5}},{"Repetition":{"atom":20,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[17,19,21]}},{"Repetition":{"atom":22,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[11,12,13,14,15,23]}},{"Entity":{"atom":29}},{"Str":{"pattern":"ATN"}},{"Entity":{"atom":5}},{"Repetition":{"atom":27,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[26,28]}},{"Alternative":{"atoms":[2,10,25]}},{"Entity":{"atom":32}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":31,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"number","atom":33}},{"Entity":{"atom":36}},{"Str":{"pattern":"."}},{"Entity":{"atom":42}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[_]"}},{"Alternative":{"atoms":[38,39,40,41]}},{"Repetition":{"atom":37,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"slug","atom":43}},{"Sequence":{"atoms":[35,44]}},{"Repetition":{"atom":45,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[1,34,46]}},{"Named":{"name":"tech_note","atom":47}},{"Entity":{"atom":53}},{"Entity":{"atom":32}},{"Repetition":{"atom":50,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"number","atom":51}},{"Named":{"name":"tech_note_bare","atom":52}},{"Entity":{"atom":87}},{"Str":{"pattern":"Adobe Publication "}},{"Repetition":{"atom":55,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":86}},{"Entity":{"atom":82}},{"Entity":{"atom":75}},{"Entity":{"atom":69}},{"Entity":{"atom":62}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Repetition":{"atom":61,"min":1,"max":null,"tag":"Repetition"}},{"Entity":{"atom":67}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Alternative":{"atoms":[65,66]}},{"Repetition":{"atom":64,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[63,68]}},{"Entity":{"atom":71}},{"Str":{"pattern":"-"}},{"Entity":{"atom":69}},{"Sequence":{"atoms":[70,72]}},{"Repetition":{"atom":73,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[60,74]}},{"Named":{"name":"slug","atom":59}},{"Entity":{"atom":71}},{"Entity":{"atom":32}},{"Repetition":{"atom":78,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"version","atom":79}},{"Sequence":{"atoms":[76,77,80]}},{"Named":{"name":"publication_version","atom":81}},{"Entity":{"atom":85}},{"Entity":{"atom":75}},{"Named":{"name":"slug","atom":84}},{"Alternative":{"atoms":[58,83]}},{"Sequence":{"atoms":[56,57]}},{"Alternative":{"atoms":[0,49,54]}}],"root":88},"bindings":[]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule tech_note_prefix: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)","rule tech_note_prefix: branches 1 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule tech_note_prefix: branches 2 and 3 are order-dependent (shared first bytes; ordered choice is decisive)","rule publication_slug_or_version: 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)"]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"Adobe TN 15","expect":{}},{"entry":"identifier","kind":"accept","input":"ATN 30","expect":{}},{"entry":"identifier","kind":"accept","input":"Adobe Technical Note #12","expect":{}},{"entry":"identifier","kind":"reject","input":"###","expect":{}}],"docs":{},"source":"# Port of lib/pubid/adobe/parser.rb: tech notes (Adobe TN 5014, legacy\n# \"Adobe Technical Note #5014\", ATN5014, bare 5014) and publications\n# (adobe-glyph-list, adobe-japan1-7 with the version split tried first).\n\ngrammar PubidAdobe version \"0.1.0\" {\n tech_note_tn_prefix = \"Adobe\" space \"TN\" [space]\n tech_note_long_prefix = \"Adobe\" space \"Technical\" space \"Note\" [[space] [\"#\"] [space]]\n tech_note_atn_prefix = \"ATN\" [space]\n tech_note_prefix = tech_note_tn_prefix / tech_note_long_prefix / tech_note_atn_prefix\n # Capture precedence: (*rep) as name wraps the whole repetition, so a\n # digit run joins to one string instead of re-keying every character;\n # slug is captured exactly once (the bare form) or as a sibling of\n # version inside the publication_version capture.\n tech_note = (tech_note_prefix (1*digit) as number [(dot (1*slug_char) as slug)]) as tech_note\n bare_number = ((1*digit) as number) as tech_note_bare\n slug_segment = 1*lower1 *lowerd\n slug_text = slug_segment *(dash slug_segment)\n slug_inner = (slug_text) as slug\n publication_with_version = ((slug_text) as slug dash (1*digit) as version) as publication_version\n publication_slug_or_version = publication_with_version / slug_inner\n publication = [\"Adobe Publication \"] publication_slug_or_version\n identifier = tech_note / bare_number / publication\n slug_char = %x41-5A / %x61-7A / %x30-39 / %x5F\n lower1 = %x61-7A\n lowerd = %x61-7A / %x30-39\n digit = %x30-39\n space = \" \"\n dot = \".\"\n dash = \"-\"\n entry identifier: identifier\n\n test {\n accept \"Adobe TN 15\"\n accept \"ATN 30\"\n accept \"Adobe Technical Note #12\"\n reject \"###\"\n }\n}\n","checksum":"sha256:38e8488c3da1c0f4a36fe2f63d87fee551d9317c5dae62f56972da1efe842a52"}