@excom/quark-formatter 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.
- package/.rush/temp/chunked-rush-logs/quark-formatter.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark-formatter.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/index.ts +1 -0
- package/package.json +41 -0
- package/rush-logs/quark-formatter.apply-exports.cache.log +1 -0
- package/rush-logs/quark-formatter.apply-exports.log +1 -0
- package/rush-logs/quark-formatter.build_package-metas.cache.log +1 -0
- package/rush-logs/quark-formatter.build_package-metas.log +1 -0
- package/src/doc.ts +297 -0
- package/src/printer.ts +625 -0
- package/support/docs/README.md +70 -0
- package/support/package-meta.json +33 -0
- package/support/tests/formatter.test.ts +976 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
import { format } from "../../index";
|
|
2
|
+
import { parse, QuarkParseError } from "@excom/quark-parser";
|
|
3
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("format: rules and declarations", () => {
|
|
8
|
+
it("normalizes indentation, spacing, and semicolons", () => {
|
|
9
|
+
const input = `main{ color:red;\n\n\n\n span {content:"hi"}\n}`;
|
|
10
|
+
expect(format(input)).toBe(
|
|
11
|
+
`main {
|
|
12
|
+
color: red;
|
|
13
|
+
|
|
14
|
+
span {
|
|
15
|
+
content: "hi";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("puts each selector of a list on its own line", () => {
|
|
23
|
+
expect(format(`a,b span,c[open] { x: 1; }`)).toBe(
|
|
24
|
+
`a,
|
|
25
|
+
b span,
|
|
26
|
+
c[open] {
|
|
27
|
+
x: 1;
|
|
28
|
+
}
|
|
29
|
+
`
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("prints combinators and pseudo selectors", () => {
|
|
34
|
+
const input = `section[aria-label="preview"]>template{x:1;}
|
|
35
|
+
li:not([is-active],.done)+ul::before{y:2;}
|
|
36
|
+
&:has(include-content[is-active]){z:3;}`;
|
|
37
|
+
expect(format(input)).toBe(
|
|
38
|
+
`section[aria-label="preview"] > template {
|
|
39
|
+
x: 1;
|
|
40
|
+
}
|
|
41
|
+
li:not([is-active], .done) + ul::before {
|
|
42
|
+
y: 2;
|
|
43
|
+
}
|
|
44
|
+
&:has(include-content[is-active]) {
|
|
45
|
+
z: 3;
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("format: Quark accessor deviations", () => {
|
|
53
|
+
it("prints dot accessors compactly", () => {
|
|
54
|
+
expect(
|
|
55
|
+
format(`a { x: $my-var . myProp ; y: prop("provision") . body ; }`)
|
|
56
|
+
).toBe(
|
|
57
|
+
`a {
|
|
58
|
+
x: $my-var.myProp;
|
|
59
|
+
y: prop("provision").body;
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("prints bracket accessors compactly", () => {
|
|
66
|
+
expect(format(`a { x: $my-var[ 'my-prop-2' ]; y: $list[ $i ]; }`)).toBe(
|
|
67
|
+
`a {
|
|
68
|
+
x: $my-var['my-prop-2'];
|
|
69
|
+
y: $list[$i];
|
|
70
|
+
}
|
|
71
|
+
`
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("prints chained calls and accessors compactly", () => {
|
|
76
|
+
const input = `a { $ref: closest("include-content[data-demo]") . getAttribute( "data-demo" ); }`;
|
|
77
|
+
expect(format(input)).toBe(
|
|
78
|
+
`a {
|
|
79
|
+
$ref: closest("include-content[data-demo]").getAttribute("data-demo");
|
|
80
|
+
}
|
|
81
|
+
`
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("format: expressions", () => {
|
|
87
|
+
it("spaces binary operators and preserves precedence parens", () => {
|
|
88
|
+
const input = `a { x: 1+2 * 3; y: ($a or $b)and $c; z: $a or($b and $c); }`;
|
|
89
|
+
expect(format(input)).toBe(
|
|
90
|
+
`a {
|
|
91
|
+
x: 1 + 2 * 3;
|
|
92
|
+
y: ($a or $b) and $c;
|
|
93
|
+
z: $a or $b and $c;
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("re-parses to an equivalent precedence tree", () => {
|
|
100
|
+
const input = `a { x: (1 + 2) * 3; y: 1 + (2 * 3); }`;
|
|
101
|
+
const output = format(input);
|
|
102
|
+
expect(output).toContain("x: (1 + 2) * 3;");
|
|
103
|
+
expect(output).toContain("y: 1 + 2 * 3;");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("formats if() arms", () => {
|
|
107
|
+
const input = `a { content: if( item.inheritedFrom :template("#x") ;else: none ); }`;
|
|
108
|
+
expect(format(input)).toBe(
|
|
109
|
+
`a {
|
|
110
|
+
content: if(item.inheritedFrom: template("#x"); else: none);
|
|
111
|
+
}
|
|
112
|
+
`
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("formats lists and maps", () => {
|
|
117
|
+
const input = `a { m: (x:1,y:2); l: 1px 2px 3px; b: [1,2, 3]; }`;
|
|
118
|
+
expect(format(input)).toBe(
|
|
119
|
+
`a {
|
|
120
|
+
m: (x: 1, y: 2);
|
|
121
|
+
l: 1px 2px 3px;
|
|
122
|
+
b: [1, 2, 3];
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("formats interpolations in urls and strings", () => {
|
|
129
|
+
expect(
|
|
130
|
+
format(`.icon { background: url(/img/#{$name}.svg); content:"a #{$b} c" }`)
|
|
131
|
+
).toBe(
|
|
132
|
+
`.icon {
|
|
133
|
+
background: url(/img/#{$name}.svg);
|
|
134
|
+
content: "a #{$b} c";
|
|
135
|
+
}
|
|
136
|
+
`
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("format: comments and blank lines", () => {
|
|
142
|
+
it("preserves comments in place", () => {
|
|
143
|
+
const input = `/* header */
|
|
144
|
+
a {
|
|
145
|
+
/* leading note */
|
|
146
|
+
x: 1; /* trailing note */
|
|
147
|
+
}`;
|
|
148
|
+
expect(format(input)).toBe(
|
|
149
|
+
`/* header */
|
|
150
|
+
a {
|
|
151
|
+
/* leading note */
|
|
152
|
+
x: 1; /* trailing note */
|
|
153
|
+
}
|
|
154
|
+
`
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("collapses runs of blank lines to one", () => {
|
|
159
|
+
expect(format(`a { x: 1; }\n\n\n\n\nb { y: 2; }`)).toBe(
|
|
160
|
+
`a {
|
|
161
|
+
x: 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
b {
|
|
165
|
+
y: 2;
|
|
166
|
+
}
|
|
167
|
+
`
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("format: at-rules", () => {
|
|
173
|
+
it("formats @use", () => {
|
|
174
|
+
const input = `@use "/import-files" as * ;\n@use "/lib/list" as list;\n@use "/lib/math";`;
|
|
175
|
+
expect(format(input)).toBe(
|
|
176
|
+
`@use "/import-files" as *;
|
|
177
|
+
@use "/lib/list" as list;
|
|
178
|
+
@use "/lib/math";
|
|
179
|
+
`
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("formats @scope blocks", () => {
|
|
184
|
+
const input = `@scope{:scope{data-x:1}ul li{content:item.name}}`;
|
|
185
|
+
expect(format(input)).toBe(
|
|
186
|
+
`@scope {
|
|
187
|
+
:scope {
|
|
188
|
+
data-x: 1;
|
|
189
|
+
}
|
|
190
|
+
ul li {
|
|
191
|
+
content: item.name;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
`
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("formats @on statements: event lists as written, options as expressions", () => {
|
|
199
|
+
const input = `form{@on submit (prevent-default ,handle:saveDraft($draft)) ;@on "super-form-success",reset(handle:follow)}`;
|
|
200
|
+
expect(format(input)).toBe(
|
|
201
|
+
`form {
|
|
202
|
+
@on submit (prevent-default, handle: saveDraft($draft));
|
|
203
|
+
@on "super-form-success", reset (handle: follow);
|
|
204
|
+
}
|
|
205
|
+
`
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("formats @on options: flags bare, values as expressions, empty group dropped, long groups one per line", () => {
|
|
210
|
+
const input = `ul{@on click(target:"li[data-id]",once,debounce:100*3,key:"Shift+K",host:window,handle:pick);@on keydown ( self , capture ){is-open:none;}@on input(){x:1;}}`;
|
|
211
|
+
const expected = `ul {
|
|
212
|
+
@on click (
|
|
213
|
+
target: "li[data-id]",
|
|
214
|
+
once,
|
|
215
|
+
debounce: 100 * 3,
|
|
216
|
+
key: "Shift+K",
|
|
217
|
+
host: window,
|
|
218
|
+
handle: pick
|
|
219
|
+
);
|
|
220
|
+
@on keydown (self, capture) {
|
|
221
|
+
is-open: none;
|
|
222
|
+
}
|
|
223
|
+
@on input {
|
|
224
|
+
x: 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
`;
|
|
228
|
+
expect(format(input)).toBe(expected);
|
|
229
|
+
expect(format(expected)).toBe(expected);
|
|
230
|
+
const after = (parse(expected).body[0] as any).block.body;
|
|
231
|
+
expect(after[0].options.map((o: any) => o.name)).toEqual([
|
|
232
|
+
"target",
|
|
233
|
+
"once",
|
|
234
|
+
"debounce",
|
|
235
|
+
"key",
|
|
236
|
+
"host",
|
|
237
|
+
"handle",
|
|
238
|
+
]);
|
|
239
|
+
expect(after[1].options.map((o: any) => o.name)).toEqual([
|
|
240
|
+
"self",
|
|
241
|
+
"capture",
|
|
242
|
+
]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("formats handle lists and other expression values (calls, members, operators, if())", () => {
|
|
246
|
+
const input = `button{@on click(prevent-default,handle:(resetDemo( $src ),api.follow,item.name.trim(),3+$n*2,if($a:1;else:2)));}`;
|
|
247
|
+
expect(format(input)).toBe(
|
|
248
|
+
`button {
|
|
249
|
+
@on click (
|
|
250
|
+
prevent-default,
|
|
251
|
+
handle: (
|
|
252
|
+
resetDemo($src),
|
|
253
|
+
api.follow,
|
|
254
|
+
item.name.trim(),
|
|
255
|
+
3 + $n * 2,
|
|
256
|
+
if($a: 1; else: 2)
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
`
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("formats @on blocks with several events and nested statements", () => {
|
|
265
|
+
const input = `form{@on input,change{data-draft:event.target.value;}@on submit (prevent-default,handle:save){is-saved:"";#status{content:"saved";}}@on reset{}}`;
|
|
266
|
+
const expected = `form {
|
|
267
|
+
@on input, change {
|
|
268
|
+
data-draft: event.target.value;
|
|
269
|
+
}
|
|
270
|
+
@on submit (prevent-default, handle: save) {
|
|
271
|
+
is-saved: "";
|
|
272
|
+
#status {
|
|
273
|
+
content: "saved";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
@on reset {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`;
|
|
280
|
+
expect(format(input)).toBe(expected);
|
|
281
|
+
expect(format(expected)).toBe(expected);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("formats @dispatch and @command statements like @on's head", () => {
|
|
285
|
+
const input = `button{@on click{@dispatch cart-add,"cart:changed"(detail:(sku:$sku,qty:1),target:"cart-view",bubbles:false);@command --refresh(target:"#feed");@dispatch ping}}`;
|
|
286
|
+
const expected = `button {
|
|
287
|
+
@on click {
|
|
288
|
+
@dispatch cart-add, "cart:changed" (
|
|
289
|
+
detail: (sku: $sku, qty: 1),
|
|
290
|
+
target: "cart-view",
|
|
291
|
+
bubbles: false
|
|
292
|
+
);
|
|
293
|
+
@command --refresh (target: "#feed");
|
|
294
|
+
@dispatch ping;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
`;
|
|
298
|
+
expect(format(input)).toBe(expected);
|
|
299
|
+
expect(format(expected)).toBe(expected);
|
|
300
|
+
const on = (parse(expected).body[0] as any).block.body[0];
|
|
301
|
+
expect(on.block.body.map((s: any) => s.name)).toEqual([
|
|
302
|
+
"dispatch",
|
|
303
|
+
"command",
|
|
304
|
+
"dispatch",
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("keeps @on in place among declarations, nested rules, comments and blank lines", () => {
|
|
309
|
+
const input = `form {
|
|
310
|
+
$draft: none;
|
|
311
|
+
@on submit (prevent-default, handle: save($draft)); /* trailing */
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
&[is-locked] { @on submit (once, handle: save($draft)); }
|
|
315
|
+
/* block */
|
|
316
|
+
@on "custom:evt" (handle: a);
|
|
317
|
+
data-x: 1;
|
|
318
|
+
}
|
|
319
|
+
@on load (handle: boot);`;
|
|
320
|
+
expect(format(input)).toBe(
|
|
321
|
+
`form {
|
|
322
|
+
$draft: none;
|
|
323
|
+
@on submit (prevent-default, handle: save($draft)); /* trailing */
|
|
324
|
+
|
|
325
|
+
&[is-locked] {
|
|
326
|
+
@on submit (once, handle: save($draft));
|
|
327
|
+
}
|
|
328
|
+
/* block */
|
|
329
|
+
@on "custom:evt" (handle: a);
|
|
330
|
+
data-x: 1;
|
|
331
|
+
}
|
|
332
|
+
@on load (handle: boot);
|
|
333
|
+
`
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("formats @delay blocks and @warn / @debug statements inside rules", () => {
|
|
338
|
+
const input = `button[data-copy]{@on click{data-copied:"";@delay 2000{data-copied:none;}}@delay $ms*2 {is-stale:"";span{content:none;}}}img:not([alt]){@warn "img needs alt" ;@debug "size",attr("width");}`;
|
|
339
|
+
const expected = `button[data-copy] {
|
|
340
|
+
@on click {
|
|
341
|
+
data-copied: "";
|
|
342
|
+
@delay 2000 {
|
|
343
|
+
data-copied: none;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
@delay $ms * 2 {
|
|
347
|
+
is-stale: "";
|
|
348
|
+
span {
|
|
349
|
+
content: none;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
img:not([alt]) {
|
|
354
|
+
@warn "img needs alt";
|
|
355
|
+
@debug "size", attr("width");
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
expect(format(input)).toBe(expected);
|
|
359
|
+
expect(format(expected)).toBe(expected);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("formats @view-transition blocks: options normalized, empty group dropped, long groups wrapped", () => {
|
|
363
|
+
const input = `provider-fetch[is-success]{@view-transition(types:"todo-change",timeout:1500){ul{content:iterate($todos,none,"id");}}@view-transition ( ){data-x:1;}}@view-transition(types:"a b",timeout:300*5,delay:200,first-render,if-active:replace,until:"[is-success], [is-error]"){#out{content:"done";}}`;
|
|
364
|
+
const expected = `provider-fetch[is-success] {
|
|
365
|
+
@view-transition (types: "todo-change", timeout: 1500) {
|
|
366
|
+
ul {
|
|
367
|
+
content: iterate($todos, none, "id");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
@view-transition {
|
|
371
|
+
data-x: 1;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
@view-transition (
|
|
375
|
+
types: "a b",
|
|
376
|
+
timeout: 300 * 5,
|
|
377
|
+
delay: 200,
|
|
378
|
+
first-render,
|
|
379
|
+
if-active: replace,
|
|
380
|
+
until: "[is-success], [is-error]"
|
|
381
|
+
) {
|
|
382
|
+
#out {
|
|
383
|
+
content: "done";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
`;
|
|
387
|
+
expect(format(input)).toBe(expected);
|
|
388
|
+
expect(format(expected)).toBe(expected);
|
|
389
|
+
const after = parse(expected).body as any[];
|
|
390
|
+
expect(after[1].options.map((o: any) => o.name)).toEqual([
|
|
391
|
+
"types",
|
|
392
|
+
"timeout",
|
|
393
|
+
"delay",
|
|
394
|
+
"first-render",
|
|
395
|
+
"if-active",
|
|
396
|
+
"until",
|
|
397
|
+
]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("keeps @view-transition in place among declarations, @on blocks, comments and blank lines", () => {
|
|
401
|
+
const input = `form {
|
|
402
|
+
data-x: 1; /* trailing */
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@view-transition (types: "save") { is-saved: ""; @on reset { is-saved: none; } }
|
|
406
|
+
/* block */
|
|
407
|
+
@on submit (prevent-default) { @view-transition { #status { content: "saved"; } } }
|
|
408
|
+
@view-transition {
|
|
409
|
+
}
|
|
410
|
+
}`;
|
|
411
|
+
const expected = `form {
|
|
412
|
+
data-x: 1; /* trailing */
|
|
413
|
+
|
|
414
|
+
@view-transition (types: "save") {
|
|
415
|
+
is-saved: "";
|
|
416
|
+
@on reset {
|
|
417
|
+
is-saved: none;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/* block */
|
|
421
|
+
@on submit (prevent-default) {
|
|
422
|
+
@view-transition {
|
|
423
|
+
#status {
|
|
424
|
+
content: "saved";
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
@view-transition {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
432
|
+
expect(format(input)).toBe(expected);
|
|
433
|
+
expect(format(expected)).toBe(expected);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("is idempotent and re-parses @on rules to the same events and options", () => {
|
|
437
|
+
const input = `a{@on click , change ( handle : ( a , b( $c , "d" ) ) ) ;@on 'evt' ( once , handle : x )}`;
|
|
438
|
+
const once = format(input);
|
|
439
|
+
expect(format(once)).toBe(once);
|
|
440
|
+
const shape = (src: string) =>
|
|
441
|
+
(parse(src).body[0] as any).block.body.map((n: any) => [
|
|
442
|
+
n.name,
|
|
443
|
+
n.events.map((e: any) => e.name),
|
|
444
|
+
n.options.map((o: any) => o.name),
|
|
445
|
+
]);
|
|
446
|
+
expect(shape(once)).toEqual(shape(input));
|
|
447
|
+
expect(shape(once)).toEqual([
|
|
448
|
+
["on", ["click", "change"], ["handle"]],
|
|
449
|
+
["on", ["evt"], ["once", "handle"]],
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe("format: real-world sheets", () => {
|
|
455
|
+
const viewsDir = resolve(__dirname, "../../../docs-site/public/views");
|
|
456
|
+
const sheets = readdirSync(viewsDir, { recursive: true })
|
|
457
|
+
.map(String)
|
|
458
|
+
.filter((f) => f.endsWith(".quark"));
|
|
459
|
+
|
|
460
|
+
it("finds the docs-site sheets", () => {
|
|
461
|
+
expect(sheets.length).toBeGreaterThanOrEqual(4);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it.each(sheets)("%s: formats, re-parses, and is idempotent", (sheet) => {
|
|
465
|
+
const source = readFileSync(resolve(viewsDir, sheet), "utf8");
|
|
466
|
+
const once = format(source);
|
|
467
|
+
expect(() => parse(once)).not.toThrow();
|
|
468
|
+
expect(format(once)).toBe(once);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe("format: idempotency on synthetic inputs", () => {
|
|
473
|
+
const inputs = [
|
|
474
|
+
`a{x:( $a or $b )and $c;}`,
|
|
475
|
+
`#x{content:iterate($packageMeta.elementApis);[bind-tag]{content:item.tag;}}`,
|
|
476
|
+
`a{/* one */x:1;/* two */}`,
|
|
477
|
+
`@scope{a{x:1;}@on click{y:2;}}`,
|
|
478
|
+
];
|
|
479
|
+
it.each(inputs)("format(format(x)) === format(x): %s", (input) => {
|
|
480
|
+
const once = format(input);
|
|
481
|
+
expect(format(once)).toBe(once);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
/** Formats once, asserts idempotency and re-parseability, returns the output. */
|
|
486
|
+
const fmt = (input: string, options?: Parameters<typeof format>[1]): string => {
|
|
487
|
+
const once = format(input, options);
|
|
488
|
+
expect(() => parse(once)).not.toThrow();
|
|
489
|
+
expect(format(once, options)).toBe(once);
|
|
490
|
+
return once;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/** Deep-clones an AST without `start` / `end` spans. */
|
|
494
|
+
const stripSpans = (node: unknown): unknown => {
|
|
495
|
+
if (Array.isArray(node)) return node.map(stripSpans);
|
|
496
|
+
if (node && typeof node === "object") {
|
|
497
|
+
const out: Record<string, unknown> = {};
|
|
498
|
+
for (const [key, value] of Object.entries(node)) {
|
|
499
|
+
if (key === "start" || key === "end") continue;
|
|
500
|
+
out[key] = stripSpans(value);
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
return node;
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/** Formats and asserts the output parses to the same span-less AST. */
|
|
508
|
+
const roundTrips = (input: string): string => {
|
|
509
|
+
const once = fmt(input);
|
|
510
|
+
expect(stripSpans(parse(once))).toEqual(stripSpans(parse(input)));
|
|
511
|
+
return once;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
describe("format: blocks, options, and errors", () => {
|
|
515
|
+
it("prints empty blocks", () => {
|
|
516
|
+
expect(fmt("a{}")).toBe("a {\n}\n");
|
|
517
|
+
expect(fmt("@scope{}")).toBe("@scope {\n}\n");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("honours the indent option", () => {
|
|
521
|
+
expect(fmt("a{b{c:1}}", { indent: "\t" })).toBe(
|
|
522
|
+
"a {\n\tb {\n\t\tc: 1;\n\t}\n}\n"
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("rethrows parse errors", () => {
|
|
527
|
+
expect(() => format("a { b: ?; }")).toThrow(QuarkParseError);
|
|
528
|
+
expect(() => format("a { b: c")).toThrow(/Unclosed block/);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("surfaces the parser's rejection of non-Quark constructs unchanged", () => {
|
|
532
|
+
for (const source of [
|
|
533
|
+
"@media (x){a{b:c}}",
|
|
534
|
+
"%ph{x:1}",
|
|
535
|
+
"#{$sel}{x:1}",
|
|
536
|
+
"a{x:1 !important}",
|
|
537
|
+
"a{font: bold{family:serif}}",
|
|
538
|
+
'@use "x" with ($a: 1);',
|
|
539
|
+
]) {
|
|
540
|
+
const thrown = (() => {
|
|
541
|
+
try {
|
|
542
|
+
parse(source);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
return error as Error;
|
|
545
|
+
}
|
|
546
|
+
throw new Error(`expected a parse error: ${source}`);
|
|
547
|
+
})();
|
|
548
|
+
expect(() => format(source)).toThrow(QuarkParseError);
|
|
549
|
+
expect(() => format(source)).toThrow(thrown.message);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe("format: comment placement", () => {
|
|
555
|
+
it("keeps a trailing multi-line block comment verbatim", () => {
|
|
556
|
+
expect(fmt("a {\n x: 1; /* one\n two */\n}")).toBe(
|
|
557
|
+
`a {
|
|
558
|
+
x: 1; /* one
|
|
559
|
+
two */
|
|
560
|
+
}
|
|
561
|
+
`
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("re-indents the interior lines of a leading block comment", () => {
|
|
566
|
+
expect(fmt("a {\n /* multi\n line */\n x: 1;\n}")).toBe(
|
|
567
|
+
`a {
|
|
568
|
+
/* multi
|
|
569
|
+
line */
|
|
570
|
+
x: 1;
|
|
571
|
+
}
|
|
572
|
+
`
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("leaves interior lines that do not share the comment's indent", () => {
|
|
577
|
+
expect(fmt("a {\n /* one\ntwo */\n}")).toBe(
|
|
578
|
+
`a {
|
|
579
|
+
/* one
|
|
580
|
+
two */
|
|
581
|
+
}
|
|
582
|
+
`
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("keeps blank lines after comments and comments after closing braces", () => {
|
|
587
|
+
expect(fmt("/* a */\n\n\n\nb { x: 1; } /* done */\n\n\nc { y: 2; }")).toBe(
|
|
588
|
+
`/* a */
|
|
589
|
+
|
|
590
|
+
b {
|
|
591
|
+
x: 1;
|
|
592
|
+
} /* done */
|
|
593
|
+
|
|
594
|
+
c {
|
|
595
|
+
y: 2;
|
|
596
|
+
}
|
|
597
|
+
`
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("keeps a comment between selector groups and a trailing block comment", () => {
|
|
602
|
+
expect(fmt("a{x:1}/* between */\nb{y:2;/* end */}")).toBe(
|
|
603
|
+
`a {
|
|
604
|
+
x: 1;
|
|
605
|
+
} /* between */
|
|
606
|
+
b {
|
|
607
|
+
y: 2; /* end */
|
|
608
|
+
}
|
|
609
|
+
`
|
|
610
|
+
);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe("format: structured at-rules", () => {
|
|
615
|
+
it("prints @debug / @warn / @error", () => {
|
|
616
|
+
expect(fmt('@debug "x";@warn $w;@error "boom";')).toBe(
|
|
617
|
+
`@debug "x";
|
|
618
|
+
@warn $w;
|
|
619
|
+
@error "boom";
|
|
620
|
+
`
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("prints @use namespaces", () => {
|
|
625
|
+
expect(fmt('@use "/lib/math" as m;@use "/x" as *;@use "/y";')).toBe(
|
|
626
|
+
`@use "/lib/math" as m;
|
|
627
|
+
@use "/x" as *;
|
|
628
|
+
@use "/y";
|
|
629
|
+
`
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("prints @scope, empty and nested", () => {
|
|
634
|
+
expect(fmt("@scope{}a{@scope{b{x:1}}}")).toBe(
|
|
635
|
+
`@scope {
|
|
636
|
+
}
|
|
637
|
+
a {
|
|
638
|
+
@scope {
|
|
639
|
+
b {
|
|
640
|
+
x: 1;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
`
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("normalizes single-quoted @on events to double quotes", () => {
|
|
649
|
+
expect(fmt("a{@on 'evt' (handle: x);}")).toBe(
|
|
650
|
+
`a {\n @on "evt" (handle: x);\n}\n`
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe("format: selector parts", () => {
|
|
656
|
+
it("prints attribute values, modifiers, pseudo arguments, and combinators", () => {
|
|
657
|
+
expect(
|
|
658
|
+
fmt(
|
|
659
|
+
'a[b="v" i],[b=v],[b=5],li:nth-child( 2n+1 )::part( label ),a~b,a+b,>li,&-suffix,&:hover,:is(a,b),:host(.x),*{x:1}'
|
|
660
|
+
)
|
|
661
|
+
).toBe(
|
|
662
|
+
`a[b="v" i],
|
|
663
|
+
[b=v],
|
|
664
|
+
[b=5],
|
|
665
|
+
li:nth-child(2n+1)::part(label),
|
|
666
|
+
a ~ b,
|
|
667
|
+
a + b,
|
|
668
|
+
> li,
|
|
669
|
+
&-suffix,
|
|
670
|
+
&:hover,
|
|
671
|
+
:is(a, b),
|
|
672
|
+
:host(.x),
|
|
673
|
+
* {
|
|
674
|
+
x: 1;
|
|
675
|
+
}
|
|
676
|
+
`
|
|
677
|
+
);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe("format: 80-column wrapping", () => {
|
|
682
|
+
it("keeps everything that fits on one line", () => {
|
|
683
|
+
expect(
|
|
684
|
+
fmt("a{x:fn(1,2);m:(k:1,v:2);c:if($a:1;else:2);b:$a and $b or $c}")
|
|
685
|
+
).toBe(
|
|
686
|
+
`a {
|
|
687
|
+
x: fn(1, 2);
|
|
688
|
+
m: (k: 1, v: 2);
|
|
689
|
+
c: if($a: 1; else: 2);
|
|
690
|
+
b: $a and $b or $c;
|
|
691
|
+
}
|
|
692
|
+
`
|
|
693
|
+
);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("breaks an overflowing map one entry per line", () => {
|
|
697
|
+
const input = `:scope { @on input { dataset: (trip: event.target.form.elements["data-trip"].value, outbound: event.target.form.elements["data-outbound"].value); } }`;
|
|
698
|
+
expect(roundTrips(input)).toBe(
|
|
699
|
+
`:scope {
|
|
700
|
+
@on input {
|
|
701
|
+
dataset: (
|
|
702
|
+
trip: event.target.form.elements["data-trip"].value,
|
|
703
|
+
outbound: event.target.form.elements["data-outbound"].value
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
`
|
|
708
|
+
);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("breaks overflowing call arguments and bracket lists one item per line", () => {
|
|
712
|
+
expect(
|
|
713
|
+
roundTrips(
|
|
714
|
+
`a { x: sumLengths(item.cssClasses, item.cssProperties, item.cssAliases, item.parts)[1]; y: [aaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbbbb, cccccccccccccccccc, dddddddddddddddddd]; }`
|
|
715
|
+
)
|
|
716
|
+
).toBe(
|
|
717
|
+
`a {
|
|
718
|
+
x: sumLengths(
|
|
719
|
+
item.cssClasses,
|
|
720
|
+
item.cssProperties,
|
|
721
|
+
item.cssAliases,
|
|
722
|
+
item.parts
|
|
723
|
+
)[1];
|
|
724
|
+
y: [
|
|
725
|
+
aaaaaaaaaaaaaaaa,
|
|
726
|
+
bbbbbbbbbbbbbbbbbb,
|
|
727
|
+
cccccccccccccccccc,
|
|
728
|
+
dddddddddddddddddd
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
`
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("breaks if() arms one per line and hugs a value that fits after its key", () => {
|
|
736
|
+
const input = `a { data-mode: if(event.target.name == "data-mode": event.target.value; else: preserve); }`;
|
|
737
|
+
expect(roundTrips(input)).toBe(
|
|
738
|
+
`a {
|
|
739
|
+
data-mode: if(
|
|
740
|
+
event.target.name == "data-mode": event.target.value;
|
|
741
|
+
else: preserve
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
`
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("breaks after a pair's colon when even `key: value(` does not fit", () => {
|
|
749
|
+
const input = `a { data-pickup-weekday: if(event.target.name == "data-pickup-date" and event.target.valueAsDate: event.target.valueAsDate.toLocaleDateString("en-US", (weekday: "short", timeZone: "UTC")); else: preserve); }`;
|
|
750
|
+
expect(roundTrips(input)).toBe(
|
|
751
|
+
`a {
|
|
752
|
+
data-pickup-weekday: if(
|
|
753
|
+
event.target.name == "data-pickup-date" and event.target.valueAsDate:
|
|
754
|
+
event.target.valueAsDate.toLocaleDateString(
|
|
755
|
+
"en-US",
|
|
756
|
+
(weekday: "short", timeZone: "UTC")
|
|
757
|
+
);
|
|
758
|
+
else: preserve
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("wraps operator chains like text, breaking after an operator", () => {
|
|
766
|
+
const input = `a { data-is-open: $is-expanded-by-user and $has-loaded-content or $is-forced-open and not $is-disabled; }`;
|
|
767
|
+
expect(roundTrips(input)).toBe(
|
|
768
|
+
`a {
|
|
769
|
+
data-is-open: $is-expanded-by-user and $has-loaded-content or
|
|
770
|
+
$is-forced-open and not $is-disabled;
|
|
771
|
+
}
|
|
772
|
+
`
|
|
773
|
+
);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("breaks a parenthesised sub-expression as its own group (prettier's SCSS shape)", () => {
|
|
777
|
+
const input = `a { x: ($aaaaaaaaaaaaaaaaaaaaaaaa or $bbbbbbbbbbbbbbbbbbbbbbbbbb or $cccccccccccccccccccccccc) and $d; }`;
|
|
778
|
+
// The paren group sits inside the wrapped chain, so its contents take
|
|
779
|
+
// the chain's continuation indent plus their own, as in prettier.
|
|
780
|
+
expect(roundTrips(input)).toBe(
|
|
781
|
+
`a {
|
|
782
|
+
x: (
|
|
783
|
+
$aaaaaaaaaaaaaaaaaaaaaaaa or $bbbbbbbbbbbbbbbbbbbbbbbbbb or
|
|
784
|
+
$cccccccccccccccccccccccc
|
|
785
|
+
) and
|
|
786
|
+
$d;
|
|
787
|
+
}
|
|
788
|
+
`
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("wraps space lists like text", () => {
|
|
793
|
+
const input = `a { grid-template-areas: aaaaaaaaaaaaaaa bbbbbbbbbbbbbbb ccccccccccccccc ddddddddddddddd eeeeeeeeeee; }`;
|
|
794
|
+
expect(roundTrips(input)).toBe(
|
|
795
|
+
`a {
|
|
796
|
+
grid-template-areas: aaaaaaaaaaaaaaa bbbbbbbbbbbbbbb ccccccccccccccc
|
|
797
|
+
ddddddddddddddd eeeeeeeeeee;
|
|
798
|
+
}
|
|
799
|
+
`
|
|
800
|
+
);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("breaks a comma list of multi-word values one per line, like prettier's transition", () => {
|
|
804
|
+
expect(
|
|
805
|
+
roundTrips(
|
|
806
|
+
`a { transition: opacity 0.3s ease, transform 0.3s ease; b: 1 + 2, 3; }`
|
|
807
|
+
)
|
|
808
|
+
).toBe(
|
|
809
|
+
`a {
|
|
810
|
+
transition:
|
|
811
|
+
opacity 0.3s ease,
|
|
812
|
+
transform 0.3s ease;
|
|
813
|
+
b:
|
|
814
|
+
1 + 2,
|
|
815
|
+
3;
|
|
816
|
+
}
|
|
817
|
+
`
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("wraps a comma list of single words like text, and exempts custom properties", () => {
|
|
822
|
+
const input = `a { font-family: aaaaaaaaaaaaa, bbbbbbbbbbbbb, ccccccccccccc, ddddddddddddd, eeeeeeeeeeeee; --x: opacity 0.3s ease, transform 0.3s ease; }`;
|
|
823
|
+
expect(roundTrips(input)).toBe(
|
|
824
|
+
`a {
|
|
825
|
+
font-family: aaaaaaaaaaaaa, bbbbbbbbbbbbb, ccccccccccccc, ddddddddddddd,
|
|
826
|
+
eeeeeeeeeeeee;
|
|
827
|
+
--x: opacity 0.3s ease, transform 0.3s ease;
|
|
828
|
+
}
|
|
829
|
+
`
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("never breaks strings, selectors or interpolations", () => {
|
|
834
|
+
const input = `a[data-x="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] { content: "You have booked a #{$booking["data-trip"]} flight #{if($booking["data-trip"] == "return": "from #{$booking["data-outbound"]}"; else: "on #{$booking["data-outbound"]}")}."; }`;
|
|
835
|
+
const once = roundTrips(input);
|
|
836
|
+
expect(once.split("\n")).toHaveLength(4);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it("breaks @on options and long heads of block at-rules", () => {
|
|
840
|
+
const input = `a { @on click (target: "li[data-id]", once, debounce: 100 * 3, key: "Shift+K", host: window) { x: 1; } @delay $aaaaaaaaaaaaaaaaaaaaa * $bbbbbbbbbbbbbbbbbbbbbbb + $cccccccccccccccccccccccccc { y: 2; } }`;
|
|
841
|
+
expect(roundTrips(input)).toBe(
|
|
842
|
+
`a {
|
|
843
|
+
@on click (
|
|
844
|
+
target: "li[data-id]",
|
|
845
|
+
once,
|
|
846
|
+
debounce: 100 * 3,
|
|
847
|
+
key: "Shift+K",
|
|
848
|
+
host: window
|
|
849
|
+
) {
|
|
850
|
+
x: 1;
|
|
851
|
+
}
|
|
852
|
+
@delay $aaaaaaaaaaaaaaaaaaaaa * $bbbbbbbbbbbbbbbbbbbbbbb +
|
|
853
|
+
$cccccccccccccccccccccccccc {
|
|
854
|
+
y: 2;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
`
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("measures tab indentation as four columns", () => {
|
|
862
|
+
const input = `a { b { c { x: fn(aaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc, dddddddd); } } }`;
|
|
863
|
+
expect(fmt(input, { indent: "\t" })).toBe(
|
|
864
|
+
`a {
|
|
865
|
+
\tb {
|
|
866
|
+
\t\tc {
|
|
867
|
+
\t\t\tx: fn(
|
|
868
|
+
\t\t\t\taaaaaaaaaaaaaaaa,
|
|
869
|
+
\t\t\t\tbbbbbbbbbbbbbbbb,
|
|
870
|
+
\t\t\t\tcccccccccccccccc,
|
|
871
|
+
\t\t\t\tdddddddd
|
|
872
|
+
\t\t\t);
|
|
873
|
+
\t\t}
|
|
874
|
+
\t}
|
|
875
|
+
}
|
|
876
|
+
`
|
|
877
|
+
);
|
|
878
|
+
// The same call fits within 80 columns at two-space indentation.
|
|
879
|
+
expect(fmt(input)).toContain(
|
|
880
|
+
"x: fn(aaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbbb, cccccccccccccccc, dddddddd);"
|
|
881
|
+
);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("keeps trailing comments after a wrapped declaration", () => {
|
|
885
|
+
const input = `a { m: (trip: event.target.form.elements["data-trip"].value, outbound: event.target.form.elements["data-outbound"].value); /* note */ }`;
|
|
886
|
+
expect(fmt(input)).toBe(
|
|
887
|
+
`a {
|
|
888
|
+
m: (
|
|
889
|
+
trip: event.target.form.elements["data-trip"].value,
|
|
890
|
+
outbound: event.target.form.elements["data-outbound"].value
|
|
891
|
+
); /* note */
|
|
892
|
+
}
|
|
893
|
+
`
|
|
894
|
+
);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("formats the flight-booker sheet's long lines", () => {
|
|
898
|
+
const input = `:scope {
|
|
899
|
+
data-is-bookable: if($outbound and ($trip == "one-way" or $inbound and $inbound >= $outbound): ""; else: none);
|
|
900
|
+
}`;
|
|
901
|
+
expect(roundTrips(input)).toBe(
|
|
902
|
+
`:scope {
|
|
903
|
+
data-is-bookable: if(
|
|
904
|
+
$outbound and ($trip == "one-way" or $inbound and $inbound >= $outbound):
|
|
905
|
+
"";
|
|
906
|
+
else: none
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
`
|
|
910
|
+
);
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
describe("format: expression forms and paren re-insertion", () => {
|
|
915
|
+
it("prints every literal and accessor form", () => {
|
|
916
|
+
expect(
|
|
917
|
+
roundTrips(
|
|
918
|
+
'a{x:#fff;y:true;z:null;w:fn(&);v:#{$a};o:math.$pi;p:(1,2);g:-(1 2);f:$list[$i+1];e:prop("p").body.items[0].name;d:if($a>1:"big";$a>0:"small";else:"none");c:fn($x:1,$rest...);b:"a #{$b} c";a:url("q.png")}'
|
|
919
|
+
)
|
|
920
|
+
).toBe(
|
|
921
|
+
`a {
|
|
922
|
+
x: #fff;
|
|
923
|
+
y: true;
|
|
924
|
+
z: null;
|
|
925
|
+
w: fn(&);
|
|
926
|
+
v: #{$a};
|
|
927
|
+
o: math.$pi;
|
|
928
|
+
p: (1, 2);
|
|
929
|
+
g: -(1 2);
|
|
930
|
+
f: $list[$i + 1];
|
|
931
|
+
e: prop("p").body.items[0].name;
|
|
932
|
+
d: if($a > 1: "big"; $a > 0: "small"; else: "none");
|
|
933
|
+
c: fn($x: 1, $rest...);
|
|
934
|
+
b: "a #{$b} c";
|
|
935
|
+
a: url("q.png");
|
|
936
|
+
}
|
|
937
|
+
`
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("re-inserts parens at every binding power", () => {
|
|
942
|
+
expect(
|
|
943
|
+
roundTrips(
|
|
944
|
+
"a{a:($a and $b) or $c;b:($a or $b) and $c;c:not ($a and $b);d:not $a and $b;e:($a and $b)==$c;f:(not $a)==$b;g:($a==$b)<$c;h:$a==($b!=$c);i:($a<$b)+1;j:$a<($b>$c);k:($a+$b)*$c;l:1-(2-3);m:(1-2)-3;n:1/(2*3);o:(1*2)%3;p:-(1*2);q:-($a+1);r:($a+$b).x;s:(-$a).b;t:-$a.b;u:not $a;v:-$x;w:+$y;x:$a%2}"
|
|
945
|
+
)
|
|
946
|
+
).toBe(
|
|
947
|
+
`a {
|
|
948
|
+
a: $a and $b or $c;
|
|
949
|
+
b: ($a or $b) and $c;
|
|
950
|
+
c: not ($a and $b);
|
|
951
|
+
d: not $a and $b;
|
|
952
|
+
e: ($a and $b) == $c;
|
|
953
|
+
f: (not $a) == $b;
|
|
954
|
+
g: ($a == $b) < $c;
|
|
955
|
+
h: $a == ($b != $c);
|
|
956
|
+
i: ($a < $b) + 1;
|
|
957
|
+
j: $a < ($b > $c);
|
|
958
|
+
k: ($a + $b) * $c;
|
|
959
|
+
l: 1 - (2 - 3);
|
|
960
|
+
m: 1 - 2 - 3;
|
|
961
|
+
n: 1 / (2 * 3);
|
|
962
|
+
o: 1 * 2 % 3;
|
|
963
|
+
p: -(1 * 2);
|
|
964
|
+
q: -($a + 1);
|
|
965
|
+
r: ($a + $b).x;
|
|
966
|
+
s: (-$a).b;
|
|
967
|
+
t: -$a.b;
|
|
968
|
+
u: not $a;
|
|
969
|
+
v: -$x;
|
|
970
|
+
w: +$y;
|
|
971
|
+
x: $a % 2;
|
|
972
|
+
}
|
|
973
|
+
`
|
|
974
|
+
);
|
|
975
|
+
});
|
|
976
|
+
});
|