@styx-api/core 0.1.0

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 (88) hide show
  1. package/dist/index.cjs +7947 -0
  2. package/dist/index.d.cts +1143 -0
  3. package/dist/index.d.cts.map +1 -0
  4. package/dist/index.d.mts +1143 -0
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/index.mjs +7877 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +55 -0
  9. package/src/backend/backend.ts +95 -0
  10. package/src/backend/boutiques/boutiques.ts +1049 -0
  11. package/src/backend/boutiques/index.ts +1 -0
  12. package/src/backend/code-builder.ts +49 -0
  13. package/src/backend/collect-field-info.ts +50 -0
  14. package/src/backend/collect-named-types.ts +103 -0
  15. package/src/backend/collect-output-fields.ts +222 -0
  16. package/src/backend/find-doc.ts +38 -0
  17. package/src/backend/find-struct-node.ts +66 -0
  18. package/src/backend/index.ts +39 -0
  19. package/src/backend/python/arg-builder.ts +454 -0
  20. package/src/backend/python/emit.ts +638 -0
  21. package/src/backend/python/index.ts +9 -0
  22. package/src/backend/python/outputs-emit.ts +430 -0
  23. package/src/backend/python/packaging.ts +173 -0
  24. package/src/backend/python/python.ts +558 -0
  25. package/src/backend/python/snippet.ts +84 -0
  26. package/src/backend/python/typemap.ts +131 -0
  27. package/src/backend/python/types.ts +8 -0
  28. package/src/backend/python/validate-emit.ts +356 -0
  29. package/src/backend/resolve-field-binding.ts +41 -0
  30. package/src/backend/resolve-output-tokens.ts +80 -0
  31. package/src/backend/schema/index.ts +2 -0
  32. package/src/backend/schema/jsonschema.ts +303 -0
  33. package/src/backend/scope.ts +50 -0
  34. package/src/backend/sig-entries.ts +97 -0
  35. package/src/backend/snippet-core.ts +185 -0
  36. package/src/backend/string-case.ts +30 -0
  37. package/src/backend/styxdefs-compat.ts +21 -0
  38. package/src/backend/type-keys.ts +52 -0
  39. package/src/backend/typescript/arg-builder.ts +420 -0
  40. package/src/backend/typescript/emit.ts +450 -0
  41. package/src/backend/typescript/index.ts +10 -0
  42. package/src/backend/typescript/outputs-emit.ts +389 -0
  43. package/src/backend/typescript/packaging.ts +130 -0
  44. package/src/backend/typescript/snippet.ts +60 -0
  45. package/src/backend/typescript/typemap.ts +47 -0
  46. package/src/backend/typescript/types.ts +8 -0
  47. package/src/backend/typescript/typescript.ts +507 -0
  48. package/src/backend/typescript/validate-emit.ts +341 -0
  49. package/src/backend/union-variants.ts +42 -0
  50. package/src/backend/validate-walk.ts +111 -0
  51. package/src/bindings/binding.ts +77 -0
  52. package/src/bindings/format.ts +176 -0
  53. package/src/bindings/index.ts +16 -0
  54. package/src/bindings/output-gate.ts +50 -0
  55. package/src/bindings/resolved-output.ts +56 -0
  56. package/src/bindings/types.ts +16 -0
  57. package/src/frontend/argdump/index.ts +1 -0
  58. package/src/frontend/argdump/parser.ts +914 -0
  59. package/src/frontend/boutiques/destruct-template.ts +50 -0
  60. package/src/frontend/boutiques/index.ts +1 -0
  61. package/src/frontend/boutiques/parser.ts +676 -0
  62. package/src/frontend/boutiques/split-command.ts +69 -0
  63. package/src/frontend/detect-format.ts +42 -0
  64. package/src/frontend/frontend.ts +31 -0
  65. package/src/frontend/index.ts +9 -0
  66. package/src/frontend/workbench/index.ts +1 -0
  67. package/src/frontend/workbench/parser.ts +351 -0
  68. package/src/index.ts +41 -0
  69. package/src/ir/builders.ts +69 -0
  70. package/src/ir/format.ts +157 -0
  71. package/src/ir/index.ts +32 -0
  72. package/src/ir/meta.ts +91 -0
  73. package/src/ir/node.ts +95 -0
  74. package/src/ir/passes/canonicalize.ts +108 -0
  75. package/src/ir/passes/flatten.ts +73 -0
  76. package/src/ir/passes/index.ts +7 -0
  77. package/src/ir/passes/pass.ts +86 -0
  78. package/src/ir/passes/pipeline.ts +21 -0
  79. package/src/ir/passes/remove-empty.ts +76 -0
  80. package/src/ir/passes/simplify.ts +179 -0
  81. package/src/ir/types.ts +15 -0
  82. package/src/manifest/context.ts +36 -0
  83. package/src/manifest/index.ts +3 -0
  84. package/src/manifest/types.ts +15 -0
  85. package/src/solver/assign-access.ts +218 -0
  86. package/src/solver/index.ts +4 -0
  87. package/src/solver/resolve-outputs.ts +233 -0
  88. package/src/solver/solver.ts +319 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Split a Boutiques command into a list of arguments.
