@pubid/pubid 0.1.38 → 0.1.40

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
@@ -24,5 +24,8 @@ export interface Identifier {
24
24
  toUrn(): string | undefined;
25
25
  /** Deserialize the canonical hash; the idempotency gate relies on it. */
26
26
  fromHash(hash: Record<string, unknown>): Identifier;
27
+ /** Ruby ==: canonical equality skipping equalityIgnored attributes
28
+ * (the (E) position is spelling, the language itself is not). */
29
+ equals?(other: Identifier): boolean;
27
30
  }
28
31
  export type ImplementationRegistry = Map<string, FlavorImplementation>;
@@ -0,0 +1,35 @@
1
+ import type { FlavorImplementation } from "../../conformance/implementation.js";
2
+ import { BaseIdentifier } from "../../model/identifier.js";
3
+ /**
4
+ * The PFS-compiled doi flavor (pubid/pubid#706 §2.2) — one definition
5
+ * file, both engines: the grammar runtime is the SAME baked artifact the
6
+ * Ruby flavor runs (pubid-grammar artifacts/doi_pfs.json, compiled from
7
+ * definitions/doi.pfs), loaded through the artifact runtime, so grammar
8
+ * changes land once and this side cannot drift. Nothing is hand-rolled:
9
+ * the model is declared attributes (serialization derives from
10
+ * BaseIdentifier), the builder is the shared one, and the render body is
11
+ * the definition's canonical template. The parity gate
12
+ * (test/doi-pfs-parity.test.ts) pins byte-identity with the coded
13
+ * {@linkcode import("../doi").DoiIdentifier} over the shared conformance
14
+ * set.
15
+ */
16
+ export declare class DoiPfsIdentifier extends BaseIdentifier {
17
+ /**
18
+ * The SCHEME-canonical wire tag (declared in the definition): to_hash
19
+ * is byte-identical with the coded flavor. Deliberately NOT registered
20
+ * in the global type map — one scheme, one `_type`, one global
21
+ * resolution (the coded class); {@linkcode fromHash} routes the tag
22
+ * back here flavor-locally, mirroring Ruby's DOI_TYPE_MAP.
23
+ */
24
+ static polymorphicName: string;
25
+ static attributes: import("../../model/attribute.js").AttributeTable;
26
+ static mappings: import("../../model/attribute.js").FieldMapping[];
27
+ render(): string;
28
+ readonly prefix: string;
29
+ readonly suffix: string;
30
+ /** The alias's own from_hash: the scheme tag routes HERE, while the
31
+ * global resolution keeps answering the coded class (coexistence, not
32
+ * cutover). */
33
+ static fromHash(hash: Record<string, unknown>): BaseIdentifier;
34
+ }
35
+ export declare function doiPfsGrammarImplementation(): FlavorImplementation;
@@ -0,0 +1,61 @@
1
+ import { ParseFailed } from "../../grammar/engine.js";
2
+ import { PubidRuntime } from "../../parg/runtime.js";
3
+ import { BaseIdentifier } from "../../model/identifier.js";
4
+ import { extendAttributes, keyValue } from "../../model/attribute.js";
5
+ /**
6
+ * The PFS-compiled doi flavor (pubid/pubid#706 §2.2) — one definition
7
+ * file, both engines: the grammar runtime is the SAME baked artifact the
8
+ * Ruby flavor runs (pubid-grammar artifacts/doi_pfs.json, compiled from
9
+ * definitions/doi.pfs), loaded through the artifact runtime, so grammar
10
+ * changes land once and this side cannot drift. Nothing is hand-rolled:
11
+ * the model is declared attributes (serialization derives from
12
+ * BaseIdentifier), the builder is the shared one, and the render body is
13
+ * the definition's canonical template. The parity gate
14
+ * (test/doi-pfs-parity.test.ts) pins byte-identity with the coded
15
+ * {@linkcode import("../doi").DoiIdentifier} over the shared conformance
16
+ * set.
17
+ */
18
+ export class DoiPfsIdentifier extends BaseIdentifier {
19
+ /**
20
+ * The SCHEME-canonical wire tag (declared in the definition): to_hash
21
+ * is byte-identical with the coded flavor. Deliberately NOT registered
22
+ * in the global type map — one scheme, one `_type`, one global
23
+ * resolution (the coded class); {@linkcode fromHash} routes the tag
24
+ * back here flavor-locally, mirroring Ruby's DOI_TYPE_MAP.
25
+ */
26
+ static polymorphicName = "pubid:doi:resource";
27
+ static attributes = extendAttributes(BaseIdentifier, {
28
+ prefix: { type: "string" },
29
+ suffix: { type: "string" },
30
+ });
31
+ static mappings = keyValue({ wire: "prefix", to: "prefix" }, { wire: "suffix", to: "suffix" });
32
+ render() {
33
+ return `doi:${this.prefix}/${this.suffix}`;
34
+ }
35
+ /** The alias's own from_hash: the scheme tag routes HERE, while the
36
+ * global resolution keeps answering the coded class (coexistence, not
37
+ * cutover). */
38
+ static fromHash(hash) {
39
+ return new DoiPfsIdentifier(this.applyMappings(this.inflateScalarComponents(hash)));
40
+ }
41
+ }
42
+ /**
43
+ * The artifact's declared bindings (prefix -> prefix, suffix -> suffix)
44
+ * carry the whole printed prefix face ("10.1000") — the doi.pfs pattern
45
+ * captures the "10." face the coded grammar re-assembles — so the bound
46
+ * map lands on the model fields directly, no builder cast.
47
+ */
48
+ let cachedRuntime;
49
+ export function doiPfsGrammarImplementation() {
50
+ return {
51
+ parse(input) {
52
+ cachedRuntime ??= PubidRuntime.load("doi_pfs");
53
+ const bound = cachedRuntime.parseAndBind(input);
54
+ if (bound === null || typeof bound !== "object") {
55
+ throw new ParseFailed("doi_pfs: the artifact rejected the input", 0);
56
+ }
57
+ const identifier = new DoiPfsIdentifier(bound);
58
+ return identifier;
59
+ },
60
+ };
61
+ }
@@ -2,6 +2,7 @@ import { lookupUrnParser, parseUrnFor } from "../urn/parsers.js";
2
2
  import { oimlGrammarImplementation } from "./oiml/implementation.js";
3
3
  import { xsfGrammarImplementation } from "./xsf/index.js";
4
4
  import { doiGrammarImplementation } from "./doi/index.js";
5
+ import { doiPfsGrammarImplementation } from "./doi_pfs/index.js";
5
6
  import { ianaGrammarImplementation } from "./iana/index.js";
