@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
package/src/rule.ts
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
export type * from "./types";
|
|
2
|
+
import { Action } from "./actions";
|
|
3
|
+
import {
|
|
4
|
+
declarationStrings,
|
|
5
|
+
eventNamesText,
|
|
6
|
+
listenerOptionSources,
|
|
7
|
+
type RuleNode,
|
|
8
|
+
ruleSelectorTexts,
|
|
9
|
+
sliceNode,
|
|
10
|
+
type Statement,
|
|
11
|
+
type TransitionRule,
|
|
12
|
+
transitionSourceText,
|
|
13
|
+
viewBlock,
|
|
14
|
+
} from "./ast";
|
|
15
|
+
import { CHILD_REMOVED } from "./constants";
|
|
16
|
+
import {
|
|
17
|
+
Attribute,
|
|
18
|
+
Delay,
|
|
19
|
+
Diagnostic,
|
|
20
|
+
Listener,
|
|
21
|
+
recordSeq,
|
|
22
|
+
StyleProperty,
|
|
23
|
+
Variable,
|
|
24
|
+
} from "./properties";
|
|
25
|
+
import type { Quark } from "./quark";
|
|
26
|
+
import type { TQuarkElement } from "./quark-internal";
|
|
27
|
+
import { SCOPE_ATTR } from "./scope-id";
|
|
28
|
+
import {
|
|
29
|
+
analyzeSelector,
|
|
30
|
+
buildSelectorLine,
|
|
31
|
+
type CompoundDependency,
|
|
32
|
+
EMPTY_ANALYSIS,
|
|
33
|
+
type FanOutRoot,
|
|
34
|
+
type SelectorAnalysis,
|
|
35
|
+
splitScopePrefix,
|
|
36
|
+
} from "./selector-utils";
|
|
37
|
+
import type { MutationMap, QuarkOptions, TransitionSpec } from "./types";
|
|
38
|
+
import { isInfoLogging, QuarkLogger } from "./utils";
|
|
39
|
+
import { selectAll, tc } from "@excom/kit-utils";
|
|
40
|
+
|
|
41
|
+
let quarkRuleIdCounter = 0;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Elements not contained by another in the list, in the list's own
|
|
45
|
+
* order (fan-out order is observable: it decides which match's
|
|
46
|
+
* properties run first). Containment is decided on a document-ordered
|
|
47
|
+
* copy: a nested element always follows its container there, and kept
|
|
48
|
+
* elements are disjoint, so each candidate needs one `contains` against
|
|
49
|
+
* the last kept, not against every other.
|
|
50
|
+
*/
|
|
51
|
+
export const outermostElements = (elements: HTMLElement[]): HTMLElement[] => {
|
|
52
|
+
if (elements.length < 2) return elements;
|
|
53
|
+
const sorted = elements
|
|
54
|
+
.slice()
|
|
55
|
+
.sort((a, b) =>
|
|
56
|
+
a === b
|
|
57
|
+
? 0
|
|
58
|
+
: a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
59
|
+
? -1
|
|
60
|
+
: 1
|
|
61
|
+
);
|
|
62
|
+
const kept = new Set<HTMLElement>();
|
|
63
|
+
let last: HTMLElement | undefined;
|
|
64
|
+
for (const el of sorted) {
|
|
65
|
+
if (!last || (last !== el && !last.contains(el))) {
|
|
66
|
+
kept.add(el);
|
|
67
|
+
last = el;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return elements.filter((el) => kept.has(el));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mutation kinds run structural first: `RUN_ALL`, then `NEW_SELF`, then
|
|
75
|
+
* `content` / `CHILD_REMOVED`, then attr names and `$bindings`.
|
|
76
|
+
*/
|
|
77
|
+
const KIND_ORDER: Record<string, number> = {
|
|
78
|
+
RUN_ALL: 0,
|
|
79
|
+
NEW_SELF: 1,
|
|
80
|
+
content: 2,
|
|
81
|
+
[CHILD_REMOVED]: 3,
|
|
82
|
+
};
|
|
83
|
+
const kindOrder = (a: string, b: string) =>
|
|
84
|
+
(KIND_ORDER[a] ?? 4) - (KIND_ORDER[b] ?? 4);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The spec a `@view-transition` node gives the writes inside it.
|
|
88
|
+
* `ownerRule` is the rule the block is written in (`null` at sheet level).
|
|
89
|
+
*/
|
|
90
|
+
export const transitionSpec = (
|
|
91
|
+
node: TransitionRule,
|
|
92
|
+
source: string,
|
|
93
|
+
ownerRule: Rule | null
|
|
94
|
+
): TransitionSpec => ({
|
|
95
|
+
optionSources: listenerOptionSources(node, source),
|
|
96
|
+
source: transitionSourceText(node, source),
|
|
97
|
+
firstRender: node.options.some((option) => option.name === "first-render"),
|
|
98
|
+
ownerRule,
|
|
99
|
+
warned: new Set(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export class Rule {
|
|
103
|
+
// for debugging
|
|
104
|
+
id: number;
|
|
105
|
+
numberOfRuns: number = 0;
|
|
106
|
+
// for runtime
|
|
107
|
+
/** Authored selector (may contain `:scope`); parent for nested rules. */
|
|
108
|
+
selector: string;
|
|
109
|
+
/**
|
|
110
|
+
* Selector usable with DOM APIs (no `:scope`). Empty string: the rule
|
|
111
|
+
* targets the host itself (`:scope { ... }`).
|
|
112
|
+
*/
|
|
113
|
+
matchSelector: string;
|
|
114
|
+
/**
|
|
115
|
+
* Set when the selector is `:scope`-anchored: remainder of the first
|
|
116
|
+
* compound, matched against the host (`:scope[is-on]` → `"[is-on]"`,
|
|
117
|
+
* bare `:scope` → `""`). `null` if the selector has no `:scope`.
|
|
118
|
+
*/
|
|
119
|
+
hostCompound: string | null;
|
|
120
|
+
/**
|
|
121
|
+
* The rule's own selector text, before composition with the parent's
|
|
122
|
+
* (`+ provider-fetch`)
|
|
123
|
+
*/
|
|
124
|
+
ownSelector: string;
|
|
125
|
+
/** Rules inside an `@scope` block only match within the host's subtree. */
|
|
126
|
+
isScoped: boolean;
|
|
127
|
+
path: string[];
|
|
128
|
+
/** Selector attribute gates plus literal `attr("x")` reads, observer input. */
|
|
129
|
+
observedAttrs: Set<string>;
|
|
130
|
+
/** What `matchSelector` depends on and where its subjects sit relative to each change. */
|
|
131
|
+
deps: SelectorAnalysis;
|
|
132
|
+
/** Same for `hostCompound` (`:scope:has([x])`), or `null`. */
|
|
133
|
+
hostDeps: SelectorAnalysis | null;
|
|
134
|
+
/**
|
|
135
|
+
* Match can change when elements are removed (`:has()`, sibling
|
|
136
|
+
* position, sibling combinators). The sheet observes child removals
|
|
137
|
+
* only when some rule says so.
|
|
138
|
+
*/
|
|
139
|
+
reactsToRemovals: boolean;
|
|
140
|
+
attributes: Attribute[] = [];
|
|
141
|
+
variables: Variable[] = [];
|
|
142
|
+
listeners: Listener[] = [];
|
|
143
|
+
/** `@warn` / `@debug` / `@error` statements, run after the writes. */
|
|
144
|
+
diagnostics: Diagnostic[] = [];
|
|
145
|
+
/** `@delay` blocks, scheduled after the writes. */
|
|
146
|
+
delays: Delay[] = [];
|
|
147
|
+
/** `@dispatch` / `@command` statements, run after the block's writes and nested rules. */
|
|
148
|
+
actions: Action[] = [];
|
|
149
|
+
parent?: Rule;
|
|
150
|
+
quarkInstance: Quark;
|
|
151
|
+
/**
|
|
152
|
+
* True for the rule built from an `@on … { }` block (and its nested
|
|
153
|
+
* rules). Event-block rules never join `quarkInstance.rules`, so sheet
|
|
154
|
+
* passes, observer filter, and binding / prop indexes ignore them.
|
|
155
|
+
* They run only through `runEvent`.
|
|
156
|
+
*/
|
|
157
|
+
isEventBlock: boolean;
|
|
158
|
+
/** Nested rules, kept only for event-block rules (see `runEvent`). */
|
|
159
|
+
childRules: Rule[] = [];
|
|
160
|
+
/**
|
|
161
|
+
* True inside an `@on … { }` block (its nested rules and `@delay`
|
|
162
|
+
* blocks included): the only place `@dispatch` / `@command` may appear,
|
|
163
|
+
* so an outgoing event is always caused by an event the sheet heard.
|
|
164
|
+
*/
|
|
165
|
+
inListenerBlock: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* The `@view-transition` block enclosing this rule, inherited by its
|
|
168
|
+
* writes and nested rules (a block written inside the rule stamps its
|
|
169
|
+
* own spec on the writes it holds: see `Property.transition`).
|
|
170
|
+
*/
|
|
171
|
+
transition: TransitionSpec | null;
|
|
172
|
+
constructor({
|
|
173
|
+
quarkInstance,
|
|
174
|
+
statement,
|
|
175
|
+
source,
|
|
176
|
+
parent,
|
|
177
|
+
parentSelector,
|
|
178
|
+
selectorText,
|
|
179
|
+
scoped = false,
|
|
180
|
+
isEventBlock = false,
|
|
181
|
+
inListenerBlock = false,
|
|
182
|
+
transition = null,
|
|
183
|
+
}: {
|
|
184
|
+
quarkInstance: Quark;
|
|
185
|
+
statement: RuleNode;
|
|
186
|
+
source: string;
|
|
187
|
+
parent?: Rule;
|
|
188
|
+
parentSelector?: string;
|
|
189
|
+
/** Set when fanning out a comma-separated selector list. */
|
|
190
|
+
selectorText?: string;
|
|
191
|
+
scoped?: boolean;
|
|
192
|
+
isEventBlock?: boolean;
|
|
193
|
+
inListenerBlock?: boolean;
|
|
194
|
+
transition?: TransitionSpec | null;
|
|
195
|
+
}) {
|
|
196
|
+
this.isEventBlock = isEventBlock;
|
|
197
|
+
this.inListenerBlock = inListenerBlock;
|
|
198
|
+
this.transition = transition;
|
|
199
|
+
if (transition) quarkInstance.hasTransitions = true;
|
|
200
|
+
const selectorTexts = selectorText
|
|
201
|
+
? [selectorText]
|
|
202
|
+
: ruleSelectorTexts(statement, source);
|
|
203
|
+
this.ownSelector = selectorTexts[0].trim();
|
|
204
|
+
this.selector = buildSelectorLine(selectorTexts[0], parentSelector);
|
|
205
|
+
selectorTexts.slice(1).forEach((sel) => {
|
|
206
|
+
new Rule({
|
|
207
|
+
quarkInstance,
|
|
208
|
+
statement,
|
|
209
|
+
source,
|
|
210
|
+
parent,
|
|
211
|
+
parentSelector,
|
|
212
|
+
selectorText: sel,
|
|
213
|
+
scoped,
|
|
214
|
+
isEventBlock,
|
|
215
|
+
inListenerBlock,
|
|
216
|
+
transition,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
const { hostCompound, rest } = splitScopePrefix(this.selector);
|
|
220
|
+
this.hostCompound = hostCompound;
|
|
221
|
+
this.matchSelector = rest;
|
|
222
|
+
// `:scope` explicitly references the host, so such rules are
|
|
223
|
+
// host-anchored even at the top level of a global sheet
|
|
224
|
+
this.isScoped = scoped || hostCompound !== null;
|
|
225
|
+
this.deps = rest ? analyzeSelector(rest) : EMPTY_ANALYSIS;
|
|
226
|
+
this.path = this.deps.path;
|
|
227
|
+
this.observedAttrs = new Set(this.deps.attrs);
|
|
228
|
+
// `:scope[is-on]`, host attribute changes must trigger this rule
|
|
229
|
+
this.hostDeps = hostCompound ? analyzeSelector(hostCompound) : null;
|
|
230
|
+
this.hostDeps?.attrs.forEach((a) => this.observedAttrs.add(a));
|
|
231
|
+
this.reactsToRemovals =
|
|
232
|
+
this.deps.reactsToRemovals || !!this.hostDeps?.reactsToRemovals;
|
|
233
|
+
if (!isEventBlock) {
|
|
234
|
+
// warn where the pseudo is written, once, not again in every nested rule
|
|
235
|
+
const ownText = selectorTexts[0];
|
|
236
|
+
const unobserved = [
|
|
237
|
+
...this.deps.unobserved,
|
|
238
|
+
...(this.hostDeps?.unobserved ?? []),
|
|
239
|
+
].filter((name) => ownText.includes(name));
|
|
240
|
+
if (unobserved.length) {
|
|
241
|
+
QuarkLogger.warn({
|
|
242
|
+
method: "rule",
|
|
243
|
+
message: `Quark: ${[...new Set(unobserved)].join(", ")} in "${this.selector}" is not observed — the rule matches on its first run only. Select on reflected attributes or listen for events instead.`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.parent = parent;
|
|
248
|
+
this.quarkInstance = quarkInstance;
|
|
249
|
+
/*
|
|
250
|
+
* A `@view-transition { … }` written in this rule is not a rule of
|
|
251
|
+
* its own: its declarations and `@on`s join this rule, stamped with
|
|
252
|
+
* the block's spec, and its nested rules become children carrying
|
|
253
|
+
* it. Blocks nest; the innermost wins.
|
|
254
|
+
*/
|
|
255
|
+
const children: Array<[RuleNode, TransitionSpec | null]> = [];
|
|
256
|
+
const collect = (body: Statement[], spec: TransitionSpec | null) => {
|
|
257
|
+
const view = viewBlock(body);
|
|
258
|
+
view.declarations.forEach((declaration) => {
|
|
259
|
+
const { key, value } = declarationStrings(declaration, source);
|
|
260
|
+
const args = { key, value, parent: this, transition: spec };
|
|
261
|
+
if (key.startsWith("$")) {
|
|
262
|
+
this.variables.push(new Variable(args));
|
|
263
|
+
} else if (key.startsWith("--")) {
|
|
264
|
+
this.attributes.push(new StyleProperty(args));
|
|
265
|
+
} else {
|
|
266
|
+
this.attributes.push(new Attribute(args));
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
view.diagnostics.forEach((node) => {
|
|
270
|
+
this.diagnostics.push(
|
|
271
|
+
new Diagnostic({
|
|
272
|
+
level: node.name,
|
|
273
|
+
value: sliceNode(source, node.value).trim(),
|
|
274
|
+
parent: this,
|
|
275
|
+
transition: spec,
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
view.delays.forEach((node) => {
|
|
280
|
+
this.delays.push(
|
|
281
|
+
new Delay({
|
|
282
|
+
value: sliceNode(source, node.duration).trim(),
|
|
283
|
+
parent: this,
|
|
284
|
+
transition: spec,
|
|
285
|
+
// the block body is a rule at this rule's own selector, applied
|
|
286
|
+
// to the matched element once when the timer fires
|
|
287
|
+
block: new Rule({
|
|
288
|
+
quarkInstance,
|
|
289
|
+
statement: { block: node.block } as RuleNode,
|
|
290
|
+
source,
|
|
291
|
+
parent: this,
|
|
292
|
+
selectorText: this.selector,
|
|
293
|
+
scoped,
|
|
294
|
+
isEventBlock: true,
|
|
295
|
+
inListenerBlock,
|
|
296
|
+
transition: spec,
|
|
297
|
+
}),
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
view.actions.forEach((node) => {
|
|
302
|
+
if (!inListenerBlock) {
|
|
303
|
+
QuarkLogger.error({
|
|
304
|
+
method: "rule",
|
|
305
|
+
message: `Quark: @${node.name} must be written inside an @on block — a rule matching is not an occurrence (${this.selector})`,
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.actions.push(
|
|
310
|
+
new Action({
|
|
311
|
+
kind: node.name,
|
|
312
|
+
names: node.names,
|
|
313
|
+
optionSources: listenerOptionSources(node, source),
|
|
314
|
+
parent: this,
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
view.listeners.forEach((node) => {
|
|
319
|
+
if (isEventBlock) {
|
|
320
|
+
QuarkLogger.error({
|
|
321
|
+
method: "rule",
|
|
322
|
+
message: `Quark: @${node.name} inside an @on / @delay block is not supported (${this.selector})`,
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.listeners.push(
|
|
327
|
+
new Listener({
|
|
328
|
+
eventTypes: node.events.map((e) => e.name),
|
|
329
|
+
eventsText: eventNamesText(node.events),
|
|
330
|
+
optionSources: listenerOptionSources(node, source),
|
|
331
|
+
parent: this,
|
|
332
|
+
transition: spec,
|
|
333
|
+
// the block body is a rule at this rule's own selector, applied
|
|
334
|
+
// to the matched element once per event (never by the passes)
|
|
335
|
+
block: node.block
|
|
336
|
+
? new Rule({
|
|
337
|
+
quarkInstance,
|
|
338
|
+
statement: { block: node.block } as RuleNode,
|
|
339
|
+
source,
|
|
340
|
+
parent: this,
|
|
341
|
+
selectorText: this.selector,
|
|
342
|
+
scoped,
|
|
343
|
+
isEventBlock: true,
|
|
344
|
+
inListenerBlock: true,
|
|
345
|
+
transition: spec,
|
|
346
|
+
})
|
|
347
|
+
: null,
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
view.children.forEach((child) => {
|
|
352
|
+
if (child.type === "rule") children.push([child, spec]);
|
|
353
|
+
else collect(child.block.body, transitionSpec(child, source, this));
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
collect(statement.block.body, transition);
|
|
357
|
+
|
|
358
|
+
// if there are no attributes or variables, do not pass reference to self anywhere
|
|
359
|
+
// so garbage collection can clean this instance up
|
|
360
|
+
const hasProperties =
|
|
361
|
+
this.attributes.length > 0 ||
|
|
362
|
+
this.variables.length > 0 ||
|
|
363
|
+
this.listeners.length > 0 ||
|
|
364
|
+
this.diagnostics.length > 0 ||
|
|
365
|
+
this.delays.length > 0 ||
|
|
366
|
+
this.actions.length > 0;
|
|
367
|
+
if (hasProperties || isEventBlock) {
|
|
368
|
+
if (!isEventBlock) quarkInstance.rules.push(this);
|
|
369
|
+
this.id = quarkRuleIdCounter;
|
|
370
|
+
quarkRuleIdCounter++;
|
|
371
|
+
}
|
|
372
|
+
children.forEach(([child, childTransition]) => {
|
|
373
|
+
const childRule = new Rule({
|
|
374
|
+
quarkInstance,
|
|
375
|
+
statement: child,
|
|
376
|
+
source,
|
|
377
|
+
parent: hasProperties || isEventBlock ? this : parent,
|
|
378
|
+
parentSelector: this.selector,
|
|
379
|
+
scoped,
|
|
380
|
+
isEventBlock,
|
|
381
|
+
inListenerBlock,
|
|
382
|
+
transition: childTransition,
|
|
383
|
+
});
|
|
384
|
+
if (isEventBlock) this.childRules.push(childRule);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Apply a block rule (`@on … { }`, `@delay … { }`) to `element` once:
|
|
389
|
+
* every declaration runs against the element (no re-run gating; a
|
|
390
|
+
* block is a one-shot), then nested rules run against matching
|
|
391
|
+
* descendants, then `@dispatch` / `@command` statements run (after
|
|
392
|
+
* every write of the block is queued). `event` is exposed to
|
|
393
|
+
* expressions through the options (`undefined` for a `@delay`
|
|
394
|
+
* scheduled outside an `@on` block).
|
|
395
|
+
*/
|
|
396
|
+
runEvent(
|
|
397
|
+
element: HTMLElement,
|
|
398
|
+
event: Event | undefined,
|
|
399
|
+
baseOptions: QuarkOptions,
|
|
400
|
+
eventTarget: Element | null = null
|
|
401
|
+
) {
|
|
402
|
+
const options: QuarkOptions = {
|
|
403
|
+
...baseOptions,
|
|
404
|
+
event,
|
|
405
|
+
eventTarget: eventTarget ?? (event?.target as Element | null) ?? null,
|
|
406
|
+
rule: this,
|
|
407
|
+
property: undefined,
|
|
408
|
+
properties: undefined,
|
|
409
|
+
propertiesToRun: undefined,
|
|
410
|
+
isFirstRun: false,
|
|
411
|
+
isAsyncRun: false,
|
|
412
|
+
changedBinding: undefined,
|
|
413
|
+
};
|
|
414
|
+
this.numberOfRuns++;
|
|
415
|
+
const target = element as TQuarkElement;
|
|
416
|
+
this.variables.forEach((v) => v._run(target, options));
|
|
417
|
+
this.attributes.forEach((a) => a._run(target, options));
|
|
418
|
+
this.diagnostics.forEach((d) => d._run(target, options));
|
|
419
|
+
this.delays.forEach((d) => d._run(target, options));
|
|
420
|
+
this.childRules.forEach((child) => {
|
|
421
|
+
/*
|
|
422
|
+
* A nested selector starting with a sibling combinator means the
|
|
423
|
+
* element's siblings, not its descendants
|
|
424
|
+
*/
|
|
425
|
+
if (/^[+~]/.test(child.ownSelector)) {
|
|
426
|
+
const siblings = tc(() =>
|
|
427
|
+
selectAll(`:scope ${child.ownSelector}`, { scope: element })
|
|
428
|
+
);
|
|
429
|
+
siblings?.forEach((el) => {
|
|
430
|
+
child.runEvent(el, event, baseOptions, eventTarget);
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const selector = child.isScoped
|
|
435
|
+
? child.scopedSelector()
|
|
436
|
+
: child.matchSelector;
|
|
437
|
+
if (!selector) return;
|
|
438
|
+
element.querySelectorAll(selector).forEach((el) => {
|
|
439
|
+
child.runEvent(el as HTMLElement, event, baseOptions, eventTarget);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
this.actions.forEach((action) => action.run(target, options));
|
|
443
|
+
}
|
|
444
|
+
run(
|
|
445
|
+
mutationMap: MutationMap,
|
|
446
|
+
{
|
|
447
|
+
host,
|
|
448
|
+
options,
|
|
449
|
+
}: {
|
|
450
|
+
host: HTMLElement;
|
|
451
|
+
options: QuarkOptions;
|
|
452
|
+
}
|
|
453
|
+
) {
|
|
454
|
+
const propertiesToRun = this.filterPropertiesToRun(options);
|
|
455
|
+
if (propertiesToRun.length > 0) {
|
|
456
|
+
this._run(mutationMap, {
|
|
457
|
+
host,
|
|
458
|
+
options: { ...options, propertiesToRun },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
_run(
|
|
463
|
+
mutationMap: MutationMap,
|
|
464
|
+
{
|
|
465
|
+
host,
|
|
466
|
+
options,
|
|
467
|
+
}: {
|
|
468
|
+
host: HTMLElement;
|
|
469
|
+
options: QuarkOptions;
|
|
470
|
+
}
|
|
471
|
+
) {
|
|
472
|
+
const elementsToMutate = new Set<HTMLElement>();
|
|
473
|
+
// `:scope[x] …` rules are inert while the host doesn't match the compound
|
|
474
|
+
if (this.hostCompound && !host.matches(this.hostCompound)) return;
|
|
475
|
+
const targetsHost = this.hostCompound !== null && !this.matchSelector;
|
|
476
|
+
/*
|
|
477
|
+
Scoping lives in the host marker, not tree walks. Registered hosts
|
|
478
|
+
carry `q-scope="<id>"` and scoped selectors are prefixed with
|
|
479
|
+
`[q-scope="<id>"] ` (see scope-id.ts), so the native engine
|
|
480
|
+
enforces CSS `@scope`. Every compound matches strict descendants;
|
|
481
|
+
the host itself only via explicit `:scope` (hostCompound /
|
|
482
|
+
targetsHost). `host.contains()` and query-root clamping below bound
|
|
483
|
+
the work and keep stale cloned markers outside the host inert.
|
|
484
|
+
*/
|
|
485
|
+
const runSelector = this.isScoped
|
|
486
|
+
? this.scopedSelector()
|
|
487
|
+
: this.matchSelector;
|
|
488
|
+
const matchesRule = (el: HTMLElement) => this.matchesElement(el, host);
|
|
489
|
+
// a full-property fan-out visits every match below its query roots.
|
|
490
|
+
// recorded so deferred binding writes can tell which readers it reached
|
|
491
|
+
const trace = this.quarkInstance.runTrace;
|
|
492
|
+
const coverage =
|
|
493
|
+
trace && !options.properties
|
|
494
|
+
? (trace.coverage.get(this) ??
|
|
495
|
+
trace.coverage.set(this, new Set()).get(this))
|
|
496
|
+
: undefined;
|
|
497
|
+
const { compounds, usesHas } = this.deps;
|
|
498
|
+
const hostDeps = this.hostDeps?.compounds[0];
|
|
499
|
+
const hostReactsToChildren = !!this.hostDeps?.usesHas;
|
|
500
|
+
/*
|
|
501
|
+
Fan-out roots: elements whose subtree is re-queried for subjects,
|
|
502
|
+
in queue order (observable: it decides which match's properties
|
|
503
|
+
run first). Duplicates and nested roots collapse in
|
|
504
|
+
`outermostElements`.
|
|
505
|
+
*/
|
|
506
|
+
const roots = new Set<HTMLElement>();
|
|
507
|
+
const fanOut = (root: HTMLElement | null) => {
|
|
508
|
+
if (root) roots.add(root);
|
|
509
|
+
};
|
|
510
|
+
const rootFor = (el: HTMLElement, where: FanOutRoot) =>
|
|
511
|
+
where === "parent" ? (el.parentElement ?? el) : el;
|
|
512
|
+
/**
|
|
513
|
+
* Ancestors-or-self of `el` that are compound `c`'s element (the
|
|
514
|
+
* subject itself for the last compound). Scoped rules stop at the
|
|
515
|
+
* host; the marker keeps every compound a strict descendant anyway.
|
|
516
|
+
* Global rules walk to the root.
|
|
517
|
+
*/
|
|
518
|
+
const matchingAncestors = (
|
|
519
|
+
el: HTMLElement,
|
|
520
|
+
c: CompoundDependency
|
|
521
|
+
): HTMLElement[] => {
|
|
522
|
+
const found: HTMLElement[] = [];
|
|
523
|
+
for (let a: HTMLElement | null = el; a; a = a.parentElement) {
|
|
524
|
+
if (a === host && this.isScoped) break;
|
|
525
|
+
if (c.isSubject ? matchesRule(a) : this.matchesPrefix(a, c)) {
|
|
526
|
+
found.push(a);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return found;
|
|
530
|
+
};
|
|
531
|
+
mutationMap?.forEach((attrs, element) => {
|
|
532
|
+
let addedSelf = false;
|
|
533
|
+
const mutateSelf = (el: HTMLElement = element) => {
|
|
534
|
+
if (el === element) {
|
|
535
|
+
if (addedSelf) return;
|
|
536
|
+
addedSelf = true;
|
|
537
|
+
} else if (elementsToMutate.has(el)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.numberOfRuns++;
|
|
541
|
+
elementsToMutate.add(el);
|
|
542
|
+
};
|
|
543
|
+
/** Compound `c`'s element `el` was affected: it is a subject, or subjects sit below / beside it. */
|
|
544
|
+
const reach = (el: HTMLElement, c: CompoundDependency) => {
|
|
545
|
+
if (c.isSubject) mutateSelf(el);
|
|
546
|
+
else fanOut(rootFor(el, c.root));
|
|
547
|
+
};
|
|
548
|
+
/**
|
|
549
|
+
* Elements came or went below `el`: `:has()` / `:empty` candidates
|
|
550
|
+
* on the way up may have flipped.
|
|
551
|
+
*/
|
|
552
|
+
const childrenChanged = (el: HTMLElement) => {
|
|
553
|
+
for (const c of compounds) {
|
|
554
|
+
if (!c.reactsToChildren) continue;
|
|
555
|
+
if (c.broad) fanOut(host);
|
|
556
|
+
else matchingAncestors(el, c).forEach((a) => reach(a, c));
|
|
557
|
+
}
|
|
558
|
+
if (hostReactsToChildren) fanOut(host);
|
|
559
|
+
};
|
|
560
|
+
[...attrs].sort(kindOrder).forEach((attr) => {
|
|
561
|
+
if (attr === "RUN_ALL") {
|
|
562
|
+
// on init, if host matches selector
|
|
563
|
+
if (element === host && matchesRule(element)) mutateSelf();
|
|
564
|
+
fanOut(element);
|
|
565
|
+
} else if (attr === "NEW_SELF") {
|
|
566
|
+
// if self added children
|
|
567
|
+
if (matchesRule(element)) mutateSelf();
|
|
568
|
+
if (host.contains(element)) fanOut(element);
|
|
569
|
+
} else if (attr === "content" || attr === CHILD_REMOVED) {
|
|
570
|
+
if (attr === CHILD_REMOVED && !this.reactsToRemovals) return;
|
|
571
|
+
if (!host.contains(element)) return;
|
|
572
|
+
// matching children were added / removed below `element`
|
|
573
|
+
fanOut(element);
|
|
574
|
+
if (usesHas || hostReactsToChildren) childrenChanged(element);
|
|
575
|
+
} else if (attr === "PROP") {
|
|
576
|
+
// a JS property changed on `element`; `prop()` reads are on
|
|
577
|
+
// the matched element only, so only it re-runs
|
|
578
|
+
if (matchesRule(element)) mutateSelf();
|
|
579
|
+
} else if (attr.startsWith("$")) {
|
|
580
|
+
/*
|
|
581
|
+
* A $binding changed on `element` (the owner). Bindings scope
|
|
582
|
+
* to the DOM tree, so consumers are the owner plus matching
|
|
583
|
+
* descendants, regardless of rule nesting. Owners above the
|
|
584
|
+
* host (another sheet) still fan out; the scoped selector
|
|
585
|
+
* keeps matches inside this sheet's subtree.
|
|
586
|
+
*/
|
|
587
|
+
if (matchesRule(element)) mutateSelf();
|
|
588
|
+
else if (host.contains(element) || element.contains(host)) {
|
|
589
|
+
fanOut(element);
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
/*
|
|
593
|
+
* An observed attr changed on `element`. A literal `attr("x")`
|
|
594
|
+
* read re-runs a matching subject; every compound that depends
|
|
595
|
+
* on the name says where its subjects went (see
|
|
596
|
+
* `CompoundDependency`).
|
|
597
|
+
*/
|
|
598
|
+
if (this.readsAttr(attr) && matchesRule(element)) mutateSelf();
|
|
599
|
+
for (const c of compounds) {
|
|
600
|
+
if (
|
|
601
|
+
c.selfAttrs.has(attr) &&
|
|
602
|
+
(c.isSubject
|
|
603
|
+
? matchesRule(element)
|
|
604
|
+
: this.matchesPrefix(element, c))
|
|
605
|
+
) {
|
|
606
|
+
reach(element, c);
|
|
607
|
+
}
|
|
608
|
+
if (c.looseAttrs.has(attr)) fanOut(rootFor(element, c.looseRoot));
|
|
609
|
+
if (c.hasAttrs.has(attr)) {
|
|
610
|
+
matchingAncestors(element, c).forEach((a) => reach(a, c));
|
|
611
|
+
}
|
|
612
|
+
if (c.broadAttrs.has(attr)) fanOut(host);
|
|
613
|
+
}
|
|
614
|
+
if (hostDeps) {
|
|
615
|
+
/*
|
|
616
|
+
* Host state changed on a `:scope[attr] …` rule, or a
|
|
617
|
+
* descendant named in `:scope:has(…)`. The host is the first
|
|
618
|
+
* path segment, so re-run the rule for its subtree.
|
|
619
|
+
*/
|
|
620
|
+
const affected =
|
|
621
|
+
element === host
|
|
622
|
+
? hostDeps.selfAttrs.has(attr) || hostDeps.looseAttrs.has(attr)
|
|
623
|
+
: hostDeps.hasAttrs.has(attr) || hostDeps.broadAttrs.has(attr);
|
|
624
|
+
if (affected) fanOut(host);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
// find the most distant ancestors
|
|
630
|
+
outermostElements([...roots]).forEach((ancestor) => {
|
|
631
|
+
/*
|
|
632
|
+
* Scoped rules clamp to the host so fan-out from an ancestor
|
|
633
|
+
* above it (cross-sheet binding owners, providers above scope)
|
|
634
|
+
* never leaks into sibling scopes. Unscoped rules run in the
|
|
635
|
+
* root context, so a host-level fan-out (RUN_ALL) expands to the
|
|
636
|
+
* root.
|
|
637
|
+
*/
|
|
638
|
+
const queryRoot = this.isScoped
|
|
639
|
+
? host.contains(ancestor)
|
|
640
|
+
? ancestor
|
|
641
|
+
: host
|
|
642
|
+
: ancestor === host
|
|
643
|
+
? (host.getRootNode() as Document | HTMLElement)
|
|
644
|
+
: ancestor;
|
|
645
|
+
if (targetsHost) {
|
|
646
|
+
if (queryRoot.contains(host)) elementsToMutate.add(host);
|
|
647
|
+
} else {
|
|
648
|
+
if (queryRoot instanceof Element) coverage?.add(queryRoot);
|
|
649
|
+
queryRoot.querySelectorAll(runSelector).forEach((el) => {
|
|
650
|
+
elementsToMutate.add(el as HTMLElement);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
this.numberOfRuns++;
|
|
654
|
+
});
|
|
655
|
+
// execute mutations
|
|
656
|
+
if (elementsToMutate.size > 0 && isInfoLogging()) {
|
|
657
|
+
QuarkLogger.info({
|
|
658
|
+
method: "foundElements",
|
|
659
|
+
runId: options.runId,
|
|
660
|
+
sheetId: this.quarkInstance.id,
|
|
661
|
+
ruleId: this.id,
|
|
662
|
+
elements: [[Array.from(elementsToMutate)]],
|
|
663
|
+
options,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
elementsToMutate.forEach((element) => {
|
|
667
|
+
this.runProps(mutationMap, element, options);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Whether `el` is compound `c`'s element: it matches the selector up
|
|
672
|
+
* to and including that compound, host-scoped when the rule is.
|
|
673
|
+
*/
|
|
674
|
+
matchesPrefix(el: Element, c: CompoundDependency): boolean {
|
|
675
|
+
// a leading combinator (`> span` after `:scope`) is only valid scoped;
|
|
676
|
+
// other prefixes stay bare, the fan-out root is clamped to the host
|
|
677
|
+
const selector =
|
|
678
|
+
c.prefixNeedsScope && this.isScoped ? this.scoped(c.prefix) : c.prefix;
|
|
679
|
+
return tc(() => el.matches(selector)) === true;
|
|
680
|
+
}
|
|
681
|
+
/** Whether `el` is a subject of this rule under `host` (scope-aware). */
|
|
682
|
+
matchesElement(el: Element, host: HTMLElement): boolean {
|
|
683
|
+
if (this.hostCompound !== null && !this.matchSelector) return el === host;
|
|
684
|
+
return (
|
|
685
|
+
!!this.matchSelector &&
|
|
686
|
+
(!this.isScoped || host.contains(el)) &&
|
|
687
|
+
el.matches(this.isScoped ? this.scopedSelector() : this.matchSelector)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
/** Selector cache for the current scope id (rebuilt after re-register). */
|
|
691
|
+
private scopedCache: { id: string; selectors: Map<string, string> } | null =
|
|
692
|
+
null;
|
|
693
|
+
/**
|
|
694
|
+
* `selector` prefixed with the host's `q-scope` marker so the native
|
|
695
|
+
* engine clamps every compound to strict descendants of the host (CSS
|
|
696
|
+
* `@scope`, see scope-id.ts). Bare selector until the sheet is
|
|
697
|
+
* registered.
|
|
698
|
+
*/
|
|
699
|
+
scoped(selector: string): string {
|
|
700
|
+
const id = this.quarkInstance.scopeId;
|
|
701
|
+
if (!id) return selector;
|
|
702
|
+
if (this.scopedCache?.id !== id) {
|
|
703
|
+
this.scopedCache = { id, selectors: new Map() };
|
|
704
|
+
}
|
|
705
|
+
let scoped = this.scopedCache.selectors.get(selector);
|
|
706
|
+
if (!scoped) {
|
|
707
|
+
scoped = `[${SCOPE_ATTR}="${id}"] ${selector}`;
|
|
708
|
+
this.scopedCache.selectors.set(selector, scoped);
|
|
709
|
+
}
|
|
710
|
+
return scoped;
|
|
711
|
+
}
|
|
712
|
+
/** The whole `matchSelector`, host-scoped (see `scoped`). */
|
|
713
|
+
scopedSelector(): string {
|
|
714
|
+
return this.scoped(this.matchSelector);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** True when a property body reads this name via literal `attr("x")`. */
|
|
718
|
+
readsAttr(attr: string): boolean {
|
|
719
|
+
const inProps = (p: Variable | Attribute | Listener) =>
|
|
720
|
+
p.referencedAttrNames.includes(attr);
|
|
721
|
+
return (
|
|
722
|
+
this.variables.some(inProps) ||
|
|
723
|
+
this.attributes.some(inProps) ||
|
|
724
|
+
this.listeners.some(inProps) ||
|
|
725
|
+
this.diagnostics.some(inProps) ||
|
|
726
|
+
this.delays.some(inProps)
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
runProps(
|
|
731
|
+
mutationMap: MutationMap,
|
|
732
|
+
element: HTMLElement,
|
|
733
|
+
options: QuarkOptions
|
|
734
|
+
) {
|
|
735
|
+
const { propertiesToRun } = options;
|
|
736
|
+
const mutatedProperties = Array.from(
|
|
737
|
+
mutationMap.get(element as HTMLElement) || []
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const trace = this.quarkInstance.runTrace;
|
|
741
|
+
const shouldRunProperty = (property: Variable | Attribute | Listener) => {
|
|
742
|
+
if (propertiesToRun && !propertiesToRun.includes(property)) return false;
|
|
743
|
+
// this second check ensures that we avoid running a given property if
|
|
744
|
+
// that property is what triggered the change to begin with. `class`
|
|
745
|
+
// is exempt: its tokens are separate facts (`.a { class: (b: true) }`
|
|
746
|
+
// must apply when `a` arrives); class cycles are cut by the loop guard
|
|
747
|
+
if (
|
|
748
|
+
property.key !== "class" &&
|
|
749
|
+
mutatedProperties.includes(property.key)
|
|
750
|
+
) {
|
|
751
|
+
// not refreshed by this run: a deferred write it reads must reach it
|
|
752
|
+
if (trace) recordSeq(trace.visited, element, property, 0);
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
return true;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
this.variables.forEach((v) => {
|
|
759
|
+
if (shouldRunProperty(v)) {
|
|
760
|
+
v.run(element, options);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
this.listeners.forEach((l) => {
|
|
764
|
+
if (shouldRunProperty(l)) {
|
|
765
|
+
l.run(element, options);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
this.attributes.forEach((a) => {
|
|
769
|
+
if (shouldRunProperty(a)) {
|
|
770
|
+
a.run(element, options);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
// after the writes: a diagnostic reads what this pass set; a delay
|
|
774
|
+
// restarts on every application of the rule
|
|
775
|
+
this.diagnostics.forEach((d) => {
|
|
776
|
+
if (shouldRunProperty(d)) {
|
|
777
|
+
d.run(element, options);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
this.delays.forEach((d) => {
|
|
781
|
+
if (shouldRunProperty(d)) {
|
|
782
|
+
d.run(element, options);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
filterPropertiesToRun(options: QuarkOptions) {
|
|
787
|
+
const allProps = [
|
|
788
|
+
...this.variables,
|
|
789
|
+
...this.attributes,
|
|
790
|
+
...this.listeners,
|
|
791
|
+
...this.diagnostics,
|
|
792
|
+
...this.delays,
|
|
793
|
+
];
|
|
794
|
+
return !options.properties
|
|
795
|
+
? allProps
|
|
796
|
+
: allProps.filter((prop) => {
|
|
797
|
+
return options.properties?.includes(prop);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|