@excom/quark-parser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.rush/temp/chunked-rush-logs/quark-parser.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark-parser.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +5 -0
  11. package/index.ts +12 -0
  12. package/package.json +39 -0
  13. package/rush-logs/quark-parser.apply-exports.cache.log +1 -0
  14. package/rush-logs/quark-parser.apply-exports.log +1 -0
  15. package/rush-logs/quark-parser.build_package-metas.cache.log +1 -0
  16. package/rush-logs/quark-parser.build_package-metas.log +1 -0
  17. package/src/error.ts +24 -0
  18. package/src/parser.ts +1482 -0
  19. package/src/tables.ts +77 -0
  20. package/src/tokenizer.ts +443 -0
  21. package/src/types.ts +497 -0
  22. package/support/docs/README.md +443 -0
  23. package/support/package-meta.json +33 -0
  24. package/support/tests/grammar-docs.test.ts +109 -0
  25. package/support/tests/parser-at-rules.test.ts +430 -0
  26. package/support/tests/parser-declarations.test.ts +152 -0
  27. package/support/tests/parser-edge-cases.test.ts +296 -0
  28. package/support/tests/parser-expressions.test.ts +413 -0
  29. package/support/tests/parser-real-world.test.ts +429 -0
  30. package/support/tests/parser-selectors.test.ts +169 -0
  31. package/support/tests/tokenizer.test.ts +268 -0
  32. package/tsconfig.json +5 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Error branches and rarely-hit grammar shapes: every `fail()` site in the
