@fluixi/template-parser 0.1.0-alpha.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fluixi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @fluixi/template-parser
2
+
3
+ The parser for Fluixi `` html`...` `` templates: a dependency-free tokenizer +
4
+ recursive-descent parser that produces a **located, recoverable Template AST**.
5
+
6
+ It is the shared foundation for everything that needs to understand a template —
7
+ the compiler, the language server, the TypeScript plugin, and the Biome plugin —
8
+ so they all agree on one grammar, one node model, and one set of diagnostics.
9
+
10
+ ## Why a separate parser
11
+
12
+ The compiler used to scan template text straight into codegen IR in one pass.
13
+ That was fine for emitting code but useless for tooling: no source spans, no
14
+ diagnostics, no error recovery, and hard-wired to Babel. This package is the
15
+ parse layer split out so every tool can reuse it:
16
+
17
+ ```
18
+ pieces → Reader (lexing + holes) → parseTemplate → Template AST
19
+ │
20
+ ┌────────────────────────────────────────┼───────────────┐
21
+ compiler (→ IR) language server Biome / formatter TS plugin
22
+ ```
23
+
24
+ The parser never resolves the JavaScript inside `${…}`. Each hole is recorded as
25
+ an **opaque slot** (an index + a source span). Downstream, each tool binds slots
26
+ to its own representation — a Babel node in the compiler, a `ts.Expression` in the
27
+ LSP, raw text in Biome. That single seam is what keeps the parser host-agnostic.
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import { parse } from '@fluixi/template-parser';
33
+
34
+ const { root, diagnostics } = parse([
35
+ '<button @click=', // static segment 0
36
+ '>', // hole 0 sits between segment 0 and 1
37
+ '</button>', // static segment 1
38
+ ]); // → <button @click=${…}>${…}</button>
39
+ ```
40
+
41
+ `parse(statics)` takes the static string segments (one more than the number of
42
+ holes) and returns `{ root, diagnostics }`. Hosts with real source positions
43
+ build `Piece[]` directly (via `parseTemplate`) so every span maps back to the
44
+ original file.
45
+
46
+ ## What it parses
47
+
48
+ Standard HTML, components (`<Button/>`, `<Ctx.Provider>`), fragments (`<>`),
49
+ comments, SVG, raw-text elements, void elements, and the full binding surface:
50
+
51
+ | Syntax | Node |
52
+ | --- | --- |
53
+ | `name`, `name="x"`, `name=${x}`, `name="a ${x} b"` | `AttributeNode` |
54
+ | `?disabled` | `AttributeNode` (boolean) |
55
+ | `.currentTime=${t}` | `PropertyBindingNode` |
56
+ | `@click=${h}` / `onClick=${h}` | `EventBindingNode` |
57
+ | `ref=${el}` | `RefBindingNode` |
58
+ | `...${props}` / `${obj}` | `SpreadNode` |
59
+ | `if=${c}` / `else` | `IfDirectiveNode` / `ElseDirectiveNode` |
60
+ | `each=${xs}` (+ `key`) | `EachDirectiveNode` |
61
+ | `bind:value=${sig}` | `BindDirectiveNode` |
62
+ | `class:active=${on}` | `ClassDirectiveNode` |
63
+ | `style:color=${c}` | `StyleDirectiveNode` |
64
+ | `use=${d}` / `use:tooltip` | `UseDirectiveNode` |
65
+
66
+ ## Extensibility
67
+
68
+ Adding a directive is a new entry in the directive **registry**, never a change
69
+ to the core parser:
70
+
71
+ ```ts
72
+ import { CORE_DIRECTIVES, parseTemplate, type DirectiveDescriptor } from '@fluixi/template-parser';
73
+
74
+ const highlight: DirectiveDescriptor = {
75
+ id: 'highlight',
76
+ match: (name) => name === 'highlight',
77
+ build: (raw) => ({ kind: 'Attribute', name: 'data-highlight', value: raw.value, boolean: false, loc: raw.loc }),
78
+ };
79
+
80
+ parseTemplate(pieces, { directives: [highlight, ...CORE_DIRECTIVES] });
81
+ ```
82
+
83
+ ## Diagnostics
84
+
85
+ The parser never throws and never stalls. It recovers from malformed input and
86
+ reports located diagnostics: unclosed / mismatched / stray tags, unterminated
87
+ comments, duplicate directives, and invalid bindings. Every diagnostic carries a
88
+ `SourceSpan`.
89
+
90
+ ## License
91
+
92
+ MIT
package/dist/ast.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var r=Object.defineProperty;var s=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var c=Object.prototype.hasOwnProperty;var p=(e,t)=>{for(var n in t)r(e,n,{get:t[n],enumerable:!0})},N=(e,t,n,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of l(t))!c.call(e,i)&&i!==n&&r(e,i,{get:()=>t[i],enumerable:!(o=s(t,i))||o.enumerable});return e};var m=e=>N(r({},"__esModule",{value:!0}),e);var u={};p(u,{childrenOf:()=>d,setParents:()=>x});module.exports=m(u);function x(e){let t=(n,o)=>{n.parent=o;let i=d(n);for(let a of i)t(a,n)};return t(e,null),e}function d(e){switch(e.kind){case"Root":case"Element":case"Component":case"Fragment":return e.children;default:return[]}}
package/dist/ast.d.ts ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The Template AST — the stable, host-agnostic contract this package exists to
3
+ * produce. It is deliberately NOT the compiler's codegen IR: it models the
4
+ * template *as written* (every directive is an explicit node, every node keeps
5
+ * a source span, whitespace is preserved) so it can power the compiler AND the
6
+ * language server, TypeScript plugin, and Biome/formatter alike.
7
+ *
8
+ * Design rules baked into these types:
9
+ * - Expression holes (`${…}`) are OPAQUE SLOTS. A node stores the hole's
10
+ * integer `index` (its position in the template's `expressions[]`) and its
11
+ * source span — never resolved JS. Each consumer binds a slot to its own
12
+ * representation afterward (a Babel node in the compiler, a `ts.Expression`
13
+ * in the LSP, raw text in Biome). This single seam is what lets one parser
14
+ * serve every downstream tool.
15
+ * - Directives are explicit node kinds, never stringly-typed attributes.
16
+ * - Every node carries `loc`, `kind`, and (via the tree walk) a `parent`.
17
+ */
18
+ /** A half-open source span `[start, end)` in template-coordinate space. */
19
+ export interface SourceSpan {
20
+ start: number;
21
+ end: number;
22
+ }
23
+ /** Namespaces the parser tracks so SVG (and foreign content) lower correctly. */
24
+ export type Namespace = 'html' | 'svg' | 'mathml';
25
+ export type NodeKind = 'Root' | 'Element' | 'Component' | 'Fragment' | 'Text' | 'Comment' | 'Expression';
26
+ export type AttrKind = 'Attribute' | 'PropertyBinding' | 'EventBinding' | 'RefBinding' | 'Spread' | 'IfDirective' | 'ElseDirective' | 'EachDirective' | 'BindDirective' | 'ClassDirective' | 'StyleDirective' | 'UseDirective' | 'Slot';
27
+ /** Fields shared by every node. `parent` is wired by {@link setParents}. */
28
+ export interface NodeBase {
29
+ loc: SourceSpan;
30
+ parent?: TemplateNode | null;
31
+ /** Free-form bag for downstream passes (binding results, type info, etc.). */
32
+ metadata?: Record<string, unknown>;
33
+ }
34
+ export type TemplateNode = RootNode | ElementNode | ComponentNode | FragmentNode | TextNode | CommentNode | ExpressionNode;
35
+ /** Anything that can attach to an element/component opening tag. */
36
+ export type AttributeLikeNode = AttributeNode | PropertyBindingNode | EventBindingNode | RefBindingNode | SpreadNode | IfDirectiveNode | ElseDirectiveNode | EachDirectiveNode | BindDirectiveNode | ClassDirectiveNode | StyleDirectiveNode | UseDirectiveNode | SlotNode;
37
+ export interface RootNode extends NodeBase {
38
+ kind: 'Root';
39
+ children: TemplateNode[];
40
+ }
41
+ export interface ElementNode extends NodeBase {
42
+ kind: 'Element';
43
+ tag: string;
44
+ namespace: Namespace;
45
+ attributes: AttributeLikeNode[];
46
+ children: TemplateNode[];
47
+ selfClosing: boolean;
48
+ /** Raw-text element (`script`/`style`/`textarea`/`title`) — children kept verbatim. */
49
+ rawText: boolean;
50
+ }
51
+ export interface ComponentNode extends NodeBase {
52
+ kind: 'Component';
53
+ /** Capitalized name or member path (`Ctx.Provider`) as written; '' for a dynamic tag. */
54
+ tag: string;
55
+ /**
56
+ * Dynamic tag `<${Comp} />`: the hole index of the component expression. When
57
+ * set, `tag` is '' and the component identity comes from this hole — which
58
+ * also means the referenced binding is visible to TypeScript (no "unused").
59
+ */
60
+ tagHole: number | null;
61
+ attributes: AttributeLikeNode[];
62
+ children: TemplateNode[];
63
+ selfClosing: boolean;
64
+ }
65
+ export interface FragmentNode extends NodeBase {
66
+ kind: 'Fragment';
67
+ children: TemplateNode[];
68
+ }
69
+ /** Preserved verbatim (raw); whitespace normalization is a lowering concern. */
70
+ export interface TextNode extends NodeBase {
71
+ kind: 'Text';
72
+ value: string;
73
+ /** Inside a raw-text element — do not whitespace-normalize when lowering. */
74
+ raw: boolean;
75
+ }
76
+ export interface CommentNode extends NodeBase {
77
+ kind: 'Comment';
78
+ value: string;
79
+ }
80
+ /** A `${…}` in child/text position. */
81
+ export interface ExpressionNode extends NodeBase {
82
+ kind: 'Expression';
83
+ hole: number;
84
+ }
85
+ /** A single interpolation part of an attribute value. */
86
+ export type ValuePart = {
87
+ text: string;
88
+ } | {
89
+ hole: number;
90
+ };
91
+ /**
92
+ * `name`, `name="static"`, `name=${x}`, `name="a ${x} b"`, `?bool`, `?bool=${x}`.
93
+ * `value === null` is a valueless attribute (boolean true). `boolean` marks the
94
+ * lit `?name` prefix.
95
+ */
96
+ export interface AttributeNode extends NodeBase {
97
+ kind: 'Attribute';
98
+ name: string;
99
+ value: AttrValue | null;
100
+ boolean: boolean;
101
+ }
102
+ export type AttrValue = {
103
+ kind: 'static';
104
+ value: string;
105
+ } | {
106
+ kind: 'hole';
107
+ hole: number;
108
+ } | {
109
+ kind: 'mixed';
110
+ parts: ValuePart[];
111
+ };
112
+ /** `.prop=${x}` — DOM property binding. */
113
+ export interface PropertyBindingNode extends NodeBase {
114
+ kind: 'PropertyBinding';
115
+ name: string;
116
+ hole: number;
117
+ }
118
+ /**
119
+ * `@click=${h}`, `onClick=${h}`, or `on:click=${h}`, with optional dotted
120
+ * modifiers (`@click.capture.once`, `on:input.passive`). `syntax` is `'colon'`
121
+ * for the `on:name` form (a native listener), `'at'` for `@name`, `'on'` for the
122
+ * `onName` camelCase form.
123
+ */
124
+ export interface EventBindingNode extends NodeBase {
125
+ kind: 'EventBinding';
126
+ /** Lowercased DOM event name (`click`). */
127
+ name: string;
128
+ syntax: 'at' | 'on' | 'colon';
129
+ /** Dotted modifiers: capture/once/passive (listener options) + prevent/stop/self. */
130
+ modifiers: string[];
131
+ hole: number;
132
+ }
133
+ /** `ref=${el}`. */
134
+ export interface RefBindingNode extends NodeBase {
135
+ kind: 'RefBinding';
136
+ hole: number;
137
+ }
138
+ /** `...${props}`. */
139
+ export interface SpreadNode extends NodeBase {
140
+ kind: 'Spread';
141
+ hole: number;
142
+ }
143
+ /** `if=${cond}`. */
144
+ export interface IfDirectiveNode extends NodeBase {
145
+ kind: 'IfDirective';
146
+ hole: number;
147
+ }
148
+ /** `else` (valueless). */
149
+ export interface ElseDirectiveNode extends NodeBase {
150
+ kind: 'ElseDirective';
151
+ }
152
+ /** `each=${items}` with optional `key="id"` / `key=${…}`. */
153
+ export interface EachDirectiveNode extends NodeBase {
154
+ kind: 'EachDirective';
155
+ hole: number;
156
+ key: {
157
+ static: string;
158
+ } | {
159
+ hole: number;
160
+ } | null;
161
+ }
162
+ /** `bind:value=${signal}`. */
163
+ export interface BindDirectiveNode extends NodeBase {
164
+ kind: 'BindDirective';
165
+ name: string;
166
+ hole: number;
167
+ }
168
+ /** `class:active=${flag}`. */
169
+ export interface ClassDirectiveNode extends NodeBase {
170
+ kind: 'ClassDirective';
171
+ name: string;
172
+ hole: number;
173
+ }
174
+ /** `style:color=${c}`. */
175
+ export interface StyleDirectiveNode extends NodeBase {
176
+ kind: 'StyleDirective';
177
+ name: string;
178
+ hole: number;
179
+ }
180
+ /** `use=${directive}` or `use:tooltip` (optionally `use:tooltip=${opts}`). */
181
+ export interface UseDirectiveNode extends NodeBase {
182
+ kind: 'UseDirective';
183
+ /** Named form `use:tooltip` carries the name; bare `use=${d}` leaves it null. */
184
+ name: string | null;
185
+ hole: number | null;
186
+ }
187
+ /** `<slot>` element or `slot="name"` marker (reserved; wired in a later phase). */
188
+ export interface SlotNode extends NodeBase {
189
+ kind: 'Slot';
190
+ name: string | null;
191
+ }
192
+ export type Severity = 'error' | 'warning';
193
+ export interface Diagnostic {
194
+ code: DiagnosticCode;
195
+ message: string;
196
+ severity: Severity;
197
+ loc: SourceSpan;
198
+ }
199
+ export type DiagnosticCode = 'unclosed-tag' | 'mismatched-close-tag' | 'stray-close-tag' | 'unclosed-comment' | 'duplicate-directive' | 'else-without-if' | 'invalid-directive' | 'invalid-binding' | 'unsupported-combination';
200
+ /** The full result of a parse: the tree plus any recovered-from problems. */
201
+ export interface ParseResult {
202
+ root: RootNode;
203
+ diagnostics: Diagnostic[];
204
+ }
205
+ /** Wire every node's `parent` pointer by walking the tree once. */
206
+ export declare function setParents(root: RootNode): RootNode;
207
+ /** The content children of a node (attributes are not content children). */
208
+ export declare function childrenOf(node: TemplateNode): TemplateNode[];
209
+ //# sourceMappingURL=ast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
package/dist/ast.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Template AST — the stable, host-agnostic contract this package exists to
3
+ * produce. It is deliberately NOT the compiler's codegen IR: it models the
4
+ * template *as written* (every directive is an explicit node, every node keeps
5
+ * a source span, whitespace is preserved) so it can power the compiler AND the
6
+ * language server, TypeScript plugin, and Biome/formatter alike.
7
+ *
8
+ * Design rules baked into these types:
9
+ * - Expression holes (`${…}`) are OPAQUE SLOTS. A node stores the hole's
10
+ * integer `index` (its position in the template's `expressions[]`) and its
11
+ * source span — never resolved JS. Each consumer binds a slot to its own
12
+ * representation afterward (a Babel node in the compiler, a `ts.Expression`
13
+ * in the LSP, raw text in Biome). This single seam is what lets one parser
14
+ * serve every downstream tool.
15
+ * - Directives are explicit node kinds, never stringly-typed attributes.
16
+ * - Every node carries `loc`, `kind`, and (via the tree walk) a `parent`.
17
+ */
18
+ // ---------------------------------------------------------------------------
19
+ // Tree helpers
20
+ // ---------------------------------------------------------------------------
21
+ /** Wire every node's `parent` pointer by walking the tree once. */
22
+ export function setParents(root) {
23
+ const visit = (node, parent) => {
24
+ node.parent = parent;
25
+ const kids = childrenOf(node);
26
+ for (const child of kids)
27
+ visit(child, node);
28
+ };
29
+ visit(root, null);
30
+ return root;
31
+ }
32
+ /** The content children of a node (attributes are not content children). */
33
+ export function childrenOf(node) {
34
+ switch (node.kind) {
35
+ case 'Root':
36
+ case 'Element':
37
+ case 'Component':
38
+ case 'Fragment':
39
+ return node.children;
40
+ default:
41
+ return [];
42
+ }
43
+ }
package/dist/ast.mjs ADDED
@@ -0,0 +1 @@
1
+ function a(e){let n=(t,i)=>{t.parent=i;let o=d(t);for(let r of o)n(r,t)};return n(e,null),e}function d(e){switch(e.kind){case"Root":case"Element":case"Component":case"Fragment":return e.children;default:return[]}}export{d as childrenOf,a as setParents};
@@ -0,0 +1 @@
1
+ "use strict";var c=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var m=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var h=(e,t)=>{for(var i in t)c(e,i,{get:t[i],enumerable:!0})},b=(e,t,i,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of m(t))!v.call(e,o)&&o!==i&&c(e,o,{get:()=>t[o],enumerable:!(n=u(t,o))||n.enumerable});return e};var p=e=>b(c({},"__esModule",{value:!0}),e);var D={};h(D,{CORE_DIRECTIVES:()=>a,EVENT_MODIFIERS:()=>d,resolveAttribute:()=>f});module.exports=p(D);var d=new Set(["capture","once","passive","prevent","stop","self"]);function l(e,t){return e.value&&e.value.kind==="hole"?e.value.hole:(e.report("invalid-binding",`${t} requires an expression value: ${e.name}=\${…}`),-1)}var s=(e,t)=>({kind:"Attribute",name:t?e.name.slice(1):e.name,value:e.value,boolean:t,loc:e.loc}),a=[{id:"event",match:e=>e[0]==="@"||e.startsWith("on:")||/^on[A-Z]/.test(e),build:e=>{let t,i;e.name[0]==="@"?(t="at",i=e.name.slice(1)):e.name.startsWith("on:")?(t="colon",i=e.name.slice(3)):(t="on",i=e.name.slice(2));let[n,...o]=i.split(".");for(let r of o)d.has(r)||e.report("invalid-directive",`Unknown event modifier ".${r}" — expected one of ${[...d].join(", ")}`);return{kind:"EventBinding",name:n,syntax:t,modifiers:o,hole:l(e,"event binding"),loc:e.loc}}},{id:"bind",elementOnly:!0,match:e=>e.startsWith("bind:"),build:e=>({kind:"BindDirective",name:e.name.slice(5),hole:l(e,"two-way binding"),loc:e.loc})},{id:"class-directive",elementOnly:!0,match:e=>e.startsWith("class:"),build:e=>({kind:"ClassDirective",name:e.name.slice(6),hole:l(e,"class directive"),loc:e.loc})},{id:"style-directive",elementOnly:!0,match:e=>e.startsWith("style:"),build:e=>({kind:"StyleDirective",name:e.name.slice(6),hole:l(e,"style directive"),loc:e.loc})},{id:"use",match:e=>e==="use"||e.startsWith("use:"),build:e=>({kind:"UseDirective",name:e.name.startsWith("use:")?e.name.slice(4):null,hole:e.value&&e.value.kind==="hole"?e.value.hole:null,loc:e.loc})},{id:"if",elementOnly:!0,match:e=>e==="if",build:e=>({kind:"IfDirective",hole:l(e,"if directive"),loc:e.loc})},{id:"each",elementOnly:!0,match:e=>e==="each",build:e=>({kind:"EachDirective",hole:l(e,"each directive"),key:null,loc:e.loc})},{id:"else",elementOnly:!0,match:e=>e==="else",build:e=>(e.value!==null&&e.report("invalid-directive","`else` takes no value"),{kind:"ElseDirective",loc:e.loc})},{id:"ref",match:e=>e==="ref",build:e=>({kind:"RefBinding",hole:l(e,"ref binding"),loc:e.loc})},{id:"property",match:e=>e[0]===".",build:e=>({kind:"PropertyBinding",name:e.name.slice(1),hole:l(e,"property binding"),loc:e.loc})},{id:"boolean",match:e=>e[0]==="?",build:e=>s(e,!0)}];function f(e,t=a,i={component:!1}){for(let n of t)if(!(n.elementOnly&&i.component)&&n.match(e.name))return n.build(e);return s(e,!1)}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The directive registry — the extensibility seam.
3
+ *
4
+ * The core parser never hard-codes what `if`, `each`, `bind:value`, `@click`,
5
+ * `.prop` etc. mean. It parses a raw `name[=value]` pair, then asks the registry
6
+ * to turn it into a typed node. Adding future syntax is therefore a new
7
+ * descriptor here (or registered by a downstream tool), never a change to the
8
+ * parser's control flow. Descriptors are tried in order; the first whose
9
+ * `match` returns true owns the attribute. Anything unmatched falls back to a
10
+ * plain {@link AttributeNode}.
11
+ */
12
+ import type { AttributeLikeNode, AttrValue, Diagnostic, DiagnosticCode, SourceSpan } from './ast.js';
13
+ /** Recognized event modifiers (`@click.capture.once`, `on:input.passive`, …). */
14
+ export declare const EVENT_MODIFIERS: Set<string>;
15
+ /** A parsed-but-unclassified attribute handed to the registry. */
16
+ export interface RawAttribute {
17
+ /** Name exactly as written (`class`, `@click`, `bind:value`, `?disabled`). */
18
+ name: string;
19
+ nameLoc: SourceSpan;
20
+ /** null = valueless attribute. */
21
+ value: AttrValue | null;
22
+ loc: SourceSpan;
23
+ /** Emit a located diagnostic from inside a descriptor. */
24
+ report(code: DiagnosticCode, message: string): void;
25
+ }
26
+ export interface DirectiveDescriptor {
27
+ /** Stable id (for docs/debugging). */
28
+ id: string;
29
+ /** DOM-only sugar (`if`/`each`/`class:`…) is ignored on component tags. */
30
+ elementOnly?: boolean;
31
+ match(name: string): boolean;
32
+ build(raw: RawAttribute): AttributeLikeNode;
33
+ }
34
+ /** Where an attribute sits — a native element or a component tag. */
35
+ export interface AttrContext {
36
+ component: boolean;
37
+ }
38
+ /**
39
+ * The built-in directives. Order matters: prefix forms are matched before the
40
+ * plain-attribute fallback. Downstream tools can prepend their own.
41
+ */
42
+ export declare const CORE_DIRECTIVES: DirectiveDescriptor[];
43
+ /**
44
+ * Resolve a raw attribute to a typed node using `directives` (defaults to the
45
+ * core set). `each`/`key` are handled by the parser afterward because `key` is
46
+ * a sibling attribute that folds into the preceding `each` node.
47
+ */
48
+ export declare function resolveAttribute(raw: RawAttribute, directives?: DirectiveDescriptor[], ctx?: AttrContext): AttributeLikeNode;
49
+ export type { Diagnostic };
50
+ //# sourceMappingURL=directives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directives.d.ts","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EACV,iBAAiB,EAEjB,SAAS,EAGT,UAAU,EACV,cAAc,EAOd,UAAU,EAGX,MAAM,UAAU,CAAC;AAElB,iFAAiF;AACjF,eAAO,MAAM,eAAe,aAAqE,CAAC;AAElG,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,CAAC;IACpB,kCAAkC;IAClC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,UAAU,CAAC;IAChB,0DAA0D;IAC1D,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,mBAAmB;IAClC,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,iBAAiB,CAAC;CAC7C;AAED,qEAAqE;AACrE,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB;AAkBD;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,mBAAmB,EA2IhD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,YAAY,EACjB,UAAU,GAAE,mBAAmB,EAAoB,EACnD,GAAG,GAAE,WAAkC,GACtC,iBAAiB,CAMnB;AAED,YAAY,EAAE,UAAU,EAAE,CAAC"}
@@ -0,0 +1,185 @@
1
+ /** Recognized event modifiers (`@click.capture.once`, `on:input.passive`, …). */
2
+ export const EVENT_MODIFIERS = new Set(['capture', 'once', 'passive', 'prevent', 'stop', 'self']);
3
+ /** Pull the single hole out of a value, or report if the shape is wrong. */
4
+ function requireHole(raw, what) {
5
+ if (raw.value && raw.value.kind === 'hole')
6
+ return raw.value.hole;
7
+ raw.report('invalid-binding', `${what} requires an expression value: ${raw.name}=\${…}`);
8
+ // Recovery: -1 marks a missing hole so the tree stays well-formed.
9
+ return -1;
10
+ }
11
+ const attribute = (raw, boolean) => ({
12
+ kind: 'Attribute',
13
+ name: boolean ? raw.name.slice(1) : raw.name,
14
+ value: raw.value,
15
+ boolean,
16
+ loc: raw.loc,
17
+ });
18
+ /**
19
+ * The built-in directives. Order matters: prefix forms are matched before the
20
+ * plain-attribute fallback. Downstream tools can prepend their own.
21
+ */
22
+ export const CORE_DIRECTIVES = [
23
+ // Events: `@click=${h}`, `on:click=${h}` (native), `onClick=${h}`, each with
24
+ // optional dotted modifiers (`@click.capture.once`, `on:input.passive`).
25
+ {
26
+ id: 'event',
27
+ match: (n) => n[0] === '@' || n.startsWith('on:') || /^on[A-Z]/.test(n),
28
+ build: (raw) => {
29
+ let syntax;
30
+ let rest;
31
+ if (raw.name[0] === '@') {
32
+ syntax = 'at';
33
+ rest = raw.name.slice(1);
34
+ }
35
+ else if (raw.name.startsWith('on:')) {
36
+ syntax = 'colon';
37
+ rest = raw.name.slice(3);
38
+ }
39
+ else {
40
+ syntax = 'on';
41
+ rest = raw.name.slice(2);
42
+ }
43
+ // Dotted modifiers apply to the @/on: forms; the camelCase name has no dots.
44
+ const [name, ...modifiers] = rest.split('.');
45
+ for (const m of modifiers) {
46
+ if (!EVENT_MODIFIERS.has(m)) {
47
+ raw.report('invalid-directive', `Unknown event modifier ".${m}" — expected one of ${[...EVENT_MODIFIERS].join(', ')}`);
48
+ }
49
+ }
50
+ return {
51
+ kind: 'EventBinding',
52
+ name,
53
+ syntax,
54
+ modifiers,
55
+ hole: requireHole(raw, 'event binding'),
56
+ loc: raw.loc,
57
+ };
58
+ },
59
+ },
60
+ // Two-way binding: `bind:value=${signal}`.
61
+ {
62
+ id: 'bind',
63
+ elementOnly: true,
64
+ match: (n) => n.startsWith('bind:'),
65
+ build: (raw) => ({
66
+ kind: 'BindDirective',
67
+ name: raw.name.slice('bind:'.length),
68
+ hole: requireHole(raw, 'two-way binding'),
69
+ loc: raw.loc,
70
+ }),
71
+ },
72
+ // Dynamic class toggle: `class:active=${flag}`.
73
+ {
74
+ id: 'class-directive',
75
+ elementOnly: true,
76
+ match: (n) => n.startsWith('class:'),
77
+ build: (raw) => ({
78
+ kind: 'ClassDirective',
79
+ name: raw.name.slice('class:'.length),
80
+ hole: requireHole(raw, 'class directive'),
81
+ loc: raw.loc,
82
+ }),
83
+ },
84
+ // Dynamic style: `style:color=${c}`.
85
+ {
86
+ id: 'style-directive',
87
+ elementOnly: true,
88
+ match: (n) => n.startsWith('style:'),
89
+ build: (raw) => ({
90
+ kind: 'StyleDirective',
91
+ name: raw.name.slice('style:'.length),
92
+ hole: requireHole(raw, 'style directive'),
93
+ loc: raw.loc,
94
+ }),
95
+ },
96
+ // Custom directive: `use=${d}` or `use:tooltip[=${opts}]`.
97
+ {
98
+ id: 'use',
99
+ match: (n) => n === 'use' || n.startsWith('use:'),
100
+ build: (raw) => {
101
+ const named = raw.name.startsWith('use:');
102
+ return {
103
+ kind: 'UseDirective',
104
+ name: named ? raw.name.slice('use:'.length) : null,
105
+ hole: raw.value && raw.value.kind === 'hole' ? raw.value.hole : null,
106
+ loc: raw.loc,
107
+ };
108
+ },
109
+ },
110
+ // Conditional rendering: `if=${cond}`.
111
+ {
112
+ id: 'if',
113
+ elementOnly: true,
114
+ match: (n) => n === 'if',
115
+ build: (raw) => ({
116
+ kind: 'IfDirective',
117
+ hole: requireHole(raw, 'if directive'),
118
+ loc: raw.loc,
119
+ }),
120
+ },
121
+ // Loop rendering: `each=${items}` (a sibling `key` folds in at parse time).
122
+ {
123
+ id: 'each',
124
+ elementOnly: true,
125
+ match: (n) => n === 'each',
126
+ build: (raw) => ({
127
+ kind: 'EachDirective',
128
+ hole: requireHole(raw, 'each directive'),
129
+ key: null,
130
+ loc: raw.loc,
131
+ }),
132
+ },
133
+ // `else` (valueless).
134
+ {
135
+ id: 'else',
136
+ elementOnly: true,
137
+ match: (n) => n === 'else',
138
+ build: (raw) => {
139
+ if (raw.value !== null)
140
+ raw.report('invalid-directive', '`else` takes no value');
141
+ return { kind: 'ElseDirective', loc: raw.loc };
142
+ },
143
+ },
144
+ // Ref binding: `ref=${el}`.
145
+ {
146
+ id: 'ref',
147
+ match: (n) => n === 'ref',
148
+ build: (raw) => ({
149
+ kind: 'RefBinding',
150
+ hole: requireHole(raw, 'ref binding'),
151
+ loc: raw.loc,
152
+ }),
153
+ },
154
+ // Property binding: `.currentTime=${t}`.
155
+ {
156
+ id: 'property',
157
+ match: (n) => n[0] === '.',
158
+ build: (raw) => ({
159
+ kind: 'PropertyBinding',
160
+ name: raw.name.slice(1),
161
+ hole: requireHole(raw, 'property binding'),
162
+ loc: raw.loc,
163
+ }),
164
+ },
165
+ // Boolean attribute (lit `?disabled`): a plain attribute flagged boolean.
166
+ {
167
+ id: 'boolean',
168
+ match: (n) => n[0] === '?',
169
+ build: (raw) => attribute(raw, true),
170
+ },
171
+ ];
172
+ /**
173
+ * Resolve a raw attribute to a typed node using `directives` (defaults to the
174
+ * core set). `each`/`key` are handled by the parser afterward because `key` is
175
+ * a sibling attribute that folds into the preceding `each` node.
176
+ */
177
+ export function resolveAttribute(raw, directives = CORE_DIRECTIVES, ctx = { component: false }) {
178
+ for (const d of directives) {
179
+ if (d.elementOnly && ctx.component)
180
+ continue; // sugar is DOM-only
181
+ if (d.match(raw.name))
182
+ return d.build(raw);
183
+ }
184
+ return attribute(raw, false);
185
+ }
@@ -0,0 +1 @@
1
+ var d=new Set(["capture","once","passive","prevent","stop","self"]);function i(e,t){return e.value&&e.value.kind==="hole"?e.value.hole:(e.report("invalid-binding",`${t} requires an expression value: ${e.name}=\${…}`),-1)}var r=(e,t)=>({kind:"Attribute",name:t?e.name.slice(1):e.name,value:e.value,boolean:t,loc:e.loc}),s=[{id:"event",match:e=>e[0]==="@"||e.startsWith("on:")||/^on[A-Z]/.test(e),build:e=>{let t,n;e.name[0]==="@"?(t="at",n=e.name.slice(1)):e.name.startsWith("on:")?(t="colon",n=e.name.slice(3)):(t="on",n=e.name.slice(2));let[o,...l]=n.split(".");for(let c of l)d.has(c)||e.report("invalid-directive",`Unknown event modifier ".${c}" — expected one of ${[...d].join(", ")}`);return{kind:"EventBinding",name:o,syntax:t,modifiers:l,hole:i(e,"event binding"),loc:e.loc}}},{id:"bind",elementOnly:!0,match:e=>e.startsWith("bind:"),build:e=>({kind:"BindDirective",name:e.name.slice(5),hole:i(e,"two-way binding"),loc:e.loc})},{id:"class-directive",elementOnly:!0,match:e=>e.startsWith("class:"),build:e=>({kind:"ClassDirective",name:e.name.slice(6),hole:i(e,"class directive"),loc:e.loc})},{id:"style-directive",elementOnly:!0,match:e=>e.startsWith("style:"),build:e=>({kind:"StyleDirective",name:e.name.slice(6),hole:i(e,"style directive"),loc:e.loc})},{id:"use",match:e=>e==="use"||e.startsWith("use:"),build:e=>({kind:"UseDirective",name:e.name.startsWith("use:")?e.name.slice(4):null,hole:e.value&&e.value.kind==="hole"?e.value.hole:null,loc:e.loc})},{id:"if",elementOnly:!0,match:e=>e==="if",build:e=>({kind:"IfDirective",hole:i(e,"if directive"),loc:e.loc})},{id:"each",elementOnly:!0,match:e=>e==="each",build:e=>({kind:"EachDirective",hole:i(e,"each directive"),key:null,loc:e.loc})},{id:"else",elementOnly:!0,match:e=>e==="else",build:e=>(e.value!==null&&e.report("invalid-directive","`else` takes no value"),{kind:"ElseDirective",loc:e.loc})},{id:"ref",match:e=>e==="ref",build:e=>({kind:"RefBinding",hole:i(e,"ref binding"),loc:e.loc})},{id:"property",match:e=>e[0]===".",build:e=>({kind:"PropertyBinding",name:e.name.slice(1),hole:i(e,"property binding"),loc:e.loc})},{id:"boolean",match:e=>e[0]==="?",build:e=>r(e,!0)}];function a(e,t=s,n={component:!1}){for(let o of t)if(!(o.elementOnly&&n.component)&&o.match(e.name))return o.build(e);return r(e,!1)}export{s as CORE_DIRECTIVES,d as EVENT_MODIFIERS,a as resolveAttribute};