@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
package/src/types.ts ADDED
@@ -0,0 +1,155 @@
1
+ import type { ListenerOptionSource } from "./ast";
2
+ import type { Attribute, Listener, Variable } from "./properties";
3
+ import type { TQuarkElement } from "./quark-element";
4
+ import type { Rule } from "./rule";
5
+ export type UtilFn = (e: any) => any;
6
+
7
+ export type Vars = Record<string, any>;
8
+
9
+ export interface AST {
10
+ type: string;
11
+ value: string;
12
+ children: AST[];
13
+ }
14
+
15
+ export type PaintFn = () => void;
16
+
17
+ /**
18
+ * A `@view-transition` block as the writes inside it carry it
19
+ * (`Property.transition`; nested rules and `@on` blocks inherit it through
20
+ * `Rule.transition`, a nested block replaces it). Options are evaluated per
21
+ * resolve (see `resolveTransitionOptions`).
22
+ */
23
+ export interface TransitionSpec {
24
+ /** Options as written, in source order. */
25
+ optionSources: ListenerOptionSource[];
26
+ /** Display text: `@view-transition (types: "todo-change")`. */
27
+ source: string;
28
+ /** The block lists `first-render` (read statically: no evaluation needed). */
29
+ firstRender: boolean;
30
+ /**
31
+ * The rule the block is written in: the element it matches is where the
32
+ * options are evaluated and `until: "<selector>"` is checked. `null` for
33
+ * a sheet-level block, whose element is the host.
34
+ */
35
+ ownerRule: Rule | null;
36
+ /** Option names already warned about for this block. */
37
+ warned: Set<string>;
38
+ }
39
+
40
+ export interface QuarkOptions {
41
+ observe?: boolean;
42
+ /**
43
+ * Wrap the sheet in an implicit `@scope { }` at the host (quark-sheet's
44
+ * default, via `is-global` inversion). Without it, top-level rules run
45
+ * in the root context and authors write `@scope { }` themselves.
46
+ */
47
+ isScoped?: boolean;
48
+ rule?: Rule;
49
+ /** The property being resolved (set for the duration of one resolve). */
50
+ property?: Variable | Attribute | Listener;
51
+ runId?: string;
52
+ isFirstRun?: boolean;
53
+ properties?: Array<Variable | Attribute | Listener>;
54
+ propertiesToRun?: Array<Variable | Attribute | Listener>;
55
+ isAsyncRun?: boolean;
56
+ /**
57
+ * Set while an `@on` block runs, and while its per-event options
58
+ * (`handle`, `target`, …) evaluate: the DOM event being handled. The
59
+ * `event` built-in; `undefined` outside a block.
60
+ */
61
+ event?: Event;
62
+ /**
63
+ * Set while an `@on` block runs: the element the `target:` option
64
+ * matched (the delegate), else the event's target. The `target`
65
+ * built-in.
66
+ */
67
+ eventTarget?: Element | null;
68
+ /** Set on `quark-binding-change` fan-out runs; enables the nearness skip. */
69
+ changedBinding?: {
70
+ name: string;
71
+ origin: WeakRef<HTMLElement>;
72
+ /**
73
+ * For changes deferred to the end of a sheet's run: sequence number
74
+ * at write time plus the run's trace, so consumers that already ran
75
+ * after the write are not run again.
76
+ */
77
+ sinceSeq?: number;
78
+ trace?: RunTrace;
79
+ };
80
+ }
81
+
82
+ /**
83
+ * What one sheet run executed, in order: every (element, property) pair
84
+ * gets the sequence number at which it ran. Lives only for the outermost
85
+ * `Quark.run()`; binding writes during that run carry the seq they were
86
+ * made at.
87
+ */
88
+ export interface RunTrace {
89
+ seq: number;
90
+ /** Property executions: the sequence number at which each ran. */
91
+ ran: Map<Element, Map<Variable | Attribute | Listener, number>>;
92
+ /**
93
+ * Property visits (`Property.run` reached, executed or not). Seq 0
94
+ * marks a property the run skipped on purpose (its key was the
95
+ * triggering mutation); always stale for a later write.
96
+ */
97
+ visited: Map<Element, Map<Variable | Attribute | Listener, number>>;
98
+ /** `$names` written so far during the run (any owner). */
99
+ written: Set<string>;
100
+ /**
101
+ * Per rule: subtree roots the rule fanned out from with its full
102
+ * property set. Every match below such a root was visited.
103
+ */
104
+ coverage: Map<Rule, Set<Element>>;
105
+ }
106
+
107
+ export type QuarkListenerConfig = [
108
+ Node | WeakRef<Node>,
109
+ string,
110
+ (e: CustomEvent) => void,
111
+ ][];
112
+
113
+ export interface QuarkArgs {
114
+ host: HTMLElement;
115
+ sheet: string;
116
+ variables?: Vars;
117
+ }
118
+
119
+ export interface ContextSheet {
120
+ host?: HTMLElement;
121
+ hash: string;
122
+ options: QuarkOptions;
123
+ }
124
+
125
+ export interface ContextField extends ContextSheet {
126
+ element: TQuarkElement;
127
+ key: string;
128
+ value: string | null;
129
+ /**
130
+ * Set for `@on` at-rules: DOM event types, storage slot (display key,
131
+ * unique per event list + options within a rule), option sources, and
132
+ * for `@on … { }` the block rule applied once per event.
133
+ */
134
+ listener?: {
135
+ eventTypes: string[];
136
+ slot: string;
137
+ optionSources: ListenerOptionSource[];
138
+ block?: Rule;
139
+ };
140
+ }
141
+
142
+ export type MutationMap = Map<HTMLElement, Set<string>>;
143
+
144
+ export type ExpressionResult =
145
+ | {
146
+ type: "html";
147
+ value: string;
148
+ after?: () => void;
149
+ }
150
+ | {
151
+ type: "nodes";
152
+ value: Node[];
153
+ after?: () => void;
154
+ }
155
+ | undefined;
package/src/utils.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { KitLogManager } from "@excom/kit-logger";
2
+ import { isNumber } from "@excom/kit-utils";
3
+
4
+ export const stringToHash = (str: string, seed: number = 0): string => {
5
+ let h1 = 0xdeadbeef ^ seed,
6
+ h2 = 0x41c6ce57 ^ seed;
7
+ for (let i = 0, ch; i < str.length; i++) {
8
+ ch = str.charCodeAt(i);
9
+ h1 = Math.imul(h1 ^ ch, 2654435761);
10
+ h2 = Math.imul(h2 ^ ch, 1597334677);
11
+ }
12
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
13
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
14
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
15
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
16
+
17
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0) + "";
18
+ };
19
+
20
+ export const getQuarkHost = (el: Element | null): HTMLElement =>
21
+ ((["HTML", "HEAD", "BODY"].includes(el?.tagName as string)
22
+ ? document.body
23
+ : el) ?? document.body) as HTMLElement;
24
+
25
+ export const getQuarkMin = (sheet: string) => {
26
+ return minifyQuark(sheet ?? "");
27
+ };
28
+
29
+ /**
30
+ * Normalize sheet source before parsing: collapse horizontal whitespace
31
+ * to one space and blank-line runs to one newline. Comments stay for the
32
+ * parser (a regex pre-strip ate `//` inside strings like `"https://…"`).
33
+ */
34
+ export function minifyQuark(scssString: string): string {
35
+ return scssString
36
+ .replace(/[ \t\f\r]+/g, " ")
37
+ .replace(/ ?\n[ \n]*/g, "\n")
38
+ .trim();
39
+ }
40
+
41
+ interface QuarkLog {
42
+ method?: string;
43
+ sheetId?: string;
44
+ runId?: string;
45
+ ruleId?: string;
46
+ options?: Record<string, unknown>;
47
+ [key: string]: unknown;
48
+ }
49
+ export const QuarkLogger = new KitLogManager({
50
+ namespace: "Quark",
51
+ formatArgs: ([
52
+ name,
53
+ { method, sheetId, runId, ruleId, options, ...other },
54
+ ]: QuarkLog[]) => [
55
+ method !== "run" ? " " : "",
56
+ `${(runId || " ").padEnd(8)} | `,
57
+ `${(method || "").padEnd(15)} | `,
58
+ isNumber(sheetId) ? `sheet: ${sheetId} | ` : "",
59
+ isNumber(ruleId) ? `rule: ${ruleId?.toString().padEnd(3)} | ` : "",
60
+ "options: ",
61
+ [options],
62
+ ...Object.entries(other || {})
63
+ .map(([key, value]) => [` | ${key}: `, value])
64
+ .flat(2),
65
+ ` | (${name})`,
66
+ ],
67
+ });
68
+
69
+ /**
70
+ * Hot paths build log payloads (matched-element arrays, mutation maps)
71
+ * that are discarded below `info`. Gate their construction.
72
+ */
73
+ export const isInfoLogging = () => QuarkLogger.level >= 4;
74
+
75
+ export const deref = <T extends HTMLElement | Node>(
76
+ el: T | WeakRef<T>
77
+ ): T | undefined => {
78
+ return el instanceof WeakRef ? el.deref() : el;
79
+ };
@@ -0,0 +1,359 @@
1
+ import { readBinding } from "./bindings";
2
+ import { VALUE_MAP } from "./constants";
3
+ import { QuarkEvalError } from "./evaluator";
4
+ import { getQuarkInternal } from "./quark-internal";
5
+ import type { ExpressionResult, QuarkOptions } from "./types";
6
+ import { QuarkLogger } from "./utils";
7
+ import { hashObject } from "@excom/hash-object";
8
+ import {
9
+ di,
10
+ execWhenReady,
11
+ getChildren,
12
+ isPojo,
13
+ isPrimitive,
14
+ resolveTemplateContent,
15
+ } from "@excom/kit-utils";
16
+
17
+ const setLoopData = (
18
+ el,
19
+ index?: number,
20
+ item?: any,
21
+ hash?: string,
22
+ key?: string
23
+ ) => {
24
+ const elementInternal = getQuarkInternal(el);
25
+ // Always set index, whether number or string (for dicts)
26
+ if (index !== undefined) elementInternal.setLoopIndex(index);
27
+ if (item !== undefined) elementInternal.setLoopItem(item);
28
+ if (typeof hash === "string") elementInternal.setLoopHash(hash);
29
+ if (key !== undefined) elementInternal.setLoopKey(key);
30
+ return el;
31
+ };
32
+
33
+ type BuiltinContext = {
34
+ elRef: WeakRef<HTMLElement>;
35
+ options: QuarkOptions;
36
+ };
37
+
38
+ /** Queue `element` for the next rule run of the sheet resolving `options`. */
39
+ const requeue = (
40
+ options: QuarkOptions,
41
+ element: HTMLElement,
42
+ attribute: "content" | "NEW_SELF"
43
+ ) => options.rule?.quarkInstance.queueRunRules({ element, attribute });
44
+
45
+ /**
46
+ * Built-in expression names. Each factory produces the value for one
47
+ * evaluation; most return a function. `element` is the matched element;
48
+ * `item` / `index` read the element's loop context. Factories run only
49
+ * when an expression actually references the name (see `createScope`).
50
+ */
51
+ const BUILTINS: Record<string, (ctx: BuiltinContext) => unknown> = {
52
+ /*
53
+ * The matched element (the listening element inside `@on` blocks,
54
+ * where `target` is the delegate). Hand the node to `@use` functions.
55
+ * Reads through it are not observed.
56
+ */
57
+ element: ({ elRef }: BuiltinContext): HTMLElement | undefined =>
58
+ di.apply([elRef], (el) => el),
59
+ item: ({ elRef }: BuiltinContext): unknown | undefined | null =>
60
+ di.apply([elRef], (el) => {
61
+ const itemEl = el.closest("[q-loop] > *");
62
+ if (itemEl) {
63
+ return getQuarkInternal(itemEl as HTMLElement).getLoopItem();
64
+ }
65
+ }),
66
+ index: ({ elRef }: BuiltinContext): number | undefined =>
67
+ di.apply([elRef], (el) => {
68
+ const itemEl = el.closest("[q-loop] > *");
69
+ if (itemEl) {
70
+ return getQuarkInternal(itemEl as HTMLElement).getLoopIndex();
71
+ }
72
+ }),
73
+ // `@on … { }` blocks: the event being handled (see Rule.runEvent)
74
+ event: ({ options }: BuiltinContext): Event | undefined => options.event,
75
+ // `@on (target: "…") { }` blocks: the delegate element; else event.target
76
+ target: ({ options }: BuiltinContext): EventTarget | null | undefined =>
77
+ options.eventTarget ?? options.event?.target,
78
+ "prevent-default":
79
+ () =>
80
+ (e: Event): void =>
81
+ e.preventDefault(),
82
+ "stop-propagation":
83
+ () =>
84
+ (e: Event): void =>
85
+ e.stopPropagation(),
86
+ /*
87
+ * `prop("x")`: the matched element's JS property (the `attr()` of
88
+ * properties). Literal names the property statically references are
89
+ * subscribed on first read so a JS assignment re-runs it.
90
+ */
91
+ prop:
92
+ ({ elRef, options }: BuiltinContext) =>
93
+ (name: string): unknown =>
94
+ di.apply([elRef], (el) => {
95
+ if (typeof name !== "string") return undefined;
96
+ const property = options.property;
97
+ if (property?.referencedPropNames.includes(name)) {
98
+ options.rule?.quarkInstance.subscribeProp(el, name);
99
+ }
100
+ return (el as unknown as Record<string, unknown>)[name];
101
+ }),
102
+ attr:
103
+ ({ elRef }: BuiltinContext) =>
104
+ (attrName: string): string | null | undefined =>
105
+ di.apply([elRef], (el) => {
106
+ if (attrName === "content") {
107
+ return el?.innerHTML;
108
+ } else {
109
+ return el?.getAttribute(attrName);
110
+ }
111
+ }),
112
+ closest:
113
+ ({ elRef }: BuiltinContext) =>
114
+ (selector: string): unknown | undefined | null =>
115
+ di.apply([elRef], (el) => el.closest(selector)),
116
+ ternary:
117
+ () =>
118
+ (condition: boolean, trueValue: any, falseValue?: any): any =>
119
+ condition ? trueValue : (falseValue ?? null),
120
+ // debugging
121
+ log:
122
+ () =>
123
+ <A>(...args: A[]): A[] => {
124
+ console.log("Quark log() -> ", ...args);
125
+ return args;
126
+ },
127
+ debug:
128
+ () =>
129
+ <A>(...args: A[]): A[] => {
130
+ args;
131
+ debugger;
132
+ return args;
133
+ },
134
+ "dangerous-html":
135
+ ({ elRef }: BuiltinContext) =>
136
+ (result: string): ExpressionResult =>
137
+ di.apply([elRef], () => {
138
+ if (isPrimitive(typeof result)) {
139
+ return {
140
+ type: "html",
141
+ value: result as string,
142
+ };
143
+ }
144
+ }),
145
+ template:
146
+ ({ elRef }: BuiltinContext) =>
147
+ (templateRef: string): Promise<ExpressionResult> | undefined =>
148
+ di.apply([elRef], (el) => {
149
+ return execWhenReady(
150
+ resolveTemplateContent(
151
+ templateRef || ":scope > template",
152
+ {
153
+ scope: el,
154
+ }
155
+ // template needs cloning, otherwise it will be removed from the DOM
156
+ ),
157
+ (templateHtml) => {
158
+ const node = document.importNode(templateHtml, true) as Node;
159
+ return {
160
+ type: "nodes" as const,
161
+ value: [node],
162
+ };
163
+ }
164
+ );
165
+ }),
166
+ iterate:
167
+ ({ elRef, options }: BuiltinContext) =>
168
+ (
169
+ result,
170
+ templateRef,
171
+ keyProperty
172
+ ): Promise<ExpressionResult | null> | undefined | null => {
173
+ // no collection → wipe rendered children (use `preserve` to opt out)
174
+ if (result == null) return null;
175
+ return di.apply([elRef], async (el) => {
176
+ const isArray = Array.isArray(result);
177
+ const isObject = !isArray && isPojo(result);
178
+ if (!isArray && !isObject) return undefined;
179
+ return execWhenReady(
180
+ resolveTemplateContent(
181
+ templateRef || ":scope > template",
182
+ {
183
+ scope: el,
184
+ }
185
+ // template needs cloning, otherwise it will be removed from the DOM
186
+ ),
187
+ (templateHtml) => {
188
+ el.setAttribute("q-loop", "");
189
+ const existingChildren = getChildren(el).otherChildren;
190
+ const entriesResult = isArray
191
+ ? result.map((item, index) => [index, item])
192
+ : Object.entries(result);
193
+ if (!entriesResult.length) {
194
+ // empty collection → wipe non-template children
195
+ return {
196
+ type: "nodes" as const,
197
+ value: [],
198
+ };
199
+ }
200
+ QuarkLogger.info({
201
+ method: "iterate",
202
+ runId: options.runId,
203
+ result: [[entriesResult]],
204
+ options,
205
+ });
206
+ const changedElements: Node[] = [];
207
+ const returnChildren = entriesResult.map(
208
+ ([resultIndex, resultItem]) => {
209
+ const resultKey = keyProperty
210
+ ? resultItem[keyProperty]
211
+ : hashObject(resultItem);
212
+ const existingChild = existingChildren.find((c) => {
213
+ // TODO account for scenario where data matches multiple elements
214
+ return (
215
+ resultKey ===
216
+ getQuarkInternal(c as HTMLElement).getLoopKey()
217
+ );
218
+ });
219
+ if (!existingChild) {
220
+ // if key is not found, create new element
221
+ const newChild = setLoopData(
222
+ document.importNode(templateHtml, true) as Element,
223
+ resultIndex,
224
+ resultItem,
225
+ // use hash for key if key property is not provided
226
+ !keyProperty ? resultKey : hashObject(resultItem),
227
+ resultKey
228
+ );
229
+ changedElements.push(newChild);
230
+ return newChild;
231
+ }
232
+ const hash = !keyProperty ? resultKey : hashObject(resultItem);
233
+ const internal = getQuarkInternal(existingChild as HTMLElement);
234
+ let didChange = false;
235
+
236
+ if (resultIndex !== internal.getLoopIndex()) {
237
+ // if data was re-ordered, update index
238
+ setLoopData(existingChild, resultIndex);
239
+ didChange = true;
240
+ }
241
+ const existingHash = internal.getLoopHash();
242
+ const existingKey = internal.getLoopKey();
243
+ if (hash !== existingHash) {
244
+ // if data changed, update data and hash. If hash and key were the same, update key with new hash
245
+ setLoopData(
246
+ existingChild,
247
+ undefined,
248
+ resultItem,
249
+ hash,
250
+ existingHash === existingKey ? hash : undefined
251
+ );
252
+ didChange = true;
253
+ }
254
+ if (didChange) {
255
+ changedElements.push(existingChild);
256
+ }
257
+ return existingChild;
258
+ }
259
+ );
260
+ return {
261
+ type: "nodes" as const,
262
+ value: returnChildren as Node[],
263
+ after: di.bind([elRef], (el) => {
264
+ /*
265
+ * Inserted / moved rows reach rules through the childList
266
+ * observer. A keyed row whose data changed in place
267
+ * moves no DOM node, so requeue those directly. All rows
268
+ * changed: one `content` entry on the parent (dedupes
269
+ * with the observer); otherwise each changed row as
270
+ * itself.
271
+ */
272
+ if (entriesResult.length === changedElements.length) {
273
+ requeue(options, el, "content");
274
+ } else {
275
+ changedElements.forEach((c) => {
276
+ requeue(options, c as HTMLElement, "NEW_SELF");
277
+ });
278
+ }
279
+ }),
280
+ };
281
+ }
282
+ );
283
+ });
284
+ },
285
+ };
286
+
287
+ /** Built-in names, for the language-metadata sync test (`language.ts`). */
288
+ export const BUILTIN_NAMES: readonly string[] = Object.keys(BUILTINS);
289
+
290
+ const hasOwn = (obj: object, key: string): boolean =>
291
+ Object.prototype.hasOwnProperty.call(obj, key);
292
+
293
+ /**
294
+ * `@use` module exports flattened into one lookup object, cached per
295
+ * modules record. `addModules` replaces the record, so the WeakMap key
296
+ * changes exactly when the module set does.
297
+ */
298
+ const MODULE_SCOPE_CACHE = new WeakMap<object, Record<string, unknown>>();
299
+ const getModuleScope = (options: QuarkOptions): Record<string, unknown> => {
300
+ const instance = options.rule?.quarkInstance;
301
+ if (!instance) return {};
302
+ const modules: Record<string, Record<string, unknown>> | undefined =
303
+ getQuarkInternal(instance.host.deref())?.getModules(instance.hash);
304
+ if (!modules) return {};
305
+ const cached = MODULE_SCOPE_CACHE.get(modules);
306
+ if (cached) return cached;
307
+ const { dfault, ...others } = modules;
308
+ const scope: Record<string, unknown> = { ...dfault, ...others };
309
+ MODULE_SCOPE_CACHE.set(modules, scope);
310
+ return scope;
311
+ };
312
+
313
+ export interface QuarkScope {
314
+ /**
315
+ * Resolve a bare identifier or `$variable` for the evaluating element.
316
+ * Order: value keywords → `@use` exports → built-ins → `$bindings`
317
+ * (walk ancestors; `undefined` when unbound). Unknown bare ids throw.
318
+ */
319
+ lookup(name: string): unknown;
320
+ }
321
+
322
+ /**
323
+ * Lazy expression scope. Nothing resolves until the evaluator asks for
324
+ * a name, so an expression pays only for what it references: no module
325
+ * spread, no built-in construction, no `item` / `index` `closest()`
326
+ * walks unless used. $bindings are tree-scoped: walk ancestors from the
327
+ * consumer (nearest owner wins), whichever sheet set them (CSS
328
+ * custom-property semantics). VALUE_MAP supplies the behavior keywords
329
+ * (none / preserve / unset).
330
+ */
331
+ export const createScope = ({
332
+ element,
333
+ options,
334
+ }: {
335
+ element: HTMLElement;
336
+ options: QuarkOptions;
337
+ }): QuarkScope => {
338
+ const modules = getModuleScope(options);
339
+ let ctx: BuiltinContext | null = null;
340
+ let builtins: Record<string, unknown> | null = null;
341
+ return {
342
+ lookup(name: string): unknown {
343
+ if (hasOwn(VALUE_MAP, name)) return VALUE_MAP[name];
344
+ if (hasOwn(modules, name)) return modules[name];
345
+ if (hasOwn(BUILTINS, name)) {
346
+ // one instance per name per evaluation (`item` twice in one
347
+ // expression walks once)
348
+ builtins ??= {};
349
+ if (!hasOwn(builtins, name)) {
350
+ ctx ??= { elRef: new WeakRef(element), options };
351
+ builtins[name] = BUILTINS[name](ctx);
352
+ }
353
+ return builtins[name];
354
+ }
355
+ if (name.startsWith("$")) return readBinding(element, name);
356
+ throw new QuarkEvalError(`"${name}" is not defined`);
357
+ },
358
+ };
359
+ };
@@ -0,0 +1,18 @@
1
+ <div>
2
+ <details>
3
+ <summary>Open me</summary>
4
+ <p>The border turns blue while open.</p>
5
+ </details>
6
+ <quark-sheet>
7
+ details { --border-color: "silver"; }
8
+ details[open] { --border-color: "#3fa9f5"; }
9
+ </quark-sheet>
10
+ <style>
11
+ #demo-quark-css-variables > :first-child {
12
+ details {
13
+ border: 2px solid var(--border-color);
14
+ padding: 0.5rem;
15
+ }
16
+ }
17
+ </style>
18
+ </div>
@@ -0,0 +1,20 @@
1
+ <div>
2
+ <button type="button" data-action="ping">Ping the panel</button>
3
+ <section aria-label="panel">
4
+ <output></output>
5
+ </section>
6
+ <quark-sheet>
7
+ button[data-action="ping"] {
8
+ @on click {
9
+ @dispatch demo-ping (target: "[aria-label='panel']", detail: (at: event.timeStamp));
10
+ }
11
+ }
12
+ [aria-label="panel"] {
13
+ @on demo-ping {
14
+ $pings: (+attr("data-pings") or 0) + 1;
15
+ data-pings: $pings;
16
+ output { content: "pinged #{$pings}×"; }
17
+ }
18
+ }
19
+ </quark-sheet>
20
+ </div>
@@ -0,0 +1,9 @@
1
+ <div>
2
+ <a href="https://example.com">Click me — navigation is prevented</a>
3
+ <output></output>
4
+ <quark-sheet>
5
+ @use "/demo-utils" as *;
6
+
7
+ a { @on click (prevent-default, handle: noteEvent); }
8
+ </quark-sheet>
9
+ </div>
@@ -0,0 +1,13 @@
1
+ <div>
2
+ <ul>
3
+ <template>
4
+ <li bind-name></li>
5
+ </template>
6
+ </ul>
7
+ <quark-sheet>
8
+ @use "/demo-utils" as *;
9
+
10
+ ul { content: iterate(getPlanets()); }
11
+ [bind-name] { content: item; }
12
+ </quark-sheet>
13
+ </div>
@@ -0,0 +1,17 @@
1
+ <div data-demo-counter>
2
+ <button>+1</button>
3
+ <output></output>
4
+ <quark-sheet>
5
+ @use "/demo-utils" as *;
6
+
7
+ :scope {
8
+ $count: 0;
9
+ }
10
+ button {
11
+ @on click (handle: incrementFromJs(closest("[data-demo-counter]")));
12
+ }
13
+ output {
14
+ content: "Clicked #{$count} times";
15
+ }
16
+ </quark-sheet>
17
+ </div>
@@ -0,0 +1,16 @@
1
+ <div>
2
+ <provider-fetch api-url="/api/todos?_limit=4">
3
+ <ul>
4
+ <template>
5
+ <li bind-title></li>
6
+ </template>
7
+ </ul>
8
+ </provider-fetch>
9
+ <quark-sheet>
10
+ provider-fetch[is-success] {
11
+ $todos: prop("provision").body;
12
+ ul { content: iterate($todos); }
13
+ [bind-title] { content: item.title; }
14
+ }
15
+ </quark-sheet>
16
+ </div>