3
+ * parser, tolerated trailing commas / missing terminators, rare selector
4
+ * parts, string / url interpolation splitting, and at-rule tails.
5
+ */
6
+ import {
7
+ parse,
8
+ parseExpression,
9
+ parseSelectorList,
10
+ QuarkParseError,
11
+ } from "../../index";
12
+ import {
13
+ describe,
14
+ expect,
15
+ it,
16
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
17
+
18
+ const first = (src: string): any => parse(src).body[0];
19
+ const expr = (src: string): any => parseExpression(src);
20
+ /** Parts of the first selector of the first rule. */
21
+ const sel = (selector: string): any[] =>
22
+ (parse(`${selector} { x: y; }`).body[0] as any).selector.selectors[0].parts;
23
+ /** Asserts a `QuarkParseError` matching `pattern`, at `position` when given. */
24
+ const fails = (
25
+ fn: () => unknown,
26
+ pattern: RegExp,
27
+ position?: number
28
+ ): void => {
29
+ expect(fn).toThrow(QuarkParseError);
30
+ expect(fn).toThrow(pattern);
31
+ if (position === undefined) return;
32
+ try {
33
+ fn();
34
+ } catch (error) {
35
+ expect((error as QuarkParseError).position).toBe(position);
36
+ }
37
+ };
38
+
39
+ describe("parser edge cases: statements", () => {
40
+ it("rejects a bare word with neither a colon nor a block", () => {
41
+ fails(() => parse("a"), /Expected declaration or rule/);
42
+ });
43
+
44
+ it("rejects a property name split by whitespace", () => {
45
+ fails(() => parse("a b: c;"), /Expected ":" but found "b"/);
46
+ });
47
+
48
+ it("rejects a property name that is not an identifier", () => {
49
+ fails(() => parse("1: 2;"), /Expected property name/);
50
+ });
51
+
52
+ it("rejects a value followed by an at-keyword instead of a terminator", () => {
53
+ fails(() => parse("a { b: 1 @x; }"), /Expected ";" but found "x"/);
54
+ });
55
+
56
+ it("rejects a brace nested inside parens in a statement head", () => {
57
+ // the lookahead tracks the `{` at paren depth; the value parser then
58
+ // rejects it as a token
59
+ fails(() => parse("a:x({) { y: z; }"), /Unexpected token "\{"/);
60
+ });
61
+
62
+ it("reports the end of the source when input runs out", () => {
63
+ try {
64
+ parseExpression("");
65
+ throw new Error("should have thrown");
66
+ } catch (e: any) {
67
+ expect(e).toBeInstanceOf(QuarkParseError);
68
+ expect(e.message).toMatch(/Unexpected end of input/);
69
+ expect(e.position).toBe(0);
70
+ }
71
+ });
72
+ });
73
+
74
+ describe("parser edge cases: selectors", () => {
75
+ it("rejects an empty selector list", () => {
76
+ fails(() => parseSelectorList(""), /Expected selector/);
77
+ });
78
+
79
+ it("rejects tokens that cannot start a compound selector", () => {
80
+ fails(() => parse('"x" { a: b; }'), /Unexpected token "x" in selector/);
81
+ fails(() => parse("a ! { x: y; }"), /Unexpected token "!" in selector/);
82
+ });
83
+
84
+ it("rejects a class selector without a name", () => {
85
+ fails(() => parse(". { x: y; }"), /Expected identifier but found "\{"/);
86
+ });
87
+
88
+ it("rejects pseudo selectors without a name", () => {
89
+ fails(() => parseSelectorList("a:"), /Expected identifier \(/);
90
+ fails(() => parse("a::{ x: y; }"), /Expected identifier but found "\{"/);
91
+ });
92
+
93
+ it("rejects an unclosed raw pseudo argument", () => {
94
+ fails(() => parseSelectorList("a:nth-child(2n"), /Unexpected end of input/);
95
+ });
96
+
97
+ it("rejects missing or malformed attribute values", () => {
98
+ fails(() => parseSelectorList("[a="), /Expected attribute value/);
99
+ fails(() => parseSelectorList("[a=(]"), /Unexpected attribute value "\("/);
100
+ });
101
+
102
+ it("parses number attribute values", () => {
103
+ expect(sel("[data-n=5]")[0].value).toMatchObject({
104
+ type: "number",
105
+ value: 5,
106
+ unit: null,
107
+ });
108
+ expect(sel("[data-n=5px]")[0].value).toMatchObject({ unit: "px" });
109
+ });
110
+
111
+ it("parses the `s` attribute modifier", () => {
112
+ expect(sel('[a="v" s]')[0].modifier).toBe("s");
113
+ });
114
+
115
+ it("parses pseudo-elements with arguments", () => {
116
+ expect(sel("a::part(label)")[1]).toMatchObject({
117
+ type: "pseudo_element_selector",
118
+ name: "part",
119
+ argument: { type: "raw", value: "label" },
120
+ });
121
+ });
122
+
123
+ it("keeps nested parens and interpolation inside raw pseudo arguments", () => {
124
+ expect(sel("li:nth-child((2n + 1))")[1].argument.value).toBe("(2n + 1)");
125
+ expect(sel("li:nth-child(#{$n})")[1].argument.value).toBe("#{$n}");
126
+ });
127
+ });
128
+
129
+ describe("parser edge cases: expressions", () => {
130
+ it("rejects unclosed parens and trailing input", () => {
131
+ fails(() => parseExpression("(1"), /Expected "\)" \(/);
132
+ fails(() => parseExpression("1 ; 2"), /Unexpected trailing input/);
133
+ });
134
+
135
+ it("tolerates a trailing comma in a top-level list", () => {
136
+ const e = expr("a, b,");
137
+ expect(e).toMatchObject({ type: "list", separator: "," });
138
+ expect(e.items).toHaveLength(2);
139
+ });
140
+
141
+ it("tolerates trailing commas in maps and paren lists", () => {
142
+ expect(expr("(a: 1, )").entries).toHaveLength(1);
143
+ expect(expr("(1, 2, )")).toMatchObject({ type: "list", parens: true });
144
+ expect(expr("(1, 2, )").items).toHaveLength(2);
145
+ });
146
+
147
+ it("rejects a dot without a property name", () => {
148
+ fails(() => parseExpression("$a."), /Expected property name after "\."/);
149
+ fails(() => parseExpression('$a."x"'), /Expected property name after "\."/);
150
+ });
151
+
152
+ it("calls an interpolated callee", () => {
153
+ expect(expr("#{$fn}(1)")).toMatchObject({
154
+ type: "function",
155
+ callee: { type: "interpolation" },
156
+ });
157
+ });
158
+
159
+ it("parses non-hex hashes as identifiers", () => {
160
+ expect(expr("#refresh-btn")).toMatchObject({
161
+ type: "identifier",
162
+ name: "#refresh-btn",
163
+ });
164
+ });
165
+
166
+ it("rejects unclosed calls and if() arms", () => {
167
+ fails(() => parseExpression("f(1,"), /Unclosed arguments/);
168
+ fails(() => parseExpression("if($a, 1"), /Expected "\)"/);
169
+ fails(() => parseExpression("if($a: 1;"), /Unclosed if\(\)/);
170
+ });
171
+
172
+ it("splits string interpolation at the edges", () => {
173
+ expect(expr('"#{$a}b"').parts).toEqual([
174
+ expect.objectContaining({ type: "interpolation" }),
175
+ "b",
176
+ ]);
177
+ expect(expr('"a#{$b}"').parts).toEqual([
178
+ "a",
179
+ expect.objectContaining({ type: "interpolation" }),
180
+ ]);
181
+ });
182
+
183
+ it("skips escapes and nested strings while splitting interpolation", () => {
184
+ expect(expr("'it\\'s #{$x}'").parts[0]).toBe("it\\'s ");
185
+ const nested = expr('"a #{ "b" } c"');
186
+ expect(nested.parts[1].expression).toMatchObject({
187
+ type: "string",
188
+ value: "b",
189
+ });
190
+ expect(nested.parts[2]).toBe(" c");
191
+ expect(expr('"#{ "b\\"c" }"').parts[0].expression.value).toBe('b\\"c');
192
+ });
193
+
194
+ it("records interpolation spans inside strings", () => {
195
+ const src = '"a #{$b} c"';
196
+ const e = expr(src);
197
+ expect(src.slice(e.parts[1].start, e.parts[1].end)).toBe("#{$b}");
198
+ });
199
+
200
+ it("rejects malformed interpolation contents", () => {
201
+ fails(
202
+ () => parseExpression('"#{1 ; 2}"'),
203
+ /Unexpected trailing input in interpolation/,
204
+ );
205
+ fails(() => parseExpression('"#{ {} }"'), /Unexpected token "\{"/);
206
+ });
207
+
208
+ it("splits interpolation inside unquoted urls", () => {
209
+ expect(expr("url(/img/#{$name}.svg)").parts).toEqual([
210
+ "/img/",
211
+ expect.objectContaining({ type: "interpolation" }),
212
+ ".svg",
213
+ ]);
214
+ });
215
+ });
216
+
217
+ describe("parser edge cases: at-rules", () => {
218
+ it("rejects @use without a string url", () => {
219
+ fails(() => parse("@use foo;"), /Expected string but found "foo"/);
220
+ fails(() => parse("@use"), /Expected string \(/);
221
+ });
222
+
223
+ it("accepts statement at-rules without a terminator at the end of input", () => {
224
+ expect(first('@use "x"').url).toBe("x");
225
+ expect(first('@warn "x"').value.value).toBe("x");
226
+ });
227
+
228
+ it("accepts statement at-rules closed by a brace", () => {
229
+ expect(first("a { @warn 1 }").block.body[0]).toMatchObject({
230
+ name: "warn",
231
+ value: { type: "number", value: 1 },
232
+ });
233
+ });
234
+ });
235
+
236
+ /*
237
+ * Quark is a derivative of CSS: what the engine never ran does not parse.
238
+ * One case per rejection message, with the reported position.
239
+ */
240
+ describe("parser edge cases: rejected constructs", () => {
241
+ it("rejects at-rules that are not Quark's own", () => {
242
+ fails(
243
+ () => parse("@media print { a { b: c; } }"),
244
+ /@media is not a Quark at-rule/,
245
+ 0
246
+ );
247
+ });
248
+
249
+ it("rejects placeholder selectors", () => {
250
+ fails(
251
+ () => parse("a %error { x: y; }"),
252
+ /Placeholder selectors are not supported/,
253
+ 2
254
+ );
255
+ });
256
+
257
+ it("rejects interpolation outside strings", () => {
258
+ const selector = ".btn-#{$variant} { x: y; }";
259
+ fails(
260
+ () => parse(selector),
261
+ /Interpolation is only supported inside strings/,
262
+ selector.indexOf("#{")
263
+ );
264
+ const attribute = "[data-x=#{$v}] { x: y; }";
265
+ fails(
266
+ () => parse(attribute),
267
+ /Interpolation is only supported inside strings/,
268
+ attribute.indexOf("#{")
269
+ );
270
+ // inside a string it is the normal way to build text
271
+ expect(() => parse('a { x: "btn-#{$variant}"; }')).not.toThrow();
272
+ });
273
+
274
+ it("rejects ! flags", () => {
275
+ const src = "a { color: red !important; }";
276
+ fails(() => parse(src), /!important is not supported/, src.indexOf("!"));
277
+ });
278
+
279
+ it("rejects nested property blocks", () => {
280
+ const src = "a { font: { size: 1rem; } }";
281
+ fails(
282
+ () => parse(src),
283
+ /Nested property blocks are not supported/,
284
+ src.indexOf("font")
285
+ );
286
+ });
287
+
288
+ it("rejects a @use with clause", () => {
289
+ const src = '@use "x" with ($a: 1);';
290
+ fails(
291
+ () => parse(src),
292
+ /@use does not take a with clause/,
293
+ src.indexOf("with")
294
+ );
295
+ });
296
+ });