6
7
  import { unGrammarImplementation } from "./un/index.js";
7
8
  import { plateauGrammarImplementation } from "./plateau/index.js";
@@ -81,6 +82,8 @@ function grammarImplementationSwitch(flavor) {
81
82
  return xsfGrammarImplementation();
82
83
  case "doi":
83
84
  return doiGrammarImplementation();
85
+ case "doi_pfs":
86
+ return doiPfsGrammarImplementation();
84
87
  case "iana":
85
88
  return ianaGrammarImplementation();
86
89
  case "un":
@@ -14,7 +14,12 @@ import { BaseUrnGenerator } from "../../model/urn-generator.js";
14
14
  *
15
15
  * WES (welding-engineering-standard, pubid#638) rides the same flavor:
16
16
  * "WES <number>[-parts][:year[R]]…" with no series letter and no JIS
17
- * prefix — the code itself carries the "WES" root.
17
+ * prefix — the code itself carries the "WES" root. A WES id may also
18
+ * carry a TR/TS type after the root ("WES TR 2032:2024", its own
19
+ * welding-engineering-technical-report/-specification classes,
20
+ * pubid#716) and a language before the year ("WES 1112(E):2022",
21
+ * bound only when a colon follows; recorded as language_before_year,
22
+ * which equality ignores — the two (E) positions are one document).
18
23
  */
19
24
  function buildRules() {
20
25
  const rules = {};
@@ -61,11 +66,18 @@ function buildRules() {
61
66
  .then(rules["symbol_clause"].maybe()));
62
67
  // WES (Japan Welding Engineering Society) rides the JIS flavor with the
63
68
  // same tail but no prefix and no series letter; the root tries it first,
64
- // as the reference grammar does.
69
+ // as the reference grammar does. A WES id may also carry a TR/TS type
70
+ // after the root ("WES TR 2032:2024") and a language before the year
71
+ // ("WES 1112(E):2022" — bound only when a colon follows, so
72
+ // "WES 1112(E)" with no year still reads as no language).
73
+ rule("wes_type", () => (str("TR").or(str("TS"))).as("wes_type").then(space));
74
+ rule("wes_language", () => jpLparen.or(str("(")).then(match("[EJ]").as("wes_language"), jpRparen.or(str(")"))).then(colon.present()));
65
75
  rule("wes_identifier", () => str("WES").as("wes")
66
76
  .then(space)
77
+ .then(rules["wes_type"].maybe())
67
78
  .then(rules["number"])
68
79
  .then(rules["parts"])
80
+ .then(rules["wes_language"].maybe())
69
81
  .then(rules["year"].maybe())
70
82
  .then(rules["language"].maybe())
71
83
  .then(rules["all_parts"].maybe())
@@ -95,6 +107,13 @@ class JisIdentifier extends BaseIdentifier {
95
107
  yearWithReaffirmation() {
96
108
  return `${this.year}${this.reaffirmed === true ? "R" : ""}`;
97
109
  }
110
+ /** The ":year[R]" and "(lang)" tail, in the order the source used
111
+ * (ruby year_and_language). */
112
+ yearAndLanguage() {
113
+ const yearPart = this.year !== undefined ? `:${this.yearWithReaffirmation()}` : "";
114
+ const langPart = this.language !== undefined ? `(${this.language})` : "";
115
+ return this.language_before_year === true ? langPart + yearPart : yearPart + langPart;
116
+ }
98
117
  symbolSuffix() {
99
118
  if (this.symbol === undefined)
100
119
  return "";
@@ -104,7 +123,7 @@ class JisIdentifier extends BaseIdentifier {
104
123
  return "";
105
124
  }
106
125
  }
107
- const JIS_MAPPINGS = keyValue({ wire: "series", to: "series" }, { wire: "number", to: "number" }, { wire: "parts", to: "parts" }, { wire: "year", to: "year" }, { wire: "language", to: "language" }, { wire: "all_parts", to: "all_parts" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "symbol", to: "symbol" });
126
+ const JIS_MAPPINGS = keyValue({ wire: "series", to: "series" }, { wire: "number", to: "number" }, { wire: "parts", to: "parts" }, { wire: "year", to: "year" }, { wire: "language", to: "language" }, { wire: "language_before_year", to: "language_before_year" }, { wire: "all_parts", to: "all_parts" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "symbol", to: "symbol" });
108
127
  function jisClass(kind) {
109
128
  const isSupplement = kind === "amendment" || kind === "corrigendum" || kind === "explanation";
110
129
  class JisKindIdentifier extends JisIdentifier {
@@ -122,6 +141,7 @@ function jisClass(kind) {
122
141
  parts: { type: "string", collection: true },
123
142
  year: { type: "integer" },
124
143
  language: { type: "string" },
144
+ language_before_year: { type: "boolean", equalityIgnored: true },
125
145
  all_parts: { type: "boolean", default: false },
126
146
  reaffirmed: { type: "boolean" },
127
147
  symbol: { type: "string" },
@@ -137,10 +157,7 @@ function jisClass(kind) {
137
157
  const typePrefix = KIND_PREFIXES[kind];
138
158
  let result = typePrefix === undefined ? "JIS" : `JIS ${typePrefix}`;
139
159
  result += ` ${this.code()}`;
140
- if (this.year !== undefined)
141
- result += `:${this.yearWithReaffirmation()}`;
142
- if (this.language !== undefined)
143
- result += `(${this.language})`;
160
+ result += this.yearAndLanguage();
144
161
  if (this.all_parts === true)
145
162
  result += "(規格群)";
146
163
  return result + this.symbolSuffix();
@@ -167,36 +184,43 @@ const KIND_CLASSES = {
167
184
  corrigendum: jisClass("corrigendum"),
168
185
  explanation: jisClass("explanation"),
169
186
  };
187
+ const WES_TYPE_PREFIXES = {
188
+ "welding-engineering-standard": undefined,
189
+ "welding-engineering-technical-report": "TR",
190
+ "welding-engineering-technical-specification": "TS",
191
+ };
170
192
  // WES 7700-1:2019 — JSA sells Welding Engineering Standards next to JIS,
171
193
  // so they parse through the JIS flavor, but the id carries no series
172
- // letter and no JIS prefix ("WES" is the root of the designation).
173
- function wesClass() {
194
+ // letter and no JIS prefix ("WES" is the root of the designation). The
195
+ // TR/TS type token after the root picks the WES TR / WES TS classes
196
+ // (pubid#716): "WES TR 2032" is not "WES 2032" and not "TR Z 2032".
197
+ function wesClass(kind) {
174
198
  class JisWesIdentifier extends JisIdentifier {
175
- static polymorphicName = "pubid:jis:welding-engineering-standard";
199
+ static polymorphicName = `pubid:jis:${kind}`;
176
200
  static attributes = extendAttributes(BaseIdentifier, {
177
201
  number: { type: "string" },
178
202
  parts: { type: "string", collection: true },
179
203
  year: { type: "integer" },
180
204
  language: { type: "string" },
205
+ language_before_year: { type: "boolean", equalityIgnored: true },
181
206
  all_parts: { type: "boolean", default: false },
182
207
  reaffirmed: { type: "boolean" },
183
208
  symbol: { type: "string" },
184
209
  });
185
- static mappings = keyValue({ wire: "number", to: "number" }, { wire: "parts", to: "parts" }, { wire: "year", to: "year" }, { wire: "language", to: "language" }, { wire: "all_parts", to: "all_parts" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "symbol", to: "symbol" });
210
+ static mappings = keyValue({ wire: "number", to: "number" }, { wire: "parts", to: "parts" }, { wire: "year", to: "year" }, { wire: "language", to: "language" }, { wire: "language_before_year", to: "language_before_year" }, { wire: "all_parts", to: "all_parts" }, { wire: "reaffirmed", to: "reaffirmed" }, { wire: "symbol", to: "symbol" });
186
211
  code() {
187
212
  if (this.base !== undefined)
188
213
  return this.base.code();
189
- let result = `WES ${this.number}`;
214
+ const prefix = WES_TYPE_PREFIXES[kind];
215
+ const root = prefix === undefined ? "WES" : `WES ${prefix}`;
216
+ let result = `${root} ${this.number}`;
190
217
  if (this.parts !== undefined)
191
218
  result += this.parts.map((p) => `-${p}`).join("");
192
219
  return result;
193
220
  }
194
221
  render() {
195
222
  let result = this.code() ?? "";
196
- if (this.year !== undefined)
197
- result += `:${this.yearWithReaffirmation()}`;
198
- if (this.language !== undefined)
199
- result += `(${this.language})`;
223
+ result += this.yearAndLanguage();
200
224
  if (this.all_parts === true)
201
225
  result += "(規格群)";
202
226
  return result + this.symbolSuffix();
@@ -205,7 +229,11 @@ function wesClass() {
205
229
  registerType(JisWesIdentifier);
206
230
  return JisWesIdentifier;
207
231
  }
208
- const WES_CLASS = wesClass();
232
+ const WES_CLASSES = {
233
+ "welding-engineering-standard": wesClass("welding-engineering-standard"),
234
+ "welding-engineering-technical-report": wesClass("welding-engineering-technical-report"),
235
+ "welding-engineering-technical-specification": wesClass("welding-engineering-technical-specification"),
236
+ };
209
237
  class JisUrnGenerator extends BaseUrnGenerator {
210
238
  generate() {
211
239
  const id = this.identifier;
@@ -226,7 +254,7 @@ class JisUrnGenerator extends BaseUrnGenerator {
226
254
  return parts.join(":");
227
255
  }
228
256
  }
229
- for (const klass of [...Object.values(KIND_CLASSES), WES_CLASS]) {
257
+ for (const klass of [...Object.values(KIND_CLASSES), ...Object.values(WES_CLASSES)]) {
230
258
  klass.urnGenerator = JisUrnGenerator;
231
259
  }
232
260
  function isObj(v) {
@@ -294,13 +322,29 @@ class JisBuilder {
294
322
  };
295
323
  if (tree["year"] !== undefined && tree["year"] !== null)
296
324
  attrs["year"] = Number(tree["year"]);
297
- if (tree["language"] !== undefined && tree["language"] !== null)
325
+ const wesType = tree["wes_type"] === undefined || tree["wes_type"] === null ? undefined : String(tree["wes_type"]);
326
+ const kind = wesType === "TR"
327
+ ? "welding-engineering-technical-report"
328
+ : wesType === "TS"
329
+ ? "welding-engineering-technical-specification"
330
+ : "welding-engineering-standard";
331
+ // "wes_language" is a language printed before the year
332
+ // ("WES 1112(E):2022"); an id carries one language, not two.
333
+ if (tree["wes_language"] !== undefined && tree["wes_language"] !== null) {
334
+ if (tree["language"] !== undefined && tree["language"] !== null) {
335
+ throw new ParseFailed("JIS: a WES id carries one language, not two", 0);
336
+ }
337
+ attrs["language"] = String(tree["wes_language"]);
338
+ attrs["language_before_year"] = true;
339
+ }
340
+ else if (tree["language"] !== undefined && tree["language"] !== null) {
298
341
  attrs["language"] = String(tree["language"]);
342
+ }
299
343
  if (tree["all_parts"] !== undefined)
300
344
  attrs["all_parts"] = true;
301
345
  if (tree["reaffirmed"] !== undefined && tree["reaffirmed"] !== null)
302
346
  attrs["reaffirmed"] = true;
303
- return new WES_CLASS(attrs);
347
+ return new WES_CLASSES[kind](attrs);
304
348
  }
305
349
  buildSupplement(tree, kind, key, prefix) {
306
350
  const supp = isObj(tree[key]) ? tree[key] : {};
@@ -180,6 +180,18 @@ function locateIdentifierKlass(h) {
180
180
  return classForKind("dated-document");
181
181
  }
182
182
  let series = s(h["series"]);
183
+ // The default-series plain-number branch (pubid#711): the grammar's
184
+ // trailing alternatives accept a bare report number ("800-90B") and
185
+ // its publisher-led spelling ("NIST 800-90B") with NO series token.
186
+ // The SP series is the default: the docid-derived citation of
187
+ // NIST.SP.800-90B is exactly the bare docnumber "800-90B", and the
188
+ // Builder fills the same SP series the spelled form carries, so the
189
+ // two spellings are one identity to a lookup. No other branch can
190
+ // arrive here series-less (dated shapes returned above; every
191
+ // series-bearing branch captures :series).
192
+ if (series === undefined) {
193
+ return classForKind("special-publication");
194
+ }
183
195
  if (series?.startsWith("NBS ") === true || series?.startsWith("NBS.") === true || series?.startsWith("NIST.") === true) {
184
196
  const separator = series.startsWith("NBS ") ? "NBS " : series.startsWith("NBS.") ? "NBS." : "NIST.";
185
197
  let simple = series.slice(separator.length);
@@ -857,6 +869,21 @@ class NistBuilder {
857
869
  const klass = locateIdentifierKlass(h);
858
870
  const policy = seriesPolicy(s(h["series"]));
859
871
  const identifier = new klass();
872
+ // The default-series plain-number branch (pubid#711): the grammar's
873
+ // trailing alternatives carry no series token ("800-90B" /
874
+ // "NIST 800-90B"), so the identity gains the class's default series
875
+ // and parses == to the spelled "NIST SP 800-90B" form. The bare
876
+ // publisher rides the series-default-publisher fill at the tail
877
+ // (the #704 shape: filled after publisher_was_parsed is recorded,
878
+ // so the bare face keeps rendering bare). The policy stays the
879
+ // series-less Base one — Ruby's Series.for reads parsed_hash, not
880
+ // the filled attribute (SP's own policy is Base anyway).
881
+ if (h["series"] === undefined) {
882
+ const defaultSeries = klass.nistDefaultSeries;
883
+ if (defaultSeries !== undefined) {
884
+ identifier["series"] = new NistCode({ value: defaultSeries });
885
+ }
886
+ }
860
887
  const attrs = {};
861
888
  const toEdition = (v) => {
862
889
  if (isObj(v)) {
@@ -1123,16 +1150,19 @@ class NistBuilder {
1123
1150
  self["supplement"] = NistSupplement.fromRaw(supp.value, supp.hasRevision);
1124
1151
  }
1125
1152
  }
1126
- // Default publisher for series-less renders.
1153
+ // apply_series_default_publisher (the #704 rule): a series that
1154
+ // implies its publisher (bare "FIPS 197" is "NIST FIPS 197"; bare
1155
+ // "800-90B" is "NIST SP 800-90B" — pubid#712) gets it now, after the
1156
+ // publisher_was_parsed: false above, so the two spellings are ==
1157
+ // while the bare face keeps rendering bare (the printed-publisher
1158
+ // views read the flag, not the filled value). Runs only when the
1159
+ // publisher is absent AND unspelled — at this point that is exactly
1160
+ // `publisher === undefined` (the flag was recorded false above iff
1161
+ // no publisher was parsed).
1127
1162
  if (self["publisher"] === undefined) {
1128
1163
  const defaultPublisher = identifier.constructor.nistDefaultPublisher;
1129
1164
  if (defaultPublisher !== undefined && defaultPublisher !== "") {
1130
1165
  self["publisher"] = defaultPublisher;
1131
- if (self["publisher_was_parsed"] === false) {
1132
- // Circular/RPT/MP render "NBS <series>" through the default even
1133
- // when not parsed — keep the flag false (Ruby publisher_was_parsed
1134
- // stays false but to_short_style uses default only via series).
1135
- }
1136
1166
  }
1137
1167
  }
1138
1168
  return identifier;
@@ -680,6 +680,11 @@ function nistClass(spec) {
680
680
  class NistConcrete extends (spec.supplementSeam === true ? NistSupplementIdentifier : NistIdentifier) {
681
681
  static polymorphicName = `pubid:nist:${spec.kind}`;
682
682
  static urnGenerator = NistUrnGenerator;
683
+ /** Ruby `self.default_series` (pubid#711) — read by the Builder. */
684
+ static nistDefaultSeries = spec.defaultSeries;
685
+ /** Ruby `self.series_default_publisher` (#704/#712) — read by the
686
+ * Builder's series-default-publisher fill. */
687
+ static nistDefaultPublisher = spec.seriesDefaultPublisher;
683
688
  seriesCode() {
684
689
  return spec.seriesCode;
685
690
  }
@@ -819,7 +824,17 @@ export class NistSupplementIdentifier extends NistIdentifier {
819
824
  }
820
825
  }
821
826
  const TYPE_SPECS = [
822
- { kind: "special-publication", seriesCode: "SP" },
827
+ {
828
+ kind: "special-publication", seriesCode: "SP",
829
+ // The default-series plain-number branch (pubid#711): a citation
830
+ // shaped like the docnumber ("800-90B", publisher-led
831
+ // "NIST 800-90B") names an SP document — the docid-derived
832
+ // citation of NIST.SP.800-90B is exactly "800-90B" — and a bare
833
+ // plain-number parse carries the series' NIST publisher (the #704
834
+ // shape), so it is == to the spelled "NIST SP 800-90B" form a
835
+ // lookup compares against.
836
+ defaultSeries: "SP", seriesDefaultPublisher: "NIST",
837
+ },
823
838
  { kind: "federal-information-processing-standards", seriesCode: "FIPS", defaultPublisher: "", style: "fips" },
824
839
  { kind: "interagency-report", seriesCode: "IR", forceShort: true },
825
840
  { kind: "handbook", seriesCode: "HB" },
@@ -18,6 +18,9 @@ export interface AttributeSpec {
18
18
  collection?: boolean;
19
19
  /** Materialize [] when the input hash omits the key (tgpp parts). */
20
20
  initializeEmpty?: boolean;
21
+ /** Ruby `equality_ignored`: the attribute serializes, but `==` skips it
22
+ * (jis language_before_year — where the source printed "(E)" is spelling). */
23
+ equalityIgnored?: boolean;
21
24
  }
22
25
  export type AttributeTable = Record<string, AttributeSpec>;
23
26
  /**
@@ -62,6 +62,13 @@ export declare abstract class BaseIdentifier {
62
62
  * `PubidUrn.parse(this.toPubidUrn()).toHash == this.toHash`. */
63
63
  toPubidUrn(): string;
64
64
  fromHash(hash: Record<string, unknown>): BaseIdentifier;
65
+ /** Ruby == (lutaml equality with `equality_ignored`): canonical-hash
66
+ * equality that skips the attributes a flavor declares
67
+ * equalityIgnored. The skipped keys stay in to_hash — it is ==, not
68
+ * serialization, that reads the flag. */
69
+ equals(other: BaseIdentifier): boolean;
70
+ /** The canonical hash minus every equalityIgnored attribute's wire key. */
71
+ protected equalityHash(): Record<string, unknown>;
65
72
  /** SubsetMatch: an all-parts reference skips the part attributes. */
66
73
  subsetAllPartsWildcard(): boolean;
67
74
  protected classAttributes(): AttributeTable;
@@ -116,6 +116,23 @@ export class BaseIdentifier {
116
116
  fromHash(hash) {
117
117
  return this.constructor.fromHash(hash);
118
118
  }
119
+ /** Ruby == (lutaml equality with `equality_ignored`): canonical-hash
120
+ * equality that skips the attributes a flavor declares
121
+ * equalityIgnored. The skipped keys stay in to_hash — it is ==, not
122
+ * serialization, that reads the flag. */
123
+ equals(other) {
124
+ return canonicalKey(this.equalityHash()) === canonicalKey(other.equalityHash());
125
+ }
126
+ /** The canonical hash minus every equalityIgnored attribute's wire key. */
127
+ equalityHash() {
128
+ const hash = { ...this.toHash() };
129
+ for (const [name, spec] of Object.entries(this.classAttributes())) {
130
+ if (spec.equalityIgnored !== true)
131
+ continue;
132
+ delete hash[this.wireKeyFor(name).wire];
133
+ }
134
+ return hash;
135
+ }
119
136
  /** SubsetMatch: an all-parts reference skips the part attributes. */
120
137
  subsetAllPartsWildcard() {
121
138
  return Boolean(this["all_parts"]);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The pubid-scheme URN schema of DOI_PFS — the transcription
3
+ * of lib/pubid/<flavor>/pubid_urn_schema.rb (pubid#656, completed by
4
+ * #670; design in docs/URN-SCHEMES.adoc). GENERATED by
5
+ * scripts/port-urn-schemas.rb from the ruby declaration: edit the ruby
6
+ * schema and regenerate, never this table by hand.
7
+ */
8
+ export declare const doiPfsSchema: import("../schema.js").FlavorSchema;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The pubid-scheme URN schema of DOI_PFS — the transcription
3
+ * of lib/pubid/<flavor>/pubid_urn_schema.rb (pubid#656, completed by
4
+ * #670; design in docs/URN-SCHEMES.adoc). GENERATED by
5
+ * scripts/port-urn-schemas.rb from the ruby declaration: edit the ruby
6
+ * schema and regenerate, never this table by hand.
7
+ */
8
+ import { define } from "../index.js";
9
+ export const doiPfsSchema = define("doi_pfs", (s) => {
10
+ s.atom("prefix", "prefix");
11
+ s.atom("suffix", "suffix");
12
+ s.type("pubid:doi_pfs:identifier", "id", { core: [] });
13
+ s.type("pubid:doi_pfs:resource", "resource", { core: [] });
14
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The registry of all 44 flavor pubid-urn schema tables — the TS mirror
2
+ * The registry of all 45 flavor pubid-urn schema tables — the TS mirror
3
3
  * of lib/pubid.rb's autoloaded Pubid::<Flavor>::PubidUrnSchema chain.
4
4
  * Declared tables only: importing this module has no side effects; the
5
5
  * pubid-urn module builds its flavor lookup lazily from this array.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The registry of all 44 flavor pubid-urn schema tables — the TS mirror
2
+ * The registry of all 45 flavor pubid-urn schema tables — the TS mirror
3
3
  * of lib/pubid.rb's autoloaded Pubid::<Flavor>::PubidUrnSchema chain.
4
4
  * Declared tables only: importing this module has no side effects; the
5
5
  * pubid-urn module builds its flavor lookup lazily from this array.
@@ -19,6 +19,7 @@ import { cencenelecSchema } from "./cencenelec.js";
19
19
  import { cieSchema } from "./cie.js";
20
20
  import { csaSchema } from "./csa.js";
21
21
  import { doiSchema } from "./doi.js";
22
+ import { doiPfsSchema } from "./doi_pfs.js";
22
23
  import { eascSchema } from "./easc.js";
23
24
  import { ecmaSchema } from "./ecma.js";
24
25
  import { etsiSchema } from "./etsi.js";
@@ -65,6 +66,7 @@ export const PUBID_URN_SCHEMAS = [
65
66
  cieSchema,
66
67
  csaSchema,
67
68
  doiSchema,
69
+ doiPfsSchema,
68
70
  eascSchema,
69
71
  ecmaSchema,
70
72
  etsiSchema,
@@ -11,14 +11,21 @@ export const jisSchema = define("jis", (s) => {
11
11
  s.atom("parts", "part", "list");
12
12
  s.atom("year", "year", "int");
13
13
  s.atom("reaffirmed", "reaffirmed", "bool");
14
+ s.atom("language", "lang");
15
+ s.facet("language_before_year", "langfirst", "bool");
14
16
  s.type("pubid:jis:all-parts", "all");
15
- s.type("pubid:jis:amendment", "amd");
16
- s.type("pubid:jis:corrigendum", "cor");
17
- s.type("pubid:jis:explanation", "e");
17
+ s.type("pubid:jis:amendment", "amd", { core: [["number", "int"]] });
18
+ s.type("pubid:jis:corrigendum", "cor", { core: [["number", "int"]] });
19
+ s.type("pubid:jis:explanation", "e", {
20
+ core: [],
21
+ atoms: [["number", "n", "int"]],
22
+ });
18
23
  s.type("pubid:jis:identifier", "id");
19
24
  s.type("pubid:jis:japanese-industrial-standard", "jis");
20
25
  s.type("pubid:jis:standard", "std");
21
26
  s.type("pubid:jis:technical-report", "tr");
22
27
  s.type("pubid:jis:technical-specification", "ts");
23
28
  s.type("pubid:jis:welding-engineering-standard", "wes");
29
+ s.type("pubid:jis:welding-engineering-technical-report", "westr");
30
+ s.type("pubid:jis:welding-engineering-technical-specification", "wests");
24
31
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pubid/pubid",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
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",
@@ -0,0 +1 @@
1
+ {"bindings":null,"version":"0.1.0","grammar":"PubidCalconnectPfs","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"CC"}},{"Str":{"pattern":"/"}},{"Entity":{"atom":8}},{"Entity":{"atom":6}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Alternative":{"atoms":[4,5]}},{"Repetition":{"atom":3,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"series","atom":7}},{"Sequence":{"atoms":[1,2]}},{"Repetition":{"atom":9,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":" "}},{"Entity":{"atom":24}},{"Entity":{"atom":14}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":13,"min":1,"max":null,"tag":"Repetition"}},{"Str":{"pattern":"."}},{"Str":{"pattern":"-"}},{"Alternative":{"atoms":[16,17]}},{"Entity":{"atom":14}},{"Repetition":{"atom":19,"min":1,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[18,20]}},{"Repetition":{"atom":21,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[15,22]}},{"Named":{"name":"number","atom":23}},{"Str":{"pattern":":"}},{"Entity":{"atom":29}},{"Entity":{"atom":14}},{"Repetition":{"atom":27,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"year","atom":28}},{"Str":{"pattern":"-"}},{"Entity":{"atom":34}},{"Entity":{"atom":14}},{"Repetition":{"atom":32,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"month","atom":33}},{"Str":{"pattern":"-"}},{"Entity":{"atom":39}},{"Entity":{"atom":14}},{"Repetition":{"atom":37,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"day","atom":38}},{"Sequence":{"atoms":[30,31,35,36]}},{"Repetition":{"atom":40,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[25,26,41]}},{"Repetition":{"atom":42,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[0,10,11,12,43]}}],"root":44},"bindings":[{"capture":"series","path":"series","type":"string","card":null,"preprocess":null},{"capture":"number","path":"number","type":"string","card":null,"preprocess":null},{"capture":"year","path":"year","type":"string","card":null,"preprocess":null},{"capture":"month","path":"month","type":"string","card":null,"preprocess":null},{"capture":"day","path":"day","type":"string","card":null,"preprocess":null}]}},"preprocess":{},"tables":{},"lint":{"order_warnings":[]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"CC 18011","expect":{}},{"entry":"identifier","kind":"accept","input":"CC 18011:2018","expect":{}},{"entry":"identifier","kind":"accept","input":"CC 11001:2024","expect":{}},{"entry":"identifier","kind":"accept","input":"CC/DIR 10006:2019","expect":{}},{"entry":"identifier","kind":"accept","input":"CC/Adv 0707.1:2007","expect":{}},{"entry":"identifier","kind":"accept","input":"CC/A 0812-1:2008","expect":{}},{"entry":"identifier","kind":"accept","input":"CC/WD 51017:2024-07-23","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}},{"entry":"identifier","kind":"reject","input":"CC","expect":{}},{"entry":"identifier","kind":"reject","input":"CC/WD","expect":{}},{"entry":"identifier","kind":"reject","input":"CC 18011:","expect":{}},{"entry":"identifier","kind":"reject","input":"CC 18011:2024-","expect":{}},{"entry":"identifier","kind":"example","input":"CC 18011","expect":{"number":"18011"}},{"entry":"identifier","kind":"example","input":"CC 18011:2018","expect":{"number":"18011","year":"2018"}},{"entry":"identifier","kind":"example","input":"CC 11001:2024","expect":{"number":"11001","year":"2024"}},{"entry":"identifier","kind":"example","input":"CC/DIR 10006:2019","expect":{"series":"DIR","number":"10006","year":"2019"}},{"entry":"identifier","kind":"example","input":"CC/Adv 0707.1:2007","expect":{"series":"Adv","number":"0707.1","year":"2007"}},{"entry":"identifier","kind":"example","input":"CC/A 0812-1:2008","expect":{"series":"A","number":"0812-1","year":"2008"}},{"entry":"identifier","kind":"example","input":"CC/WD 51017:2024-07-23","expect":{"series":"WD","number":"51017","year":"2024","month":"07","day":"23"}}],"docs":{},"source":"# PFS-generated grammar — DO NOT EDIT BY HAND.\n# Source: definitions/calconnect.pfs (pfs:1) sha256:2e864f0185a1a2f18a392f2f401ad3c9d65d1f32a9ec3f3f04f7dfca9b2226c4\n# Compiler: pubid/grammar/pfs 0.1.0\n# Flavor key: calconnect_pfs (grammar PubidCalconnectPfs)\n# Regenerate: ruby scripts/pfs compile definitions/calconnect.pfs --as calconnect_pfs\n\ngrammar PubidCalconnectPfs version \"0.1.0\" edition 2 {\n\n # Grammar fragments: named lexical terms declared under `grammar:`.\n digit = %x30-39\n letter = %x41-5A / %x61-7A\n space = \" \"\n slash = \"/\"\n colon = \":\"\n dash = \"-\"\n\n # Fields: one capture rule per declared field; the whole pattern\n # is the capture.\n calconnect_series = (1*letter) as series\n calconnect_number = (1*digit [(\".\" / \"-\") 1*digit]) as number\n calconnect_year = (1*digit) as year\n calconnect_month = (1*digit) as month\n calconnect_day = (1*digit) as day\n\n # Identifier rule: the canonical template first, then the\n # declared alternates (parse-in spellings; they normalize to the\n # canonical render face).\n identifier = \"CC\" [\"/\" calconnect_series] \" \" calconnect_number [\":\" calconnect_year [\"-\" calconnect_month \"-\" calconnect_day]]\n entry identifier: identifier\n\n # Typed model bindings: every bound capture lands on the\n # consumer model's field of the same name.\n bindings identifier {\n series -> series (string)\n number -> number (string)\n year -> year (string)\n month -> month (string)\n day -> day (string)\n }\n\n # REQUIRED examples from the definition — accepts, rejects, and\n # the capture expectations the compiler replayed at compile time.\n test {\n accept \"CC 18011\"\n accept \"CC 18011:2018\"\n accept \"CC 11001:2024\"\n accept \"CC/DIR 10006:2019\"\n accept \"CC/Adv 0707.1:2007\"\n accept \"CC/A 0812-1:2008\"\n accept \"CC/WD 51017:2024-07-23\"\n reject \"nonsense\"\n reject \"CC\"\n reject \"CC/WD\"\n reject \"CC 18011:\"\n reject \"CC 18011:2024-\"\n example \"CC 18011\" { number: \"18011\" }\n example \"CC 18011:2018\" { number: \"18011\" year: \"2018\" }\n example \"CC 11001:2024\" { number: \"11001\" year: \"2024\" }\n example \"CC/DIR 10006:2019\" { series: \"DIR\" number: \"10006\" year: \"2019\" }\n example \"CC/Adv 0707.1:2007\" { series: \"Adv\" number: \"0707.1\" year: \"2007\" }\n example \"CC/A 0812-1:2008\" { series: \"A\" number: \"0812-1\" year: \"2008\" }\n example \"CC/WD 51017:2024-07-23\" { series: \"WD\" number: \"51017\" year: \"2024\" month: \"07\" day: \"23\" }\n }\n}\n","source_hash":"35a33d659c86202186a5e6beb8c7df58af7bd6667dd55b6cb3cecb8855713b85","unicode_tables":"onigmo (Ruby 3.4.8)","compiler_version":"1.3.126","edition":2,"semantics_hash":"sha256:9fbceffd1828c207a9ec854699d0801fad9e52521a972e39dc1838fb80440dc8","checksum":"sha256:6557fd4360e2deab7ca4062b72e5cd6e60b6e3cd0252cb5b1e30d66f3905f467"}
@@ -0,0 +1 @@
1
+ {"bindings":null,"version":"0.1.0","grammar":"PubidCenPfs","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"EN"}},{"Str":{"pattern":" "}},{"Entity":{"atom":8}},{"Re":{"pattern":"[\\x31-\\x39]"}},{"Entity":{"atom":5}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":4,"min":0,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[3,6]}},{"Named":{"name":"number","atom":7}},{"Str":{"pattern":"-"}},{"Entity":{"atom":13}},{"Entity":{"atom":5}},{"Repetition":{"atom":11,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"part","atom":12}},{"Sequence":{"atoms":[9,10]}},{"Repetition":{"atom":14,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":":"}},{"Entity":{"atom":20}},{"Entity":{"atom":5}},{"Repetition":{"atom":18,"min":4,"max":null,"tag":"Repetition"}},{"Named":{"name":"year","atom":19}},{"Sequence":{"atoms":[16,17]}},{"Repetition":{"atom":21,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[0,1,2,15,22]}}],"root":23},"bindings":[{"capture":"number","path":"number","type":"string","card":null,"preprocess":null},{"capture":"part","path":"part","type":"string","card":null,"preprocess":null},{"capture":"year","path":"year","type":"string","card":null,"preprocess":null}]}},"preprocess":{},"tables":{},"lint":{"order_warnings":[]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"EN 1325","expect":{}},{"entry":"identifier","kind":"accept","input":"EN 12464-1:2011","expect":{}},{"entry":"identifier","kind":"accept","input":"EN 1177:2018","expect":{}},{"entry":"identifier","kind":"reject","input":"EN","expect":{}},{"entry":"identifier","kind":"reject","input":"EN ISO 9001:2015","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}},{"entry":"identifier","kind":"reject","input":"CEN/TS 12345","expect":{}},{"entry":"identifier","kind":"example","input":"EN 1325","expect":{"number":"1325"}},{"entry":"identifier","kind":"example","input":"EN 12464-1:2011","expect":{"number":"12464","part":"1","year":"2011"}},{"entry":"identifier","kind":"example","input":"EN 1177:2018","expect":{"number":"1177","year":"2018"}}],"docs":{},"source":"# PFS-generated grammar — DO NOT EDIT BY HAND.\n# Source: definitions/cen.pfs (pfs:1) sha256:bc1edd5874f8938b5f946416a2b052d9d9b9df3d99f6c20a3d88fcab9ae1d70c\n# Compiler: pubid/grammar/pfs 0.1.0\n# Flavor key: cen_pfs (grammar PubidCenPfs)\n# Regenerate: ruby scripts/pfs compile definitions/cen.pfs --as cen_pfs\n# Module: iso.inner v1 (sha256:6f04cf475f1c...)\n# Module: en.overlay v1 (sha256:851af8c5f31b...)\n# Module: national.prefix v1 (sha256:c7aa2f2442e8...)\n# Module: supplements.core v1 (sha256:9fa9656375c7...)\n\ngrammar PubidCenPfs version \"0.1.0\" edition 2 {\n\n # Grammar fragments: named lexical terms declared under `grammar:`.\n iso_space = \" \"\n iso_colon = \":\"\n iso_dash = \"-\"\n iso_year = 4*digit\n iso_part = 1*digit [\"-\" 1*digit]\n iso_number = 1*(\"0\" %x31-39) *digit [iso_dash iso_part]\n iso_document = \"ISO\" iso_space iso_number [iso_colon iso_year]\n en_space = \" \"\n en_draft = (\"pr\" / \"Fpr\")\n en_face = [en_draft] \"EN\"\n en_adoption = en_face en_space iso_document\n supplement_number = 1*digit\n supplement_year = 4*digit\n supplement_month = 2*digit\n digit = %x30-39\n space = \" \"\n slash = \"/\"\n colon = \":\"\n dash = \"-\"\n\n # Fields: one capture rule per declared field; the whole pattern\n # is the capture.\n cen_cenelec_number = ((%x31-39) *digit) as number\n cen_cenelec_part = (1*digit) as part\n cen_cenelec_year = (4*digit) as year\n\n # Identifier rule: the canonical template first, then the\n # declared alternates (parse-in spellings; they normalize to the\n # canonical render face).\n identifier = \"EN\" \" \" cen_cenelec_number [\"-\" cen_cenelec_part] [\":\" cen_cenelec_year]\n entry identifier: identifier\n\n # Typed model bindings: every bound capture lands on the\n # consumer model's field of the same name.\n bindings identifier {\n number -> number (string)\n part -> part (string)\n year -> year (string)\n }\n\n # REQUIRED examples from the definition — accepts, rejects, and\n # the capture expectations the compiler replayed at compile time.\n test {\n accept \"EN 1325\"\n accept \"EN 12464-1:2011\"\n accept \"EN 1177:2018\"\n reject \"EN\"\n reject \"EN ISO 9001:2015\"\n reject \"nonsense\"\n reject \"CEN/TS 12345\"\n example \"EN 1325\" { number: \"1325\" }\n example \"EN 12464-1:2011\" { number: \"12464\" part: \"1\" year: \"2011\" }\n example \"EN 1177:2018\" { number: \"1177\" year: \"2018\" }\n }\n}\n","source_hash":"4b84b6acda6f62fbbff60e0ff603d72e3ff00b71906914855633b6f0404869db","unicode_tables":"onigmo (Ruby 3.4.8)","compiler_version":"1.3.126","edition":2,"semantics_hash":"sha256:4363f170082d3242a35648e8b019037e9670c804d4ea951a51487697eb0c4cee","checksum":"sha256:e19eae585ed8ee560c88d97c8289d827384d21214db02634bc3eeff5a0296ac9"}
@@ -0,0 +1 @@
1
+ {"bindings":null,"version":"0.1.0","grammar":"PubidDoiPfs","shape":"parsanol-tree/v2","binding_version":1,"entries":{"identifier":{"root":"identifier","grammar":{"atoms":[{"Str":{"pattern":"doi:"}},{"Entity":{"atom":8}},{"Str":{"pattern":"10"}},{"Str":{"pattern":"."}},{"Entity":{"atom":5}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Repetition":{"atom":4,"min":2,"max":null,"tag":"Repetition"}},{"Sequence":{"atoms":[2,3,6]}},{"Named":{"name":"prefix","atom":7}},{"Str":{"pattern":"/"}},{"Entity":{"atom":23}},{"Entity":{"atom":21}},{"Re":{"pattern":"[\\x30-\\x39]"}},{"Re":{"pattern":"[\\x41-\\x5a]"}},{"Re":{"pattern":"[\\x61-\\x7a]"}},{"Re":{"pattern":"[.]"}},{"Re":{"pattern":"[_]"}},{"Re":{"pattern":"[\\-]"}},{"Re":{"pattern":"[/]"}},{"Re":{"pattern":"[(]"}},{"Re":{"pattern":"[)]"}},{"Alternative":{"atoms":[12,13,14,15,16,17,18,19,20]}},{"Repetition":{"atom":11,"min":1,"max":null,"tag":"Repetition"}},{"Named":{"name":"suffix","atom":22}},{"Sequence":{"atoms":[0,1,9,10]}},{"Entity":{"atom":33}},{"Str":{"pattern":"https://"}},{"Str":{"pattern":"http://"}},{"Alternative":{"atoms":[26,27]}},{"Repetition":{"atom":28,"min":0,"max":1,"tag":"Maybe"}},{"Str":{"pattern":"doi.org"}},{"Repetition":{"atom":30,"min":0,"max":1,"tag":"Maybe"}},{"Sequence":{"atoms":[29,31]}},{"Repetition":{"atom":32,"min":0,"max":1,"tag":"Maybe"}},{"Named":{"name":"url_prefix","atom":25}},{"Repetition":{"atom":34,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":41}},{"Str":{"pattern":"doi:"}},{"Str":{"pattern":"DOI:"}},{"Str":{"pattern":"Doi:"}},{"Alternative":{"atoms":[37,38,39]}},{"Repetition":{"atom":40,"min":0,"max":1,"tag":"Maybe"}},{"Named":{"name":"scheme_prefix","atom":36}},{"Repetition":{"atom":42,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":45}},{"Str":{"pattern":"/"}},{"Named":{"name":"slash","atom":44}},{"Repetition":{"atom":46,"min":0,"max":1,"tag":"Maybe"}},{"Entity":{"atom":8}},{"Str":{"pattern":"/"}},{"Entity":{"atom":23}},{"Sequence":{"atoms":[35,43,47,48,49,50]}},{"Alternative":{"atoms":[24,51]}}],"root":52},"bindings":[{"capture":"prefix","path":"prefix","type":"string","card":null,"preprocess":null},{"capture":"suffix","path":"suffix","type":"string","card":null,"preprocess":null}]}},"preprocess":{},"tables":{},"lint":{"order_warnings":["rule identifier: branches 1 and 2 are order-dependent (shared first bytes; ordered choice is decisive)"]},"default_entry":"identifier","render":{},"derive":{},"tests":[{"entry":"identifier","kind":"accept","input":"doi:10.1000/182","expect":{}},{"entry":"identifier","kind":"accept","input":"10.1006/jmbi.1998.2354","expect":{}},{"entry":"identifier","kind":"accept","input":"DOI:10.6028/NIST.2022-04-15.001","expect":{}},{"entry":"identifier","kind":"accept","input":"https://doi.org/10.1000/182","expect":{}},{"entry":"identifier","kind":"reject","input":"doi:","expect":{}},{"entry":"identifier","kind":"reject","input":"doi:10","expect":{}},{"entry":"identifier","kind":"reject","input":"doi:10.X/abc","expect":{}},{"entry":"identifier","kind":"reject","input":"doi:10.1000/","expect":{}},{"entry":"identifier","kind":"reject","input":"nonsense","expect":{}},{"entry":"identifier","kind":"example","input":"doi:10.1000/182","expect":{"prefix":"10.1000","suffix":"182"}},{"entry":"identifier","kind":"example","input":"10.1006/jmbi.1998.2354","expect":{"prefix":"10.1006","suffix":"jmbi.1998.2354"}},{"entry":"identifier","kind":"example","input":"DOI:10.6028/NIST.2022-04-15.001","expect":{"prefix":"10.6028","suffix":"NIST.2022-04-15.001"}},{"entry":"identifier","kind":"example","input":"https://doi.org/10.1000/182","expect":{"prefix":"10.1000","suffix":"182"}}],"docs":{},"source":"# PFS-generated grammar — DO NOT EDIT BY HAND.\n# Source: definitions/doi.pfs (pfs:1) sha256:60abc3c403c61019d727ee44d9a72591c1549672451e6609a72d36189cae6340\n# Compiler: pubid/grammar/pfs 0.1.0\n# Flavor key: doi_pfs (grammar PubidDoiPfs)\n# Regenerate: ruby scripts/pfs compile definitions/doi.pfs --as doi_pfs\n\ngrammar PubidDoiPfs version \"0.1.0\" edition 2 {\n\n # Grammar fragments: named lexical terms declared under `grammar:`.\n digit = %x30-39\n suffix_char = %x30-39 / %x41-5A / %x61-7A / %x2E / %x5F / %x2D / %x2F / %x28 / %x29\n slash = \"/\"\n url_prefix = [[\"https://\" / \"http://\"] [\"doi.org\"]]\n scheme_prefix = [\"doi:\" / \"DOI:\" / \"Doi:\"]\n\n # Fields: one capture rule per declared field; the whole pattern\n # is the capture.\n doi_prefix = (\"10\" \".\" 2*digit) as prefix\n doi_suffix = (1*suffix_char) as suffix\n\n # Identifier rule: the canonical template first, then the\n # declared alternates (parse-in spellings; they normalize to the\n # canonical render face).\n identifier = {\n \"doi:\" doi_prefix \"/\" doi_suffix\n / [url_prefix] [scheme_prefix] [slash] doi_prefix \"/\" doi_suffix\n }\n entry identifier: identifier\n\n # Typed model bindings: every bound capture lands on the\n # consumer model's field of the same name.\n bindings identifier {\n prefix -> prefix (string)\n suffix -> suffix (string)\n }\n\n # REQUIRED examples from the definition — accepts, rejects, and\n # the capture expectations the compiler replayed at compile time.\n test {\n accept \"doi:10.1000/182\"\n accept \"10.1006/jmbi.1998.2354\"\n accept \"DOI:10.6028/NIST.2022-04-15.001\"\n accept \"https://doi.org/10.1000/182\"\n reject \"doi:\"\n reject \"doi:10\"\n reject \"doi:10.X/abc\"\n reject \"doi:10.1000/\"\n reject \"nonsense\"\n example \"doi:10.1000/182\" { prefix: \"10.1000\" suffix: \"182\" }\n example \"10.1006/jmbi.1998.2354\" { prefix: \"10.1006\" suffix: \"jmbi.1998.2354\" }\n example \"DOI:10.6028/NIST.2022-04-15.001\" { prefix: \"10.6028\" suffix: \"NIST.2022-04-15.001\" }\n example \"https://doi.org/10.1000/182\" { prefix: \"10.1000\" suffix: \"182\" }\n }\n}\n","source_hash":"c872de21dfa95b1add4eed4785182c6e661fc79a0a8c15c07eb6209621a20f2c","unicode_tables":"onigmo (Ruby 3.4.8)","compiler_version":"1.3.126","edition":2,"semantics_hash":"sha256:06d0039460d751282b122dd5834bd505046c2cb32605c051ac296f2c29f964b3","checksum":"sha256:25f3643ce0fe181ae16b91a87a43177d495079ed590c6eee3c3cf36825e1fc05"}