@pubid/pubid 0.1.3 → 0.1.4

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 (183) hide show
  1. package/artifact/corpus.json.gz +0 -0
  2. package/dist/conformance/cli.js +5 -1
  3. package/dist/flavors/amca/grammar.d.ts +4 -0
  4. package/dist/flavors/amca/grammar.js +87 -0
  5. package/dist/flavors/amca/index.d.ts +4 -0
  6. package/dist/flavors/amca/index.js +76 -0
  7. package/dist/flavors/amca/model.d.ts +29 -0
  8. package/dist/flavors/amca/model.js +146 -0
  9. package/dist/flavors/ansi/index.d.ts +24 -0
  10. package/dist/flavors/ansi/index.js +147 -0
  11. package/dist/flavors/api/grammar.d.ts +4 -0
  12. package/dist/flavors/api/grammar.js +79 -0
  13. package/dist/flavors/api/index.d.ts +4 -0
  14. package/dist/flavors/api/index.js +71 -0
  15. package/dist/flavors/api/model.d.ts +27 -0
  16. package/dist/flavors/api/model.js +114 -0
  17. package/dist/flavors/ashrae/grammar.d.ts +2 -0
  18. package/dist/flavors/ashrae/grammar.js +667 -0
  19. package/dist/flavors/ashrae/index.d.ts +4 -0
  20. package/dist/flavors/ashrae/index.js +306 -0
  21. package/dist/flavors/ashrae/model.d.ts +87 -0
  22. package/dist/flavors/ashrae/model.js +335 -0
  23. package/dist/flavors/ashrae/preprocessor.d.ts +7 -0
  24. package/dist/flavors/ashrae/preprocessor.js +44 -0
  25. package/dist/flavors/asme/grammar.d.ts +2 -0
  26. package/dist/flavors/asme/grammar.js +200 -0
  27. package/dist/flavors/asme/index.d.ts +4 -0
  28. package/dist/flavors/asme/index.js +138 -0
  29. package/dist/flavors/asme/model.d.ts +34 -0
  30. package/dist/flavors/asme/model.js +123 -0
  31. package/dist/flavors/astm/grammar.d.ts +2 -0
  32. package/dist/flavors/astm/grammar.js +110 -0
  33. package/dist/flavors/astm/index.d.ts +2 -0
  34. package/dist/flavors/astm/index.js +127 -0
  35. package/dist/flavors/astm/model.d.ts +42 -0
  36. package/dist/flavors/astm/model.js +263 -0
  37. package/dist/flavors/bipm/index.d.ts +30 -0
  38. package/dist/flavors/bipm/index.js +434 -0
  39. package/dist/flavors/bsi/grammar.d.ts +4 -0
  40. package/dist/flavors/bsi/grammar.js +535 -0
  41. package/dist/flavors/bsi/index.d.ts +4 -0
  42. package/dist/flavors/bsi/index.js +941 -0
  43. package/dist/flavors/bsi/model.d.ts +127 -0
  44. package/dist/flavors/bsi/model.js +863 -0
  45. package/dist/flavors/calconnect/index.d.ts +21 -0
  46. package/dist/flavors/calconnect/index.js +113 -0
  47. package/dist/flavors/cen_cenelec/grammar.d.ts +4 -0
  48. package/dist/flavors/cen_cenelec/grammar.js +107 -0
  49. package/dist/flavors/cen_cenelec/index.d.ts +5 -0
  50. package/dist/flavors/cen_cenelec/index.js +263 -0
  51. package/dist/flavors/cen_cenelec/model.d.ts +45 -0
  52. package/dist/flavors/cen_cenelec/model.js +490 -0
  53. package/dist/flavors/cie/grammar.d.ts +5 -0
  54. package/dist/flavors/cie/grammar.js +127 -0
  55. package/dist/flavors/cie/index.d.ts +2 -0
  56. package/dist/flavors/cie/index.js +311 -0
  57. package/dist/flavors/cie/model.d.ts +122 -0
  58. package/dist/flavors/cie/model.js +464 -0
  59. package/dist/flavors/csa/grammar.d.ts +6 -0
  60. package/dist/flavors/csa/grammar.js +239 -0
  61. package/dist/flavors/csa/index.d.ts +2 -0
  62. package/dist/flavors/csa/index.js +634 -0
  63. package/dist/flavors/csa/model.d.ts +33 -0
  64. package/dist/flavors/csa/model.js +505 -0
  65. package/dist/flavors/doi/index.d.ts +13 -0
  66. package/dist/flavors/doi/index.js +65 -0
  67. package/dist/flavors/easc/index.d.ts +4 -0
  68. package/dist/flavors/easc/index.js +107 -0
  69. package/dist/flavors/ecma/index.d.ts +4 -0
  70. package/dist/flavors/ecma/index.js +117 -0
  71. package/dist/flavors/etsi/index.d.ts +14 -0
  72. package/dist/flavors/etsi/index.js +307 -0
  73. package/dist/flavors/evs/grammar.d.ts +2 -0
  74. package/dist/flavors/evs/grammar.js +22 -0
  75. package/dist/flavors/evs/index.d.ts +2 -0
  76. package/dist/flavors/evs/index.js +24 -0
  77. package/dist/flavors/evs/model.d.ts +19 -0
  78. package/dist/flavors/evs/model.js +36 -0
  79. package/dist/flavors/gb/index.d.ts +24 -0
  80. package/dist/flavors/gb/index.js +111 -0
  81. package/dist/flavors/gost/grammar.d.ts +2 -0
  82. package/dist/flavors/gost/grammar.js +46 -0
  83. package/dist/flavors/gost/index.d.ts +2 -0
  84. package/dist/flavors/gost/index.js +165 -0
  85. package/dist/flavors/gost/model.d.ts +61 -0
  86. package/dist/flavors/gost/model.js +147 -0
  87. package/dist/flavors/gost/prefix_routes.d.ts +15 -0
  88. package/dist/flavors/gost/prefix_routes.js +192 -0
  89. package/dist/flavors/iala/index.d.ts +32 -0
  90. package/dist/flavors/iala/index.js +300 -0
  91. package/dist/flavors/iana/index.d.ts +19 -0
  92. package/dist/flavors/iana/index.js +71 -0
  93. package/dist/flavors/idf/grammar.d.ts +2 -0
  94. package/dist/flavors/idf/grammar.js +71 -0
  95. package/dist/flavors/idf/index.d.ts +2 -0
  96. package/dist/flavors/idf/index.js +100 -0
  97. package/dist/flavors/idf/model.d.ts +54 -0
  98. package/dist/flavors/idf/model.js +359 -0
  99. package/dist/flavors/iec/grammar.d.ts +3 -0
  100. package/dist/flavors/iec/grammar.js +187 -0
  101. package/dist/flavors/iec/index.d.ts +4 -0
  102. package/dist/flavors/iec/index.js +507 -0
  103. package/dist/flavors/iec/model.d.ts +54 -0
  104. package/dist/flavors/iec/model.js +437 -0
  105. package/dist/flavors/ieee/grammar.d.ts +2 -0
  106. package/dist/flavors/ieee/grammar.js +626 -0
  107. package/dist/flavors/ieee/index.d.ts +3 -0
  108. package/dist/flavors/ieee/index.js +1186 -0
  109. package/dist/flavors/ieee/model.d.ts +142 -0
  110. package/dist/flavors/ieee/model.js +1148 -0
  111. package/dist/flavors/ieee/preprocessor.d.ts +8 -0
  112. package/dist/flavors/ieee/preprocessor.js +410 -0
  113. package/dist/flavors/ieee/update_codes.d.ts +8 -0
  114. package/dist/flavors/ieee/update_codes.js +191 -0
  115. package/dist/flavors/ietf/index.d.ts +30 -0
  116. package/dist/flavors/ietf/index.js +151 -0
  117. package/dist/flavors/iho/index.d.ts +4 -0
  118. package/dist/flavors/iho/index.js +126 -0
  119. package/dist/flavors/index.d.ts +9 -0
  120. package/dist/flavors/index.js +136 -0
  121. package/dist/flavors/isbn/index.d.ts +13 -0
  122. package/dist/flavors/isbn/index.js +97 -0
  123. package/dist/flavors/iso/grammar.d.ts +2 -0
  124. package/dist/flavors/iso/grammar.js +207 -0
  125. package/dist/flavors/iso/index.d.ts +2 -0
  126. package/dist/flavors/iso/index.js +343 -0
  127. package/dist/flavors/iso/model.d.ts +159 -0
  128. package/dist/flavors/iso/model.js +802 -0
  129. package/dist/flavors/iso/stages.generated.d.ts +13 -0
  130. package/dist/flavors/iso/stages.generated.js +391 -0
  131. package/dist/flavors/itu/grammar.d.ts +4 -0
  132. package/dist/flavors/itu/grammar.js +205 -0
  133. package/dist/flavors/itu/index.d.ts +2 -0
  134. package/dist/flavors/itu/index.js +224 -0
  135. package/dist/flavors/itu/model.d.ts +184 -0
  136. package/dist/flavors/itu/model.js +599 -0
  137. package/dist/flavors/jcgm/index.d.ts +7 -0
  138. package/dist/flavors/jcgm/index.js +300 -0
  139. package/dist/flavors/jis/index.d.ts +4 -0
  140. package/dist/flavors/jis/index.js +267 -0
  141. package/dist/flavors/nist/grammar.d.ts +2 -0
  142. package/dist/flavors/nist/grammar.js +287 -0
  143. package/dist/flavors/nist/index.d.ts +2 -0
  144. package/dist/flavors/nist/index.js +1244 -0
  145. package/dist/flavors/nist/model.d.ts +155 -0
  146. package/dist/flavors/nist/model.js +823 -0
  147. package/dist/flavors/nist/preprocessor.d.ts +11 -0
  148. package/dist/flavors/nist/preprocessor.js +281 -0
  149. package/dist/flavors/oasis/index.d.ts +23 -0
  150. package/dist/flavors/oasis/index.js +110 -0
  151. package/dist/flavors/ogc/index.d.ts +20 -0
  152. package/dist/flavors/ogc/index.js +75 -0
  153. package/dist/flavors/oiml/grammar.d.ts +2 -0
  154. package/dist/flavors/oiml/grammar.js +180 -0
  155. package/dist/flavors/oiml/implementation.d.ts +9 -0
  156. package/dist/flavors/oiml/implementation.js +16 -0
  157. package/dist/flavors/oiml/model.d.ts +4 -0
  158. package/dist/flavors/oiml/model.js +462 -0
  159. package/dist/flavors/omg/index.d.ts +14 -0
  160. package/dist/flavors/omg/index.js +73 -0
  161. package/dist/flavors/plateau/index.d.ts +4 -0
  162. package/dist/flavors/plateau/index.js +97 -0
  163. package/dist/flavors/tgpp/index.d.ts +4 -0
  164. package/dist/flavors/tgpp/index.js +122 -0
  165. package/dist/flavors/un/index.d.ts +13 -0
  166. package/dist/flavors/un/index.js +90 -0
  167. package/dist/flavors/w3c/index.d.ts +14 -0
  168. package/dist/flavors/w3c/index.js +148 -0
  169. package/dist/flavors/xsf/index.d.ts +18 -0
  170. package/dist/flavors/xsf/index.js +65 -0
  171. package/dist/grammar/engine.d.ts +78 -0
  172. package/dist/grammar/engine.js +351 -0
  173. package/dist/model/attribute.d.ts +54 -0
  174. package/dist/model/attribute.js +53 -0
  175. package/dist/model/builder.d.ts +39 -0
  176. package/dist/model/builder.js +124 -0
  177. package/dist/model/component.d.ts +70 -0
  178. package/dist/model/component.js +170 -0
  179. package/dist/model/identifier.d.ts +83 -0
  180. package/dist/model/identifier.js +270 -0
  181. package/dist/model/urn-generator.d.ts +27 -0
  182. package/dist/model/urn-generator.js +91 -0
  183. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ import { match, str } from "../../grammar/engine.js";
