@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/constants.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutation kind queued on the parent when elements are removed below it.
|
|
3
|
+
* Only rules whose match depends on children or sibling position react
|
|
4
|
+
* (`Rule.reactsToRemovals`). A sheet observes removals only if a rule
|
|
5
|
+
* does.
|
|
6
|
+
*/
|
|
7
|
+
export const CHILD_REMOVED = "CHILD_REMOVED";
|
|
8
|
+
|
|
9
|
+
// names Quark never observes (engine markers, inline style, the `content` key);
|
|
10
|
+
// `class` / `id` are observed (`.x` / `#x` gate like attributes)
|
|
11
|
+
export const ATTRIBUTE_BLACKLIST_REGEXES = [
|
|
12
|
+
/^q-/,
|
|
13
|
+
/^n-/,
|
|
14
|
+
/^style$/,
|
|
15
|
+
/^content$/,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* What an expression result does. Kept here so resolvers stay dumb:
|
|
20
|
+
*
|
|
21
|
+
* - wipe: remove the attr / clear content. `undefined` wipes too. A
|
|
22
|
+
* declaration that resolves to nothing clears its target (fresh over
|
|
23
|
+
* stale).
|
|
24
|
+
* - no-op: leave the target as-is. Opt-in via `preserve` (`content:
|
|
25
|
+
* $todo.title or preserve;` for loading). Failed evals also no-op,
|
|
26
|
+
* never wipe.
|
|
27
|
+
* - unset: variables only. Deletes the binding so descendants fall
|
|
28
|
+
* through (CSS `unset` / inherit). On attr / content it degrades to a
|
|
29
|
+
* wipe.
|
|
30
|
+
*/
|
|
31
|
+
export const SYMBOL_NOOP = Symbol("preserve");
|
|
32
|
+
export const SYMBOL_UNSET = Symbol("unset");
|
|
33
|
+
|
|
34
|
+
export const TYPES_WIPE: unknown[] = [undefined, null, SYMBOL_UNSET];
|
|
35
|
+
|
|
36
|
+
export const TYPES_NOOP: unknown[] = [SYMBOL_NOOP];
|
|
37
|
+
|
|
38
|
+
/** Keyword → value bindings available in every expression scope. */
|
|
39
|
+
export const VALUE_MAP = {
|
|
40
|
+
none: null,
|
|
41
|
+
preserve: SYMBOL_NOOP,
|
|
42
|
+
unset: SYMBOL_UNSET,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const isWipe = (value: unknown): boolean => TYPES_WIPE.includes(value);
|
|
46
|
+
|
|
47
|
+
export const isNoop = (value: unknown): boolean => TYPES_NOOP.includes(value);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static write-cycle warning, once per sheet at construction.
|
|
3
|
+
*
|
|
4
|
+
* A rule's selector gates on attrs (`[is-open]`, `:not([x])`, a literal
|
|
5
|
+
* `attr("x")` read) and its declarations write attrs. If A writes what
|
|
6
|
+
* B gates on and B writes what A gates on, they can re-trigger each
|
|
7
|
+
* other. Cycles that settle (Quark skips equal writes) are fine; ones
|
|
8
|
+
* that don't get cut by the loop guard. Still worth naming at build,
|
|
9
|
+
* once, with the rules involved.
|
|
10
|
+
*
|
|
11
|
+
* Self-edges (`input[value] { value: … }`) are the usual idempotent
|
|
12
|
+
* idiom and are not reported. `content`, `dataset`, `ariaset`, `class`,
|
|
13
|
+
* and `--custom-property` do not name an attr and are ignored. `@on`
|
|
14
|
+
* block rules never run in passes and are skipped.
|
|
15
|
+
*/
|
|
16
|
+
import { StyleProperty } from "./properties";
|
|
17
|
+
import type { Quark } from "./quark";
|
|
18
|
+
import type { Rule } from "./rule";
|
|
19
|
+
import { QuarkLogger } from "./utils";
|
|
20
|
+
import { LoopGuard } from "@excom/kit-utils";
|
|
21
|
+
|
|
22
|
+
const NON_ATTRIBUTE_KEYS = new Set(["content", "dataset", "ariaset", "class"]);
|
|
23
|
+
|
|
24
|
+
/** Attribute names a rule writes with plain `name: value` declarations. */
|
|
25
|
+
const writtenAttrs = (rule: Rule): Set<string> => {
|
|
26
|
+
const names = new Set<string>();
|
|
27
|
+
rule.attributes.forEach((prop) => {
|
|
28
|
+
if (prop instanceof StyleProperty) return;
|
|
29
|
+
if (NON_ATTRIBUTE_KEYS.has(prop.key)) return;
|
|
30
|
+
names.add(prop.key);
|
|
31
|
+
});
|
|
32
|
+
return names;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Strongly connected components with more than one rule (Tarjan). */
|
|
36
|
+
const cycles = (rules: Rule[], edges: Map<Rule, Set<Rule>>): Rule[][] => {
|
|
37
|
+
let index = 0;
|
|
38
|
+
const indices = new Map<Rule, number>();
|
|
39
|
+
const lowlinks = new Map<Rule, number>();
|
|
40
|
+
const onStack = new Set<Rule>();
|
|
41
|
+
const stack: Rule[] = [];
|
|
42
|
+
const found: Rule[][] = [];
|
|
43
|
+
const connect = (rule: Rule) => {
|
|
44
|
+
indices.set(rule, index);
|
|
45
|
+
lowlinks.set(rule, index);
|
|
46
|
+
index++;
|
|
47
|
+
stack.push(rule);
|
|
48
|
+
onStack.add(rule);
|
|
49
|
+
edges.get(rule)?.forEach((next) => {
|
|
50
|
+
if (!indices.has(next)) {
|
|
51
|
+
connect(next);
|
|
52
|
+
lowlinks.set(rule, Math.min(lowlinks.get(rule)!, lowlinks.get(next)!));
|
|
53
|
+
} else if (onStack.has(next)) {
|
|
54
|
+
lowlinks.set(rule, Math.min(lowlinks.get(rule)!, indices.get(next)!));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (lowlinks.get(rule) === indices.get(rule)) {
|
|
58
|
+
const component: Rule[] = [];
|
|
59
|
+
let member: Rule;
|
|
60
|
+
do {
|
|
61
|
+
member = stack.pop()!;
|
|
62
|
+
onStack.delete(member);
|
|
63
|
+
component.push(member);
|
|
64
|
+
} while (member !== rule);
|
|
65
|
+
if (component.length > 1) found.push(component.reverse());
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
rules.forEach((rule) => {
|
|
69
|
+
if (!indices.has(rule)) connect(rule);
|
|
70
|
+
});
|
|
71
|
+
return found;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Warn (once per sheet) when attr writes and selector gates form a
|
|
76
|
+
* cycle. Returns the cycles for tests / tooling.
|
|
77
|
+
*/
|
|
78
|
+
export const warnStaticCycles = (quark: Quark): Rule[][] => {
|
|
79
|
+
const rules = quark.rules.filter((rule) => !rule.isEventBlock);
|
|
80
|
+
if (rules.length < 2) return [];
|
|
81
|
+
const writes = new Map(rules.map((rule) => [rule, writtenAttrs(rule)]));
|
|
82
|
+
const edges = new Map<Rule, Set<Rule>>();
|
|
83
|
+
rules.forEach((from) => {
|
|
84
|
+
const written = writes.get(from)!;
|
|
85
|
+
if (!written.size) return;
|
|
86
|
+
rules.forEach((to) => {
|
|
87
|
+
if (to === from) return;
|
|
88
|
+
for (const name of written) {
|
|
89
|
+
if (to.observedAttrs.has(name)) {
|
|
90
|
+
let targets = edges.get(from);
|
|
91
|
+
if (!targets) edges.set(from, (targets = new Set()));
|
|
92
|
+
targets.add(to);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
if (!edges.size) return [];
|
|
99
|
+
const found = cycles(rules, edges);
|
|
100
|
+
found.forEach((component) => {
|
|
101
|
+
const members = new Set(component);
|
|
102
|
+
const description = component
|
|
103
|
+
.map((rule) => {
|
|
104
|
+
const shared = [...writes.get(rule)!].filter((name) =>
|
|
105
|
+
component.some(
|
|
106
|
+
(other) =>
|
|
107
|
+
other !== rule &&
|
|
108
|
+
members.has(other) &&
|
|
109
|
+
other.observedAttrs.has(name)
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
return `\`${rule.selector}\` writes [${shared.join(", ")}]`;
|
|
113
|
+
})
|
|
114
|
+
.join("; ");
|
|
115
|
+
QuarkLogger.warn({
|
|
116
|
+
method: "cycleCheck",
|
|
117
|
+
message: `Quark: rules gate on attributes they write for each other — ${description}. A cycle that never settles is cut by the loop guard after ${LoopGuard.limit} hops; make the writes converge or break the cycle.`,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return found;
|
|
121
|
+
};
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { evaluateExpression, getExpressionAst } from "./evaluator";
|
|
2
|
+
import { type Quark, QuarkRegistry } from "./quark";
|
|
3
|
+
import type { QuarkInternal, TQuarkElement } from "./quark-internal";
|
|
4
|
+
import type { Rule } from "./rule";
|
|
5
|
+
import { createScope } from "./variables";
|
|
6
|
+
import {
|
|
7
|
+
type DevtoolsRenderer,
|
|
8
|
+
NUCLEUS_DEVTOOLS_HOOK_VERSION,
|
|
9
|
+
registerRenderer,
|
|
10
|
+
toDevtoolsJson,
|
|
11
|
+
} from "@excom/kit-devtools";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Quark DevTools probe. Hook is in `@excom/kit-devtools`; this
|
|
15
|
+
* adds the Quark renderer and helpers.
|
|
16
|
+
*
|
|
17
|
+
* publicize(["quark", "sheet", "registered"], { weakElement: host, sheetId, … })
|
|
18
|
+
* publicize(["quark", "sheet", "unregistered"], { weakElement: host, sheetId })
|
|
19
|
+
* publicize(["quark", "apply"], { weakElement, selector, key, expression, result, … })
|
|
20
|
+
* publicize(["quark", "error"], { weakElement, selector, key, expression, errorMessage, … })
|
|
21
|
+
*
|
|
22
|
+
* `apply` is one property resolve (selector ≡ lifecycle, `{ key: result }`
|
|
23
|
+
* ≡ effect). Group a rule run by `runId`.
|
|
24
|
+
*/
|
|
25
|
+
export {
|
|
26
|
+
attachDevtools,
|
|
27
|
+
type DevtoolsHook,
|
|
28
|
+
getDevtoolsHook,
|
|
29
|
+
publicize,
|
|
30
|
+
} from "@excom/kit-devtools";
|
|
31
|
+
|
|
32
|
+
export type QuarkInspectSnapshot = {
|
|
33
|
+
tag: string;
|
|
34
|
+
id: string | null;
|
|
35
|
+
/** `$variable` bindings owned by this element. */
|
|
36
|
+
vars: Record<string, unknown>;
|
|
37
|
+
/** Attributes Quark has written on this element, with their current value. */
|
|
38
|
+
attributes: Record<string, string | null>;
|
|
39
|
+
/** CSS custom properties Quark has written on this element's inline style. */
|
|
40
|
+
styleProperties: Record<string, string | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Listeners from Quark `@on` at-rules, per event type, one entry per
|
|
43
|
+
* attached function, tagged with the declaration that produced it
|
|
44
|
+
* (`$expression`, e.g. `setOutputFromDetail`).
|
|
45
|
+
*/
|
|
46
|
+
listeners: Record<string, DevtoolsFunctionRef[]>;
|
|
47
|
+
/** `iterate()` row context, when this element is a rendered row. */
|
|
48
|
+
loop: { index: unknown; key: unknown } | null;
|
|
49
|
+
/** Sheet hashes that have touched this element. */
|
|
50
|
+
sheets: string[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** JSON-safe stand-in for a function, as rendered by DevTools. */
|
|
54
|
+
export type DevtoolsFunctionRef = {
|
|
55
|
+
$constructor: "Function";
|
|
56
|
+
/** Quark declaration source that produced it, when known. */
|
|
57
|
+
$expression: string | null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** `@on <events> [(options)]` handle text of a registered rule, if found. */
|
|
61
|
+
const listenerExpression = (
|
|
62
|
+
sheetHash: string,
|
|
63
|
+
ruleId: number,
|
|
64
|
+
slot: string
|
|
65
|
+
): string | null => {
|
|
66
|
+
const rule = QuarkRegistry.findRules(ruleId).find(
|
|
67
|
+
(r) => r.quarkInstance.hash === sheetHash
|
|
68
|
+
);
|
|
69
|
+
const listener = rule?.listeners.find((l) => l.key === slot);
|
|
70
|
+
if (!listener) return null;
|
|
71
|
+
// `@on click (handle: a) { … }` → "a { … }"; a bare block shows as "{ … }"
|
|
72
|
+
return [listener.handleText, listener.block ? "{ … }" : ""]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(" ");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** One declaration of a rule, as authored (`key: value`). */
|
|
78
|
+
export type QuarkDeclarationInfo = {
|
|
79
|
+
key: string;
|
|
80
|
+
value: string;
|
|
81
|
+
/** The `@view-transition` block the write sits in (`@view-transition (types: "t")`). */
|
|
82
|
+
transition?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** One `@warn` / `@debug` / `@error` statement of a rule. */
|
|
86
|
+
export type QuarkDiagnosticInfo = {
|
|
87
|
+
level: "warn" | "debug" | "error";
|
|
88
|
+
/** The statement's expression as authored. */
|
|
89
|
+
expression: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** One `@delay` block of a rule. */
|
|
93
|
+
export type QuarkDelayInfo = {
|
|
94
|
+
/** The duration expression as authored (`2000`, `$ms * 2`). */
|
|
95
|
+
duration: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** One `@on` at-rule of a rule. */
|
|
99
|
+
export type QuarkListenerInfo = {
|
|
100
|
+
/** Display key: `@on click`, `@on keydown (key: "Escape")`, `@on input, change (handle: save)`. */
|
|
101
|
+
key: string;
|
|
102
|
+
/** DOM event types the listener registers for. */
|
|
103
|
+
events: string[];
|
|
104
|
+
/** The `handle:` option as written, `""` when absent. */
|
|
105
|
+
handlers: string;
|
|
106
|
+
hasBlock: boolean;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** A rule as the agent tooling sees it (no element references). */
|
|
110
|
+
export type QuarkRuleInfo = {
|
|
111
|
+
sheetId: number;
|
|
112
|
+
ruleId: number;
|
|
113
|
+
/** Authored selector, nested rules expanded to their full line. */
|
|
114
|
+
selector: string;
|
|
115
|
+
isScoped: boolean;
|
|
116
|
+
numberOfRuns: number;
|
|
117
|
+
declarations: QuarkDeclarationInfo[];
|
|
118
|
+
listeners: QuarkListenerInfo[];
|
|
119
|
+
diagnostics: QuarkDiagnosticInfo[];
|
|
120
|
+
delays: QuarkDelayInfo[];
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** A registered sheet. `host` is live; callers serialize it themselves. */
|
|
124
|
+
export type QuarkSheetInfo = {
|
|
125
|
+
sheetId: number;
|
|
126
|
+
hash: string;
|
|
127
|
+
host: Element | null;
|
|
128
|
+
scopeId: string | null;
|
|
129
|
+
isScoped: boolean;
|
|
130
|
+
isRegistered: boolean;
|
|
131
|
+
ruleCount: number;
|
|
132
|
+
src: string;
|
|
133
|
+
rules: QuarkRuleInfo[];
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type QuarkRenderer = DevtoolsRenderer & {
|
|
137
|
+
kind: "quark";
|
|
138
|
+
isQuarkElement: (el: Element) => boolean;
|
|
139
|
+
inspect: (el: Element) => QuarkInspectSnapshot | null;
|
|
140
|
+
/** Every registered sheet with its rules (agent tooling). */
|
|
141
|
+
sheets: () => QuarkSheetInfo[];
|
|
142
|
+
/**
|
|
143
|
+
* Rules whose selector matches `el` right now, in definition order
|
|
144
|
+
* across sheets (later wins on conflicting writes). Scope-aware.
|
|
145
|
+
*/
|
|
146
|
+
matchingRules: (el: Element) => QuarkRuleInfo[];
|
|
147
|
+
/**
|
|
148
|
+
* Evaluate a Quark expression as if declared on a rule matching `el`
|
|
149
|
+
* (`$bindings`, `attr()`, `prop()`, `@use` modules of `sheetId` or of the
|
|
150
|
+
* first sheet touching the element). Returns the raw value; throws the
|
|
151
|
+
* evaluator's error. Read-only by the language's design, but a `@use`
|
|
152
|
+
* function may still side-effect.
|
|
153
|
+
*/
|
|
154
|
+
evaluate: (el: Element, expression: string, sheetId?: number) => unknown;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const liveSheets = (): Quark[] =>
|
|
158
|
+
QuarkRegistry.sheets
|
|
159
|
+
.map((ref) => ref.deref())
|
|
160
|
+
.filter((sheet): sheet is Quark => !!sheet);
|
|
161
|
+
|
|
162
|
+
const ruleInfo = (rule: Rule): QuarkRuleInfo => ({
|
|
163
|
+
sheetId: rule.quarkInstance.id,
|
|
164
|
+
ruleId: rule.id,
|
|
165
|
+
selector: rule.selector,
|
|
166
|
+
isScoped: rule.isScoped,
|
|
167
|
+
numberOfRuns: rule.numberOfRuns,
|
|
168
|
+
declarations: [...rule.variables, ...rule.attributes].map(
|
|
169
|
+
({ key, value, transition }) => ({
|
|
170
|
+
key,
|
|
171
|
+
value,
|
|
172
|
+
...(transition ? { transition: transition.source } : {}),
|
|
173
|
+
})
|
|
174
|
+
),
|
|
175
|
+
listeners: rule.listeners.map((listener) => ({
|
|
176
|
+
key: listener.key,
|
|
177
|
+
events: listener.eventTypes,
|
|
178
|
+
handlers: listener.handleText,
|
|
179
|
+
hasBlock: !!listener.block,
|
|
180
|
+
})),
|
|
181
|
+
diagnostics: rule.diagnostics.map(({ level, value }) => ({
|
|
182
|
+
level,
|
|
183
|
+
expression: value,
|
|
184
|
+
})),
|
|
185
|
+
delays: rule.delays.map(({ value }) => ({ duration: value })),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const sheetInfo = (sheet: Quark): QuarkSheetInfo => ({
|
|
189
|
+
sheetId: sheet.id,
|
|
190
|
+
hash: sheet.hash,
|
|
191
|
+
host: sheet.host?.deref() ?? null,
|
|
192
|
+
scopeId: sheet.scopeId,
|
|
193
|
+
isScoped: !!sheet.options.isScoped,
|
|
194
|
+
isRegistered: sheet.isRegistered,
|
|
195
|
+
ruleCount: sheet.rules.length,
|
|
196
|
+
src: sheet.src,
|
|
197
|
+
rules: sheet.rules.map(ruleInfo),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const rulesMatching = (el: Element): Rule[] =>
|
|
201
|
+
liveSheets().flatMap((sheet) => {
|
|
202
|
+
const host = sheet.host?.deref();
|
|
203
|
+
if (!host || !sheet.isRegistered) return [];
|
|
204
|
+
return sheet.rules.filter((rule) => rule.matchesElement(el, host));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const evaluateOn = (
|
|
208
|
+
el: Element,
|
|
209
|
+
expression: string,
|
|
210
|
+
sheetId?: number
|
|
211
|
+
): unknown => {
|
|
212
|
+
// module scope comes from a rule's sheet; pick the requested sheet, else
|
|
213
|
+
// the first one whose rules match the element, else any registered sheet
|
|
214
|
+
const sheets = liveSheets();
|
|
215
|
+
const sheet =
|
|
216
|
+
sheetId !== undefined
|
|
217
|
+
? sheets.find((s) => s.id === sheetId)
|
|
218
|
+
: (rulesMatching(el)[0]?.quarkInstance ?? sheets[0]);
|
|
219
|
+
const rule = sheet?.rules[0];
|
|
220
|
+
const scope = createScope({
|
|
221
|
+
element: el as HTMLElement,
|
|
222
|
+
options: rule ? { rule } : {},
|
|
223
|
+
});
|
|
224
|
+
return evaluateExpression(getExpressionAst(expression), {
|
|
225
|
+
scope,
|
|
226
|
+
element: el,
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const inspectInternal = (
|
|
231
|
+
el: Element,
|
|
232
|
+
internal: QuarkInternal
|
|
233
|
+
): QuarkInspectSnapshot => {
|
|
234
|
+
const vars: Record<string, unknown> = {};
|
|
235
|
+
for (const name of Object.keys(internal.vars)) {
|
|
236
|
+
try {
|
|
237
|
+
vars[name] = toDevtoolsJson(internal.getVar(name));
|
|
238
|
+
} catch {
|
|
239
|
+
vars[name] = "[unreadable]";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const attributes: Record<string, string | null> = {};
|
|
243
|
+
const styleProperties: Record<string, string | null> = {};
|
|
244
|
+
const listeners: Record<string, DevtoolsFunctionRef[]> = {};
|
|
245
|
+
const style = (el as HTMLElement).style;
|
|
246
|
+
for (const [sheetHash, instance] of Object.entries(internal.instances)) {
|
|
247
|
+
for (const name of Object.keys(instance.attributes)) {
|
|
248
|
+
if (name === "content") continue;
|
|
249
|
+
if (name.startsWith("--")) {
|
|
250
|
+
const value = style?.getPropertyValue(name);
|
|
251
|
+
styleProperties[name] = value ? value : null;
|
|
252
|
+
} else {
|
|
253
|
+
attributes[name] = el.getAttribute(name);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const [ruleId, rule] of Object.entries(instance.rules)) {
|
|
257
|
+
for (const [slot, fns] of Object.entries(rule.listeners)) {
|
|
258
|
+
const attached = fns.filter(Boolean);
|
|
259
|
+
if (!attached.length) continue;
|
|
260
|
+
const $expression = listenerExpression(sheetHash, Number(ruleId), slot);
|
|
261
|
+
// keyed by events plus options: `click`, `keydown (key: "Escape")`, `input, change (debounce: 300)`
|
|
262
|
+
const eventType = slot.replace(/^@on /, "");
|
|
263
|
+
listeners[eventType] = [
|
|
264
|
+
...(listeners[eventType] ?? []),
|
|
265
|
+
...attached.map(() => ({
|
|
266
|
+
$constructor: "Function" as const,
|
|
267
|
+
$expression,
|
|
268
|
+
})),
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const { index, key } = internal.loop;
|
|
274
|
+
return {
|
|
275
|
+
tag: el.localName,
|
|
276
|
+
id: el.id || null,
|
|
277
|
+
vars,
|
|
278
|
+
attributes,
|
|
279
|
+
styleProperties,
|
|
280
|
+
listeners,
|
|
281
|
+
loop:
|
|
282
|
+
index !== undefined || key !== undefined
|
|
283
|
+
? { index: index ?? null, key: toDevtoolsJson(key ?? null) }
|
|
284
|
+
: null,
|
|
285
|
+
sheets: Object.keys(internal.instances),
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const renderer: QuarkRenderer = {
|
|
290
|
+
version: NUCLEUS_DEVTOOLS_HOOK_VERSION,
|
|
291
|
+
kind: "quark",
|
|
292
|
+
isQuarkElement: (el) => !!(el as TQuarkElement)?._q_,
|
|
293
|
+
inspect: (el) => {
|
|
294
|
+
const internal = (el as TQuarkElement)?._q_;
|
|
295
|
+
if (!internal) return null;
|
|
296
|
+
return inspectInternal(el, internal);
|
|
297
|
+
},
|
|
298
|
+
sheets: () => liveSheets().map(sheetInfo),
|
|
299
|
+
matchingRules: (el) => rulesMatching(el).map(ruleInfo),
|
|
300
|
+
evaluate: evaluateOn,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
registerRenderer(renderer);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `element.quark`: the JS side of `$variable` bindings, shaped like
|
|
3
|
+
* `element.style` (`setProperty` / `removeProperty` / `getPropertyValue`).
|
|
4
|
+
*
|
|
5
|
+
* The element you call it on becomes the binding's owner, exactly as a
|
|
6
|
+
* rule declaring `$name` there would: descendants (in any sheet) resolve
|
|
7
|
+
* it by walking up, a nearer owner shadows it, and a real change fires
|
|
8
|
+
* the bubbling `quark-binding-change` that re-runs readers. It works on
|
|
9
|
+
* elements no rule has matched — that is the owner case. Writes carry
|
|
10
|
+
* `sheetId: null` so DevTools can tell a JS write from a rule's.
|
|
11
|
+
*
|
|
12
|
+
* Change detection is by value (`hashObject` for objects): pass a new
|
|
13
|
+
* object, an in-place mutation is invisible — same as a rule. Primitives
|
|
14
|
+
* that should be selectable belong in attributes (`setAttribute` +
|
|
15
|
+
* `attr()`); this API earns its place for rich values and for `$name`
|
|
16
|
+
* reads in a subtree. One writer per name per element: a rule that
|
|
17
|
+
* declares `$name` on the same element rewrites a JS value when its def
|
|
18
|
+
* re-runs (last writer wins; revisited with specificity / priority).
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
type BindingChangeDetail,
|
|
22
|
+
dispatchBindingChange,
|
|
23
|
+
readBinding,
|
|
24
|
+
writeBinding,
|
|
25
|
+
} from "./bindings";
|
|
26
|
+
import { SYMBOL_UNSET } from "./constants";
|
|
27
|
+
|
|
28
|
+
/** `count` and `$count` both name the `$count` binding. */
|
|
29
|
+
const bindingName = (name: string): string =>
|
|
30
|
+
name.startsWith("$") ? name : `$${name}`;
|
|
31
|
+
|
|
32
|
+
const JS_WRITE: Omit<BindingChangeDetail, "name"> = { sheetId: null };
|
|
33
|
+
|
|
34
|
+
export class QuarkElementApi {
|
|
35
|
+
private readonly element: Element;
|
|
36
|
+
constructor(element: Element) {
|
|
37
|
+
this.element = element;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Write a binding on this element. Returns `true` when the stored value
|
|
41
|
+
* changed (readers re-run synchronously before it returns).
|
|
42
|
+
*/
|
|
43
|
+
setProperty(name: string, value: unknown): boolean {
|
|
44
|
+
return writeBinding(this.element, bindingName(name), value, JS_WRITE);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Write several bindings, announcing them after all are stored, so a
|
|
48
|
+
* reader of two names sees both new values on its one re-run.
|
|
49
|
+
*/
|
|
50
|
+
setProperties(values: Record<string, unknown>): boolean {
|
|
51
|
+
const pending: BindingChangeDetail[] = [];
|
|
52
|
+
let changed = false;
|
|
53
|
+
for (const [name, value] of Object.entries(values)) {
|
|
54
|
+
changed =
|
|
55
|
+
writeBinding(
|
|
56
|
+
this.element,
|
|
57
|
+
bindingName(name),
|
|
58
|
+
value,
|
|
59
|
+
JS_WRITE,
|
|
60
|
+
(_owner, detail) => {
|
|
61
|
+
pending.push(detail);
|
|
62
|
+
}
|
|
63
|
+
) || changed;
|
|
64
|
+
}
|
|
65
|
+
pending.forEach((detail) => dispatchBindingChange(this.element, detail));
|
|
66
|
+
return changed;
|
|
67
|
+
}
|
|
68
|
+
/** `unset`: delete the binding so descendants fall through to an ancestor. */
|
|
69
|
+
removeProperty(name: string): boolean {
|
|
70
|
+
return writeBinding(
|
|
71
|
+
this.element,
|
|
72
|
+
bindingName(name),
|
|
73
|
+
SYMBOL_UNSET,
|
|
74
|
+
JS_WRITE
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
/** The value a rule on this element would read (nearest owner, self first). */
|
|
78
|
+
getPropertyValue(name: string): unknown {
|
|
79
|
+
return readBinding(this.element, bindingName(name));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
declare global {
|
|
84
|
+
interface Element {
|
|
85
|
+
/** Quark bindings on this element, see `QuarkElementApi`. */
|
|
86
|
+
readonly quark: QuarkElementApi;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const handles = new WeakMap<Element, QuarkElementApi>();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Define the lazy `quark` accessor on `Element.prototype` (SVG nodes can
|
|
94
|
+
* own bindings too). Idempotent; `configurable` so tests can remove it.
|
|
95
|
+
*/
|
|
96
|
+
export const installElementApi = (
|
|
97
|
+
proto: object = typeof Element === "undefined" ? {} : Element.prototype
|
|
98
|
+
): void => {
|
|
99
|
+
if (Object.getOwnPropertyDescriptor(proto, "quark")) return;
|
|
100
|
+
Object.defineProperty(proto, "quark", {
|
|
101
|
+
configurable: true,
|
|
102
|
+
enumerable: false,
|
|
103
|
+
get(this: Element): QuarkElementApi {
|
|
104
|
+
let handle = handles.get(this);
|
|
105
|
+
if (!handle) handles.set(this, (handle = new QuarkElementApi(this)));
|
|
106
|
+
return handle;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
};
|