3
+ *
4
+ * @param command - The Boutiques command.
5
+ * @returns The list of arguments.
6
+ * @throws {Error} If command is null or undefined.
7
+ */
8
+ export function boutiquesSplitCommand(command: string): string[] {
9
+ if (command == null) {
10
+ throw new Error("Command cannot be null or undefined");
11
+ }
12
+
13
+ const args: string[] = [];
14
+ let current = "";
15
+ let inSingleQuote = false;
16
+ let inDoubleQuote = false;
17
+ let escaped = false;
18
+
19
+ for (const char of command) {
20
+ if (escaped) {
21
+ // In double quotes, only certain escapes are meaningful
22
+ if (inDoubleQuote && !["\\", '"', "$", "`", "\n"].includes(char)) {
23
+ current += "\\";
24
+ }
25
+ current += char;
26
+ escaped = false;
27
+ continue;
28
+ }
29
+
30
+ if (char === "\\" && !inSingleQuote) {
31
+ escaped = true;
32
+ continue;
33
+ }
34
+
35
+ if (char === "'" && !inDoubleQuote) {
36
+ inSingleQuote = !inSingleQuote;
37
+ continue;
38
+ }
39
+
40
+ if (char === '"' && !inSingleQuote) {
41
+ inDoubleQuote = !inDoubleQuote;
42
+ continue;
43
+ }
44
+
45
+ if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) {
46
+ if (current) {
47
+ args.push(current);
48
+ current = "";
49
+ }
50
+ continue;
51
+ }
52
+
53
+ current += char;
54
+ }
55
+
56
+ if (inSingleQuote || inDoubleQuote) {
57
+ throw new Error("Unclosed quote in command string");
58
+ }
59
+
60
+ if (escaped) {
61
+ throw new Error("Trailing backslash in command string");
62
+ }
63
+
64
+ if (current) {
65
+ args.push(current);
66
+ }
67
+
68
+ return args;
69
+ }
@@ -0,0 +1,42 @@
1
+ export type FormatName = "boutiques" | "argdump" | "workbench";
2
+
3
+ /**
4
+ * Auto-detect the format of a JSON descriptor source string.
5
+ * Returns null if the format cannot be determined.
6
+ */
7
+ export function detectFormat(source: string): FormatName | null {
8
+ let parsed: unknown;
9
+ try {
10
+ parsed = JSON.parse(source);
11
+ } catch {
12
+ return null;
13
+ }
14
+
15
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16
+ return null;
17
+ }
18
+
19
+ const obj = parsed as Record<string, unknown>;
20
+
21
+ // Check $schema for argdump
22
+ if (typeof obj.$schema === "string" && obj.$schema.includes("argdump")) {
23
+ return "argdump";
24
+ }
25
+
26
+ // Workbench: has a "command" switch plus "short_description"
27
+ if (typeof obj.command === "string" && typeof obj.short_description === "string") {
28
+ return "workbench";
29
+ }
30
+
31
+ // Boutiques: has "command-line" or "inputs" array
32
+ if ("command-line" in obj || (Array.isArray(obj.inputs) && "name" in obj)) {
33
+ return "boutiques";
34
+ }
35
+
36
+ // Argdump: has "actions" array + "prog"
37
+ if (Array.isArray(obj.actions) && "prog" in obj) {
38
+ return "argdump";
39
+ }
40
+
41
+ return null;
42
+ }
@@ -0,0 +1,31 @@
1
+ import type { Expr } from "../ir/index.js";
2
+ import type { AppMeta } from "../ir/meta.js";
3
+
4
+ export interface ParseResult {
5
+ meta?: AppMeta;
6
+ expr: Expr;
7
+ errors: ParseError[];
8
+ warnings: ParseWarning[];
9
+ }
10
+
11
+ export interface ParseError {
12
+ message: string;
13
+ location?: SourceLocation;
14
+ }
15
+
16
+ export interface ParseWarning {
17
+ message: string;
18
+ location?: SourceLocation;
19
+ }
20
+
21
+ export interface SourceLocation {
22
+ file?: string;
23
+ line?: number;
24
+ column?: number;
25
+ }
26
+
27
+ export interface Frontend {
28
+ readonly name: string;
29
+ readonly extensions: string[];
30
+ parse(source: string, filename?: string): ParseResult;
31
+ }
@@ -0,0 +1,9 @@
1
+ export type {
2
+ Frontend,
3
+ ParseError,
4
+ ParseResult,
5
+ ParseWarning,
6
+ SourceLocation,
7
+ } from "./frontend.js";
8
+ export { detectFormat } from "./detect-format.js";
9
+ export type { FormatName } from "./detect-format.js";
@@ -0,0 +1 @@
1
+ export { WorkbenchParser } from "./parser.js";
@@ -0,0 +1,351 @@
1
+ import { lit, opt, rep, seq, str, int, float, alt } from "../../ir/builders.js";
2
+ import { nodeRef } from "../../ir/meta.js";
3
+ import type { AppMeta, NodeMeta, Output } from "../../ir/meta.js";
4
+ import type { Documentation } from "../../ir/types.js";
5
+ import type { Expr, Path, Sequence } from "../../ir/node.js";
6
+ import { snakeCase } from "../../backend/string-case.js";
7
+ import type {
8
+ Frontend,
9
+ ParseError,
10
+ ParseResult,
11
+ ParseWarning,
12
+ SourceLocation,
13
+ } from "../frontend.js";
14
+
15
+ // Type guards
16
+
17
+ function isObject(x: unknown): x is Record<string, unknown> {
18
+ return typeof x === "object" && x !== null && !Array.isArray(x);
19
+ }
20
+
21
+ function isString(x: unknown): x is string {
22
+ return typeof x === "string";
23
+ }
24
+
25
+ function isArray(x: unknown): x is unknown[] {
26
+ return Array.isArray(x);
27
+ }
28
+
29
+ // Workbench scalar/file type strings. See the v1 loader's model.py.
30
+ const TYPE_STRING = "String";
31
+ const TYPE_FLOATING_POINT = "Floating Point";
32
+ const TYPE_INTEGER = "Integer";
33
+ const TYPE_BOOLEAN = "Boolean";
34
+
35
+ const FILE_TYPES = new Set<string>([
36
+ "Surface File",
37
+ "Border File",
38
+ "Metric File",
39
+ "Annotation File",
40
+ "Cifti File",
41
+ "Volume File",
42
+ "Label File",
43
+ "Foci File",
44
+ ]);
45
+
46
+ /** A fresh empty root sequence for error returns (IR passes mutate in place,
47
+ * so callers must not share a single instance). */
48
+ function emptyExpr(): Sequence {
49
+ return { kind: "sequence", attrs: { nodes: [] } };
50
+ }
51
+
52
+ /**
53
+ * Parser for Connectome Workbench command definitions (`workbench.json`).
54
+ *
55
+ * The format is recursive but flat in expressivity: positional `params`,
56
+ * positional `outputs`, optional `options`, and `repeatable_options`, where an
57
+ * option may nest its own options/repeatable_options arbitrarily deep. There
58
+ * are no unions, constraints, or conditionals. We lower it onto the same `Expr`
59
+ * shapes the Boutiques parser already emits for flagged sub-sequences:
60
+ *
61
+ * wb_command <command> <positionals...> [option ...] ...
62
+ *
63
+ * - param -> typed terminal (required)
64
+ * - output -> str terminal (the user-supplied filename) + an Output
65
+ * entry on the enclosing struct's meta, referencing it
66
+ * - option -> opt(seq(lit(switch), ...)) (or opt(lit(switch)) flag)
67
+ * - repeatable_option-> rep(seq(lit(switch), ...)) (or rep(lit(switch)) count)
68
+ *
69
+ * v1 reference: ../niwrap/tooling/src/wrap/apps/build/loaders/workbench/.
70
+ */
71
+ export class WorkbenchParser implements Frontend {
72
+ readonly name = "workbench";
73
+ readonly extensions = ["json"];
74
+
75
+ private errors: ParseError[] = [];
76
+ private warnings: ParseWarning[] = [];
77
+
78
+ private reset(): void {
79
+ this.errors = [];
80
+ this.warnings = [];
81
+ }
82
+
83
+ private error(message: string, location?: SourceLocation): void {
84
+ this.errors.push({ message, location });
85
+ }
86
+
87
+ private warn(message: string, location?: SourceLocation): void {
88
+ this.warnings.push({ message, location });
89
+ }
90
+
91
+ private parseJSON(source: string): Record<string, unknown> | null {
92
+ let parsed: unknown;
93
+ try {
94
+ parsed = JSON.parse(source);
95
+ } catch (e) {
96
+ this.error(e instanceof SyntaxError ? e.message : "Invalid JSON");
97
+ return null;
98
+ }
99
+ if (!isObject(parsed)) {
100
+ this.error("JSON source is not an object");
101
+ return null;
102
+ }
103
+ return parsed;
104
+ }
105
+
106
+ // -- Metadata --
107
+
108
+ private docFrom(description: unknown): Documentation | undefined {
109
+ return isString(description) && description.length > 0 ? { description } : undefined;
110
+ }
111
+
112
+ private buildAppMeta(cmd: Record<string, unknown>): AppMeta | undefined {
113
+ const command = cmd.command;
114
+ if (!isString(command)) {
115
+ this.error("Workbench descriptor missing required 'command' string");
116
+ return undefined;
117
+ }
118
+
119
+ const id = normalizeName(command);
120
+ const title = cmd.short_description;
121
+ const description = cmd.help_text;
122
+ const doc: Documentation = {
123
+ ...(isString(title) && { title }),
124
+ ...(isString(description) && { description }),
125
+ };
126
+
127
+ const version = extractVersion(cmd.version_info);
128
+ return {
129
+ id,
130
+ ...(version && { version }),
131
+ ...(Object.keys(doc).length > 0 && { doc }),
132
+ };
133
+ }
134
+
135
+ // -- Terminals --
136
+
137
+ /** Map a workbench scalar/file type string to an IR terminal node. */
138
+ private buildTerminal(type: string, meta: NodeMeta, ctx: string): Expr | null {
139
+ switch (type) {
140
+ case TYPE_STRING:
141
+ return str(meta);
142
+ case TYPE_INTEGER:
143
+ return int(meta);
144
+ case TYPE_FLOATING_POINT:
145
+ return float(meta);
146
+ case TYPE_BOOLEAN: {
147
+ // A positional boolean emits the literal token "true" or "false". The
148
+ // idiomatic styx2 representation is a two-literal choice (v1 used a
149
+ // value_true/value_false Bool, which produces the same tokens).
150
+ const node = alt(lit("true"), lit("false"));
151
+ node.meta = meta;
152
+ return node;
153
+ }
154
+ default: {
155
+ if (FILE_TYPES.has(type)) {
156
+ const node: Path = {
157
+ kind: "path",
158
+ attrs: { mediaTypes: [`workbench/${type}`] },
159
+ meta,
160
+ };
161
+ return node;
162
+ }
163
+ this.error(`Unknown workbench type '${type}' for '${ctx}'`);
164
+ return null;
165
+ }
166
+ }
167
+ }
168
+
169
+ // -- Params / outputs --
170
+
171
+ /** Positional parameter -> required typed terminal. */
172
+ private buildParam(param: unknown): Expr | null {
173
+ if (!isObject(param)) {
174
+ this.warn("Skipping non-object param");
175
+ return null;
176
+ }
177
+ const shortName = param.short_name;
178
+ const type = param.type;
179
+ if (!isString(shortName) || !isString(type)) {
180
+ this.error("Workbench param missing 'short_name'/'type'");
181
+ return null;
182
+ }
183
+ const doc = this.docFrom(param.description);
184
+ const meta: NodeMeta = { name: snakeCase(shortName), ...(doc && { doc }) };
185
+ return this.buildTerminal(type, meta, shortName);
186
+ }
187
+
188
+ /**
189
+ * Positional/option output -> a `str` terminal (the user-supplied output
190
+ * filename) plus an `Output` declaration referencing it by name. Mirrors v1's
191
+ * `_load_output`, which emits both a String param and an OutputParamReference.
192
+ */
193
+ private buildOutput(output: unknown): { node: Expr; output: Output } | null {
194
+ if (!isObject(output)) {
195
+ this.warn("Skipping non-object output");
196
+ return null;
197
+ }
198
+ const shortName = output.short_name;
199
+ const type = output.type;
200
+ if (!isString(shortName) || !isString(type)) {
201
+ this.error("Workbench output missing 'short_name'/'type'");
202
+ return null;
203
+ }
204
+ if (!FILE_TYPES.has(type)) {
205
+ this.error(`Workbench output '${shortName}' has non-file type '${type}'`);
206
+ return null;
207
+ }
208
+ const name = snakeCase(shortName);
209
+ const doc = this.docFrom(output.description);
210
+ const node = str({ name, ...(doc && { doc }) });
211
+ const out: Output = {
212
+ name,
213
+ ...(doc && { doc }),
214
+ tokens: [{ kind: "ref", target: nodeRef(name) }],
215
+ mediaTypes: [`workbench/${type}`],
216
+ };
217
+ return { node, output: out };
218
+ }
219
+
220
+ // -- Options --
221
+
222
+ /**
223
+ * An option/repeatable_option -> an optional (or repeated) switch group.
224
+ *
225
+ * With no sub-content it collapses to a bare flag (`opt(lit(switch))` ->
226
+ * bool, `rep(lit(switch))` -> count). With content it is a struct sequence
227
+ * `seq(lit(switch), ...)`; the struct's name + any outputs live on that
228
+ * sequence's meta, the option's doc on the optional/repeat wrapper (matching
229
+ * the Boutiques metadata-hoisting convention).
230
+ */
231
+ private buildOption(option: unknown, repeatable: boolean): Expr | null {
232
+ if (!isObject(option)) {
233
+ this.warn("Skipping non-object option");
234
+ return null;
235
+ }
236
+ const sw = option.option_switch;
237
+ if (!isString(sw)) {
238
+ this.error("Workbench option missing 'option_switch'");
239
+ return null;
240
+ }
241
+ const name = normalizeName(sw);
242
+ const doc = this.docFrom(option.description);
243
+
244
+ const inner: Expr[] = [lit(sw)];
245
+ const outputs: Output[] = [];
246
+
247
+ for (const p of asArray(option.params)) {
248
+ const node = this.buildParam(p);
249
+ if (node) inner.push(node);
250
+ }
251
+ for (const o of asArray(option.outputs)) {
252
+ const built = this.buildOutput(o);
253
+ if (built) {
254
+ inner.push(built.node);
255
+ outputs.push(built.output);
256
+ }
257
+ }
258
+ for (const o of asArray(option.options)) {
259
+ const node = this.buildOption(o, false);
260
+ if (node) inner.push(node);
261
+ }
262
+ for (const o of asArray(option.repeatable_options)) {
263
+ const node = this.buildOption(o, true);
264
+ if (node) inner.push(node);
265
+ }
266
+
267
+ const wrapperMeta: NodeMeta = { ...(doc && { doc }) };
268
+
269
+ if (inner.length === 1) {
270
+ // Pure flag: no parameters or sub-options.
271
+ const flagMeta: NodeMeta = { name, ...wrapperMeta };
272
+ if (!repeatable) flagMeta.defaultValue = false;
273
+ return repeatable ? rep(lit(sw), flagMeta) : opt(lit(sw), flagMeta);
274
+ }
275
+
276
+ const structSeq = seq(...inner);
277
+ structSeq.meta = { name, ...(outputs.length > 0 && { outputs }) };
278
+ return repeatable ? rep(structSeq, wrapperMeta) : opt(structSeq, wrapperMeta);
279
+ }
280
+
281
+ // -- Public API --
282
+
283
+ parse(source: string, _filename?: string): ParseResult {
284
+ this.reset();
285
+
286
+ const cmd = this.parseJSON(source);
287
+ if (!cmd) {
288
+ return { expr: emptyExpr(), errors: this.errors, warnings: this.warnings };
289
+ }
290
+
291
+ const meta = this.buildAppMeta(cmd);
292
+ if (!meta || !isString(cmd.command)) {
293
+ return { expr: emptyExpr(), errors: this.errors, warnings: this.warnings };
294
+ }
295
+
296
+ const nodes: Expr[] = [lit("wb_command"), lit(cmd.command)];
297
+ const rootOutputs: Output[] = [];
298
+
299
+ for (const p of asArray(cmd.params)) {
300
+ const node = this.buildParam(p);
301
+ if (node) nodes.push(node);
302
+ }
303
+ for (const o of asArray(cmd.outputs)) {
304
+ const built = this.buildOutput(o);
305
+ if (built) {
306
+ nodes.push(built.node);
307
+ rootOutputs.push(built.output);
308
+ }
309
+ }
310
+ for (const o of asArray(cmd.options)) {
311
+ const node = this.buildOption(o, false);
312
+ if (node) nodes.push(node);
313
+ }
314
+ for (const o of asArray(cmd.repeatable_options)) {
315
+ const node = this.buildOption(o, true);
316
+ if (node) nodes.push(node);
317
+ }
318
+
319
+ const rootSeq = seq(...nodes);
320
+ rootSeq.meta = { name: meta.id, ...(rootOutputs.length > 0 && { outputs: rootOutputs }) };
321
+
322
+ return { meta, expr: rootSeq, errors: this.errors, warnings: this.warnings };
323
+ }
324
+ }
325
+
326
+ // -- Helpers --
327
+
328
+ /** Strip a leading dash from a switch/command and snake_case it. */
329
+ function normalizeName(name: string): string {
330
+ return snakeCase(name.replace(/^-+/, ""));
331
+ }
332
+
333
+ /** Coerce a possibly-missing list field to an array. */
334
+ function asArray(x: unknown): unknown[] {
335
+ return isArray(x) ? x : [];
336
+ }
337
+
338
+ /**
339
+ * Pull a clean version out of workbench's noisy `version_info` lines
340
+ * (e.g. "Version: 2.1.0"). Returns undefined if none is present.
341
+ */
342
+ function extractVersion(versionInfo: unknown): string | undefined {
343
+ if (!isArray(versionInfo)) return undefined;
344
+ for (const line of versionInfo) {
345
+ if (isString(line)) {
346
+ const m = /^Version:\s*(.+)$/.exec(line.trim());
347
+ if (m && m[1]) return m[1].trim();
348
+ }
349
+ }
350
+ return undefined;
351
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { ArgdumpParser } from "./frontend/argdump/index.js";
2
+ import { BoutiquesParser } from "./frontend/boutiques/index.js";
3
+ import { WorkbenchParser } from "./frontend/workbench/index.js";
4
+ import { detectFormat } from "./frontend/detect-format.js";
5
+ import type { FormatName } from "./frontend/detect-format.js";
6
+ import type { ParseResult } from "./frontend/frontend.js";
7
+
8
+ export * from "./backend/index.js";
9
+ export * from "./bindings/index.js";
10
+ export * from "./frontend/index.js";
11
+ export * from "./ir/index.js";
12
+ export * from "./manifest/index.js";
13
+ export * from "./solver/index.js";
14
+
15
+ export function compile(
16
+ source: string,
17
+ filenameOrOptions?: string | { format?: FormatName; filename?: string },
18
+ ): ParseResult {
19
+ const options =
20
+ typeof filenameOrOptions === "string"
21
+ ? { filename: filenameOrOptions }
22
+ : (filenameOrOptions ?? {});
23
+
24
+ const format = options.format ?? detectFormat(source);
25
+
26
+ if (!format) {
27
+ return {
28
+ expr: { kind: "sequence", attrs: { nodes: [] } },
29
+ errors: [{ message: "Could not detect input format. Specify format explicitly." }],
30
+ warnings: [],
31
+ };
32
+ }
33
+
34
+ const parser =
35
+ format === "argdump"
36
+ ? new ArgdumpParser()
37
+ : format === "workbench"
38
+ ? new WorkbenchParser()
39
+ : new BoutiquesParser();
40
+ return parser.parse(source, options.filename);
41
+ }
@@ -0,0 +1,69 @@
1
+ import type { NodeMeta } from "./meta.js";
2
+ import type {
3
+ Alternative,
4
+ Expr,
5
+ Float,
6
+ Int,
7
+ Literal,
8
+ Optional,
9
+ Path,
10
+ Repeat,
11
+ Sequence,
12
+ Str,
13
+ } from "./node.js";
14
+
15
+ // -- Terminals --
16
+
17
+ export function lit(str: string): Literal {
18
+ return { kind: "literal", attrs: { str } };
19
+ }
20
+
21
+ export function str(meta?: NodeMeta | string): Str {
22
+ return { kind: "str", attrs: {}, meta: normalizeMeta(meta) };
23
+ }
24
+
25
+ export function int(meta?: NodeMeta | string): Int {
26
+ return { kind: "int", attrs: {}, meta: normalizeMeta(meta) };
27
+ }
28
+
29
+ export function float(meta?: NodeMeta | string): Float {
30
+ return { kind: "float", attrs: {}, meta: normalizeMeta(meta) };
31
+ }
32
+
33
+ export function path(meta?: NodeMeta | string): Path {
34
+ return { kind: "path", attrs: {}, meta: normalizeMeta(meta) };
35
+ }
36
+
37
+ // -- Structural --
38
+
39
+ export function seq(...nodes: Expr[]): Sequence {
40
+ return { kind: "sequence", attrs: { nodes } };
41
+ }
42
+
43
+ export function seqJoin(join: string, ...nodes: Expr[]): Sequence {
44
+ return { kind: "sequence", attrs: { nodes, join } };
45
+ }
46
+
47
+ export function opt(node: Expr, meta?: NodeMeta | string): Optional {
48
+ return { kind: "optional", attrs: { node }, meta: normalizeMeta(meta) };
49
+ }
50
+
51
+ export function rep(node: Expr, meta?: NodeMeta | string): Repeat {
52
+ return { kind: "repeat", attrs: { node }, meta: normalizeMeta(meta) };
53
+ }
54
+
55
+ export function repJoin(join: string, node: Expr, meta?: NodeMeta | string): Repeat {
56
+ return { kind: "repeat", attrs: { node, join }, meta: normalizeMeta(meta) };
57
+ }
58
+
59
+ export function alt(...alts: Expr[]): Alternative {
60
+ return { kind: "alternative", attrs: { alts } };
61
+ }
62
+
63
+ // -- Helpers --
64
+
65
+ function normalizeMeta(meta: NodeMeta | string | undefined): NodeMeta | undefined {
66
+ if (meta === undefined) return undefined;
67
+ if (typeof meta === "string") return { name: meta };
68
+ return meta;
69
+ }