2
+ import { ParseFailed, parseGrammar } from "../../grammar/engine.js";
3
+ import { BaseIdentifier, registerType } from "../../model/identifier.js";
4
+ import { extendAttributes, keyValue } from "../../model/attribute.js";
5
+ import { BaseUrnGenerator } from "../../model/urn-generator.js";
6
+ import { BaseBuilder } from "../../model/builder.js";
7
+ /**
8
+ * 1:1 port of lib/pubid/xsf/ on the unified model. XEP numbers, plus the
9
+ * two named documents ("XEP README", "XEP xxxx") kept verbatim. URN:
10
+ * urn:xsf:xep:<number> (the Ruby flavor's own generator).
11
+ */
12
+ // The editor README and the xep-xxxx template reach pubid as real
13
+ // primary docids.
14
+ const SPECIAL_NUMBERS = ["README", "xxxx"];
15
+ function buildRules() {
16
+ const rules = {};
17
+ const rule = (name, build) => {
18
+ rules[name] = build();
19
+ };
20
+ const specialNumber = SPECIAL_NUMBERS.map((name) => str(name)).reduce((a, b) => a.or(b));
21
+ rule("special_number", () => specialNumber);
22
+ const digits = match("[0-9]").repeat(1, Infinity);
23
+ rule("digits", () => digits);
24
+ rule("identifier", () => str("XEP").then(str(" "), digits.or(specialNumber).as("number")));
25
+ rule("root", () => rules["identifier"]);
26
+ return rules;
27
+ }
28
+ export const xsfGrammar = {
29
+ rules: buildRules(),
30
+ root: "root",
31
+ };
32
+ class XsfUrnGenerator extends BaseUrnGenerator {
33
+ generate() {
34
+ return ["urn", "xsf", "xep", this.identifier.number].join(":");
35
+ }
36
+ }
37
+ export class XsfIdentifier extends BaseIdentifier {
38
+ static polymorphicName = "pubid:xsf:xep";
39
+ static attributes = extendAttributes(BaseIdentifier, {
40
+ number: { type: "string" },
41
+ });
42
+ static mappings = keyValue({ wire: "number", to: "number" });
43
+ static urnGenerator = XsfUrnGenerator;
44
+ render() {
45
+ return `XEP ${this.number}`;
46
+ }
47
+ }
48
+ registerType(XsfIdentifier);
49
+ class XsfBuilder extends BaseBuilder {
50
+ defaultIdentifierClass() {
51
+ return XsfIdentifier;
52
+ }
53
+ }
54
+ export function xsfGrammarImplementation() {
55
+ const builder = new XsfBuilder();
56
+ return {
57
+ parse(input) {
58
+ const tree = parseGrammar(xsfGrammar, input);
59
+ if (typeof tree !== "object" || tree === null || Array.isArray(tree)) {
60
+ throw new ParseFailed("XSF: unexpected parse tree", 0);
61
+ }
62
+ return builder.build(tree);
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * A parslet-compatible parser-combinator engine.
3
+ *
4
+ * The Ruby pubid grammars are declarative parslet rule trees; porting a
5
+ * flavor 1:1 requires the TypeScript engine to reproduce parslet's tree
6
+ * semantics exactly, because the Ruby builders read those trees. The
7
+ * semantics below were pinned empirically against parslet 2.0 (see the
8
+ * porting guide in docs/PORTING.md):
9
+ *
10
+ * - atoms produce a string, a capture hash, an array, or nil
11
+ * - sequences fold with `combine`: string+string concat; a plain string
12
+ * is DISCARDED when combined with a capture; hash+hash merges and a
13
+ * duplicate key raises; nil is transparent
14
+ * - `.as(key)` wraps the result: {key: result}
15
+ * - `.repeat` is greedy and does NOT backtrack a successful iteration to
16
+ * satisfy an outer minimum (parslet: "Expected at least N of ...");
17
+ * results are an array when any iteration captured, else a concat string
18
+ * - `.maybe` is special-cased like parslet's Maybe: one attempt, nil on
19
+ * failure (not an empty array)
20
+ * - `absent?`/`present?` are zero-width lookaheads
21
+ * - full input consumption is enforced the way parslet 2.0 does it: a
22
+ * `consumeAll` flag is threaded down the parse spine — the root starts
23
+ * with it, a Sequence hands it ONLY to its last child, Alternatives
24
+ * pass it to every branch, and Repetition iterations never get it. An
25
+ * atom that succeeds while unconsumed input remains FAILS (parslet
26
+ * Base#apply's "Don't know what to do with ..." check). Because that
27
+ * failure is an ordinary match failure, an enclosing Alternative
28
+ * retries its next branch — so `a | b` DOES re-try `b` when `a`
29
+ * matched but left trailing input (pinned against parslet: bare
30
+ * "080442957X" reaches ISBN's second body alternative this way).
31
+ */
32
+ export declare class ParseFailed extends Error {
33
+ readonly pos: number;
34
+ constructor(message: string, pos: number);
35
+ }
36
+ export type Tree = string | number | TreeObject | Tree[] | null | undefined;
37
+ export interface TreeObject {
38
+ [key: string]: Tree;
39
+ }
40
+ /** Context threaded through every match attempt. */
41
+ declare class Ctx {
42
+ input: string;
43
+ pos: number;
44
+ constructor(input: string);
45
+ }
46
+ export interface Atom {
47
+ /** Attempt a match at ctx.pos; failure throws Fail. */
48
+ _match(ctx: Ctx, consumeAll: boolean): Tree | undefined;
49
+ }
50
+ /** Builder-side sugar: chainable wrapper. */
51
+ export declare class P {
52
+ readonly atom: Atom;
53
+ constructor(atom: Atom);
54
+ then(...next: (P | Atom)[]): P;
55
+ or(...others: (P | Atom)[]): P;
56
+ repeat(min?: number, max?: number): P;
57
+ maybe(): P;
58
+ as(key: string): P;
59
+ absent(): P;
60
+ present(): P;
61
+ }
62
+ export declare function unwrap(p: P | Atom): Atom;
63
+ export declare function str(s: string): P;
64
+ /** parslet's match: a single-character regex match, e.g. match("[a-z]"). */
65
+ export declare function match(pattern: string): P;
66
+ /** A named rule reference (lazily resolved — grammars are recursive). */
67
+ export declare function ref(rules: Record<string, P | Atom>, name: string): P;
68
+ export interface Grammar {
69
+ rules: Record<string, P | Atom>;
70
+ root: string;
71
+ }
72
+ /**
73
+ * Parse the whole input with the grammar's root rule, parslet-style:
74
+ * full consumption is enforced by the root's consumeAll flag, so an
75
+ * Alternative at the spine retries a branch that left trailing input.
76
+ */
77
+ export declare function parseGrammar(grammar: Grammar, input: string): Tree;
78
+ export {};
@@ -0,0 +1,351 @@
1
+ /**
2
+ * A parslet-compatible parser-combinator engine.
3
+ *
4
+ * The Ruby pubid grammars are declarative parslet rule trees; porting a
5
+ * flavor 1:1 requires the TypeScript engine to reproduce parslet's tree
6
+ * semantics exactly, because the Ruby builders read those trees. The
7
+ * semantics below were pinned empirically against parslet 2.0 (see the
8
+ * porting guide in docs/PORTING.md):
9
+ *
10
+ * - atoms produce a string, a capture hash, an array, or nil
11
+ * - sequences fold with `combine`: string+string concat; a plain string
12
+ * is DISCARDED when combined with a capture; hash+hash merges and a
13
+ * duplicate key raises; nil is transparent
14
+ * - `.as(key)` wraps the result: {key: result}
15
+ * - `.repeat` is greedy and does NOT backtrack a successful iteration to
16
+ * satisfy an outer minimum (parslet: "Expected at least N of ...");
17
+ * results are an array when any iteration captured, else a concat string
18
+ * - `.maybe` is special-cased like parslet's Maybe: one attempt, nil on
19
+ * failure (not an empty array)
20
+ * - `absent?`/`present?` are zero-width lookaheads
21
+ * - full input consumption is enforced the way parslet 2.0 does it: a
22
+ * `consumeAll` flag is threaded down the parse spine — the root starts
23
+ * with it, a Sequence hands it ONLY to its last child, Alternatives
24
+ * pass it to every branch, and Repetition iterations never get it. An
25
+ * atom that succeeds while unconsumed input remains FAILS (parslet
26
+ * Base#apply's "Don't know what to do with ..." check). Because that
27
+ * failure is an ordinary match failure, an enclosing Alternative
28
+ * retries its next branch — so `a | b` DOES re-try `b` when `a`
29
+ * matched but left trailing input (pinned against parslet: bare
30
+ * "080442957X" reaches ISBN's second body alternative this way).
31
+ */
32
+ export class ParseFailed extends Error {
33
+ pos;
34
+ constructor(message, pos) {
35
+ super(`${message} at line 1 char ${pos + 1}`);
36
+ this.name = "ParseFailed";
37
+ this.pos = pos;
38
+ }
39
+ }
40
+ /** Context threaded through every match attempt. */
41
+ class Ctx {
42
+ input;
43
+ pos = 0;
44
+ constructor(input) {
45
+ this.input = input;
46
+ }
47
+ }
48
+ class Fail extends Error {
49
+ }
50
+ function attempt(ctx, fn) {
51
+ const saved = ctx.pos;
52
+ try {
53
+ return fn();
54
+ }
55
+ catch (e) {
56
+ if (e instanceof Fail) {
57
+ ctx.pos = saved;
58
+ return undefined;
59
+ }
60
+ throw e;
61
+ }
62
+ }
63
+ /**
64
+ * parslet's Base#apply: run the atom's try, then — when the caller
65
+ * demanded full consumption and input is left over — rewind and fail.
66
+ * This is what lets an enclosing Alternative retry after a branch that
67
+ * matched but did not consume everything.
68
+ */
69
+ function applyAtom(atom, ctx, consumeAll) {
70
+ const saved = ctx.pos;
71
+ const result = atom._match(ctx, consumeAll);
72
+ if (consumeAll && ctx.pos < ctx.input.length) {
73
+ ctx.pos = saved;
74
+ throw new Fail(`Don't know what to do with ${JSON.stringify(ctx.input.slice(ctx.pos, ctx.pos + 10))}`);
75
+ }
76
+ return result;
77
+ }
78
+ /** Combine two sequence results the way parslet's Sequence does. */
79
+ function combine(a, b) {
80
+ if (a === undefined || a === null)
81
+ return b;
82
+ if (b === undefined || b === null)
83
+ return a;
84
+ if (typeof a === "string" && typeof b === "string")
85
+ return a + b;
86
+ // A plain string is discarded when the other side carries structure.
87
+ if (typeof a === "string" && typeof b === "object")
88
+ return b;
89
+ if (typeof b === "string" && typeof a === "object")
90
+ return a;
91
+ if (typeof a === "object" && typeof b === "object") {
92
+ if (!Array.isArray(a) && !Array.isArray(b)) {
93
+ // parslet merge on duplicate keys: warn and keep the LATTER value
94
+ // (astm's dual unit relies on this — the second :letter wins).
95
+ const out = { ...a };
96
+ for (const [k, v] of Object.entries(b)) {
97
+ if (k in out) {
98
+ console.warn(`Duplicate subtrees while merging result of sequence (keys: :${k}); only the values of the latter will be kept.`);
99
+ }
100
+ out[k] = v;
101
+ }
102
+ return out;
103
+ }
104
+ return [...flatten(a), ...flatten(b)];
105
+ }
106
+ // number + anything: stringify
107
+ return typeof b === "object" ? b : `${a}${b}`;
108
+ }
109
+ function flatten(t) {
110
+ return Array.isArray(t) ? t : [t];
111
+ }
112
+ class Str {
113
+ s;
114
+ constructor(s) {
115
+ this.s = s;
116
+ }
117
+ _match(ctx, _consumeAll) {
118
+ if (ctx.input.startsWith(this.s, ctx.pos)) {
119
+ ctx.pos += this.s.length;
120
+ return this.s;
121
+ }
122
+ throw new Fail(`Expected ${JSON.stringify(this.s)}`);
123
+ }
124
+ }
125
+ class Regex {
126
+ re;
127
+ constructor(pattern) {
128
+ // parslet anchors the pattern and consumes exactly ONE match
129
+ this.re = new RegExp(`^(?:${pattern})`);
130
+ }
131
+ _match(ctx, _consumeAll) {
132
+ const m = this.re.exec(ctx.input.slice(ctx.pos));
133
+ if (!m)
134
+ throw new Fail(`Expected match on ${this.re.source}`);
135
+ const matched = m[0];
136
+ ctx.pos += matched.length;
137
+ return matched;
138
+ }
139
+ }
140
+ class Seq {
141
+ parts;
142
+ constructor(parts) {
143
+ this.parts = parts;
144
+ }
145
+ _match(ctx, consumeAll) {
146
+ let acc = undefined;
147
+ for (let i = 0; i < this.parts.length; i++) {
148
+ // parslet Sequence: only the LAST child inherits the must-consume
149
+ // obligation.
150
+ const r = applyAtom(this.parts[i], ctx, consumeAll && i === this.parts.length - 1);
151
+ acc = acc === undefined && r === undefined ? undefined : combine(acc, r);
152
+ }
153
+ return acc;
154
+ }
155
+ }
156
+ class Alt {
157
+ options;
158
+ constructor(options) {
159
+ this.options = options;
160
+ }
161
+ _match(ctx, consumeAll) {
162
+ let lastFail = "no alternative matched";
163
+ for (const option of this.options) {
164
+ const r = attempt(ctx, () => applyAtom(option, ctx, consumeAll));
165
+ if (r !== undefined)
166
+ return r;
167
+ lastFail = "alternative failed";
168
+ }
169
+ throw new Fail(lastFail);
170
+ }
171
+ }
172
+ class Repeat {
173
+ atom;
174
+ min;
175
+ max;
176
+ constructor(atom, min, max) {
177
+ this.atom = atom;
178
+ this.min = min;
179
+ this.max = max;
180
+ }
181
+ get inner() {
182
+ return this.atom instanceof P ? this.atom.atom : this.atom;
183
+ }
184
+ _match(ctx, consumeAll) {
185
+ const results = [];
186
+ let count = 0;
187
+ while (count < this.max) {
188
+ // parslet Repetition: iterations never carry the consume-all flag.
189
+ const r = attempt(ctx, () => applyAtom(this.inner, ctx, false));
190
+ if (r === undefined)
191
+ break;
192
+ results.push(r);
193
+ count++;
194
+ if (ctx.pos >= ctx.input.length && count < this.min)
195
+ break;
196
+ }
197
+ if (count < this.min) {
198
+ throw new Fail(`Expected at least ${this.min} of repetition`);
199
+ }
200
+ // A repetition that stopped on its own inner failure while input
201
+ // remains and full consumption was demanded fails here (parslet's
202
+ // post-loop unconsumed check); a repetition that hit its max leaves
203
+ // that to applyAtom.
204
+ if (consumeAll && count < this.max && ctx.pos < ctx.input.length) {
205
+ throw new Fail("Don't know what to do with trailing input after repetition");
206
+ }
207
+ if (results.some((r) => typeof r === "object"))
208
+ return results;
209
+ return results.join("");
210
+ }
211
+ }
212
+ class Maybe {
213
+ atom;
214
+ constructor(atom) {
215
+ this.atom = atom;
216
+ }
217
+ _match(ctx, consumeAll) {
218
+ const inner = this.atom instanceof P ? this.atom.atom : this.atom;
219
+ const r = attempt(ctx, () => applyAtom(inner, ctx, consumeAll));
220
+ if (r === undefined || r === "")
221
+ return undefined;
222
+ return r;
223
+ }
224
+ }
225
+ class As {
226
+ atom;
227
+ key;
228
+ constructor(atom, key) {
229
+ this.atom = atom;
230
+ this.key = key;
231
+ }
232
+ get inner() {
233
+ return this.atom instanceof P ? this.atom.atom : this.atom;
234
+ }
235
+ _match(ctx, consumeAll) {
236
+ const r = this.inner._match(ctx, consumeAll);
237
+ return { [this.key]: r === undefined ? null : r };
238
+ }
239
+ }
240
+ class Absent {
241
+ atom;
242
+ constructor(atom) {
243
+ this.atom = atom;
244
+ }
245
+ _match(ctx, _consumeAll) {
246
+ const inner = this.atom instanceof P ? this.atom.atom : this.atom;
247
+ const r = attempt(ctx, () => inner._match(ctx, false));
248
+ if (r !== undefined)
249
+ throw new Fail("unexpectedly matched");
250
+ return "";
251
+ }
252
+ }
253
+ class Present {
254
+ atom;
255
+ constructor(atom) {
256
+ this.atom = atom;
257
+ }
258
+ _match(ctx, _consumeAll) {
259
+ const inner = this.atom instanceof P ? this.atom.atom : this.atom;
260
+ // A true lookahead that NEVER consumes input, and fails exactly when
261
+ // the probe fails (parslet's present?): the position is restored in
262
+ // both cases, but a non-match re-raises the ordinary failure so an
263
+ // enclosing Alternative retries its next branch.
264
+ const saved = ctx.pos;
265
+ try {
266
+ inner._match(ctx, false);
267
+ }
268
+ catch (e) {
269
+ if (!(e instanceof Fail))
270
+ throw e;
271
+ ctx.pos = saved;
272
+ throw new Fail("present? probe did not match");
273
+ }
274
+ ctx.pos = saved;
275
+ return "";
276
+ }
277
+ }
278
+ class Ref {
279
+ rules;
280
+ name;
281
+ resolved;
282
+ constructor(rules, name) {
283
+ this.rules = rules;
284
+ this.name = name;
285
+ }
286
+ _match(ctx, consumeAll) {
287
+ if (!this.resolved) {
288
+ const rule = this.rules[this.name];
289
+ if (!rule)
290
+ throw new Error(`unknown rule :${this.name}`);
291
+ this.resolved = rule instanceof P ? rule.atom : rule;
292
+ }
293
+ return this.resolved._match(ctx, consumeAll);
294
+ }
295
+ }
296
+ /** Builder-side sugar: chainable wrapper. */
297
+ export class P {
298
+ atom;
299
+ constructor(atom) {
300
+ this.atom = atom;
301
+ }
302
+ then(...next) {
303
+ return new P(new Seq([this.atom, ...next.map(unwrap)]));
304
+ }
305
+ or(...others) {
306
+ return new P(new Alt([this.atom, ...others.map(unwrap)]));
307
+ }
308
+ repeat(min = 0, max = Infinity) {
309
+ return new P(new Repeat(this.atom, min, max));
310
+ }
311
+ maybe() {
312
+ return new P(new Maybe(this.atom));
313
+ }
314
+ as(key) {
315
+ return new P(new As(this.atom, key));
316
+ }
317
+ absent() {
318
+ return new P(new Absent(this.atom));
319
+ }
320
+ present() {
321
+ return new P(new Present(this.atom));
322
+ }
323
+ }
324
+ export function unwrap(p) {
325
+ return p instanceof P ? p.atom : p;
326
+ }
327
+ export function str(s) {
328
+ return new P(new Str(s));
329
+ }
330
+ /** parslet's match: a single-character regex match, e.g. match("[a-z]"). */
331
+ export function match(pattern) {
332
+ return new P(new Regex(pattern));
333
+ }
334
+ /** A named rule reference (lazily resolved — grammars are recursive). */
335
+ export function ref(rules, name) {
336
+ return new P(new Ref(rules, name));
337
+ }
338
+ /**
339
+ * Parse the whole input with the grammar's root rule, parslet-style:
340
+ * full consumption is enforced by the root's consumeAll flag, so an
341
+ * Alternative at the spine retries a branch that left trailing input.
342
+ */
343
+ export function parseGrammar(grammar, input) {
344
+ const ctx = new Ctx(input);
345
+ const root = new Ref(grammar.rules, grammar.root);
346
+ const result = attempt(ctx, () => applyAtom(root, ctx, true));
347
+ if (result === undefined) {
348
+ throw new ParseFailed(`Expected one of [${grammar.root.toUpperCase()}]`, ctx.pos);
349
+ }
350
+ return result;
351
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Declarative attributes and wire mappings — the TS mirror of pubid's
3
+ * `attribute` DSL + lutaml key_value mappings. Semantics pinned in
4
+ * TODO.unified/01-study-pubid-core.md §1-3.
5
+ *
6
+ * A flavor DECLARES its shape; BaseIdentifier derives serialization.
7
+ * The only imperative wire code allowed is a converter pair
8
+ * (`toWire`/`fromWire`), mirroring Ruby's `map "x", with: {to:, from:}`.
9
+ */
10
+ import type { Component } from "./component.js";
11
+ export type ScalarType = "string" | "integer" | "boolean";
12
+ export type ComponentCtor = new (attrs: Record<string, unknown>) => Component;
13
+ export type AttributeType = ScalarType | ComponentCtor | (new (...args: never[]) => unknown);
14
+ export interface AttributeSpec {
15
+ type: AttributeType;
16
+ /** Value or thunk; a value equal to the default is dropped from the canonical hash. */
17
+ default?: unknown;
18
+ collection?: boolean;
19
+ /** Materialize [] when the input hash omits the key (tgpp parts). */
20
+ initializeEmpty?: boolean;
21
+ }
22
+ export type AttributeTable = Record<string, AttributeSpec>;
23
+ /**
24
+ * Ruby snapshots the parent attribute table into each subclass at
25
+ * class-definition time; TS composes the same way — a subclass's table
26
+ * is the parent's plus its overrides.
27
+ */
28
+ export declare function extendAttributes(parent: {
29
+ attributes: AttributeTable;
30
+ }, defs: AttributeTable): AttributeTable;
31
+ /**
32
+ * The base table mirrors Pubid::Identifier (identifier.rb:503-525),
33
+ * minus the slots no ported flavor populates yet (type, stage, locality,
34
+ * typed_stage — they join when a flavor needs them).
35
+ */
36
+ export declare const BASE_ATTRIBUTES: AttributeTable;
37
+ export interface FieldMapping {
38
+ /** Wire key. */
39
+ wire: string;
40
+ /** Attribute name. */
41
+ to: string;
42
+ /** Custom wire converter (Ruby `map ... with: {to:}`). */
43
+ toWire?: (model: Record<string, unknown>) => unknown;
44
+ /** Custom reader (Ruby `map ... with: {from:}`). */
45
+ fromWire?: (hash: Record<string, unknown>) => unknown;
46
+ }
47
+ export declare function keyValue(...fields: FieldMapping[]): FieldMapping[];
48
+ /**
49
+ * The flat-scalar tables (identifier.rb:24-36): attribute name → the wire
50
+ * key its degenerate scalar is emitted under (note `date` RENAMES to
51
+ * `year`), and the single field each component degenerates to.
52
+ */
53
+ export declare const FLAT_SCALAR_COMPONENTS: Record<string, string>;
54
+ export declare const FLAT_SCALAR_FIELDS: Record<string, string>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Declarative attributes and wire mappings — the TS mirror of pubid's
3
+ * `attribute` DSL + lutaml key_value mappings. Semantics pinned in
4
+ * TODO.unified/01-study-pubid-core.md §1-3.
5
+ *
6
+ * A flavor DECLARES its shape; BaseIdentifier derives serialization.
7
+ * The only imperative wire code allowed is a converter pair
8
+ * (`toWire`/`fromWire`), mirroring Ruby's `map "x", with: {to:, from:}`.
9
+ */
10
+ import { PubidDate, Publisher, Language, Edition, Iteration } from "./component.js";
11
+ /**
12
+ * Ruby snapshots the parent attribute table into each subclass at
13
+ * class-definition time; TS composes the same way — a subclass's table
14
+ * is the parent's plus its overrides.
15
+ */
16
+ export function extendAttributes(parent, defs) {
17
+ return { ...parent.attributes, ...defs };
18
+ }
19
+ /**
20
+ * The base table mirrors Pubid::Identifier (identifier.rb:503-525),
21
+ * minus the slots no ported flavor populates yet (type, stage, locality,
22
+ * typed_stage — they join when a flavor needs them).
23
+ */
24
+ export const BASE_ATTRIBUTES = {
25
+ number: { type: "string" },
26
+ part: { type: "string" },
27
+ subpart: { type: "string" },
28
+ stage_iteration: { type: Iteration },
29
+ date: { type: PubidDate },
30
+ edition: { type: Edition },
31
+ languages: { type: Language, collection: true },
32
+ publisher: { type: Publisher },
33
+ copublishers: { type: Publisher, collection: true },
34
+ all_parts: { type: "boolean", default: false },
35
+ };
36
+ export function keyValue(...fields) {
37
+ return fields;
38
+ }
39
+ /**
40
+ * The flat-scalar tables (identifier.rb:24-36): attribute name → the wire
41
+ * key its degenerate scalar is emitted under (note `date` RENAMES to
42
+ * `year`), and the single field each component degenerates to.
43
+ */
44
+ export const FLAT_SCALAR_COMPONENTS = {
45
+ edition: "edition",
46
+ date: "year",
47
+ stage_iteration: "stage_iteration",
48
+ };
49
+ export const FLAT_SCALAR_FIELDS = {
50
+ edition: "number",
51
+ date: "year",
52
+ stage_iteration: "string",
53
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * BaseBuilder — the TS mirror of Pubid::Builder::Base
3
+ * (TODO.unified/01 §5). The shared build loop: flatten the parslet tree
4
+ * (array of hashes → merged hash), ask selectClass for the concrete
5
+ * class, then assign each tree key through cast(); unknown keys are
6
+ * silently skipped; cast may return a hash to fan out.
7
+ */
8
+ import type { BaseIdentifier } from "./identifier.js";
9
+ import type { IdentifierStatic } from "./identifier.js";
10
+ import { PubidDate } from "./component.js";
11
+ import { Language } from "./component.js";
12
+ type TreeValue = unknown;
13
+ export declare abstract class BaseBuilder {
14
+ protected abstract defaultIdentifierClass(): IdentifierStatic;
15
+ /** Override to dispatch by data shape (type token, supplement marker…). */
16
+ protected selectClass(_data: Record<string, TreeValue>): IdentifierStatic;
17
+ /**
18
+ * Override to convert raw parsed values to components. Return a hash to
19
+ * assign multiple attributes, a single value, or null/undefined to skip.
20
+ */
21
+ protected cast(_key: string, value: TreeValue): TreeValue;
22
+ /** Override for non-attribute side effects; return true when consumed. */
23
+ protected handleKey(_identifier: BaseIdentifier, _key: string, _value: TreeValue): boolean;
24
+ build(data: Record<string, TreeValue> | Record<string, TreeValue>[]): BaseIdentifier;
25
+ private flattenArray;
26
+ private assignAttributes;
27
+ protected parseDate(value: unknown): PubidDate;
28
+ protected parseLanguages(value: unknown): Language[];
29
+ private static readonly ROMAN;
30
+ protected convertRomanToInteger(roman: unknown): string;
31
+ /**
32
+ * "1234-1-2" → {number, part?, subpart?}; roman parts convert to
33
+ * integers; normalizeNumberWithPart / extractLegacyYear are hooks.
34
+ */
35
+ protected parseNumberWithPart(value: unknown): Record<string, string | undefined>;
36
+ protected normalizeNumberWithPart(value: string): string;
37
+ protected extractLegacyYear(_number: string | undefined, _part: string | undefined): Record<string, string | undefined> | undefined;
38
+ }
39
+ export {};