@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/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
|
+
};
|
package/src/variables.ts
ADDED
|
@@ -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,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>
|