@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/language.ts
ADDED
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language metadata as data: value keywords, declaration kinds, built-ins,
|
|
3
|
+
* and methods an expression may call. The engine never imports this
|
|
4
|
+
* module. Prose-free tables live in `language-tables.ts`
|
|
5
|
+
* (`METHOD_ALLOWLIST`, `PSEUDO_CLASS_SUPPORT`) so docs stay out of
|
|
6
|
+
* production. Tests check `variables.ts` / `constants.ts` against it;
|
|
7
|
+
* `support/scripts/build-language-docs.mjs` renders the README language
|
|
8
|
+
* reference. Published as `@excom/quark/language`. No deps, so plain
|
|
9
|
+
* Node can import it.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
ALLOWED_METHOD_NAMES,
|
|
13
|
+
METHOD_ALLOWLIST,
|
|
14
|
+
PSEUDO_CLASS_SUPPORT,
|
|
15
|
+
type PseudoClassKind,
|
|
16
|
+
type PseudoClassSupport,
|
|
17
|
+
} from "./language-tables";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
ALLOWED_METHOD_NAMES,
|
|
21
|
+
METHOD_ALLOWLIST,
|
|
22
|
+
PSEUDO_CLASS_SUPPORT,
|
|
23
|
+
type PseudoClassKind,
|
|
24
|
+
type PseudoClassSupport,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface KeywordDoc {
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bare words with a fixed meaning in every expression scope. Resolve
|
|
34
|
+
* before `@use` exports and built-ins.
|
|
35
|
+
*/
|
|
36
|
+
export const VALUE_KEYWORDS: readonly KeywordDoc[] = [
|
|
37
|
+
{
|
|
38
|
+
name: "none",
|
|
39
|
+
description:
|
|
40
|
+
"`null`: removes the attribute / CSS variable, clears content, " +
|
|
41
|
+
"or stores `null` in a `$variable`.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "preserve",
|
|
45
|
+
description:
|
|
46
|
+
"Explicit no-op: leaves the attribute / content / binding exactly " +
|
|
47
|
+
"as it is. Idiom for loading states: `content: $todo.title or preserve`.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "unset",
|
|
51
|
+
description:
|
|
52
|
+
"`$variables` only: deletes the binding from the matched element so " +
|
|
53
|
+
"consumers fall through to the next ancestor. On any other target it " +
|
|
54
|
+
"degrades to a wipe.",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export interface DeclarationKindDoc {
|
|
59
|
+
/** Key shape as written in a sheet. */
|
|
60
|
+
key: string;
|
|
61
|
+
/** What the resolved value does to the matched element. */
|
|
62
|
+
description: string;
|
|
63
|
+
/** Accepted result shapes, in prose. */
|
|
64
|
+
accepts: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Declaration kinds, dispatched on the property key. First matching row
|
|
69
|
+
* wins (`$…` before `--…`, named keys before the attr fallback).
|
|
70
|
+
* Listeners are not declarations: see `AT_RULES`.
|
|
71
|
+
*/
|
|
72
|
+
export const DECLARATION_KINDS: readonly DeclarationKindDoc[] = [
|
|
73
|
+
{
|
|
74
|
+
key: "$name",
|
|
75
|
+
description:
|
|
76
|
+
"Stores a binding on each matched element; consumers resolve it " +
|
|
77
|
+
"by walking up from their own element (CSS custom-property " +
|
|
78
|
+
"semantics, shared across sheets).",
|
|
79
|
+
accepts: "Any value. `unset` deletes the binding.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "--name",
|
|
83
|
+
description:
|
|
84
|
+
"Writes the CSS custom property `--name` on the element's inline " +
|
|
85
|
+
"style. Write-only: Quark never reads CSS variables back. A trailing " +
|
|
86
|
+
"`!important` inside the string sets the priority.",
|
|
87
|
+
accepts:
|
|
88
|
+
'A string or number. CSS literals must be quoted (`"#ccc"`, ' +
|
|
89
|
+
'`"10px"`); bare `#hex` / unit numbers are rejected at build.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "content",
|
|
93
|
+
description:
|
|
94
|
+
"Replaces the element's rendered children (a source `<template>` " +
|
|
95
|
+
"child is kept). Promises are awaited. Writing into a `<template>` " +
|
|
96
|
+
"targets its `.content`. On a `<textarea>` a text result is also " +
|
|
97
|
+
"mirrored to the live `.value` (the text is only the default value).",
|
|
98
|
+
accepts:
|
|
99
|
+
"A string (text), a `Node` / `NodeList`, or the result of " +
|
|
100
|
+
"`template()` / `iterate()` / `dangerous-html()`. Wipe values clear.",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
key: "class",
|
|
104
|
+
description: "Sets the `class` attribute.",
|
|
105
|
+
accepts:
|
|
106
|
+
"A string (replaces), an array (joined with spaces), or an object " +
|
|
107
|
+
"(`{ name: boolean }` toggles each class). Wipe values remove the " +
|
|
108
|
+
"attribute.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "dataset",
|
|
112
|
+
description:
|
|
113
|
+
"Writes one `data-*` attribute per key (camelCase → dash-case) and " +
|
|
114
|
+
"removes `data-*` attributes this sheet set earlier.",
|
|
115
|
+
accepts:
|
|
116
|
+
"An object. Strings / numbers write as-is, booleans as present / " +
|
|
117
|
+
"absent, string arrays space-joined, objects and other arrays as " +
|
|
118
|
+
"their length.",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "ariaset",
|
|
122
|
+
description: "Same as `dataset`, with the `aria-` prefix.",
|
|
123
|
+
accepts: "An object (same conversions as `dataset`).",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "<anything else>",
|
|
127
|
+
description:
|
|
128
|
+
"Sets the attribute of that name on the matched element (`none` " +
|
|
129
|
+
"removes it). Always contains a dash in practice; " +
|
|
130
|
+
'`autofocus: ""` sets a boolean attribute. On native form controls ' +
|
|
131
|
+
"the attribute is authoritative: `value` / `checked` on `<input>` " +
|
|
132
|
+
"and `selected` on `<option>` also set the live property, so a " +
|
|
133
|
+
"control the user has edited still follows the rule.",
|
|
134
|
+
accepts:
|
|
135
|
+
'A string or number (written as text), a boolean (`true` → `""`, ' +
|
|
136
|
+
"`false` → removed), or a wipe value.",
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
export interface AtRuleDoc {
|
|
141
|
+
/** Statement shape as written in a sheet. */
|
|
142
|
+
syntax: string;
|
|
143
|
+
description: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Quark's at-rules — the whole set the language has. Any other name
|
|
148
|
+
* (`@media`, `@if`, `@keyframes`, …) is a parse error.
|
|
149
|
+
*/
|
|
150
|
+
export const AT_RULES: readonly AtRuleDoc[] = [
|
|
151
|
+
{
|
|
152
|
+
syntax: '@use "url" [as name | as *];',
|
|
153
|
+
description:
|
|
154
|
+
"Imports a JS module anywhere in the sheet. The namespace defaults to " +
|
|
155
|
+
"the URL's last path segment without its extension; `as *` merges " +
|
|
156
|
+
"exports into the bare scope, last import winning. A `with (…)` " +
|
|
157
|
+
"clause is a parse error.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
syntax: "@scope { … }",
|
|
161
|
+
description:
|
|
162
|
+
"Rules inside stay anchored to the host in a global sheet (the " +
|
|
163
|
+
"implicit wrapper of a scoped sheet). It takes no prelude.",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
syntax: "@on <event>[, <event>] [(options)] { … }",
|
|
167
|
+
description:
|
|
168
|
+
"Inside a rule: listens for the events (bare names such as `click` " +
|
|
169
|
+
"or `super-form-success`, or strings; a comma list shares one " +
|
|
170
|
+
"listener) on the matched element and applies the block once per " +
|
|
171
|
+
"event — a one-shot transaction. The block is an ordinary rule body: " +
|
|
172
|
+
"declarations write the matched element (attributes, `$variables`, " +
|
|
173
|
+
"`--props`, `content`), nested rules write its matching descendants, " +
|
|
174
|
+
"or its siblings when the nested selector starts with `+` / `~`, " +
|
|
175
|
+
"`@dispatch` / `@command` statements fire after those writes are " +
|
|
176
|
+
"queued. `event` names the DOM event and `target` the delegate (or " +
|
|
177
|
+
"`event.target`) inside the block and in its per-event options. " +
|
|
178
|
+
"`@on` inside a block is not supported.",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
syntax: "@on <event> (option, option: value) …",
|
|
182
|
+
description:
|
|
183
|
+
"An options group after the events gates and configures the " +
|
|
184
|
+
"listener; with it the block is optional (`@on submit " +
|
|
185
|
+
"(prevent-default);`). A bare name is a flag. Filters: `target: " +
|
|
186
|
+
'"<selector>"` (delegation — fires only when the event target is ' +
|
|
187
|
+
"inside a matching descendant; that element is `target` in the " +
|
|
188
|
+
"block), `self` (only when the event target is the matched " +
|
|
189
|
+
'element), `key: "Escape"` / `"Shift+K"` (keyboard chords; ' +
|
|
190
|
+
"space-separated alternatives). Event flags: `prevent-default`, " +
|
|
191
|
+
"`stop-propagation`, `stop-immediate-propagation`. Timing: " +
|
|
192
|
+
"`debounce: <ms>`, `throttle: <ms>`. JS: `handle: fn` — a function " +
|
|
193
|
+
"(or a call returning one, or a list `(a, b)`) called with the event " +
|
|
194
|
+
"before the block, `this` being the element. Registration: `once` " +
|
|
195
|
+
"(removed after the first event that passes the filters), `passive`, " +
|
|
196
|
+
"`capture`, `host: window` / `host: document` (listen there while the " +
|
|
197
|
+
"element is connected; `target` then resolves against the whole " +
|
|
198
|
+
"document). `target`, `key`, `debounce`, `throttle` and `handle` are " +
|
|
199
|
+
"evaluated when the event fires, in the block's scope; the rest once " +
|
|
200
|
+
"per match. Two `@on`s for one event may coexist when their options " +
|
|
201
|
+
"differ.",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
syntax: "@dispatch <event>[, <event>] [(options)];",
|
|
205
|
+
description:
|
|
206
|
+
"Inside an `@on` block (or a nested rule / `@delay` block within " +
|
|
207
|
+
"one): dispatches a `CustomEvent` of each name from the block's " +
|
|
208
|
+
"element after the block's writes are queued — synchronously, " +
|
|
209
|
+
"before they paint, so the event is an occurrence, not a delivery " +
|
|
210
|
+
"of State. Options, evaluated per event: `detail: <expression>`; " +
|
|
211
|
+
'`target: "<selector>"` (every match in the element\'s document; ' +
|
|
212
|
+
"`:scope` = the block's element, not the sheet host — resolved as " +
|
|
213
|
+
"`<event-handler target-ref>` is) or `target: <element | list>` " +
|
|
214
|
+
'(`closest("provider-fetch")`); `host: window` / `host: document`; ' +
|
|
215
|
+
'`form: "<selector>"` or `form: <form>` (its field values become ' +
|
|
216
|
+
"the detail, an explicit `detail` map merges over them); the flags " +
|
|
217
|
+
"`bubbles` (default true), `cancelable` (default true), `composed` " +
|
|
218
|
+
"(default false), each settable to `false`. Dispatching the " +
|
|
219
|
+
"enclosing `@on` event is refused; every dispatch is one loop-guard " +
|
|
220
|
+
"hop, so an event cycle is cut. Not allowed at rule level: a rule " +
|
|
221
|
+
"matching is not an occurrence.",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
syntax: "@command <name>[, <name>] [(target: …)];",
|
|
225
|
+
description:
|
|
226
|
+
"Inside an `@on` block: invokes each command on the target elements " +
|
|
227
|
+
"(the block's element by default; `target` as for `@dispatch`) the " +
|
|
228
|
+
"way a `<button command commandfor>` would — native commands " +
|
|
229
|
+
"(`show-modal`, `close`, `request-close`, `show-popover`, " +
|
|
230
|
+
"`hide-popover`, `toggle-popover`) and custom `--names`, which " +
|
|
231
|
+
"reach the target as a `command` event. Where the browser lacks the " +
|
|
232
|
+
"Invoker Commands API, custom commands are dispatched as a synthetic " +
|
|
233
|
+
"`command` event and native ones call the element's method. Only " +
|
|
234
|
+
"`target` is an option.",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
syntax: "@view-transition [(options)] { … }",
|
|
238
|
+
description:
|
|
239
|
+
"Inside a rule, around rules, or inside an `@on` block: every paint " +
|
|
240
|
+
"of the writes in the block — its declarations (on the rule's " +
|
|
241
|
+
"element) and its nested rules' — commits inside " +
|
|
242
|
+
"`document.startViewTransition()`, so CSS animates the change " +
|
|
243
|
+
"(`view-transition-name`, `::view-transition-*`). It scopes *how* " +
|
|
244
|
+
"writes land, never *when* rules run. The transition waits for " +
|
|
245
|
+
"Quark to settle before the new state is captured, so writes that " +
|
|
246
|
+
"react to these land in the same cut. Committed without a " +
|
|
247
|
+
"transition: writes that change nothing, the sheet's first render, " +
|
|
248
|
+
"`prefers-reduced-motion: reduce`, browsers without the API, and " +
|
|
249
|
+
"writes while another view transition is active.",
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
syntax: "@view-transition (option, option: value) { … }",
|
|
253
|
+
description:
|
|
254
|
+
'`types: "a b"` names the transition for ' +
|
|
255
|
+
"`:active-view-transition-type()` (a string or a list). " +
|
|
256
|
+
"`timeout: <ms>` caps the settle wait (default 300). " +
|
|
257
|
+
"`delay: <ms>` holds these writes back first. `first-render` also " +
|
|
258
|
+
"animates the sheet's first render. `if-active: skip | replace`: " +
|
|
259
|
+
"while another transition runs, commit unanimated (default) or " +
|
|
260
|
+
"start anyway, which skips the running one. " +
|
|
261
|
+
'`until: "<selector>"` keeps the transition open until the ' +
|
|
262
|
+
"block's element matches the selector, `until: <promise>` until " +
|
|
263
|
+
"it settles (default timeout 1000; the page is frozen meanwhile, so " +
|
|
264
|
+
"for short waits only). Values are evaluated per write.",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
syntax: "@delay <ms> { … }",
|
|
268
|
+
description:
|
|
269
|
+
"Inside a rule or an `@on` / `@delay` block: applies the block once, " +
|
|
270
|
+
"`<ms>` milliseconds (an expression) after the rule applied or the " +
|
|
271
|
+
"event fired — provided the element is still in the document and " +
|
|
272
|
+
"the rule still matches; otherwise the block is dropped. Applying " +
|
|
273
|
+
"the rule again restarts the timer (one per element). The block is " +
|
|
274
|
+
"an ordinary rule body (declarations write the matched element, " +
|
|
275
|
+
"nested rules its descendants; `event` / `target` are kept inside an " +
|
|
276
|
+
"`@on` block). Timers keep the loop guard's causal depth and are " +
|
|
277
|
+
"cleared when the sheet unregisters.",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
syntax: "@warn <expression>; / @debug <expression>; / @error <expression>;",
|
|
281
|
+
description:
|
|
282
|
+
"Inside a rule or a block: evaluates the expression on the matched " +
|
|
283
|
+
"element and reports it — to the console at that level (`@debug` " +
|
|
284
|
+
"is silent below debug logging) and to DevTools as " +
|
|
285
|
+
"`quark/diagnostic`. The selector is the condition " +
|
|
286
|
+
'(`img:not([alt]) { @warn "img needs alt"; }`). `@warn` / `@error` ' +
|
|
287
|
+
"speak once per element and rule; `@debug` speaks on every " +
|
|
288
|
+
"application, so it re-logs when a binding or `prop()` it reads " +
|
|
289
|
+
"changes. A comma list reports one value per item.",
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
export interface BuiltinDoc {
|
|
294
|
+
name: string;
|
|
295
|
+
/** Call signature, or the bare name for value built-ins. */
|
|
296
|
+
signature: string;
|
|
297
|
+
description: string;
|
|
298
|
+
/** Presentation group in the reference. */
|
|
299
|
+
group: "element" | "loop" | "render" | "event" | "state" | "util" | "debug";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Built-in expression names. Resolved after value keywords and `@use`
|
|
304
|
+
* exports, so a same-name module export shadows the built-in. Runtime
|
|
305
|
+
* lives in `variables.ts` (`BUILTINS`); a test keeps the lists in sync.
|
|
306
|
+
*/
|
|
307
|
+
export const BUILTIN_FUNCTIONS: readonly BuiltinDoc[] = [
|
|
308
|
+
{
|
|
309
|
+
name: "attr",
|
|
310
|
+
signature: 'attr("name")',
|
|
311
|
+
description:
|
|
312
|
+
"The matched element's attribute value (`null` when absent). " +
|
|
313
|
+
'`attr("content")` returns its `innerHTML`. A literal name is ' +
|
|
314
|
+
"observed: the rule re-runs when that attribute changes, even if it " +
|
|
315
|
+
"is not in the selector. `attr($name)` reads but does not subscribe.",
|
|
316
|
+
group: "element",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "prop",
|
|
320
|
+
signature: 'prop("name")',
|
|
321
|
+
description:
|
|
322
|
+
'The matched element\'s JS property (`prop("provision")` reads a ' +
|
|
323
|
+
"Neutron provision). A literal name is observed: the rule re-runs " +
|
|
324
|
+
"when JS assigns `element.name` (coalesced per microtask). In-place " +
|
|
325
|
+
"mutation and browser-driven native state are not observed. " +
|
|
326
|
+
"`prop($name)` reads but does not subscribe.",
|
|
327
|
+
group: "element",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "closest",
|
|
331
|
+
signature: 'closest("selector")',
|
|
332
|
+
description:
|
|
333
|
+
"`element.closest(selector)` from the matched element: the nearest " +
|
|
334
|
+
"ancestor-or-self matching the selector, else `null`. Not observed.",
|
|
335
|
+
group: "element",
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "element",
|
|
339
|
+
signature: "element",
|
|
340
|
+
description:
|
|
341
|
+
"The matched element itself — the node the rule is applied to " +
|
|
342
|
+
"(inside an `@on … { }` block the listening element; `target` is the " +
|
|
343
|
+
"delegate). Hand it to `@use` functions that need the node: " +
|
|
344
|
+
"`@on click fire(element)`, `$chart: mount(element)`. Reads through " +
|
|
345
|
+
"it are not observed — use `attr()` / `prop()` for reactive reads.",
|
|
346
|
+
group: "element",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "item",
|
|
350
|
+
signature: "item",
|
|
351
|
+
description:
|
|
352
|
+
"Inside an `iterate()` row: the current collection item (the value " +
|
|
353
|
+
"for objects). `undefined` outside a row.",
|
|
354
|
+
group: "loop",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "index",
|
|
358
|
+
signature: "index",
|
|
359
|
+
description:
|
|
360
|
+
"Inside an `iterate()` row: the current position (the key for " +
|
|
361
|
+
"objects). `undefined` outside a row.",
|
|
362
|
+
group: "loop",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "iterate",
|
|
366
|
+
signature: 'iterate(collection, "template-ref"?, "key-property"?)',
|
|
367
|
+
description:
|
|
368
|
+
"For `content`: renders one clone of the element's `<template>` " +
|
|
369
|
+
"child (or the template at `template-ref`, a selector / URL) per " +
|
|
370
|
+
"array item or object entry, keyed by `key-property` (else a content " +
|
|
371
|
+
"hash) so existing rows are reused. `null` / `undefined` wipes the " +
|
|
372
|
+
"rows; an empty collection clears them; a non-collection no-ops.",
|
|
373
|
+
group: "render",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "template",
|
|
377
|
+
signature: 'template("template-ref"?)',
|
|
378
|
+
description:
|
|
379
|
+
"For `content`: renders one clone of the referenced `<template>` " +
|
|
380
|
+
"(selector or URL; defaults to the element's own `<template>` " +
|
|
381
|
+
"child).",
|
|
382
|
+
group: "render",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "dangerous-html",
|
|
386
|
+
signature: "dangerous-html(html)",
|
|
387
|
+
description:
|
|
388
|
+
"For `content`: sets `innerHTML` to the string. No sanitizing: " +
|
|
389
|
+
"never pass user-controlled markup.",
|
|
390
|
+
group: "render",
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "event",
|
|
394
|
+
signature: "event",
|
|
395
|
+
description:
|
|
396
|
+
"Inside an `@on … { }` block, its per-event options and its " +
|
|
397
|
+
"`@dispatch` / `@command` statements: the DOM event being handled " +
|
|
398
|
+
"(`event.target`, `event.detail`, …). `undefined` elsewhere.",
|
|
399
|
+
group: "event",
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: "target",
|
|
403
|
+
signature: "target",
|
|
404
|
+
description:
|
|
405
|
+
"Inside an `@on … { }` block and its per-event options: the element " +
|
|
406
|
+
"the `target:` option matched (the delegate), or `event.target` " +
|
|
407
|
+
"without that option. `undefined` elsewhere.",
|
|
408
|
+
group: "event",
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "prevent-default",
|
|
412
|
+
signature: "prevent-default",
|
|
413
|
+
description:
|
|
414
|
+
"A listener that calls `event.preventDefault()`, for `handle:`. The " +
|
|
415
|
+
"`(prevent-default)` flag is the shorter form.",
|
|
416
|
+
group: "event",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "stop-propagation",
|
|
420
|
+
signature: "stop-propagation",
|
|
421
|
+
description:
|
|
422
|
+
"A listener that calls `event.stopPropagation()`, for `handle:`. The " +
|
|
423
|
+
"`(stop-propagation)` flag is the shorter form.",
|
|
424
|
+
group: "event",
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "ternary",
|
|
428
|
+
signature: "ternary(condition, whenTrue, whenFalse?)",
|
|
429
|
+
description:
|
|
430
|
+
"`whenTrue` if `condition` is truthy, else `whenFalse` (`null` when " +
|
|
431
|
+
"omitted). Prefer `if()` for multi-arm conditionals.",
|
|
432
|
+
group: "util",
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: "log",
|
|
436
|
+
signature: "log(...values)",
|
|
437
|
+
description: "Logs the values to the console and returns them as an array.",
|
|
438
|
+
group: "debug",
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: "debug",
|
|
442
|
+
signature: "debug(...values)",
|
|
443
|
+
description:
|
|
444
|
+
"Hits a `debugger` statement and returns the values as an array.",
|
|
445
|
+
group: "debug",
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
export interface ModuleFunctionDoc {
|
|
450
|
+
name: string;
|
|
451
|
+
/** Call signature (`clamp(min, value, max)`), or the bare name for values (`$pi`). */
|
|
452
|
+
signature: string;
|
|
453
|
+
description: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface ModuleDoc {
|
|
457
|
+
/** Module name without the scheme: `math` for `@use "quark:math"`. */
|
|
458
|
+
name: string;
|
|
459
|
+
description: string;
|
|
460
|
+
functions: readonly ModuleFunctionDoc[];
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Built-in modules, imported like JS modules (`@use "quark:math" as math;`
|
|
465
|
+
* or `as *`); nothing here is global. Grouped the way Sass groups
|
|
466
|
+
* `sass:math` / `sass:list` / `sass:map` / `sass:string`. Every function is
|
|
467
|
+
* pure and null-tolerant: a missing collection reads as empty, a missing
|
|
468
|
+
* value passes through, and results are copies. Collection functions take
|
|
469
|
+
* a dot path (`"user.name"`) instead of a callback. Runtime lives in
|
|
470
|
+
* `builtin-modules.ts` (`QUARK_MODULES`); a test keeps the lists in sync.
|
|
471
|
+
*/
|
|
472
|
+
export const BUILTIN_MODULES: readonly ModuleDoc[] = [
|
|
473
|
+
{
|
|
474
|
+
name: "math",
|
|
475
|
+
description:
|
|
476
|
+
"Numbers. Arguments are coerced with `parseFloat`; the CSS argument order is kept.",
|
|
477
|
+
functions: [
|
|
478
|
+
{ name: "$pi", signature: "math.$pi", description: "π." },
|
|
479
|
+
{ name: "$e", signature: "math.$e", description: "Euler's number." },
|
|
480
|
+
{
|
|
481
|
+
name: "min",
|
|
482
|
+
signature: "min(...values)",
|
|
483
|
+
description: "The smallest value (lists are flattened).",
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "max",
|
|
487
|
+
signature: "max(...values)",
|
|
488
|
+
description: "The largest value (lists are flattened).",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "clamp",
|
|
492
|
+
signature: "clamp(min, value, max)",
|
|
493
|
+
description: "`value` limited to the range, in CSS argument order.",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "round",
|
|
497
|
+
signature: "round(value, digits?)",
|
|
498
|
+
description: "Rounded to `digits` decimals (default 0).",
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: "floor",
|
|
502
|
+
signature: "floor(value)",
|
|
503
|
+
description: "Rounded down.",
|
|
504
|
+
},
|
|
505
|
+
{ name: "ceil", signature: "ceil(value)", description: "Rounded up." },
|
|
506
|
+
{ name: "abs", signature: "abs(value)", description: "Absolute value." },
|
|
507
|
+
{
|
|
508
|
+
name: "mod",
|
|
509
|
+
signature: "mod(value, divisor)",
|
|
510
|
+
description:
|
|
511
|
+
"Wrapping modulo: `mod(-1, 3)` is `2` (the `%` operator keeps the sign). `NaN` for a zero divisor.",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "pow",
|
|
515
|
+
signature: "pow(base, exponent)",
|
|
516
|
+
description: "`base` to the power `exponent`.",
|
|
517
|
+
},
|
|
518
|
+
{ name: "sqrt", signature: "sqrt(value)", description: "Square root." },
|
|
519
|
+
{
|
|
520
|
+
name: "percentage",
|
|
521
|
+
signature: "percentage(fraction)",
|
|
522
|
+
description:
|
|
523
|
+
'`"25%"` for `0.25` — a string, ready for an attribute or CSS variable.',
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: "list",
|
|
529
|
+
description:
|
|
530
|
+
'Arrays (an object counts as the list of its values). `"path"` arguments are dot paths into each item; comparisons are loose (`==`), like `find()`.',
|
|
531
|
+
functions: [
|
|
532
|
+
{
|
|
533
|
+
name: "count",
|
|
534
|
+
signature: 'count(list, "path"?, value?)',
|
|
535
|
+
description:
|
|
536
|
+
"The number of items; with a path, the items whose value at it is non-empty; with a value too, the items equal to it. `0` for a missing list.",
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "find",
|
|
540
|
+
signature: 'find(list, "path", value)',
|
|
541
|
+
description:
|
|
542
|
+
"The first item whose value at the path equals `value`, else `undefined` (was a global built-in before 2026-09-13).",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: "filter",
|
|
546
|
+
signature: 'filter(list, "path", value?)',
|
|
547
|
+
description:
|
|
548
|
+
"The items whose value at the path equals `value` — or is non-empty when `value` is omitted.",
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "reject",
|
|
552
|
+
signature: 'reject(list, "path", value?)',
|
|
553
|
+
description: "The complement of `filter`.",
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "pluck",
|
|
557
|
+
signature: 'pluck(list, "path")',
|
|
558
|
+
description: "The value at the path of every item.",
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "sort-by",
|
|
562
|
+
signature: 'sort-by(list, "path"?, "desc"?)',
|
|
563
|
+
description:
|
|
564
|
+
'A sorted copy: numbers numerically, everything else with a locale-aware, numeric-aware comparison; `null` last. `"desc"` reverses.',
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "sum",
|
|
568
|
+
signature: 'sum(list, "path"?)',
|
|
569
|
+
description:
|
|
570
|
+
"The total of the items (or of their value at the path); non-numbers count as 0.",
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: "range",
|
|
574
|
+
signature: "range(end) / range(start, end, step?)",
|
|
575
|
+
description:
|
|
576
|
+
"`range(3)` → `[0, 1, 2]`; `range(1, 4)` → `[1, 2, 3]`; counts down when `start > end`. For skeleton rows and pagination.",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "unique",
|
|
580
|
+
signature: 'unique(list, "path"?)',
|
|
581
|
+
description:
|
|
582
|
+
"A copy without duplicates (by the value at the path when given), first occurrence kept.",
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: "group-by",
|
|
586
|
+
signature: 'group-by(list, "path")',
|
|
587
|
+
description:
|
|
588
|
+
"A map from each distinct value at the path to the items carrying it.",
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "first",
|
|
592
|
+
signature: "first(list)",
|
|
593
|
+
description: "The first item, or `undefined`.",
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: "last",
|
|
597
|
+
signature: "last(list)",
|
|
598
|
+
description: "The last item, or `undefined`.",
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: "reverse",
|
|
602
|
+
signature: "reverse(list)",
|
|
603
|
+
description:
|
|
604
|
+
"A reversed copy (was a global built-in before 2026-09-13).",
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: "compact",
|
|
608
|
+
signature: "compact(list)",
|
|
609
|
+
description:
|
|
610
|
+
'A copy without `null`, `undefined`, `""`, empty lists and empty maps.',
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
name: "map",
|
|
616
|
+
description:
|
|
617
|
+
"Plain objects (`(key: value)` literals, provisions, `dataset`-shaped data). Never mutates; returns copies.",
|
|
618
|
+
functions: [
|
|
619
|
+
{
|
|
620
|
+
name: "get",
|
|
621
|
+
signature: 'get(map, "path", fallback?)',
|
|
622
|
+
description:
|
|
623
|
+
"The value at the dot path, or `fallback` when it is missing.",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "has-key",
|
|
627
|
+
signature: 'has-key(map, "path")',
|
|
628
|
+
description: "Whether the dot path resolves to a value.",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: "keys",
|
|
632
|
+
signature: "keys(map)",
|
|
633
|
+
description: "The keys, in insertion order.",
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "values",
|
|
637
|
+
signature: "values(map)",
|
|
638
|
+
description: "The values, in insertion order.",
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "entries",
|
|
642
|
+
signature: "entries(map)",
|
|
643
|
+
description:
|
|
644
|
+
"`[(key: …, value: …), …]` — iterate a map with `item.key` / `item.value` in the rows.",
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "merge",
|
|
648
|
+
signature: "merge(...maps)",
|
|
649
|
+
description: "A shallow merge, later maps winning.",
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "pick",
|
|
653
|
+
signature: "pick(map, ...keys)",
|
|
654
|
+
description: "A copy holding only the named keys.",
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: "omit",
|
|
658
|
+
signature: "omit(map, ...keys)",
|
|
659
|
+
description: "A copy without the named keys.",
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: "string",
|
|
665
|
+
description:
|
|
666
|
+
'Text. Values are stringified first; `null` / `undefined` read as `""`.',
|
|
667
|
+
functions: [
|
|
668
|
+
{
|
|
669
|
+
name: "plural",
|
|
670
|
+
signature: "plural(count, forms, locale?)",
|
|
671
|
+
description:
|
|
672
|
+
'The form for `count` from a map keyed by `Intl.PluralRules` category (`one`, `other`, `few`, …); `#` in the form is replaced by the count: `plural($n, (one: "# item", other: "# items"))`.',
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "escape-html",
|
|
676
|
+
signature: "escape-html(value)",
|
|
677
|
+
description:
|
|
678
|
+
"`& < > \" '` escaped, for text that goes through `dangerous-html()`.",
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "truncate",
|
|
682
|
+
signature: "truncate(value, max, suffix?)",
|
|
683
|
+
description:
|
|
684
|
+
"Cut to `max` characters including the suffix (default `…`).",
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: "capitalize",
|
|
688
|
+
signature: "capitalize(value)",
|
|
689
|
+
description: "First character upper-cased.",
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: "slugify",
|
|
693
|
+
signature: "slugify(value)",
|
|
694
|
+
description:
|
|
695
|
+
'Lower-case ASCII with dashes: `"Héllo World!"` → `"hello-world"`.',
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
name: "date",
|
|
701
|
+
description:
|
|
702
|
+
'Dates arrive as strings; these parse and format them. Every function accepts a `Date`, an ISO string or a timestamp and returns `null` / `""` for an unparseable value.',
|
|
703
|
+
functions: [
|
|
704
|
+
{
|
|
705
|
+
name: "parse",
|
|
706
|
+
signature: "parse(value)",
|
|
707
|
+
description:
|
|
708
|
+
"A `Date`, or `null` — for the allowed date methods (`toLocaleDateString()`, `toISOString()`).",
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
name: "is-valid",
|
|
712
|
+
signature: "is-valid(value)",
|
|
713
|
+
description: "Whether the value parses as a date.",
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: "format",
|
|
717
|
+
signature: "format(value, locale?, options?)",
|
|
718
|
+
description:
|
|
719
|
+
'`Intl.DateTimeFormat` output: `format($when, "en-GB", (dateStyle: "medium"))`.',
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "add",
|
|
723
|
+
signature: "add(value, amount, unit?)",
|
|
724
|
+
description:
|
|
725
|
+
'A new date `amount` units later (`"days"` by default; seconds … weeks, months, years — months and years step the calendar).',
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: "diff",
|
|
729
|
+
signature: "diff(later, earlier, unit?)",
|
|
730
|
+
description:
|
|
731
|
+
'Whole units between two dates (`"days"` by default), negative when `later` is earlier.',
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "url",
|
|
737
|
+
description: "Query strings, via `URLSearchParams`.",
|
|
738
|
+
functions: [
|
|
739
|
+
{
|
|
740
|
+
name: "query",
|
|
741
|
+
signature: "query(map)",
|
|
742
|
+
description:
|
|
743
|
+
'`"q=a+b&page=2"` from a map; `null` / `""` values are dropped, lists repeat the key.',
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "params",
|
|
747
|
+
signature: "params(url)",
|
|
748
|
+
description:
|
|
749
|
+
"The query of a URL (or a bare query string) as a map; repeated keys become lists.",
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: "encode",
|
|
753
|
+
signature: "encode(value)",
|
|
754
|
+
description: "`encodeURIComponent`.",
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "util",
|
|
760
|
+
description: "Small value helpers.",
|
|
761
|
+
functions: [
|
|
762
|
+
{
|
|
763
|
+
name: "coalesce",
|
|
764
|
+
signature: "coalesce(...values)",
|
|
765
|
+
description:
|
|
766
|
+
'The first value that is not `null` / `undefined` (`or` also skips `0`, `""` and `false`).',
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "is-empty",
|
|
770
|
+
signature: "is-empty(value)",
|
|
771
|
+
description:
|
|
772
|
+
'`true` for `null`, `undefined`, `""`, an empty list or an empty map.',
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "type-of",
|
|
776
|
+
signature: "type-of(value)",
|
|
777
|
+
description:
|
|
778
|
+
'`"string"`, `"number"`, `"boolean"`, `"list"`, `"map"`, `"date"`, `"null"`, `"undefined"` or `"function"`.',
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "to-json",
|
|
782
|
+
signature: "to-json(value, indent?)",
|
|
783
|
+
description: "`JSON.stringify`, for `content` or a `data-*` attribute.",
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: "from-json",
|
|
787
|
+
signature: "from-json(text)",
|
|
788
|
+
description: "`JSON.parse`, or `null` when the text is not JSON.",
|
|
789
|
+
},
|
|
790
|
+
],
|
|
791
|
+
},
|
|
792
|
+
];
|
|
793
|
+
|
|
794
|
+
export type MethodReceiver = "string" | "array" | "number" | "date" | "element";
|
|
795
|
+
|
|
796
|
+
export interface MethodDoc {
|
|
797
|
+
name: string;
|
|
798
|
+
/** Receiver types the method is meaningful on. */
|
|
799
|
+
on: readonly MethodReceiver[];
|
|
800
|
+
/** Call shape, without the receiver. */
|
|
801
|
+
signature: string;
|
|
802
|
+
description: string;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Prototype methods an expression may call via the dot accessor
|
|
807
|
+
* (`item.name.toLowerCase()`). Anything else throws. Own-property
|
|
808
|
+
* functions (`@use` namespaces, provided objects) are callable without
|
|
809
|
+
* being listed. Names must match `ALLOWED_METHOD_NAMES` in
|
|
810
|
+
* `language-tables.ts` (tested).
|
|
811
|
+
*/
|
|
812
|
+
export const ALLOWED_METHODS: readonly MethodDoc[] = [
|
|
813
|
+
// string
|
|
814
|
+
{
|
|
815
|
+
name: "toLowerCase",
|
|
816
|
+
on: ["string"],
|
|
817
|
+
signature: "toLowerCase()",
|
|
818
|
+
description: "Lower-cased copy.",
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: "toUpperCase",
|
|
822
|
+
on: ["string"],
|
|
823
|
+
signature: "toUpperCase()",
|
|
824
|
+
description: "Upper-cased copy.",
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
name: "trim",
|
|
828
|
+
on: ["string"],
|
|
829
|
+
signature: "trim()",
|
|
830
|
+
description: "Copy without leading / trailing whitespace.",
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "split",
|
|
834
|
+
on: ["string"],
|
|
835
|
+
signature: "split(separator, limit?)",
|
|
836
|
+
description: "Array of substrings.",
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: "charAt",
|
|
840
|
+
on: ["string"],
|
|
841
|
+
signature: "charAt(index)",
|
|
842
|
+
description: "The character at `index`.",
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: "startsWith",
|
|
846
|
+
on: ["string"],
|
|
847
|
+
signature: "startsWith(search, position?)",
|
|
848
|
+
description: "Boolean.",
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
name: "endsWith",
|
|
852
|
+
on: ["string"],
|
|
853
|
+
signature: "endsWith(search, length?)",
|
|
854
|
+
description: "Boolean.",
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
name: "padStart",
|
|
858
|
+
on: ["string"],
|
|
859
|
+
signature: "padStart(length, fill?)",
|
|
860
|
+
description: "Left-padded copy.",
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
name: "padEnd",
|
|
864
|
+
on: ["string"],
|
|
865
|
+
signature: "padEnd(length, fill?)",
|
|
866
|
+
description: "Right-padded copy.",
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "replace",
|
|
870
|
+
on: ["string"],
|
|
871
|
+
signature: "replace(search, replacement)",
|
|
872
|
+
description: "Copy with the first match replaced (string search only).",
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
name: "replaceAll",
|
|
876
|
+
on: ["string"],
|
|
877
|
+
signature: "replaceAll(search, replacement)",
|
|
878
|
+
description: "Copy with every match replaced (string search only).",
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: "toString",
|
|
882
|
+
on: ["string", "array", "number", "date"],
|
|
883
|
+
signature: "toString()",
|
|
884
|
+
description: "String form.",
|
|
885
|
+
},
|
|
886
|
+
// string + array
|
|
887
|
+
{
|
|
888
|
+
name: "includes",
|
|
889
|
+
on: ["string", "array"],
|
|
890
|
+
signature: "includes(search)",
|
|
891
|
+
description: "Boolean.",
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: "slice",
|
|
895
|
+
on: ["string", "array"],
|
|
896
|
+
signature: "slice(start?, end?)",
|
|
897
|
+
description: "Sub-range copy.",
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
name: "indexOf",
|
|
901
|
+
on: ["string", "array"],
|
|
902
|
+
signature: "indexOf(search)",
|
|
903
|
+
description: "First position, or `-1`.",
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: "lastIndexOf",
|
|
907
|
+
on: ["string", "array"],
|
|
908
|
+
signature: "lastIndexOf(search)",
|
|
909
|
+
description: "Last position, or `-1`.",
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "concat",
|
|
913
|
+
on: ["string", "array"],
|
|
914
|
+
signature: "concat(...values)",
|
|
915
|
+
description: "Joined copy.",
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
name: "at",
|
|
919
|
+
on: ["string", "array"],
|
|
920
|
+
signature: "at(index)",
|
|
921
|
+
description: "Element at `index`; negative counts from the end.",
|
|
922
|
+
},
|
|
923
|
+
// array
|
|
924
|
+
{
|
|
925
|
+
name: "join",
|
|
926
|
+
on: ["array"],
|
|
927
|
+
signature: "join(separator?)",
|
|
928
|
+
description: "String of the items.",
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: "flat",
|
|
932
|
+
on: ["array"],
|
|
933
|
+
signature: "flat(depth?)",
|
|
934
|
+
description: "Flattened copy.",
|
|
935
|
+
},
|
|
936
|
+
// number
|
|
937
|
+
{
|
|
938
|
+
name: "toFixed",
|
|
939
|
+
on: ["number"],
|
|
940
|
+
signature: "toFixed(digits?)",
|
|
941
|
+
description: "Fixed-point string.",
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "toLocaleString",
|
|
945
|
+
on: ["number", "date", "array"],
|
|
946
|
+
signature: "toLocaleString(locale?, options?)",
|
|
947
|
+
description: "Locale-formatted string.",
|
|
948
|
+
},
|
|
949
|
+
// date
|
|
950
|
+
{
|
|
951
|
+
name: "toLocaleDateString",
|
|
952
|
+
on: ["date"],
|
|
953
|
+
signature: "toLocaleDateString(locale?, options?)",
|
|
954
|
+
description: "Locale-formatted date.",
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
name: "toLocaleTimeString",
|
|
958
|
+
on: ["date"],
|
|
959
|
+
signature: "toLocaleTimeString(locale?, options?)",
|
|
960
|
+
description: "Locale-formatted time.",
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
name: "toISOString",
|
|
964
|
+
on: ["date"],
|
|
965
|
+
signature: "toISOString()",
|
|
966
|
+
description: "ISO 8601 string.",
|
|
967
|
+
},
|
|
968
|
+
// DOM reads
|
|
969
|
+
{
|
|
970
|
+
name: "getAttribute",
|
|
971
|
+
on: ["element"],
|
|
972
|
+
signature: 'getAttribute("name")',
|
|
973
|
+
description: "Attribute value or `null`. Not observed.",
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: "hasAttribute",
|
|
977
|
+
on: ["element"],
|
|
978
|
+
signature: 'hasAttribute("name")',
|
|
979
|
+
description: "Boolean. Not observed.",
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
name: "matches",
|
|
983
|
+
on: ["element"],
|
|
984
|
+
signature: 'matches("selector")',
|
|
985
|
+
description: "Boolean.",
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: "closest",
|
|
989
|
+
on: ["element"],
|
|
990
|
+
signature: 'closest("selector")',
|
|
991
|
+
description: "Nearest ancestor-or-self matching the selector, or `null`.",
|
|
992
|
+
},
|
|
993
|
+
];
|
|
994
|
+
|
|
995
|
+
/** Method names the evaluator accepts on prototype lookups. */
|
|
996
|
+
export interface CombinatorDoc {
|
|
997
|
+
/** As written between two compounds. */
|
|
998
|
+
syntax: string;
|
|
999
|
+
description: string;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Selector combinators and how the observer follows each. The native
|
|
1004
|
+
* engine matches; these rows say which changes re-run a rule that uses
|
|
1005
|
+
* them.
|
|
1006
|
+
*/
|
|
1007
|
+
export const COMBINATORS: readonly CombinatorDoc[] = [
|
|
1008
|
+
{
|
|
1009
|
+
syntax: "a b",
|
|
1010
|
+
description:
|
|
1011
|
+
"Descendant. An attribute change on `a` re-runs the rule for the " +
|
|
1012
|
+
"matching `b`s below it.",
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
syntax: "a > b",
|
|
1016
|
+
description: "Child. Same observation as the descendant combinator.",
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
syntax: "a + b",
|
|
1020
|
+
description:
|
|
1021
|
+
"Next sibling. An attribute change on `a` re-runs the rule from the " +
|
|
1022
|
+
"parent; elements inserted or removed under that parent re-run it too.",
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
syntax: "a ~ b",
|
|
1026
|
+
description: "Subsequent siblings. Same observation as `+`.",
|
|
1027
|
+
},
|
|
1028
|
+
];
|
|
1029
|
+
|
|
1030
|
+
export interface PseudoClassRow {
|
|
1031
|
+
/** Name without the colon (`nth-child`). */
|
|
1032
|
+
name: string;
|
|
1033
|
+
/** As written, with a placeholder argument where one is taken. */
|
|
1034
|
+
syntax: string;
|
|
1035
|
+
description: string;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
export interface PseudoClassDoc extends PseudoClassRow, PseudoClassSupport {}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Pseudo-classes and what Quark observes. `kind` / `attributes` come
|
|
1042
|
+
* from `PSEUDO_CLASS_SUPPORT` in `language-tables.ts`; `selector-utils.ts`
|
|
1043
|
+
* classifies from that table, so a pseudo cannot be observed without a
|
|
1044
|
+
* row here (tested both ways). Anything missing is `unobserved`: first
|
|
1045
|
+
* run only, warned at build.
|
|
1046
|
+
*/
|
|
1047
|
+
const PSEUDO_CLASS_ROWS: readonly PseudoClassRow[] = [
|
|
1048
|
+
// logical
|
|
1049
|
+
{
|
|
1050
|
+
name: "is",
|
|
1051
|
+
syntax: ":is(…)",
|
|
1052
|
+
description:
|
|
1053
|
+
"Attributes anywhere in the argument list are observed like the " +
|
|
1054
|
+
"compound's own. A complex argument (`:is(section[x] li)`) re-runs " +
|
|
1055
|
+
"the rule from the changed element down.",
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
name: "where",
|
|
1059
|
+
syntax: ":where(…)",
|
|
1060
|
+
description: "Same as `:is()` (Quark has no specificity).",
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
name: "not",
|
|
1064
|
+
syntax: ":not(…)",
|
|
1065
|
+
description: "Same observation as `:is()`.",
|
|
1066
|
+
},
|
|
1067
|
+
// relational
|
|
1068
|
+
{
|
|
1069
|
+
name: "has",
|
|
1070
|
+
syntax: ":has(…)",
|
|
1071
|
+
description:
|
|
1072
|
+
"Attributes named in the argument are observed on descendants, and " +
|
|
1073
|
+
"elements inserted or removed below a candidate re-check it: the " +
|
|
1074
|
+
"rule re-runs for every matching ancestor of the change. " +
|
|
1075
|
+
"Sibling-relative arguments (`:has(+ …)`, `:has(~ …)`) and a `:has()` " +
|
|
1076
|
+
"nested in a complex `:is()` / `:not()` argument re-run the whole " +
|
|
1077
|
+
"rule from the host instead. Without rule reversion, pair it with " +
|
|
1078
|
+
"the inverse `:not(:has(…))` rule.",
|
|
1079
|
+
},
|
|
1080
|
+
// structural
|
|
1081
|
+
{
|
|
1082
|
+
name: "first-child",
|
|
1083
|
+
syntax: ":first-child",
|
|
1084
|
+
description:
|
|
1085
|
+
"Sibling position: re-runs when elements are inserted or removed " +
|
|
1086
|
+
"under the parent.",
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
name: "last-child",
|
|
1090
|
+
syntax: ":last-child",
|
|
1091
|
+
description: "Same as `:first-child`.",
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
name: "only-child",
|
|
1095
|
+
syntax: ":only-child",
|
|
1096
|
+
description: "Same as `:first-child`.",
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
name: "nth-child",
|
|
1100
|
+
syntax: ":nth-child(An+B [of S])",
|
|
1101
|
+
description:
|
|
1102
|
+
"Same as `:first-child`. With `of S`, attributes in `S` are observed " +
|
|
1103
|
+
"on the siblings and each change re-runs the rule from the parent.",
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
name: "nth-last-child",
|
|
1107
|
+
syntax: ":nth-last-child(An+B [of S])",
|
|
1108
|
+
description: "Same as `:nth-child()`.",
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
name: "first-of-type",
|
|
1112
|
+
syntax: ":first-of-type",
|
|
1113
|
+
description: "Same as `:first-child`.",
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
name: "last-of-type",
|
|
1117
|
+
syntax: ":last-of-type",
|
|
1118
|
+
description: "Same as `:first-child`.",
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: "only-of-type",
|
|
1122
|
+
syntax: ":only-of-type",
|
|
1123
|
+
description: "Same as `:first-child`.",
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
name: "nth-of-type",
|
|
1127
|
+
syntax: ":nth-of-type(An+B)",
|
|
1128
|
+
description: "Same as `:first-child`.",
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
name: "nth-last-of-type",
|
|
1132
|
+
syntax: ":nth-last-of-type(An+B)",
|
|
1133
|
+
description: "Same as `:first-child`.",
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: "empty",
|
|
1137
|
+
syntax: ":empty",
|
|
1138
|
+
description:
|
|
1139
|
+
"Re-checked when elements are inserted or removed below the " +
|
|
1140
|
+
"element. Text-only changes are not observed.",
|
|
1141
|
+
},
|
|
1142
|
+
// attribute-backed
|
|
1143
|
+
{
|
|
1144
|
+
name: "disabled",
|
|
1145
|
+
syntax: ":disabled",
|
|
1146
|
+
description:
|
|
1147
|
+
"Observes `disabled` on the element and on ancestors (a disabled " +
|
|
1148
|
+
"`<fieldset>`).",
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
name: "enabled",
|
|
1152
|
+
syntax: ":enabled",
|
|
1153
|
+
description: "Same as `:disabled`.",
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
name: "required",
|
|
1157
|
+
syntax: ":required",
|
|
1158
|
+
description: "Observes `required`.",
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
name: "optional",
|
|
1162
|
+
syntax: ":optional",
|
|
1163
|
+
description: "Same as `:required`.",
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
name: "read-only",
|
|
1167
|
+
syntax: ":read-only",
|
|
1168
|
+
description:
|
|
1169
|
+
"Observes `readonly`, `disabled` and `contenteditable` on the " +
|
|
1170
|
+
"element and its ancestors.",
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
name: "read-write",
|
|
1174
|
+
syntax: ":read-write",
|
|
1175
|
+
description: "Same as `:read-only`.",
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
name: "any-link",
|
|
1179
|
+
syntax: ":any-link",
|
|
1180
|
+
description: "Observes `href`.",
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
name: "lang",
|
|
1184
|
+
syntax: ":lang(…)",
|
|
1185
|
+
description: "Observes `lang` on the element and its ancestors.",
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
name: "open",
|
|
1189
|
+
syntax: ":open",
|
|
1190
|
+
description:
|
|
1191
|
+
"Observes the `open` attribute (`<details>`, `<dialog>`). A " +
|
|
1192
|
+
"`<select>` / `<input>` picker opening is not observed.",
|
|
1193
|
+
},
|
|
1194
|
+
// static
|
|
1195
|
+
{
|
|
1196
|
+
name: "scope",
|
|
1197
|
+
syntax: ":scope",
|
|
1198
|
+
description: "The host; never changes.",
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
name: "root",
|
|
1202
|
+
syntax: ":root",
|
|
1203
|
+
description: "The document element; never changes.",
|
|
1204
|
+
},
|
|
1205
|
+
// unobserved (match on the first run only)
|
|
1206
|
+
...Object.keys(PSEUDO_CLASS_SUPPORT)
|
|
1207
|
+
.filter((name) => PSEUDO_CLASS_SUPPORT[name].kind === "unobserved")
|
|
1208
|
+
.map((name) => ({
|
|
1209
|
+
name,
|
|
1210
|
+
syntax: `:${name}${name === "dir" ? "(…)" : ""}`,
|
|
1211
|
+
description:
|
|
1212
|
+
"Interaction or browser state with no attribute behind it: matches " +
|
|
1213
|
+
"on the first run only (warned at build). Select on reflected " +
|
|
1214
|
+
"attributes instead.",
|
|
1215
|
+
})),
|
|
1216
|
+
];
|
|
1217
|
+
|
|
1218
|
+
export const PSEUDO_CLASSES: readonly PseudoClassDoc[] = PSEUDO_CLASS_ROWS.map(
|
|
1219
|
+
(row) => ({ ...row, ...PSEUDO_CLASS_SUPPORT[row.name] })
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
/** Pseudo-class name → its row, for the selector classifier. */
|
|
1223
|
+
export const PSEUDO_CLASS_DOCS: ReadonlyMap<string, PseudoClassDoc> = new Map(
|
|
1224
|
+
PSEUDO_CLASSES.map((doc) => [doc.name, doc])
|
|
1225
|
+
);
|