@leuduan/block 0.0.0-stage → 3.0.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.
@@ -0,0 +1,98 @@
1
+ // Port of py/dbox/dbox/block/helpers.py
2
+ // Line-fragment helpers for common SQL constructs.
3
+ import * as l from './line.js';
4
+ import {} from './line.js';
5
+ import { mapEntries } from './sql.js';
6
+ // ---------------------------------------------------------------------------
7
+ // CTE helpers
8
+ // ---------------------------------------------------------------------------
9
+ /** Build WITH / |> WITH clause from a ctes mapping (insertion-order preserved). */
10
+ export function ctes(ctesMap, opts) {
11
+ const withPipe = opts?.withPipe ?? false;
12
+ const cteLines = mapEntries(ctesMap).map(([name, cte]) => aliasSubquery(name, cte.toLine()));
13
+ return l.indentNext([withPipe ? l.keyword('|> WITH') : l.keyword('WITH'), l.stackComma(cteLines)]);
14
+ }
15
+ // ---------------------------------------------------------------------------
16
+ // Collection helpers
17
+ // ---------------------------------------------------------------------------
18
+ export function array(elements) {
19
+ return l.bracket(l.stackComma(elements.map((e) => l.ensure(e))));
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // Expression / column helpers
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Wrap an expression with an optional alias.
26
+ * Uses combine([keyword("AS"), lit(alias)]) — matching helpers.py:29.
27
+ */
28
+ export function expression(expr, alias) {
29
+ const exprLine = l.ensure(expr);
30
+ if (alias !== undefined) {
31
+ return l.indentNext([exprLine, l.combine([l.keyword('AS'), l.lit(alias)])]);
32
+ }
33
+ return exprLine;
34
+ }
35
+ export function analytic(expr, window) {
36
+ return l.indentNext([
37
+ l.ensure(expr),
38
+ window !== undefined ? l.callLike({ call: l.keyword('OVER'), line: l.ensure(window) }) : null,
39
+ ]);
40
+ }
41
+ export function window(partitionBy, orderBy = []) {
42
+ const partLine = partitionBy.length > 0
43
+ ? l.indentNext([l.keyword('PARTITION BY'), l.stackComma(partitionBy.map((p) => l.ensure(p)))])
44
+ : null;
45
+ const orderLine = orderBy.length > 0 ? l.indentNext([l.keyword('ORDER BY'), l.stackComma(orderBy.map((o) => l.ensure(o)))]) : null;
46
+ return l.stack([partLine, orderLine]);
47
+ }
48
+ export function set(name, value) {
49
+ return l.indentNext([l.lit(`${name} =`), l.ensure(value)]);
50
+ }
51
+ export function desc(expr) {
52
+ return l.stack([l.ensure(expr), l.keyword('DESC')]);
53
+ }
54
+ export function asc(expr) {
55
+ return l.stack([l.ensure(expr), l.keyword('ASC')]);
56
+ }
57
+ /** Build "name AS (query)" — used for CTE-like subquery aliases. */
58
+ export function aliasSubquery(name, query) {
59
+ return l.callLike({
60
+ call: l.combine([l.lit(name), l.keyword('AS')]),
61
+ line: l.ensure(query),
62
+ sep: l.SPACE,
63
+ });
64
+ }
65
+ /** Build "SELECT AS STRUCT col1, col2, ..." line fragment. */
66
+ export function selectAsStruct(columns) {
67
+ return l.indentNext([l.keyword('select as struct'), l.stackComma(columns.map((c) => l.ensure(c)))]);
68
+ }
69
+ /** UNNEST expression with optional alias. */
70
+ export function unnest(source, alias) {
71
+ const inner = l.ensure(source);
72
+ let unnestExpr = l.callLike({ call: l.keyword('UNNEST'), line: inner });
73
+ if (alias !== undefined) {
74
+ unnestExpr = l.indentNext([unnestExpr, l.combine([l.keyword('AS'), l.lit(alias)])]);
75
+ }
76
+ return unnestExpr;
77
+ }
78
+ /** Build STRUCT<field1, field2, ...> type expression. */
79
+ export function structType(fields) {
80
+ let fieldLines;
81
+ if (Array.isArray(fields)) {
82
+ fieldLines = fields.map((f) => l.ensure(f));
83
+ }
84
+ else {
85
+ // dict form: [{name: type}] → each becomes "name type"
86
+ fieldLines = mapEntries(fields).map(([k, v]) => l.indentNext([l.lit(k), l.ensure(v)]));
87
+ }
88
+ return l.wrap({
89
+ prefix: l.keyword('struct'),
90
+ wrapper: 'diamond',
91
+ line: l.stackComma(fieldLines),
92
+ });
93
+ }
94
+ /** Build "WINDOW name AS (window_def), ..." clause. */
95
+ export function windows(windowsMap) {
96
+ const windowLines = mapEntries(windowsMap).map(([name, win]) => aliasSubquery(name, win));
97
+ return l.indentNext([l.keyword('WINDOW'), l.stackComma(windowLines)]);
98
+ }
@@ -0,0 +1,18 @@
1
+ export * as l from './line.js';
2
+ export * as e from './expr.js';
3
+ export * as h from './helpers.js';
4
+ export * as p from './pipe.js';
5
+ export * as q from './query.js';
6
+ export * as t from './t.js';
7
+ export type { Line, LineOrStr, Options } from './line.js';
8
+ export { DEFAULT_OPTIONS } from './line.js';
9
+ export type { SqlSegment, Pipe, TableExpression, GoogleSqlQuery } from './sql.js';
10
+ export { QueryExpression, from, pipe, sql } from './sql.js';
11
+ export { define } from './define.js';
12
+ export { Dataset, PARAM_TYPES } from './descriptor.js';
13
+ export { problemsOf } from './problems.js';
14
+ export type { Problem } from './problems.js';
15
+ export { datasetBuilder } from './build.js';
16
+ export type { BuildOptions, BuiltDataset, DatasetBuilder, Parameter } from './build.js';
17
+ export type { BuildContext, Definition, Frame, ParamSpec, ParamsSpec, ParamType, ParamValue, QueryDefinition, TableDefinition, } from './descriptor.js';
18
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ // Public entry of @leuduan/block, the GoogleSQL builder.
2
+ // ---------------------------------------------------------------------------
3
+ // Namespaces: l lines, e expressions, h clause helpers, p pipes, q queries, t table expressions
4
+ // ---------------------------------------------------------------------------
5
+ export * as l from './line.js';
6
+ export * as e from './expr.js';
7
+ export * as h from './helpers.js';
8
+ export * as p from './pipe.js';
9
+ export * as q from './query.js';
10
+ export * as t from './t.js';
11
+ export { DEFAULT_OPTIONS } from './line.js';
12
+ export { QueryExpression, from, pipe, sql } from './sql.js';
13
+ // ---------------------------------------------------------------------------
14
+ // Dataset declarations and their build
15
+ // ---------------------------------------------------------------------------
16
+ export { define } from './define.js';
17
+ export { Dataset, PARAM_TYPES } from './descriptor.js';
18
+ export { problemsOf } from './problems.js';
19
+ export { datasetBuilder } from './build.js';
package/dist/line.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ export declare const SPACE = " ";
2
+ export declare const BLANK = "";
3
+ export declare const NEWLINE = "\n";
4
+ export declare const COMMA = ",";
5
+ export declare const OPEN_PARENTHESIS = "(";
6
+ export declare const CLOSE_PARENTHESIS = ")";
7
+ export declare const OPEN_BRACKET = "[";
8
+ export declare const CLOSE_BRACKET = "]";
9
+ export declare const OPEN_CURLY_BRACE = "{";
10
+ export declare const CLOSE_CURLY_BRACE = "}";
11
+ export interface OptionsProps {
12
+ maxLength?: number;
13
+ keywordHandling?: 'upper_case' | 'lower_case';
14
+ }
15
+ export declare class Options {
16
+ readonly maxLength: number;
17
+ readonly keywordHandling: 'upper_case' | 'lower_case';
18
+ constructor(props?: OptionsProps);
19
+ }
20
+ export declare const DEFAULT_OPTIONS: Options;
21
+ export interface ResultProps {
22
+ can?: boolean;
23
+ lines?: Line[];
24
+ }
25
+ export declare class Result {
26
+ readonly can: boolean;
27
+ readonly lines: Line[];
28
+ constructor(props?: ResultProps);
29
+ }
30
+ export type LineOrStr = Line | string;
31
+ export declare abstract class Line {
32
+ abstract render(opt?: Options): string;
33
+ abstract get size(): number;
34
+ decompose(opt: Options): Result;
35
+ }
36
+ export declare function breakLine(line: Line, opt: Options, shouldBreak?: (line: Line) => boolean): Line[];
37
+ export declare function formatLine(line: Line, opt: Options): string;
38
+ export declare function lit(value: string): Line;
39
+ export declare function keyword(name: string): Line;
40
+ export declare function ensure(s: LineOrStr): Line;
41
+ export declare function bracket(line: Line, sep?: string): Line;
42
+ export declare function curlyBrace(line: Line | null, sep?: string): Line;
43
+ export declare function callLike(opts: {
44
+ call: Line;
45
+ line: Line | null;
46
+ sep?: string;
47
+ }): Line;
48
+ export declare function wrap(opts: {
49
+ prefix: Line;
50
+ wrapper: 'paren' | 'bracket' | 'curly' | 'diamond';
51
+ line: Line;
52
+ wrapSep?: string;
53
+ }): Line;
54
+ export declare function combine(lines: ReadonlyArray<Line | null | undefined> | Line, opts?: {
55
+ sep?: string;
56
+ }): Line;
57
+ export declare function indent(line: Line, spaces?: number): Line;
58
+ export declare function indentMid(lines: ReadonlyArray<Line | null | undefined>, sep?: string): Line;
59
+ export declare function indentNext(lines: ReadonlyArray<Line | null | undefined>, sep?: string): Line;
60
+ export declare function ladder(lines: ReadonlyArray<Line | null | undefined>): Line;
61
+ export declare function multilineLiteral(content: string): Line;
62
+ export declare function paren(line: Line | null, sep?: string): Line;
63
+ export declare function stack(lines: ReadonlyArray<Line | null | undefined>, opts?: {
64
+ sep?: string;
65
+ forceBreak?: boolean;
66
+ }): Line;
67
+ export declare function stackComma(lines: ReadonlyArray<Line | null | undefined>): Line;
68
+ export declare function stackCommaEnd(lines: ReadonlyArray<Line | null | undefined>): Line;
69
+ export declare function stackWithSep(lines: ReadonlyArray<Line | null | undefined>, sep: Line): Line;
70
+ export declare function stickySuffix(suffix: Line, line: Line, separator?: string): Line;
71
+ export declare function threeway(lines: readonly Line[]): Line;
72
+ export declare function stackBinOp(lines: readonly Line[], op: Line): Line;
73
+ export declare const parenOpen: Line;
74
+ export declare const parenClose: Line;
75
+ export declare const bracketOpen: Line;
76
+ export declare const bracketClose: Line;
77
+ export declare const curlyBraceOpen: Line;
78
+ export declare const curlyBraceClose: Line;
79
+ //# sourceMappingURL=line.d.ts.map
package/dist/line.js ADDED
@@ -0,0 +1,450 @@
1
+ // Port of py/dbox/dbox/block/line.py
2
+ // ---------------------------------------------------------------------------
3
+ // Internal helpers
4
+ // ---------------------------------------------------------------------------
5
+ function todo(msg = '...') {
6
+ throw new Error(`TODO: ${msg}`);
7
+ }
8
+ // Mirrors Python str.splitlines(): splits on \r\n|\r|\n and drops a trailing
9
+ // empty string ("a\n".splitlines() → ["a"], not ["a",""]).
10
+ function splitlines(s) {
11
+ const parts = s.split(/\r\n|\r|\n/);
12
+ if (parts.length > 0 && parts[parts.length - 1] === '') {
13
+ parts.pop();
14
+ }
15
+ return parts;
16
+ }
17
+ function removeNulls(lines) {
18
+ return lines.filter((l) => l != null);
19
+ }
20
+ // ---------------------------------------------------------------------------
21
+ // Constants
22
+ // ---------------------------------------------------------------------------
23
+ export const SPACE = ' ';
24
+ export const BLANK = '';
25
+ export const NEWLINE = '\n';
26
+ export const COMMA = ',';
27
+ export const OPEN_PARENTHESIS = '(';
28
+ export const CLOSE_PARENTHESIS = ')';
29
+ export const OPEN_BRACKET = '[';
30
+ export const CLOSE_BRACKET = ']';
31
+ export const OPEN_CURLY_BRACE = '{';
32
+ export const CLOSE_CURLY_BRACE = '}';
33
+ export class Options {
34
+ maxLength;
35
+ keywordHandling;
36
+ constructor(props = {}) {
37
+ this.maxLength = props.maxLength ?? 120;
38
+ this.keywordHandling = props.keywordHandling ?? 'upper_case';
39
+ }
40
+ }
41
+ export const DEFAULT_OPTIONS = new Options();
42
+ export class Result {
43
+ can;
44
+ lines;
45
+ constructor(props = {}) {
46
+ this.can = props.can ?? false;
47
+ this.lines = props.lines ?? [];
48
+ }
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Abstract base: Line
52
+ // ---------------------------------------------------------------------------
53
+ export class Line {
54
+ decompose(opt) {
55
+ return new Result({ can: false });
56
+ }
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // CacheLine — memoises size and decompose
60
+ // ---------------------------------------------------------------------------
61
+ class CacheLine extends Line {
62
+ forceBreak;
63
+ #cacheSize;
64
+ #cacheDecompose;
65
+ constructor(forceBreak = false) {
66
+ super();
67
+ this.forceBreak = forceBreak;
68
+ }
69
+ get size() {
70
+ if (this.forceBreak)
71
+ return 2 << 20;
72
+ if (this.#cacheSize === undefined) {
73
+ this.#cacheSize = this.sizeImpl;
74
+ }
75
+ return this.#cacheSize;
76
+ }
77
+ decompose(opt) {
78
+ if (this.#cacheDecompose === undefined) {
79
+ this.#cacheDecompose = this.decomposeImpl(opt);
80
+ }
81
+ return this.#cacheDecompose;
82
+ }
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ // ComposableLine — joins child lines with a separator; subclasses own how it breaks
86
+ // ---------------------------------------------------------------------------
87
+ class ComposableLine extends CacheLine {
88
+ lines;
89
+ sep;
90
+ constructor(lines, sep = SPACE, forceBreak = false) {
91
+ super(forceBreak);
92
+ this.lines = lines;
93
+ this.sep = sep;
94
+ }
95
+ render(opt) {
96
+ return this.lines.map((line) => line.render(opt)).join(this.sep);
97
+ }
98
+ get sizeImpl() {
99
+ if (this.lines.length === 0)
100
+ return 0;
101
+ return this.lines.reduce((acc, line) => acc + line.size, 0) + this.sep.length * (this.lines.length - 1);
102
+ }
103
+ }
104
+ // ---------------------------------------------------------------------------
105
+ // Concrete line classes (unexported — accessed only via factory functions)
106
+ // ---------------------------------------------------------------------------
107
+ class StackingLine extends ComposableLine {
108
+ decomposeImpl(opt) {
109
+ return new Result({ can: true, lines: [...this.lines] });
110
+ }
111
+ }
112
+ class Indented extends CacheLine {
113
+ indent;
114
+ line;
115
+ constructor(indentBy, line) {
116
+ super();
117
+ this.indent = indentBy;
118
+ this.line = line;
119
+ }
120
+ get sizeImpl() {
121
+ return this.indent + this.line.size;
122
+ }
123
+ render(opt) {
124
+ return SPACE.repeat(this.indent) + this.line.render(opt);
125
+ }
126
+ decomposeImpl(opt) {
127
+ const r = this.line.decompose(opt);
128
+ if (!r.can)
129
+ return new Result({ can: false });
130
+ return new Result({ can: true, lines: r.lines.map((l) => indent(l, this.indent)) });
131
+ }
132
+ }
133
+ class Ladder extends ComposableLine {
134
+ decomposeImpl(opt) {
135
+ return new Result({
136
+ can: true,
137
+ lines: this.lines.map((line, idx) => indent(line, idx * 2)),
138
+ });
139
+ }
140
+ }
141
+ class Threeway extends ComposableLine {
142
+ constructor(lines, sep = SPACE) {
143
+ if (lines.length !== 3)
144
+ throw new Error(`Threeway requires exactly 3 lines, got ${lines.length}`);
145
+ super(lines, sep);
146
+ }
147
+ decomposeImpl(opt) {
148
+ const first = this.lines[0];
149
+ const middle = this.lines[1];
150
+ const last = this.lines[2];
151
+ if (first.size <= last.size) {
152
+ return new Result({ can: true, lines: [stack([first, middle]), indent(last)] });
153
+ }
154
+ else {
155
+ return new Result({ can: true, lines: [first, indent(stack([middle, last]))] });
156
+ }
157
+ }
158
+ }
159
+ class IndentAfterFirst extends ComposableLine {
160
+ decomposeImpl(opt) {
161
+ if (this.lines.length === 0)
162
+ todo();
163
+ const first = this.lines[0];
164
+ const rest = this.lines.slice(1);
165
+ return new Result({
166
+ can: true,
167
+ lines: [first, ...rest.map((l) => indent(l, 2))],
168
+ });
169
+ }
170
+ }
171
+ class IndentMiddle extends ComposableLine {
172
+ decomposeImpl(opt) {
173
+ if (this.lines.length < 3)
174
+ todo();
175
+ const first = this.lines[0];
176
+ const middle = this.lines.slice(1, -1);
177
+ const last = this.lines[this.lines.length - 1];
178
+ return new Result({
179
+ can: true,
180
+ lines: [first, ...middle.map((l) => indent(l, 2)), last],
181
+ });
182
+ }
183
+ }
184
+ class StackingWithComma extends ComposableLine {
185
+ constructor(lines) {
186
+ super(lines, ', ');
187
+ }
188
+ decomposeImpl(opt) {
189
+ if (this.lines.length === 0)
190
+ todo();
191
+ const newLines = this.lines.map((line, idx) => (idx === 0 ? line : new CommaStartingLine(line)));
192
+ return new Result({ can: true, lines: newLines });
193
+ }
194
+ }
195
+ class NoBreak extends ComposableLine {
196
+ decomposeImpl(opt) {
197
+ return new Result({ can: false });
198
+ }
199
+ }
200
+ class Literal extends Line {
201
+ content;
202
+ isKeyword;
203
+ constructor(content, isKeyword = false) {
204
+ super();
205
+ this.content = content;
206
+ this.isKeyword = isKeyword;
207
+ }
208
+ get size() {
209
+ return this.content.length;
210
+ }
211
+ render(opt) {
212
+ if (this.isKeyword) {
213
+ if (opt && opt.keywordHandling === 'upper_case')
214
+ return this.content.toUpperCase();
215
+ if (opt && opt.keywordHandling === 'lower_case')
216
+ return this.content.toLowerCase();
217
+ }
218
+ return this.content;
219
+ }
220
+ }
221
+ class MultilineLiteral extends Line {
222
+ content;
223
+ constructor(content) {
224
+ super();
225
+ this.content = content;
226
+ }
227
+ get size() {
228
+ return 10000; // treat as infinite to force breaking
229
+ }
230
+ render(opt) {
231
+ return this.content;
232
+ }
233
+ decompose(opt) {
234
+ return new Result({ can: true, lines: splitlines(this.content).map((l) => lit(l)) });
235
+ }
236
+ }
237
+ class CommaStartingLine extends CacheLine {
238
+ line;
239
+ constructor(line) {
240
+ super();
241
+ this.line = line;
242
+ }
243
+ get sizeImpl() {
244
+ return 2 + this.line.size;
245
+ }
246
+ render(opt) {
247
+ return ', ' + this.line.render(opt);
248
+ }
249
+ decomposeImpl(opt) {
250
+ const r = this.line.decompose(opt);
251
+ if (!r.can)
252
+ return new Result({ can: false });
253
+ const first = r.lines[0];
254
+ const rest = r.lines.slice(1);
255
+ return new Result({ can: true, lines: [new CommaStartingLine(first), ...rest] });
256
+ }
257
+ }
258
+ class StickySuffix extends CacheLine {
259
+ suffix;
260
+ line;
261
+ separator;
262
+ constructor(suffix, line, separator = SPACE) {
263
+ super();
264
+ this.suffix = suffix;
265
+ this.line = line;
266
+ this.separator = separator;
267
+ }
268
+ get sizeImpl() {
269
+ return this.line.size + this.separator.length + this.suffix.size;
270
+ }
271
+ render(opt) {
272
+ return this.line.render(opt) + this.separator + this.suffix.render(opt);
273
+ }
274
+ decomposeImpl(opt) {
275
+ const r = this.line.decompose(opt);
276
+ if (!r.can)
277
+ return new Result({ can: false });
278
+ const lines = r.lines;
279
+ const last = lines[lines.length - 1];
280
+ return new Result({
281
+ can: true,
282
+ lines: [...lines.slice(0, -1), new StickySuffix(this.suffix, last, this.separator)],
283
+ });
284
+ }
285
+ }
286
+ // ---------------------------------------------------------------------------
287
+ // Core algorithm
288
+ // ---------------------------------------------------------------------------
289
+ export function breakLine(line, opt, shouldBreak) {
290
+ let lines = [line];
291
+ while (true) {
292
+ const newLines = [];
293
+ let canBreak = false;
294
+ for (const l of lines) {
295
+ if (l.size <= opt.maxLength && !(shouldBreak && shouldBreak(l))) {
296
+ newLines.push(l);
297
+ continue;
298
+ }
299
+ const r = l.decompose(opt);
300
+ if (r.can) {
301
+ canBreak = true;
302
+ newLines.push(...r.lines);
303
+ }
304
+ else {
305
+ newLines.push(l);
306
+ }
307
+ }
308
+ if (!canBreak)
309
+ break;
310
+ lines = newLines;
311
+ }
312
+ return lines;
313
+ }
314
+ export function formatLine(line, opt) {
315
+ return breakLine(line, opt)
316
+ .map((l) => l.render(opt))
317
+ .join(NEWLINE);
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // Factory functions
321
+ // ---------------------------------------------------------------------------
322
+ export function lit(value) {
323
+ return new Literal(value);
324
+ }
325
+ export function keyword(name) {
326
+ return new Literal(name, true);
327
+ }
328
+ export function ensure(s) {
329
+ if (s instanceof Line)
330
+ return s;
331
+ const str = String(s);
332
+ if (str.includes('\n'))
333
+ return multilineLiteral(str);
334
+ return lit(str);
335
+ }
336
+ export function bracket(line, sep = SPACE) {
337
+ return new IndentMiddle([bracketOpen, line, bracketClose], sep);
338
+ }
339
+ export function curlyBrace(line, sep = SPACE) {
340
+ if (line == null)
341
+ return lit(OPEN_CURLY_BRACE + CLOSE_CURLY_BRACE);
342
+ return new IndentMiddle([curlyBraceOpen, line, curlyBraceClose], sep);
343
+ }
344
+ export function callLike(opts) {
345
+ const sep = opts.sep ?? BLANK;
346
+ if (!opts.line) {
347
+ return combine([opts.call, parenOpen, parenClose], { sep });
348
+ }
349
+ return indentMid([combine([opts.call, parenOpen], { sep }), opts.line, parenClose], sep);
350
+ }
351
+ export function wrap(opts) {
352
+ const wrapSep = opts.wrapSep ?? SPACE;
353
+ let start;
354
+ let end;
355
+ if (opts.wrapper === 'paren') {
356
+ start = combine([opts.prefix, parenOpen], { sep: wrapSep });
357
+ end = parenClose;
358
+ }
359
+ else if (opts.wrapper === 'bracket') {
360
+ start = combine([opts.prefix, bracketOpen], { sep: wrapSep });
361
+ end = bracketClose;
362
+ }
363
+ else if (opts.wrapper === 'curly') {
364
+ start = combine([opts.prefix, curlyBraceOpen], { sep: wrapSep });
365
+ end = curlyBraceClose;
366
+ }
367
+ else if (opts.wrapper === 'diamond') {
368
+ start = combine([opts.prefix, lit('<')], { sep: wrapSep });
369
+ end = lit('>');
370
+ }
371
+ else {
372
+ throw new Error(`Unsupported wrapper: ${opts.wrapper}`);
373
+ }
374
+ return indentMid([start, opts.line, end], BLANK);
375
+ }
376
+ // Deviation from Python: `l_combine(line, *args, sep=SPACE)` varargs+keyword-sep
377
+ // is not expressible in TS. TS signature: combine(lines | line, { sep }).
378
+ export function combine(lines, opts) {
379
+ const sep = opts?.sep ?? SPACE;
380
+ const arr = lines instanceof Line ? [lines] : Array.from(lines);
381
+ return new NoBreak(removeNulls(arr), sep);
382
+ }
383
+ export function indent(line, spaces = 2) {
384
+ if (line instanceof Indented) {
385
+ return new Indented(line.indent + spaces, line.line);
386
+ }
387
+ return new Indented(spaces, line);
388
+ }
389
+ export function indentMid(lines, sep = SPACE) {
390
+ return new IndentMiddle(removeNulls(lines), sep);
391
+ }
392
+ export function indentNext(lines, sep = SPACE) {
393
+ return new IndentAfterFirst(removeNulls(lines), sep);
394
+ }
395
+ export function ladder(lines) {
396
+ return new Ladder(removeNulls(lines));
397
+ }
398
+ export function multilineLiteral(content) {
399
+ return new MultilineLiteral(content);
400
+ }
401
+ export function paren(line, sep = SPACE) {
402
+ if (line == null)
403
+ return lit(OPEN_PARENTHESIS + CLOSE_PARENTHESIS);
404
+ return new IndentMiddle([parenOpen, line, parenClose], sep);
405
+ }
406
+ export function stack(lines, opts) {
407
+ return new StackingLine(removeNulls(lines), opts?.sep ?? SPACE, opts?.forceBreak ?? false);
408
+ }
409
+ export function stackComma(lines) {
410
+ return new StackingWithComma(removeNulls(lines));
411
+ }
412
+ export function stackCommaEnd(lines) {
413
+ return stack(removeNulls(lines).map((line) => stickySuffix(lit(COMMA), line, BLANK)));
414
+ }
415
+ export function stackWithSep(lines, sep) {
416
+ const clean = removeNulls(lines);
417
+ if (clean.length === 0)
418
+ throw new Error('stackWithSep requires at least one line');
419
+ if (clean.length === 1)
420
+ return clean[0];
421
+ const first = clean[0];
422
+ const rest = clean.slice(1);
423
+ const interleaved = rest.reduce((acc, line) => [...acc, sep, line], [first]);
424
+ return stack(interleaved, { forceBreak: true });
425
+ }
426
+ export function stickySuffix(suffix, line, separator = SPACE) {
427
+ return new StickySuffix(suffix, line, separator);
428
+ }
429
+ export function threeway(lines) {
430
+ return new Threeway(lines);
431
+ }
432
+ export function stackBinOp(lines, op) {
433
+ if (lines.length === 0)
434
+ throw new Error('stackBinOp requires at least one line');
435
+ if (lines.length === 1)
436
+ return lines[0];
437
+ const first = lines[0];
438
+ const rest = lines.slice(1);
439
+ const tail = rest.reduce((acc, line) => [...acc, indentNext([op, line])], []);
440
+ return stack([first, ...tail]);
441
+ }
442
+ // ---------------------------------------------------------------------------
443
+ // Pre-created singleton tokens
444
+ // ---------------------------------------------------------------------------
445
+ export const parenOpen = lit(OPEN_PARENTHESIS);
446
+ export const parenClose = lit(CLOSE_PARENTHESIS);
447
+ export const bracketOpen = lit(OPEN_BRACKET);
448
+ export const bracketClose = lit(CLOSE_BRACKET);
449
+ export const curlyBraceOpen = lit(OPEN_CURLY_BRACE);
450
+ export const curlyBraceClose = lit(CLOSE_CURLY_BRACE);