@excom/quark 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 (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.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 +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. package/tsconfig.json +5 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * `@use "quark:<name>"`: the built-in modules — resolved without a fetch,
3
+ * namespaced like JS modules, documented in `language.ts` `BUILTIN_MODULES`
4
+ * (the docs and the runtime export lists are kept in step here).
5
+ */
6
+ import { AT_RULES, BUILTIN_MODULES } from "../../src/language";
7
+ import { QUARK_MODULES } from "../../src/builtin-modules";
8
+ import { QuarkLogger } from "../../src/utils";
9
+ import {
10
+ afterEach,
11
+ describe,
12
+ expect,
13
+ it,
14
+ vi,
15
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
16
+ import { flush, mount, unregisterAll } from "./helpers";
17
+
18
+ const { math, list, map, string, date, url, util } = QUARK_MODULES;
19
+
20
+ describe("built-in modules", () => {
21
+ afterEach(() => {
22
+ unregisterAll();
23
+ document.body.innerHTML = "";
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ describe("documentation", () => {
28
+ it("documents exactly the modules and exports the runtime resolves", () => {
29
+ expect(BUILTIN_MODULES.map((m) => m.name).sort()).toEqual(
30
+ Object.keys(QUARK_MODULES).sort()
31
+ );
32
+ for (const doc of BUILTIN_MODULES) {
33
+ expect(
34
+ doc.functions.map((f) => f.name).sort(),
35
+ `quark:${doc.name}`
36
+ ).toEqual(Object.keys(QUARK_MODULES[doc.name]).sort());
37
+ for (const fn of doc.functions) {
38
+ expect(fn.signature.length, fn.name).toBeGreaterThan(0);
39
+ expect(fn.description.length, fn.name).toBeGreaterThan(0);
40
+ }
41
+ }
42
+ });
43
+
44
+ it("documents @delay and @warn / @debug / @error as executed at-rules", () => {
45
+ const syntaxes = AT_RULES.map((a) => a.syntax);
46
+ expect(syntaxes.some((s) => s.startsWith("@delay "))).toBe(true);
47
+ expect(syntaxes.some((s) => s.startsWith("@warn "))).toBe(true);
48
+ });
49
+ });
50
+
51
+ describe("in sheets", () => {
52
+ it("imports a module by namespace, by derived name, and bare with `as *`", async () => {
53
+ const { root } = mount(
54
+ `<p id="o"></p>`,
55
+ `@use "quark:math" as m;
56
+ @use "quark:list";
57
+ @use "quark:string" as *;
58
+ #o {
59
+ $rows: ((name: "b", n: 2), (name: "a", n: 1));
60
+ data-clamp: m.clamp(0, 5, 1);
61
+ data-pi: m.round(m.$pi, 2);
62
+ data-first: list.first(list.sort-by($rows, "name")).name;
63
+ data-count: list.count($rows, "n", 2);
64
+ data-plural: plural(list.sum($rows, "n"), (one: "# item", other: "# items"));
65
+ }`
66
+ );
67
+ await flush();
68
+ const out = root.querySelector("#o")!;
69
+ expect(out.getAttribute("data-clamp")).toBe("1");
70
+ expect(out.getAttribute("data-pi")).toBe("3.14");
71
+ expect(out.getAttribute("data-first")).toBe("a");
72
+ expect(out.getAttribute("data-count")).toBe("1");
73
+ expect(out.getAttribute("data-plural")).toBe("3 items");
74
+ });
75
+
76
+ it("logs and skips an unknown quark: module, naming the available ones; the sheet still runs", async () => {
77
+ const error = vi.spyOn(QuarkLogger, "error").mockImplementation(() => {});
78
+ const { root } = mount(
79
+ `<p id="o"></p>`,
80
+ `@use "quark:nope" as nope; #o { data-ok: "yes"; }`
81
+ );
82
+ await flush();
83
+ expect(root.querySelector("#o")!.getAttribute("data-ok")).toBe("yes");
84
+ const failed = error.mock.calls.find(([arg]) =>
85
+ String((arg as { message?: string })?.message).includes(
86
+ 'Failed to load @use module "quark:nope"'
87
+ )
88
+ );
89
+ expect(failed).toBeDefined();
90
+ expect(String((failed![0] as { error?: unknown[] }).error?.[0])).toMatch(
91
+ /available: "quark:math"/
92
+ );
93
+ });
94
+
95
+ it("never fetches a quark: module through the module loader", async () => {
96
+ const loader = vi.fn(async () => ({}));
97
+ const previous = (await import("../../index")).Quark.moduleLoader;
98
+ (await import("../../index")).Quark.moduleLoader = loader;
99
+ try {
100
+ mount(
101
+ `<p id="o"></p>`,
102
+ `@use "quark:util" as u; #o { data-t: u.type-of(none); }`
103
+ );
104
+ await flush();
105
+ expect(loader).not.toHaveBeenCalled();
106
+ expect(document.querySelector("#o")!.getAttribute("data-t")).toBe(
107
+ "null"
108
+ );
109
+ } finally {
110
+ (await import("../../index")).Quark.moduleLoader = previous;
111
+ }
112
+ });
113
+ });
114
+
115
+ describe("quark:math", () => {
116
+ it("handles the CSS argument order, rounding, wrapping modulo and coercion", () => {
117
+ expect(math.clamp(0, 5, 1)).toBe(1);
118
+ expect(math.clamp(0, -1, 1)).toBe(0);
119
+ expect(math.clamp("0", "0.5", "1")).toBe(0.5);
120
+ expect(math.min(3, 1, 2)).toBe(1);
121
+ expect(math.max([3, 1], 2)).toBe(3);
122
+ expect(math.round(3.14159, 2)).toBe(3.14);
123
+ expect(math.round(2.5)).toBe(3);
124
+ expect(math.floor(1.9)).toBe(1);
125
+ expect(math.ceil(1.1)).toBe(2);
126
+ expect(math.abs(-4)).toBe(4);
127
+ expect(math.mod(-1, 3)).toBe(2);
128
+ expect(math.mod(7, 3)).toBe(1);
129
+ expect(math.mod(1, 0)).toBeNaN();
130
+ expect(math.pow(2, 10)).toBe(1024);
131
+ expect(math.sqrt(16)).toBe(4);
132
+ expect(math.percentage(0.25)).toBe("25%");
133
+ expect(math.abs("nope")).toBe(0);
134
+ expect(math.$pi).toBe(Math.PI);
135
+ expect(math.$e).toBe(Math.E);
136
+ });
137
+ });
138
+
139
+ describe("quark:list", () => {
140
+ const rows = [
141
+ { id: 1, name: "Bea", done: true, tags: ["x"] },
142
+ { id: 2, name: "al", done: false, tags: [] },
143
+ { id: 3, name: "Cy", done: false, tags: null },
144
+ { id: 10, name: "al", done: true },
145
+ ];
146
+
147
+ it("counts, finds, filters and rejects by path and value", () => {
148
+ expect(list.count(rows)).toBe(4);
149
+ expect(list.count(rows, "done", true)).toBe(2);
150
+ expect(list.count(rows, "tags")).toBe(1);
151
+ expect(list.count(null)).toBe(0);
152
+ expect(list.count({ a: 1, b: 2 })).toBe(2);
153
+ expect(list.find(rows, "id", "2")).toBe(rows[1]);
154
+ expect(list.find(rows, "id", 99)).toBeUndefined();
155
+ expect(list.find(undefined, "id", 1)).toBeUndefined();
156
+ expect(list.find([{ a: { b: 1 } }, { a: { b: 2 } }], "a.b", 2)).toEqual({
157
+ a: { b: 2 },
158
+ });
159
+ expect(list.reverse(undefined)).toEqual([]);
160
+ expect(list.filter(rows, "done", false).map((r: any) => r.id)).toEqual([
161
+ 2, 3,
162
+ ]);
163
+ expect(list.filter(rows, "tags").map((r: any) => r.id)).toEqual([1]);
164
+ expect(list.reject(rows, "done", false).map((r: any) => r.id)).toEqual([
165
+ 1, 10,
166
+ ]);
167
+ expect(list.reject(rows, "tags").map((r: any) => r.id)).toEqual([
168
+ 2, 3, 10,
169
+ ]);
170
+ expect(list.filter(undefined, "x", 1)).toEqual([]);
171
+ });
172
+
173
+ it("plucks, sorts, sums, groups and dedupes", () => {
174
+ expect(list.pluck(rows, "name")).toEqual(["Bea", "al", "Cy", "al"]);
175
+ expect(list["sort-by"](rows, "name").map((r: any) => r.id)).toEqual([
176
+ 2, 10, 1, 3,
177
+ ]);
178
+ expect(list["sort-by"](rows, "id", "desc").map((r: any) => r.id)).toEqual(
179
+ [10, 3, 2, 1]
180
+ );
181
+ expect(list["sort-by"]([3, "10", 2])).toEqual([2, 3, "10"]);
182
+ expect(
183
+ list["sort-by"]([{ n: 2 }, { n: null }, { n: 1 }], "n").map(
184
+ (r: any) => r.n
185
+ )
186
+ ).toEqual([1, 2, null]);
187
+ expect(rows.map((r) => r.id)).toEqual([1, 2, 3, 10]);
188
+ expect(list.sum([1, "2", "x"])).toBe(3);
189
+ expect(list.sum(rows, "id")).toBe(16);
190
+ expect(list.sum(null)).toBe(0);
191
+ expect(list["group-by"](rows, "done")).toEqual({
192
+ true: [rows[0], rows[3]],
193
+ false: [rows[1], rows[2]],
194
+ });
195
+ expect(list.unique([1, "1", 1, 2])).toEqual([1, "1", 2]);
196
+ expect(list.unique(rows, "name").map((r: any) => r.id)).toEqual([
197
+ 1, 2, 3,
198
+ ]);
199
+ });
200
+
201
+ it("builds ranges and reads ends", () => {
202
+ expect(list.range(3)).toEqual([0, 1, 2]);
203
+ expect(list.range(1, 4)).toEqual([1, 2, 3]);
204
+ expect(list.range(0, 10, 5)).toEqual([0, 5]);
205
+ expect(list.range(3, 0)).toEqual([3, 2, 1]);
206
+ expect(list.range(0)).toEqual([]);
207
+ expect(list.first(rows)).toBe(rows[0]);
208
+ expect(list.last(rows)).toBe(rows[3]);
209
+ expect(list.first([])).toBeUndefined();
210
+ expect(list.reverse([1, 2])).toEqual([2, 1]);
211
+ expect(
212
+ list.compact([0, null, "", undefined, [], {}, "a", false])
213
+ ).toEqual([0, "a", false]);
214
+ });
215
+ });
216
+
217
+ describe("quark:map", () => {
218
+ const source = { a: 1, b: { c: 2 }, d: null };
219
+ it("reads paths, lists entries and builds copies", () => {
220
+ expect(map.get(source, "b.c")).toBe(2);
221
+ expect(map.get(source, "b.x", "fallback")).toBe("fallback");
222
+ expect(map.get(source, "d", "fallback")).toBeNull();
223
+ expect(map["has-key"](source, "b.c")).toBe(true);
224
+ expect(map["has-key"](source, "z")).toBe(false);
225
+ expect(map.keys(source)).toEqual(["a", "b", "d"]);
226
+ expect(map.values(source)).toEqual([1, { c: 2 }, null]);
227
+ expect(map.entries({ x: 1 })).toEqual([{ key: "x", value: 1 }]);
228
+ expect(map.keys(null)).toEqual([]);
229
+ expect(map.merge({ a: 1 }, null, { a: 2, b: 3 })).toEqual({ a: 2, b: 3 });
230
+ expect(map.pick(source, "a", ["d"])).toEqual({ a: 1, d: null });
231
+ expect(map.omit(source, "b")).toEqual({ a: 1, d: null });
232
+ expect(source).toEqual({ a: 1, b: { c: 2 }, d: null });
233
+ });
234
+ });
235
+
236
+ describe("quark:string", () => {
237
+ it("pluralizes, escapes, truncates, capitalizes and slugifies", () => {
238
+ const forms = { one: "# item", other: "# items" };
239
+ expect(string.plural(1, forms)).toBe("1 item");
240
+ expect(string.plural("2", forms)).toBe("2 items");
241
+ expect(string.plural(0, forms)).toBe("0 items");
242
+ expect(string.plural(1, { other: "things" })).toBe("things");
243
+ expect(string.plural(1, null)).toBe("");
244
+ expect(string["escape-html"]('<a href="x">&</a>')).toBe(
245
+ "&lt;a href=&quot;x&quot;&gt;&amp;&lt;/a&gt;"
246
+ );
247
+ expect(string["escape-html"](null)).toBe("");
248
+ expect(string.truncate("hello world", 8)).toBe("hello w…");
249
+ expect(string.truncate("hello", 8)).toBe("hello");
250
+ expect(string.truncate("hello world", 8, "...")).toBe("hello...");
251
+ expect(string.capitalize("quark")).toBe("Quark");
252
+ expect(string.capitalize(null)).toBe("");
253
+ expect(string.slugify("Héllo, World! 2")).toBe("hello-world-2");
254
+ });
255
+ });
256
+
257
+ describe("quark:date", () => {
258
+ it("parses, validates, formats, adds and diffs", () => {
259
+ const parsed = date.parse("2026-09-13T00:00:00Z");
260
+ expect(parsed).toBeInstanceOf(Date);
261
+ expect(date.parse("nope")).toBeNull();
262
+ expect(date.parse(null)).toBeNull();
263
+ expect(date["is-valid"]("2026-09-13")).toBe(true);
264
+ expect(date["is-valid"]("")).toBe(false);
265
+ expect(
266
+ date.format("2026-09-13T12:00:00Z", "en-US", {
267
+ year: "numeric",
268
+ timeZone: "UTC",
269
+ })
270
+ ).toBe("2026");
271
+ expect(date.format("nope")).toBe("");
272
+ expect(date.add("2026-09-13T00:00:00Z", 3, "days")!.toISOString()).toBe(
273
+ "2026-09-16T00:00:00.000Z"
274
+ );
275
+ expect(date.add("2026-01-31T00:00:00Z", 1, "month")!.getUTCMonth()).toBe(
276
+ 2
277
+ );
278
+ expect(
279
+ date.add("2026-09-13T00:00:00Z", 2, "years")!.getUTCFullYear()
280
+ ).toBe(2028);
281
+ expect(date.add("nope", 1)).toBeNull();
282
+ expect(date.diff("2026-09-16T00:00:00Z", "2026-09-13T00:00:00Z")).toBe(3);
283
+ expect(
284
+ date.diff("2026-09-13T00:00:00Z", "2026-09-16T00:00:00Z", "hours")
285
+ ).toBe(-72);
286
+ expect(date.diff("2027-03-01", "2026-01-01", "months")).toBe(14);
287
+ expect(date.diff("2027-03-01", "2026-01-01", "years")).toBe(1);
288
+ expect(date.diff("x", "2026-01-01")).toBeNull();
289
+ });
290
+ });
291
+
292
+ describe("quark:url", () => {
293
+ it("builds and reads query strings", () => {
294
+ expect(
295
+ url.query({
296
+ q: "a b",
297
+ page: 2,
298
+ empty: "",
299
+ none: null,
300
+ tags: ["x", "y"],
301
+ })
302
+ ).toBe("q=a+b&page=2&tags=x&tags=y");
303
+ expect(url.query(null)).toBe("");
304
+ expect(url.params("/search?q=a&page=2&t=x&t=y")).toEqual({
305
+ q: "a",
306
+ page: "2",
307
+ t: ["x", "y"],
308
+ });
309
+ expect(url.params("?only=1")).toEqual({ only: "1" });
310
+ expect(url.params("plain=1")).toEqual({ plain: "1" });
311
+ expect(url.params(null)).toEqual({});
312
+ expect(url.encode("a b&c")).toBe("a%20b%26c");
313
+ });
314
+ });
315
+
316
+ describe("quark:util", () => {
317
+ it("coalesces, tests emptiness, names types and round-trips JSON", () => {
318
+ expect(util.coalesce(null, undefined, 0, "x")).toBe(0);
319
+ expect(util.coalesce(null)).toBeUndefined();
320
+ expect(util["is-empty"]("")).toBe(true);
321
+ expect(util["is-empty"]([])).toBe(true);
322
+ expect(util["is-empty"]({})).toBe(true);
323
+ expect(util["is-empty"](0)).toBe(false);
324
+ expect(util["type-of"](null)).toBe("null");
325
+ expect(util["type-of"]([])).toBe("list");
326
+ expect(util["type-of"]({})).toBe("map");
327
+ expect(util["type-of"](new Date())).toBe("date");
328
+ expect(util["type-of"](1)).toBe("number");
329
+ expect(util["to-json"]({ a: 1 })).toBe('{"a":1}');
330
+ expect(util["to-json"]({ a: 1 }, 2)).toBe('{\n "a": 1\n}');
331
+ expect(util["to-json"](undefined)).toBe("");
332
+ expect(util["from-json"]('{"a":1}')).toEqual({ a: 1 });
333
+ expect(util["from-json"]("nope")).toBeNull();
334
+ expect(util["from-json"](5)).toBeNull();
335
+ });
336
+ });
337
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Built-in expression functions and the lazy scope: edge inputs for each
3
+ * built-in, `iterate()` row reuse when a keyed row changes in place, and
4
+ * module-scope resolution without a rule / host.
5
+ */
6
+ import { createScope } from "../../src/variables";
7
+ import { Quark } from "../../index";
8
+ import {
9
+ afterEach,
10
+ describe,
11
+ expect,
12
+ fixture,
13
+ it,
14
+ vi,
15
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
16
+ import { flush, mount, unregisterAll } from "./helpers";
17
+
18
+ describe("built-ins", () => {
19
+ afterEach(() => {
20
+ unregisterAll();
21
+ document.body.innerHTML = "";
22
+ vi.restoreAllMocks();
23
+ });
24
+
25
+ describe("scope", () => {
26
+ it("resolves keywords and built-ins without a rule, and rejects unknown names", () => {
27
+ const element = document.createElement("div");
28
+ const scope = createScope({ element, options: {} });
29
+ expect(scope.lookup("none")).toBeNull();
30
+ expect(scope.lookup("$unbound")).toBeUndefined();
31
+ expect(() => scope.lookup("nope")).toThrow('"nope" is not defined');
32
+ // one instance per name per evaluation
33
+ expect(scope.lookup("ternary")).toBe(scope.lookup("ternary"));
34
+ // list helpers moved to `quark:list` (2026-09-13)
35
+ expect(() => scope.lookup("find")).toThrow('"find" is not defined');
36
+ expect(() => scope.lookup("reverse")).toThrow('"reverse" is not defined');
37
+ expect(scope.lookup("item")).toBeUndefined();
38
+ expect(scope.lookup("index")).toBeUndefined();
39
+ expect(scope.lookup("element")).toBe(element);
40
+ });
41
+
42
+ it("falls back to an empty module scope when the host is gone", () => {
43
+ const error = vi.spyOn(console, "error").mockImplementation(() => {});
44
+ const element = document.createElement("div");
45
+ const scope = createScope({
46
+ element,
47
+ options: {
48
+ rule: {
49
+ quarkInstance: { host: { deref: () => undefined }, hash: "h" },
50
+ },
51
+ } as any,
52
+ });
53
+ expect(() => scope.lookup("greet")).toThrow('"greet" is not defined');
54
+ expect(error).toHaveBeenCalled();
55
+ });
56
+
57
+ it("exposes the helper built-ins with lenient inputs", () => {
58
+ const scope = createScope({
59
+ element: document.createElement("div"),
60
+ options: {},
61
+ });
62
+ const ternary = scope.lookup("ternary") as Function;
63
+ expect(ternary(true, "a", "b")).toBe("a");
64
+ expect(ternary(false, "a", "b")).toBe("b");
65
+ expect(ternary(false, "a")).toBeNull();
66
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
67
+ expect((scope.lookup("log") as Function)("x", 1)).toEqual(["x", 1]);
68
+ expect(log).toHaveBeenCalledTimes(1);
69
+ expect((scope.lookup("debug") as Function)("y")).toEqual(["y"]);
70
+ const html = scope.lookup("dangerous-html") as Function;
71
+ expect(html("<b>x</b>")).toEqual({ type: "html", value: "<b>x</b>" });
72
+ expect(html({ not: "primitive" })).toBeUndefined();
73
+ });
74
+ });
75
+
76
+ describe("prop() / attr()", () => {
77
+ it('reads nothing for a non-string prop name and innerHTML for attr("content")', async () => {
78
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
79
+ const { root } = mount(
80
+ `<p bind-x><b>in</b></p>`,
81
+ `:scope { $n: 5; }
82
+ [bind-x] { data-p: prop($n) or "none"; data-html: attr("content"); }`
83
+ );
84
+ await flush();
85
+ const p = root.querySelector("[bind-x]")!;
86
+ expect(p.getAttribute("data-p")).toBe("none");
87
+ expect(p.getAttribute("data-html")).toBe("<b>in</b>");
88
+ warn.mockRestore();
89
+ });
90
+ });
91
+
92
+ describe("template() / iterate()", () => {
93
+ it("defaults the template reference to the element's own <template>", async () => {
94
+ const { root } = mount(
95
+ `<article><template><p bind-body>own</p></template></article>`,
96
+ `article { content: template(); }`
97
+ );
98
+ await flush();
99
+ expect(root.querySelector("article > p")?.textContent).toBe("own");
100
+ });
101
+
102
+ it("wipes rows for a missing collection and for a non-collection", async () => {
103
+ const { root } = mount(
104
+ `<ul bind-a><template><li></li></template><li>old</li></ul>
105
+ <ul bind-b><template><li></li></template><li>old</li></ul>`,
106
+ `[bind-a] { content: iterate($missing); }
107
+ [bind-b] { content: iterate("text"); }`
108
+ );
109
+ await flush();
110
+ expect(root.querySelectorAll("[bind-a] li")).toHaveLength(0);
111
+ expect(root.querySelectorAll("[bind-b] li")).toHaveLength(0);
112
+ expect(root.querySelectorAll("template")).toHaveLength(2);
113
+ });
114
+
115
+ it("re-runs only the keyed row whose data changed in place", async () => {
116
+ const root = fixture<HTMLElement>(`<section>
117
+ <div id="sheet"></div>
118
+ <data-provider is-success>
119
+ <ul><template><li bind-label></li></template></ul>
120
+ </data-provider>
121
+ </section>`);
122
+ const provider = root.querySelector("data-provider") as HTMLElement & {
123
+ provision?: { id: string; label: string }[];
124
+ };
125
+ provider.provision = [
126
+ { id: "1", label: "one" },
127
+ { id: "2", label: "two" },
128
+ ];
129
+ const quark = new Quark({
130
+ options: { isScoped: true },
131
+ src: `data-provider[is-success] {
132
+ $items: prop("provision");
133
+ ul { content: iterate($items, ":scope > template", "id"); }
134
+ [bind-label] { content: item.label; }
135
+ }`,
136
+ });
137
+ quark.register({
138
+ sheetElement: root.querySelector("#sheet") as HTMLElement,
139
+ });
140
+ await flush();
141
+ const rows = [...root.querySelectorAll("ul > li")];
142
+ expect(rows.map((r) => r.textContent)).toEqual(["one", "two"]);
143
+
144
+ provider.provision = [
145
+ { id: "1", label: "one" },
146
+ { id: "2", label: "two!" },
147
+ ];
148
+ await flush();
149
+ const after = [...root.querySelectorAll("ul > li")];
150
+ expect(after[0]).toBe(rows[0]);
151
+ expect(after[1]).toBe(rows[1]);
152
+ expect(after.map((r) => r.textContent)).toEqual(["one", "two!"]);
153
+ quark.unregister();
154
+ });
155
+
156
+ it("reuses unkeyed rows by content hash when the list grows", async () => {
157
+ const root = fixture<HTMLElement>(`<section>
158
+ <div id="sheet"></div>
159
+ <data-provider is-success>
160
+ <ul><template><li bind-label></li></template></ul>
161
+ </data-provider>
162
+ </section>`);
163
+ const provider = root.querySelector("data-provider") as HTMLElement & {
164
+ provision?: string[];
165
+ };
166
+ provider.provision = ["a", "b"];
167
+ const quark = new Quark({
168
+ options: { isScoped: true },
169
+ src: `data-provider[is-success] {
170
+ $items: prop("provision");
171
+ ul { content: iterate($items); }
172
+ [bind-label] { content: item; }
173
+ }`,
174
+ });
175
+ quark.register({
176
+ sheetElement: root.querySelector("#sheet") as HTMLElement,
177
+ });
178
+ await flush();
179
+ const rows = [...root.querySelectorAll("ul > li")];
180
+ provider.provision = ["b", "a", "c"];
181
+ await flush();
182
+ const after = [...root.querySelectorAll("ul > li")];
183
+ expect(after.map((r) => r.textContent)).toEqual(["b", "a", "c"]);
184
+ expect(after[0]).toBe(rows[1]);
185
+ expect(after[1]).toBe(rows[0]);
186
+ quark.unregister();
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,35 @@
1
+ import "@excom/quark-sheet";
2
+ import {
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
8
+ import {
9
+ expectComplexity,
10
+ flush,
11
+ measureComplexity,
12
+ mountView,
13
+ readDemo,
14
+ } from "./view-helpers";
15
+
16
+ describe("css-variables view", () => {
17
+ afterEach(() => {
18
+ document.body.innerHTML = "";
19
+ });
20
+
21
+ it("writes --border-color for open and closed details", async () => {
22
+ const { root, quark } = await mountView(
23
+ readDemo(import.meta.url, "css-variables"),
24
+ );
25
+ const details = root.querySelector("details")!;
26
+ expect(details.style.getPropertyValue("--border-color")).toBe("silver");
27
+ const meter = measureComplexity(quark!);
28
+ details.setAttribute("open", "");
29
+ await flush();
30
+ const budget = meter.take();
31
+ meter.stop();
32
+ expect(details.style.getPropertyValue("--border-color")).toBe("#3fa9f5");
33
+ expectComplexity(budget);
34
+ });
35
+ });