@styx-api/core 0.6.1 → 0.7.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.
- package/dist/index.cjs +2079 -147
- package/dist/index.d.cts +41 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +41 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2078 -148
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/backend/argtype/__fixtures__/microsyntax.argtype +15 -0
- package/src/backend/argtype/__fixtures__/subcommands.argtype +25 -0
- package/src/backend/argtype/argtype.ts +45 -0
- package/src/backend/argtype/emit.ts +725 -0
- package/src/backend/argtype/index.ts +2 -0
- package/src/backend/index.ts +1 -0
- package/src/frontend/argtype/__fixtures__/bet.argtype +103 -0
- package/src/frontend/argtype/ast.ts +101 -0
- package/src/frontend/argtype/doc.ts +56 -0
- package/src/frontend/argtype/frontmatter.ts +193 -0
- package/src/frontend/argtype/index.ts +1 -0
- package/src/frontend/argtype/lexer.ts +244 -0
- package/src/frontend/argtype/lower.ts +561 -0
- package/src/frontend/argtype/parser-frontend.ts +51 -0
- package/src/frontend/argtype/parser.ts +487 -0
- package/src/frontend/argtype/template.ts +147 -0
- package/src/frontend/detect-format.ts +28 -3
- package/src/index.ts +23 -9
package/src/backend/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type {
|
|
|
8
8
|
EmitWarning,
|
|
9
9
|
TypeMap,
|
|
10
10
|
} from "./backend.js";
|
|
11
|
+
export { ArgtypeBackend, generateArgtype } from "./argtype/index.js";
|
|
11
12
|
export { BoutiquesBackend, generateBoutiques } from "./boutiques/index.js";
|
|
12
13
|
export { CodeBuilder } from "./code-builder.js";
|
|
13
14
|
export type { FieldInfo } from "./collect-field-info.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
exe: "bet"
|
|
3
|
+
version: "6.0.4"
|
|
4
|
+
authors:
|
|
5
|
+
- "FMRIB Analysis Group, University of Oxford"
|
|
6
|
+
urls:
|
|
7
|
+
- "https://fsl.fmrib.ox.ac.uk/fsl/fslwiki"
|
|
8
|
+
extensions:
|
|
9
|
+
- outputs
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
/// Automated brain extraction tool for FSL
|
|
13
|
+
bet: seq(
|
|
14
|
+
/// Input image (e.g. img.nii.gz).
|
|
15
|
+
infile: path,
|
|
16
|
+
|
|
17
|
+
/// Output brain mask prefix (e.g. img_bet).
|
|
18
|
+
maskfile: str = "img_bet",
|
|
19
|
+
|
|
20
|
+
set(
|
|
21
|
+
/// Fractional intensity threshold (0->1). Smaller values give larger
|
|
22
|
+
/// brain outline estimates.
|
|
23
|
+
opt("-f", fractional_intensity: float.min(0).max(1).default(0.5)),
|
|
24
|
+
|
|
25
|
+
/// Vertical gradient in fractional intensity threshold (-1->1).
|
|
26
|
+
/// Positive values give larger brain outline at bottom, smaller at top.
|
|
27
|
+
opt("-g", vg_fractional_intensity: float.min(-1).max(1).default(0)),
|
|
28
|
+
|
|
29
|
+
/// XYZ coordinates (voxels, not mm) of the center of gravity of the
|
|
30
|
+
/// initial mesh surface.
|
|
31
|
+
opt("-c", center_of_gravity: rep(float).count(3)),
|
|
32
|
+
|
|
33
|
+
/// Head radius (mm, not voxels); initial surface sphere is set to half
|
|
34
|
+
/// of this.
|
|
35
|
+
opt("-r", head_radius: float),
|
|
36
|
+
|
|
37
|
+
/// Generate brain surface outline overlaid onto original image.
|
|
38
|
+
overlay: opt("-o").output(overlay_file: `{maskfile}_overlay.nii.gz`),
|
|
39
|
+
|
|
40
|
+
/// Generate binary brain mask.
|
|
41
|
+
binary_mask_flag: opt("-m").output(binary_mask: `{maskfile}_mask.nii.gz`),
|
|
42
|
+
|
|
43
|
+
/// Generate rough skull image (not as clean as betsurf).
|
|
44
|
+
approx_skull: opt("-s").output(
|
|
45
|
+
approx_skull_img: `{maskfile}_skull.nii.gz`,
|
|
46
|
+
skull_mask: `{maskfile}_skull_mask.nii.gz`,
|
|
47
|
+
),
|
|
48
|
+
|
|
49
|
+
/// Don't generate segmented brain image output.
|
|
50
|
+
no_seg_output: opt("-n"),
|
|
51
|
+
|
|
52
|
+
/// Generate brain surface as mesh in .vtk format.
|
|
53
|
+
vtk_mesh: opt("-e").output(output_vtk_mesh: `{maskfile}_mesh.vtk`),
|
|
54
|
+
|
|
55
|
+
/// Apply thresholding to segmented brain image and mask.
|
|
56
|
+
thresholding: opt("-t"),
|
|
57
|
+
|
|
58
|
+
/// More robust brain center estimation by iterating BET with a
|
|
59
|
+
/// changing center-of-gravity.
|
|
60
|
+
robust_iters: opt("-R"),
|
|
61
|
+
|
|
62
|
+
/// Cleanup residual eye and optic nerve voxels (useful before
|
|
63
|
+
/// SIENA/SIENAX).
|
|
64
|
+
residual_optic_cleanup: opt("-S"),
|
|
65
|
+
|
|
66
|
+
/// Reduce image bias and residual neck voxels (useful before
|
|
67
|
+
/// SIENA/SIENAX).
|
|
68
|
+
reduce_bias: opt("-B"),
|
|
69
|
+
|
|
70
|
+
/// Pad end slices in both directions; useful when only a few slices
|
|
71
|
+
/// are present in the Z direction.
|
|
72
|
+
slice_padding: opt("-Z"),
|
|
73
|
+
|
|
74
|
+
/// Determine brain mask from the first volume of a 4D dataset and
|
|
75
|
+
/// apply it to the whole set. Intended for FMRI; the -f threshold
|
|
76
|
+
/// is reduced to 0.3.
|
|
77
|
+
whole_set_mask: opt("-F"),
|
|
78
|
+
|
|
79
|
+
/// Run bet2 + betsurf so skull and scalp surfaces are also produced
|
|
80
|
+
/// (requires registration to standard space).
|
|
81
|
+
additional_surfaces: opt("-A").output(
|
|
82
|
+
out_inskull_mask: `{maskfile}_inskull_mask.nii.gz`,
|
|
83
|
+
out_inskull_mesh: `{maskfile}_inskull_mesh.nii.gz`,
|
|
84
|
+
out_inskull_off: `{maskfile}_inskull_mesh.off`,
|
|
85
|
+
out_outskin_mask: `{maskfile}_outskin_mask.nii.gz`,
|
|
86
|
+
out_outskin_mesh: `{maskfile}_outskin_mesh.nii.gz`,
|
|
87
|
+
out_outskin_off: `{maskfile}_outskin_mesh.off`,
|
|
88
|
+
out_outskull_mask: `{maskfile}_outskull_mask.nii.gz`,
|
|
89
|
+
out_outskull_mesh: `{maskfile}_outskull_mesh.nii.gz`,
|
|
90
|
+
out_outskull_off: `{maskfile}_outskull_mesh.off`,
|
|
91
|
+
),
|
|
92
|
+
|
|
93
|
+
/// Same as -A, but also takes a T2 image as input to refine the
|
|
94
|
+
/// skull/scalp surfaces.
|
|
95
|
+
opt("-A2", additional_surfaces_t2: path),
|
|
96
|
+
|
|
97
|
+
/// Switch on diagnostic messages.
|
|
98
|
+
verbose: opt("-v"),
|
|
99
|
+
|
|
100
|
+
/// Don't delete temporary intermediate images.
|
|
101
|
+
debug: opt("-d"),
|
|
102
|
+
),
|
|
103
|
+
).output(outfile: `{maskfile}.nii.gz`)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST for the argtype sugar DSL. Mirrors the grammar in the argtype spec
|
|
3
|
+
* (combinators, terminals, literals, naming, aliases, method chains) and is the
|
|
4
|
+
* intermediate the parser produces before `lower.ts` translates it to Styx IR.
|
|
5
|
+
*
|
|
6
|
+
* Decorations (`name`, `doc`, `default`, value constraints, `.join`/`.count`,
|
|
7
|
+
* outputs, media types) attach to any node, so they live as optional fields on
|
|
8
|
+
* the single `AstNode` shape rather than as wrapper nodes - lowering folds them
|
|
9
|
+
* onto the corresponding `NodeMeta` / terminal attrs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A combinator keyword. `set`/`any` have no direct IR node (see `lower.ts`). */
|
|
13
|
+
export type Combinator = "seq" | "set" | "opt" | "rep" | "alt" | "any";
|
|
14
|
+
|
|
15
|
+
/** A terminal keyword. */
|
|
16
|
+
export type Terminal = "int" | "float" | "str" | "path";
|
|
17
|
+
|
|
18
|
+
export interface AstNode {
|
|
19
|
+
kind: "literal" | "terminal" | "comb" | "ref";
|
|
20
|
+
|
|
21
|
+
/** kind === "literal": the fixed token string. */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** kind === "terminal": which terminal. */
|
|
24
|
+
terminal?: Terminal;
|
|
25
|
+
/** kind === "comb": which combinator + its children. */
|
|
26
|
+
op?: Combinator;
|
|
27
|
+
children?: AstNode[];
|
|
28
|
+
/** kind === "ref": an alias reference (resolved by substitution). */
|
|
29
|
+
refName?: string;
|
|
30
|
+
|
|
31
|
+
// -- Decorations (any node) --
|
|
32
|
+
|
|
33
|
+
/** `label:` sugar / `.name("...")`. */
|
|
34
|
+
name?: string;
|
|
35
|
+
/** Title: a `# ` heading in a `///` block, or `.title("...")`. */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** Description: a `///` block (minus the title), or `.description("...")`. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/** `= value` sugar / `.default(...)` (terminal only). */
|
|
40
|
+
default?: string | number;
|
|
41
|
+
/** `.min(n)` (int/float only). */
|
|
42
|
+
min?: number;
|
|
43
|
+
/** `.max(n)` (int/float only). */
|
|
44
|
+
max?: number;
|
|
45
|
+
/** `.join(sep?)` - present means joined; separator defaults to "". */
|
|
46
|
+
join?: string;
|
|
47
|
+
/** `.count(n)` (= `.countMin(n).countMax(n)`) / `.countMin(n)` / `.countMax(n)`
|
|
48
|
+
* (rep only). */
|
|
49
|
+
countMin?: number;
|
|
50
|
+
countMax?: number;
|
|
51
|
+
/** `.mediaType(mime)` (path only; `mediatypes` extension). */
|
|
52
|
+
mediaTypes?: string[];
|
|
53
|
+
/** `.mutable()` (path only; `paths` extension). */
|
|
54
|
+
mutable?: boolean;
|
|
55
|
+
/** `.resolveParent()` (path only; `paths` extension). */
|
|
56
|
+
resolveParent?: boolean;
|
|
57
|
+
/** `.output(...)` (`outputs` extension). */
|
|
58
|
+
outputs?: AstOutput[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One output template declared via `.output(...)`. */
|
|
62
|
+
export interface AstOutput {
|
|
63
|
+
/** `label:` on the template / `.name(...)`. */
|
|
64
|
+
name?: string;
|
|
65
|
+
/** Title of the produced file (from a `# ` heading in the entry's `///`). */
|
|
66
|
+
title?: string;
|
|
67
|
+
/** Description of the produced file (from the entry's `///`). */
|
|
68
|
+
description?: string;
|
|
69
|
+
/** Output-level `.or(fallback)` on the template. */
|
|
70
|
+
fallback?: string;
|
|
71
|
+
tokens: AstOutputToken[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A piece of an output path template: literal text or a `{ref}` interpolation. */
|
|
75
|
+
export type AstOutputToken =
|
|
76
|
+
| { kind: "literal"; value: string }
|
|
77
|
+
| {
|
|
78
|
+
kind: "ref";
|
|
79
|
+
/** Target node name; undefined for a self-reference (`{}`). */
|
|
80
|
+
name?: string;
|
|
81
|
+
stripSuffix?: string[];
|
|
82
|
+
stripPrefix?: string[];
|
|
83
|
+
basename?: boolean;
|
|
84
|
+
or?: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** A top-level alias definition (`Name = expr`). */
|
|
88
|
+
export interface AstAlias {
|
|
89
|
+
name: string;
|
|
90
|
+
expr: AstNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The result of parsing a full argtype document. */
|
|
94
|
+
export interface AstDocument {
|
|
95
|
+
frontmatter?: Record<string, unknown>;
|
|
96
|
+
aliases: AstAlias[];
|
|
97
|
+
/** The root definition's name (`name: expr`). Undefined for an anonymous root
|
|
98
|
+
* (a bare top-level expression); the tool id then falls back to frontmatter. */
|
|
99
|
+
rootName?: string;
|
|
100
|
+
root: AstNode;
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doc-comment title/description handling.
|
|
3
|
+
*
|
|
4
|
+
* The `# Title` heading convention is *sugar*: it applies only when splitting a
|
|
5
|
+
* `///` block (`splitDocText`). The chaining methods `.title()` / `.description()`
|
|
6
|
+
* set the title / description directly and are not re-parsed for headings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface DocParts {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reflow a `///` description as Markdown-style prose: a single line break is a
|
|
16
|
+
* soft wrap (the lines join with a space) and a blank line starts a new
|
|
17
|
+
* paragraph (kept as `\n\n`). This lets an author wrap a long description across
|
|
18
|
+
* several `///` lines for readability without forcing hard breaks, while real
|
|
19
|
+
* paragraph structure survives. (It does not preserve Markdown lists or indented
|
|
20
|
+
* code blocks - their single line breaks are joined like ordinary prose.)
|
|
21
|
+
*/
|
|
22
|
+
function reflowProse(text: string): string {
|
|
23
|
+
return text
|
|
24
|
+
.split(/\n{2,}/) // blank line(s) -> paragraph boundary
|
|
25
|
+
.map((para) =>
|
|
26
|
+
para
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => line.trim())
|
|
29
|
+
.filter((line) => line.length > 0)
|
|
30
|
+
.join(" "),
|
|
31
|
+
)
|
|
32
|
+
.filter((para) => para.length > 0)
|
|
33
|
+
.join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Split a `///` doc block into title + description. A leading Markdown H1
|
|
38
|
+
* (`# Title` on the first line) is the title; everything after it is the
|
|
39
|
+
* description. Without a leading `# `, the whole block is the description (no
|
|
40
|
+
* title). The description is reflowed as prose (see `reflowProse`): single line
|
|
41
|
+
* breaks soft-wrap, blank lines separate paragraphs.
|
|
42
|
+
*/
|
|
43
|
+
export function splitDocText(raw: string): DocParts {
|
|
44
|
+
const newline = raw.indexOf("\n");
|
|
45
|
+
const firstLine = (newline === -1 ? raw : raw.slice(0, newline)).trim();
|
|
46
|
+
if (firstLine.startsWith("# ")) {
|
|
47
|
+
const title = firstLine.slice(2).trim();
|
|
48
|
+
const description = reflowProse(newline === -1 ? "" : raw.slice(newline + 1));
|
|
49
|
+
return {
|
|
50
|
+
...(title && { title }),
|
|
51
|
+
...(description && { description }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const description = reflowProse(raw);
|
|
55
|
+
return description ? { description } : {};
|
|
56
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontmatter handling for argtype documents.
|
|
3
|
+
*
|
|
4
|
+
* A document may begin with a `---` fenced block of YAML-ish metadata (`exe`,
|
|
5
|
+
* `version`, `extensions`, `container`, `authors`, `stdout`, ...). Rather than
|
|
6
|
+
* pull in a full YAML dependency, this parses the small subset the format uses:
|
|
7
|
+
* scalar `key: value`, block sequences (`- item`), and one level of nested
|
|
8
|
+
* mappings. That covers every key the lowering step consumes; anything more
|
|
9
|
+
* exotic is read as a raw string and ignored downstream.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface SplitResult {
|
|
13
|
+
frontmatter?: Record<string, unknown>;
|
|
14
|
+
/** The document body. Frontmatter lines are blanked (not removed) so token
|
|
15
|
+
* line numbers still line up with the original source. */
|
|
16
|
+
body: string;
|
|
17
|
+
errors: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function splitFrontmatter(source: string): SplitResult {
|
|
21
|
+
// Tolerate a leading BOM / blank lines before the opening fence.
|
|
22
|
+
const lines = source.split("\n");
|
|
23
|
+
let start = 0;
|
|
24
|
+
while (start < lines.length && lines[start]!.trim() === "") start++;
|
|
25
|
+
|
|
26
|
+
if (lines[start]?.trim() !== "---") {
|
|
27
|
+
return { body: source, errors: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Find the closing fence.
|
|
31
|
+
let end = -1;
|
|
32
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
33
|
+
if (lines[i]!.trim() === "---") {
|
|
34
|
+
end = i;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (end === -1) {
|
|
39
|
+
return { body: source, errors: ["Unterminated frontmatter block (missing closing '---')"] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fmLines = lines.slice(start + 1, end);
|
|
43
|
+
const { value, errors } = parseYamlish(fmLines);
|
|
44
|
+
|
|
45
|
+
// Blank out the frontmatter region (fences + content) to preserve line numbers.
|
|
46
|
+
const blanked = lines.map((line, i) => (i >= start && i <= end ? "" : line));
|
|
47
|
+
return { frontmatter: value, body: blanked.join("\n"), errors };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface Line {
|
|
51
|
+
indent: number;
|
|
52
|
+
content: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function tokenizeLines(raw: string[]): Line[] {
|
|
56
|
+
const out: Line[] = [];
|
|
57
|
+
for (const line of raw) {
|
|
58
|
+
const noComment = stripComment(line);
|
|
59
|
+
if (noComment.trim() === "") continue;
|
|
60
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
61
|
+
out.push({ indent, content: noComment.trim() });
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Strip a trailing `#` comment that is not inside quotes. Following YAML, a
|
|
67
|
+
* `#` only starts a comment at the start of the line or after whitespace, so an
|
|
68
|
+
* unquoted value containing `#` (e.g. a URL fragment `https://x/#frag`) is kept
|
|
69
|
+
* intact. */
|
|
70
|
+
function stripComment(line: string): string {
|
|
71
|
+
let inQuote: string | null = null;
|
|
72
|
+
for (let i = 0; i < line.length; i++) {
|
|
73
|
+
const ch = line[i]!;
|
|
74
|
+
// A backslash escape inside a double-quoted value hides the next char (so
|
|
75
|
+
// `\"` does not close the string).
|
|
76
|
+
if (inQuote === '"' && ch === "\\") {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (inQuote) {
|
|
81
|
+
if (ch === inQuote) inQuote = null;
|
|
82
|
+
} else if (ch === '"' || ch === "'") {
|
|
83
|
+
inQuote = ch;
|
|
84
|
+
} else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === "\t")) {
|
|
85
|
+
return line.slice(0, i);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return line;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseYamlish(raw: string[]): { value: Record<string, unknown>; errors: string[] } {
|
|
92
|
+
const lines = tokenizeLines(raw);
|
|
93
|
+
const errors: string[] = [];
|
|
94
|
+
let pos = 0;
|
|
95
|
+
|
|
96
|
+
function parseBlock(minIndent: number): Record<string, unknown> | unknown[] {
|
|
97
|
+
// Sequence block?
|
|
98
|
+
if (
|
|
99
|
+
pos < lines.length &&
|
|
100
|
+
lines[pos]!.indent >= minIndent &&
|
|
101
|
+
lines[pos]!.content.startsWith("- ")
|
|
102
|
+
) {
|
|
103
|
+
const seq: unknown[] = [];
|
|
104
|
+
const indent = lines[pos]!.indent;
|
|
105
|
+
while (
|
|
106
|
+
pos < lines.length &&
|
|
107
|
+
lines[pos]!.indent === indent &&
|
|
108
|
+
lines[pos]!.content.startsWith("- ")
|
|
109
|
+
) {
|
|
110
|
+
seq.push(parseScalar(lines[pos]!.content.slice(2).trim()));
|
|
111
|
+
pos++;
|
|
112
|
+
}
|
|
113
|
+
return seq;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Mapping block.
|
|
117
|
+
const map: Record<string, unknown> = {};
|
|
118
|
+
if (pos >= lines.length) return map;
|
|
119
|
+
const indent = lines[pos]!.indent;
|
|
120
|
+
while (
|
|
121
|
+
pos < lines.length &&
|
|
122
|
+
lines[pos]!.indent === indent &&
|
|
123
|
+
!lines[pos]!.content.startsWith("- ")
|
|
124
|
+
) {
|
|
125
|
+
const { content } = lines[pos]!;
|
|
126
|
+
const colon = findColon(content);
|
|
127
|
+
if (colon === -1) {
|
|
128
|
+
errors.push(`Malformed frontmatter line: '${content}'`);
|
|
129
|
+
pos++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const key = content.slice(0, colon).trim();
|
|
133
|
+
const valueText = content.slice(colon + 1).trim();
|
|
134
|
+
pos++;
|
|
135
|
+
if (valueText !== "") {
|
|
136
|
+
map[key] = parseScalar(valueText);
|
|
137
|
+
} else if (pos < lines.length && lines[pos]!.indent > indent) {
|
|
138
|
+
map[key] = parseBlock(lines[pos]!.indent);
|
|
139
|
+
} else {
|
|
140
|
+
map[key] = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return map;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const value = parseBlock(0);
|
|
147
|
+
return { value: isRecord(value) ? value : { _root: value }, errors };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findColon(s: string): number {
|
|
151
|
+
let inQuote: string | null = null;
|
|
152
|
+
for (let i = 0; i < s.length; i++) {
|
|
153
|
+
const ch = s[i]!;
|
|
154
|
+
if (inQuote === '"' && ch === "\\") {
|
|
155
|
+
i++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (inQuote) {
|
|
159
|
+
if (ch === inQuote) inQuote = null;
|
|
160
|
+
} else if (ch === '"' || ch === "'") {
|
|
161
|
+
inQuote = ch;
|
|
162
|
+
} else if (ch === ":") {
|
|
163
|
+
return i;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return -1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Reverse the backslash escapes an emitter applies to a quoted scalar
|
|
170
|
+
* (`\n`, `\t`, `\r`, `\"`, `\\`), so values with newlines/quotes round-trip. */
|
|
171
|
+
function unescapeScalar(s: string): string {
|
|
172
|
+
return s.replace(/\\(.)/g, (_, c) =>
|
|
173
|
+
c === "n" ? "\n" : c === "t" ? "\t" : c === "r" ? "\r" : c,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseScalar(text: string): unknown {
|
|
178
|
+
if (text.startsWith('"') && text.endsWith('"') && text.length >= 2) {
|
|
179
|
+
return unescapeScalar(text.slice(1, -1));
|
|
180
|
+
}
|
|
181
|
+
if (text.startsWith("'") && text.endsWith("'") && text.length >= 2) {
|
|
182
|
+
return text.slice(1, -1);
|
|
183
|
+
}
|
|
184
|
+
if (text === "true") return true;
|
|
185
|
+
if (text === "false") return false;
|
|
186
|
+
if (text === "null" || text === "~") return null;
|
|
187
|
+
if (/^-?\d+(\.\d+)?$/.test(text)) return Number(text);
|
|
188
|
+
return text;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isRecord(x: unknown): x is Record<string, unknown> {
|
|
192
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
193
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ArgtypeParser } from "./parser-frontend.js";
|