@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.
- package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark.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 +6 -0
- package/index.ts +4 -0
- package/language.ts +7 -0
- package/package.json +45 -0
- package/rush-logs/quark.apply-exports.cache.log +1 -0
- package/rush-logs/quark.apply-exports.log +1 -0
- package/rush-logs/quark.build_package-metas.cache.log +1 -0
- package/rush-logs/quark.build_package-metas.log +1 -0
- package/src/actions.ts +445 -0
- package/src/ast.ts +187 -0
- package/src/bindings.ts +115 -0
- package/src/builtin-modules.ts +401 -0
- package/src/constants.ts +47 -0
- package/src/cycle-check.ts +121 -0
- package/src/devtools-hook.ts +303 -0
- package/src/element-api.ts +109 -0
- package/src/evaluator.ts +317 -0
- package/src/form-controls.ts +69 -0
- package/src/language-tables.ts +137 -0
- package/src/language.ts +1225 -0
- package/src/observer.ts +110 -0
- package/src/paint.ts +305 -0
- package/src/properties.ts +818 -0
- package/src/props.ts +94 -0
- package/src/quark-element.ts +7 -0
- package/src/quark-internal.ts +365 -0
- package/src/quark.ts +817 -0
- package/src/resolvers.ts +867 -0
- package/src/rule.ts +800 -0
- package/src/scope-id.ts +47 -0
- package/src/selector-utils.ts +430 -0
- package/src/settle.ts +117 -0
- package/src/types.ts +155 -0
- package/src/utils.ts +79 -0
- package/src/variables.ts +359 -0
- package/support/demos/css-variables.html +18 -0
- package/support/demos/dispatch.html +20 -0
- package/support/demos/events.html +9 -0
- package/support/demos/iterate.html +13 -0
- package/support/demos/js-api.html +17 -0
- package/support/demos/provider-list.html +16 -0
- package/support/demos/toggle-content.html +14 -0
- package/support/demos/unset.html +12 -0
- package/support/demos/view-transition.html +36 -0
- package/support/docs/ATTRIBUTES.md +37 -0
- package/support/docs/AT_RULES.md +45 -0
- package/support/docs/BUILTINS.md +57 -0
- package/support/docs/CONTENT.md +40 -0
- package/support/docs/CSS_VARIABLES.md +16 -0
- package/support/docs/DECLARATIONS.md +26 -0
- package/support/docs/DELAY.md +37 -0
- package/support/docs/DIAGNOSTICS.md +32 -0
- package/support/docs/DISPATCH.md +81 -0
- package/support/docs/ELEMENT_PROPERTIES.md +34 -0
- package/support/docs/EXPRESSIONS.md +31 -0
- package/support/docs/INTERNAL.md +8 -0
- package/support/docs/JS_API.md +53 -0
- package/support/docs/JS_WRITES.md +36 -0
- package/support/docs/LIMITATIONS.md +26 -0
- package/support/docs/LOOP_GUARD.md +30 -0
- package/support/docs/METHODS.md +43 -0
- package/support/docs/MODULES.md +111 -0
- package/support/docs/NO_REVERSION.md +18 -0
- package/support/docs/ON.md +87 -0
- package/support/docs/REACTIVITY.md +24 -0
- package/support/docs/README.md +143 -0
- package/support/docs/SELECTORS.md +87 -0
- package/support/docs/SHEETS.md +67 -0
- package/support/docs/SYNTAX.md +36 -0
- package/support/docs/USE.md +24 -0
- package/support/docs/VALUES.md +27 -0
- package/support/docs/VARIABLES.md +47 -0
- package/support/docs/VIEW_TRANSITION.md +64 -0
- package/support/docs-sections.json +65 -0
- package/support/package-meta.json +208 -0
- package/support/scripts/build-language-docs.mjs +196 -0
- package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
- package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
- package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
- package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
- package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
- package/support/tests/actions.test.ts +437 -0
- package/support/tests/builtin-modules.test.ts +337 -0
- package/support/tests/builtins.test.ts +189 -0
- package/support/tests/css-variables.view.test.ts +35 -0
- package/support/tests/delay.test.ts +244 -0
- package/support/tests/devtools-hook.test.ts +496 -0
- package/support/tests/diagnostics.test.ts +224 -0
- package/support/tests/dispatch.view.test.ts +43 -0
- package/support/tests/element-api.test.ts +202 -0
- package/support/tests/evaluator.test.ts +237 -0
- package/support/tests/events.view.test.ts +40 -0
- package/support/tests/form-controls.test.ts +452 -0
- package/support/tests/helpers.ts +354 -0
- package/support/tests/internals.test.ts +568 -0
- package/support/tests/iterate.view.test.ts +40 -0
- package/support/tests/js-api.view.test.ts +45 -0
- package/support/tests/language-docs.test.ts +147 -0
- package/support/tests/lifecycle.test.ts +440 -0
- package/support/tests/loop-guard.test.ts +323 -0
- package/support/tests/provider-list.view.test.ts +47 -0
- package/support/tests/quark-features.test.ts +2887 -0
- package/support/tests/quark-with-providers.test.ts +169 -0
- package/support/tests/quark.test.ts +359 -0
- package/support/tests/resolvers.test.ts +346 -0
- package/support/tests/selector-analysis.test.ts +281 -0
- package/support/tests/selectors.test.ts +670 -0
- package/support/tests/toggle-content.view.test.ts +40 -0
- package/support/tests/unset.view.test.ts +42 -0
- package/support/tests/view-helpers.ts +81 -0
- package/support/tests/view-transition.test.ts +1069 -0
- package/support/tests/view-transition.view.test.ts +76 -0
- 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
|
+
"<a href="x">&</a>"
|
|
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
|
+
});
|