Skip to content

Commit 53d3cda

Browse files
committed
feat: implement generic pretty-printer
1 parent 8a00499 commit 53d3cda

16 files changed

Lines changed: 2219 additions & 1 deletion

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@elastic/esql",
33
"version": "0.0.1",
44
"author": "Kibana ES|QL team",
5-
"description": "A set of ts tools to parse, build and transform ES|QL queries programatically.",
5+
"description": "A set of ts tools to parse, build and transform ES|QL queries programmatically.",
66
"packageManager": "yarn@1.22.22",
77
"main": "./lib/index.js",
88
"types": "./lib/index.d.ts",
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import {
9+
text,
10+
line,
11+
softline,
12+
hardline,
13+
hardlineWithoutBreakParent,
14+
literalline,
15+
literallineWithoutBreakParent,
16+
breakParent,
17+
group,
18+
conditionalGroup,
19+
indent,
20+
indentIfBreak,
21+
align,
22+
dedent,
23+
dedentToRoot,
24+
markAsRoot,
25+
fill,
26+
ifBreak,
27+
lineSuffix,
28+
lineSuffixBoundary,
29+
label,
30+
trim,
31+
cursor,
32+
join,
33+
bracketedList,
34+
addAlignmentToDoc,
35+
} from '..';
36+
37+
describe('builders', () => {
38+
describe('text', () => {
39+
it('returns the string unchanged', () => {
40+
expect(text('hello')).toBe('hello');
41+
});
42+
43+
it('returns empty string for empty input', () => {
44+
expect(text('')).toBe('');
45+
});
46+
});
47+
48+
describe('line primitives', () => {
49+
it('line is a LineDoc with no flags', () => {
50+
expect(line).toEqual({ type: 'line' });
51+
});
52+
53+
it('softline has soft: true', () => {
54+
expect(softline).toEqual({ type: 'line', soft: true });
55+
});
56+
57+
it('hardline is [hardlineWithoutBreakParent, breakParent]', () => {
58+
expect(hardline).toEqual([{ type: 'line', hard: true }, { type: 'break-parent' }]);
59+
});
60+
61+
it('hardlineWithoutBreakParent is a hard LineDoc', () => {
62+
expect(hardlineWithoutBreakParent).toEqual({ type: 'line', hard: true });
63+
});
64+
65+
it('literalline is [literalLineDoc, breakParent]', () => {
66+
expect(literalline).toEqual([
67+
{ type: 'line', hard: true, literal: true },
68+
{ type: 'break-parent' },
69+
]);
70+
});
71+
72+
it('literallineWithoutBreakParent is a hard+literal LineDoc', () => {
73+
expect(literallineWithoutBreakParent).toEqual({
74+
type: 'line',
75+
hard: true,
76+
literal: true,
77+
});
78+
});
79+
80+
it('breakParent is a BreakParentDoc', () => {
81+
expect(breakParent).toEqual({ type: 'break-parent' });
82+
});
83+
});
84+
85+
describe('group', () => {
86+
it('creates a GroupDoc with contents', () => {
87+
const doc = group(text('hello'));
88+
expect(doc).toEqual({ type: 'group', contents: 'hello' });
89+
});
90+
91+
it('passes shouldBreak option', () => {
92+
const doc = group(text('x'), { shouldBreak: true });
93+
expect(doc).toEqual({ type: 'group', contents: 'x', shouldBreak: true });
94+
});
95+
96+
it('passes id option', () => {
97+
const id = Symbol('test');
98+
const doc = group(text('x'), { id });
99+
expect(doc).toEqual({ type: 'group', contents: 'x', id });
100+
});
101+
});
102+
103+
describe('conditionalGroup', () => {
104+
it('sets first state as contents and rest as expandedStates', () => {
105+
const doc = conditionalGroup([text('flat'), text('wrapped'), text('broken')]);
106+
expect(doc).toEqual({
107+
type: 'group',
108+
contents: 'flat',
109+
expandedStates: ['wrapped', 'broken'],
110+
});
111+
});
112+
113+
it('passes id option', () => {
114+
const id = Symbol('cg');
115+
const doc = conditionalGroup([text('a'), text('b')], { id });
116+
expect(doc.id).toBe(id);
117+
});
118+
});
119+
120+
describe('indent', () => {
121+
it('creates an IndentDoc', () => {
122+
const doc = indent(text('hello'));
123+
expect(doc).toEqual({ type: 'indent', contents: 'hello' });
124+
});
125+
});
126+
127+
describe('align', () => {
128+
it('creates an AlignDoc with positive n', () => {
129+
const doc = align(4, text('hello'));
130+
expect(doc).toEqual({ type: 'align', n: 4, contents: 'hello' });
131+
});
132+
133+
it('accepts { type: "root" } for markAsRoot', () => {
134+
const doc = align({ type: 'root' }, text('x'));
135+
expect(doc).toEqual({ type: 'align', n: { type: 'root' }, contents: 'x' });
136+
});
137+
});
138+
139+
describe('dedent / dedentToRoot / markAsRoot', () => {
140+
it('dedent wraps align(-1)', () => {
141+
const doc = dedent(text('hello'));
142+
expect(doc).toEqual({ type: 'align', n: -1, contents: 'hello' });
143+
});
144+
145+
it('dedentToRoot wraps align(-Infinity)', () => {
146+
const doc = dedentToRoot(text('hello'));
147+
expect(doc).toEqual({
148+
type: 'align',
149+
n: Number.NEGATIVE_INFINITY,
150+
contents: 'hello',
151+
});
152+
});
153+
154+
it('markAsRoot wraps align({ type: "root" })', () => {
155+
const doc = markAsRoot(text('hello'));
156+
expect(doc).toEqual({
157+
type: 'align',
158+
n: { type: 'root' },
159+
contents: 'hello',
160+
});
161+
});
162+
});
163+
164+
describe('fill', () => {
165+
it('creates a FillDoc', () => {
166+
const doc = fill([text('a'), line, text('b')]);
167+
expect(doc).toEqual({
168+
type: 'fill',
169+
parts: ['a', { type: 'line' }, 'b'],
170+
});
171+
});
172+
});
173+
174+
describe('ifBreak', () => {
175+
it('creates an IfBreakDoc with default flatContents', () => {
176+
const doc = ifBreak(text('broken'));
177+
expect(doc).toEqual({
178+
type: 'if-break',
179+
breakContents: 'broken',
180+
flatContents: '',
181+
});
182+
});
183+
184+
it('accepts explicit flatContents', () => {
185+
const doc = ifBreak(text('broken'), text('flat'));
186+
expect(doc).toEqual({
187+
type: 'if-break',
188+
breakContents: 'broken',
189+
flatContents: 'flat',
190+
});
191+
});
192+
193+
it('passes groupId', () => {
194+
const gid = Symbol('g');
195+
const doc = ifBreak(text('b'), text('f'), { groupId: gid });
196+
expect(doc.groupId).toBe(gid);
197+
});
198+
});
199+
200+
describe('indentIfBreak', () => {
201+
it('creates an IndentIfBreakDoc', () => {
202+
const gid = Symbol('g');
203+
const doc = indentIfBreak(text('x'), { groupId: gid });
204+
expect(doc).toEqual({
205+
type: 'indent-if-break',
206+
contents: 'x',
207+
groupId: gid,
208+
});
209+
});
210+
211+
it('passes negate option', () => {
212+
const gid = Symbol('g');
213+
const doc = indentIfBreak(text('x'), { groupId: gid, negate: true });
214+
expect(doc.negate).toBe(true);
215+
});
216+
});
217+
218+
describe('lineSuffix', () => {
219+
it('creates a LineSuffixDoc', () => {
220+
const doc = lineSuffix(text(' // comment'));
221+
expect(doc).toEqual({ type: 'line-suffix', contents: ' // comment' });
222+
});
223+
});
224+
225+
describe('lineSuffixBoundary', () => {
226+
it('is a LineSuffixBoundaryDoc', () => {
227+
expect(lineSuffixBoundary).toEqual({ type: 'line-suffix-boundary' });
228+
});
229+
});
230+
231+
describe('label', () => {
232+
it('wraps contents with a label', () => {
233+
const doc = label('test', text('x'));
234+
expect(doc).toEqual({ type: 'label', label: 'test', contents: 'x' });
235+
});
236+
237+
it('returns contents unwrapped if label is falsy', () => {
238+
expect(label(null, text('x'))).toBe('x');
239+
expect(label(undefined, text('x'))).toBe('x');
240+
expect(label('', text('x'))).toBe('x');
241+
expect(label(0, text('x'))).toBe('x');
242+
});
243+
});
244+
245+
describe('trim', () => {
246+
it('is a TrimDoc', () => {
247+
expect(trim).toEqual({ type: 'trim' });
248+
});
249+
});
250+
251+
describe('cursor', () => {
252+
it('is a CursorDoc', () => {
253+
expect(cursor).toEqual({ type: 'cursor' });
254+
});
255+
});
256+
257+
describe('join', () => {
258+
it('interleaves separator between docs', () => {
259+
const doc = join(text(', '), [text('a'), text('b'), text('c')]);
260+
expect(doc).toEqual(['a', ', ', 'b', ', ', 'c']);
261+
});
262+
263+
it('returns empty array for empty input', () => {
264+
expect(join(text(', '), [])).toEqual([]);
265+
});
266+
267+
it('returns single item without separator', () => {
268+
expect(join(text(', '), [text('a')])).toEqual(['a']);
269+
});
270+
});
271+
272+
describe('bracketedList', () => {
273+
it('returns [open, close] for empty items', () => {
274+
expect(bracketedList('(', ')', ',', [])).toEqual(['(', ')']);
275+
});
276+
277+
it('creates a group with indent and softlines', () => {
278+
const doc = bracketedList('(', ')', ',', [text('a'), text('b')]);
279+
expect(doc).toEqual(
280+
group(['(', indent([softline, join([',', line], ['a', 'b'])]), softline, ')'])
281+
);
282+
});
283+
});
284+
285+
describe('addAlignmentToDoc', () => {
286+
it('returns doc unchanged when size is 0', () => {
287+
const doc = text('x');
288+
expect(addAlignmentToDoc(doc, 0, 2)).toBe('x');
289+
});
290+
291+
it('wraps with indent for tab-sized chunks', () => {
292+
const doc = addAlignmentToDoc(text('x'), 4, 2);
293+
expect(doc).toEqual(dedentToRoot(indent(indent(text('x')))));
294+
});
295+
296+
it('uses align for remainder', () => {
297+
const doc = addAlignmentToDoc(text('x'), 5, 2);
298+
expect(doc).toEqual(dedentToRoot(align(1, indent(indent(text('x'))))));
299+
});
300+
});
301+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { text, softline, hardline, group, indent, conditionalGroup, join, layout } from '..';
9+
10+
describe('conditionalGroup', () => {
11+
it('uses first state (flat) when it fits', () => {
12+
const items = [text('alpha'), text('beta'), text('gamma')];
13+
14+
const doc = conditionalGroup([
15+
// Phase 1: flat with spaces — "alpha, beta, gamma"
16+
group(join(text(', '), items)),
17+
// Phase 2: compact — "alpha,beta,gamma"
18+
group(join([text(','), softline], items)),
19+
// Phase 3: one per line
20+
group([indent([hardline, join([text(','), hardline], items)]), hardline], {
21+
shouldBreak: true,
22+
}),
23+
]);
24+
25+
expect(layout(doc, { printWidth: 20 })).toBe('alpha, beta, gamma');
26+
expect(layout(doc, { printWidth: 17 })).toBe('alpha,beta,gamma');
27+
expect(layout(doc, { printWidth: 10 })).toBe('\n alpha,\n beta,\n gamma\n');
28+
});
29+
30+
it('three-phase list with brackets', () => {
31+
const items = [text('alpha'), text('beta'), text('gamma'), text('delta')];
32+
const sep = text(',');
33+
34+
const doc = conditionalGroup([
35+
// Phase 1: all on one line — "(alpha, beta, gamma, delta)" = 27
36+
group([text('('), join([sep, text(' ')], items), text(')')]),
37+
// Phase 2: compact (no spaces) — "(alpha,beta,gamma,delta)" = 24
38+
group([text('('), join([sep, softline], items), text(')')]),
39+
// Phase 3: one per line
40+
group([text('('), indent([hardline, join([sep, hardline], items)]), hardline, text(')')], {
41+
shouldBreak: true,
42+
}),
43+
]);
44+
45+
expect(layout(doc, { printWidth: 30 })).toBe('(alpha, beta, gamma, delta)');
46+
expect(layout(doc, { printWidth: 25 })).toBe('(alpha,beta,gamma,delta)');
47+
expect(layout(doc, { printWidth: 10 })).toBe('(\n alpha,\n beta,\n gamma,\n delta\n)');
48+
});
49+
50+
it('with two states only', () => {
51+
const doc = conditionalGroup([
52+
// Try flat
53+
join([text(', ')], [text('aaa'), text('bbb'), text('ccc')]),
54+
// Fallback: broken
55+
group(
56+
[indent([hardline, join([text(','), hardline], [text('aaa'), text('bbb'), text('ccc')])])],
57+
{ shouldBreak: true }
58+
),
59+
]);
60+
61+
expect(layout(doc, { printWidth: 20 })).toBe('aaa, bbb, ccc');
62+
expect(layout(doc, { printWidth: 10 })).toBe('\n aaa,\n bbb,\n ccc');
63+
});
64+
});

0 commit comments

Comments
 (0)