dalila 1.3.2 → 1.4.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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Dalila Template Runtime - bind()
3
+ *
4
+ * Binds a DOM tree to a reactive context using declarative attributes.
5
+ * No eval, no inline JS execution - only identifier resolution from ctx.
6
+ *
7
+ * @module dalila/runtime
8
+ */
9
+ export interface BindOptions {
10
+ /**
11
+ * Event types to bind (default: click, input, change, submit, keydown, keyup)
12
+ */
13
+ events?: string[];
14
+ /**
15
+ * Selectors for elements where text interpolation should be skipped
16
+ */
17
+ rawTextSelectors?: string;
18
+ }
19
+ export interface BindContext {
20
+ [key: string]: unknown;
21
+ }
22
+ export type DisposeFunction = () => void;
23
+ /**
24
+ * Bind a DOM tree to a reactive context.
25
+ *
26
+ * @param root - The root element to bind
27
+ * @param ctx - The context object containing handlers and reactive values
28
+ * @param options - Binding options
29
+ * @returns A dispose function that removes all bindings
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { bind } from 'dalila/runtime';
34
+ *
35
+ * const ctx = {
36
+ * count: signal(0),
37
+ * increment: () => count.update(n => n + 1),
38
+ * };
39
+ *
40
+ * const dispose = bind(document.getElementById('app')!, ctx);
41
+ *
42
+ * // Later, to cleanup:
43
+ * dispose();
44
+ * ```
45
+ */
46
+ export declare function bind(root: Element, ctx: BindContext, options?: BindOptions): DisposeFunction;
47
+ /**
48
+ * Automatically bind when DOM is ready.
49
+ * Useful for simple pages without a build step.
50
+ *
51
+ * @example
52
+ * ```html
53
+ * <script type="module">
54
+ * import { autoBind } from 'dalila/runtime';
55
+ * autoBind('#app', { count: signal(0) });
56
+ * </script>
57
+ * ```
58
+ */
59
+ export declare function autoBind(selector: string, ctx: BindContext, options?: BindOptions): Promise<DisposeFunction>;
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Dalila Template Runtime - bind()
3
+ *
4
+ * Binds a DOM tree to a reactive context using declarative attributes.
5
+ * No eval, no inline JS execution - only identifier resolution from ctx.
6
+ *
7
+ * @module dalila/runtime
8
+ */
9
+ import { effect, createScope, withScope, isInDevMode } from '../core/index.js';
10
+ // ============================================================================
11
+ // Utilities
12
+ // ============================================================================
13
+ /**
14
+ * Check if a value is a Dalila signal
15
+ */
16
+ function isSignal(value) {
17
+ return typeof value === 'function' && 'set' in value && 'update' in value;
18
+ }
19
+ /**
20
+ * Resolve a value from ctx - handles signals, functions, and plain values
21
+ */
22
+ function resolve(value) {
23
+ if (isSignal(value))
24
+ return value();
25
+ if (typeof value === 'function')
26
+ return value();
27
+ return value;
28
+ }
29
+ /**
30
+ * Normalize binding attribute value
31
+ * Handles both "name" and "{name}" formats
32
+ */
33
+ function normalizeBinding(raw) {
34
+ if (!raw)
35
+ return null;
36
+ const trimmed = raw.trim();
37
+ // Match {name} format and extract name
38
+ const match = trimmed.match(/^\{\s*([a-zA-Z_$][\w$]*)\s*\}$/);
39
+ return match ? match[1] : trimmed;
40
+ }
41
+ /**
42
+ * Dev mode warning helper
43
+ */
44
+ function warn(message) {
45
+ if (isInDevMode()) {
46
+ console.warn(`[Dalila] ${message}`);
47
+ }
48
+ }
49
+ // ============================================================================
50
+ // Default Options
51
+ // ============================================================================
52
+ const DEFAULT_EVENTS = ['click', 'input', 'change', 'submit', 'keydown', 'keyup'];
53
+ const DEFAULT_RAW_TEXT_SELECTORS = 'pre, code';
54
+ // ============================================================================
55
+ // Text Interpolation
56
+ // ============================================================================
57
+ /**
58
+ * Process a text node and replace {tokens} with reactive bindings
59
+ */
60
+ function bindTextNode(node, ctx, cleanups) {
61
+ const text = node.data;
62
+ const regex = /\{\s*([a-zA-Z_$][\w$]*)\s*\}/g;
63
+ // Check if there are any tokens
64
+ if (!regex.test(text))
65
+ return;
66
+ // Reset regex
67
+ regex.lastIndex = 0;
68
+ const frag = document.createDocumentFragment();
69
+ let cursor = 0;
70
+ let match;
71
+ while ((match = regex.exec(text)) !== null) {
72
+ // Add text before the token
73
+ const before = text.slice(cursor, match.index);
74
+ if (before) {
75
+ frag.appendChild(document.createTextNode(before));
76
+ }
77
+ const key = match[1];
78
+ const value = ctx[key];
79
+ if (value === undefined) {
80
+ warn(`Text interpolation: "${key}" not found in context`);
81
+ frag.appendChild(document.createTextNode(match[0]));
82
+ }
83
+ else if (isSignal(value)) {
84
+ // Reactive text node
85
+ const textNode = document.createTextNode('');
86
+ const stop = effect(() => {
87
+ textNode.data = String(value());
88
+ });
89
+ if (typeof stop === 'function') {
90
+ cleanups.push(stop);
91
+ }
92
+ frag.appendChild(textNode);
93
+ }
94
+ else {
95
+ // Static value - render once
96
+ frag.appendChild(document.createTextNode(String(value)));
97
+ }
98
+ cursor = match.index + match[0].length;
99
+ }
100
+ // Add remaining text
101
+ const after = text.slice(cursor);
102
+ if (after) {
103
+ frag.appendChild(document.createTextNode(after));
104
+ }
105
+ // Replace original node
106
+ if (node.parentNode) {
107
+ node.parentNode.replaceChild(frag, node);
108
+ }
109
+ }
110
+ // ============================================================================
111
+ // Event Binding
112
+ // ============================================================================
113
+ /**
114
+ * Bind all d-on-* events within root
115
+ */
116
+ function bindEvents(root, ctx, events, cleanups) {
117
+ for (const eventName of events) {
118
+ const attr = `d-on-${eventName}`;
119
+ const elements = root.querySelectorAll(`[${attr}]`);
120
+ elements.forEach((el) => {
121
+ const handlerName = normalizeBinding(el.getAttribute(attr));
122
+ if (!handlerName)
123
+ return;
124
+ const handler = ctx[handlerName];
125
+ if (handler === undefined) {
126
+ warn(`Event handler "${handlerName}" not found in context`);
127
+ return;
128
+ }
129
+ if (typeof handler !== 'function') {
130
+ warn(`Event handler "${handlerName}" is not a function`);
131
+ return;
132
+ }
133
+ el.addEventListener(eventName, handler);
134
+ cleanups.push(() => el.removeEventListener(eventName, handler));
135
+ });
136
+ }
137
+ }
138
+ // ============================================================================
139
+ // when Directive
140
+ // ============================================================================
141
+ /**
142
+ * Bind all [when] directives within root
143
+ */
144
+ function bindWhen(root, ctx, cleanups) {
145
+ const elements = root.querySelectorAll('[when]');
146
+ elements.forEach((el) => {
147
+ const bindingName = normalizeBinding(el.getAttribute('when'));
148
+ if (!bindingName)
149
+ return;
150
+ const binding = ctx[bindingName];
151
+ if (binding === undefined) {
152
+ warn(`when: "${bindingName}" not found in context`);
153
+ return;
154
+ }
155
+ const htmlEl = el;
156
+ const stop = effect(() => {
157
+ const value = !!resolve(binding);
158
+ htmlEl.style.display = value ? '' : 'none';
159
+ });
160
+ if (typeof stop === 'function') {
161
+ cleanups.push(stop);
162
+ }
163
+ });
164
+ }
165
+ // ============================================================================
166
+ // match Directive
167
+ // ============================================================================
168
+ /**
169
+ * Bind all [match] directives within root
170
+ */
171
+ function bindMatch(root, ctx, cleanups) {
172
+ const elements = root.querySelectorAll('[match]');
173
+ elements.forEach((el) => {
174
+ const bindingName = normalizeBinding(el.getAttribute('match'));
175
+ if (!bindingName)
176
+ return;
177
+ const binding = ctx[bindingName];
178
+ if (binding === undefined) {
179
+ warn(`match: "${bindingName}" not found in context`);
180
+ return;
181
+ }
182
+ const cases = Array.from(el.querySelectorAll('[case]'));
183
+ const stop = effect(() => {
184
+ const value = String(resolve(binding));
185
+ let matchedEl = null;
186
+ let defaultEl = null;
187
+ // First pass: hide all and find match/default
188
+ for (const caseEl of cases) {
189
+ caseEl.style.display = 'none';
190
+ const caseValue = caseEl.getAttribute('case');
191
+ if (caseValue === 'default') {
192
+ defaultEl = caseEl;
193
+ }
194
+ else if (caseValue === value && !matchedEl) {
195
+ matchedEl = caseEl;
196
+ }
197
+ }
198
+ // Second pass: show match OR default (not both)
199
+ if (matchedEl) {
200
+ matchedEl.style.display = '';
201
+ }
202
+ else if (defaultEl) {
203
+ defaultEl.style.display = '';
204
+ }
205
+ });
206
+ if (typeof stop === 'function') {
207
+ cleanups.push(stop);
208
+ }
209
+ });
210
+ }
211
+ // ============================================================================
212
+ // Main bind() Function
213
+ // ============================================================================
214
+ /**
215
+ * Bind a DOM tree to a reactive context.
216
+ *
217
+ * @param root - The root element to bind
218
+ * @param ctx - The context object containing handlers and reactive values
219
+ * @param options - Binding options
220
+ * @returns A dispose function that removes all bindings
221
+ *
222
+ * @example
223
+ * ```ts
224
+ * import { bind } from 'dalila/runtime';
225
+ *
226
+ * const ctx = {
227
+ * count: signal(0),
228
+ * increment: () => count.update(n => n + 1),
229
+ * };
230
+ *
231
+ * const dispose = bind(document.getElementById('app')!, ctx);
232
+ *
233
+ * // Later, to cleanup:
234
+ * dispose();
235
+ * ```
236
+ */
237
+ export function bind(root, ctx, options = {}) {
238
+ const events = options.events ?? DEFAULT_EVENTS;
239
+ const rawTextSelectors = options.rawTextSelectors ?? DEFAULT_RAW_TEXT_SELECTORS;
240
+ const htmlRoot = root;
241
+ // Create a scope for this template binding
242
+ const templateScope = createScope();
243
+ const cleanups = [];
244
+ // Run all bindings within the template scope
245
+ withScope(templateScope, () => {
246
+ // 1. Text interpolation
247
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
248
+ const textNodes = [];
249
+ while (walker.nextNode()) {
250
+ const node = walker.currentNode;
251
+ // Skip nodes inside raw text containers
252
+ const parent = node.parentElement;
253
+ if (parent && parent.closest(rawTextSelectors)) {
254
+ continue;
255
+ }
256
+ if (node.data.includes('{')) {
257
+ textNodes.push(node);
258
+ }
259
+ }
260
+ // Process text nodes (collect first, then process to avoid walker issues)
261
+ for (const node of textNodes) {
262
+ bindTextNode(node, ctx, cleanups);
263
+ }
264
+ // 2. Event bindings
265
+ bindEvents(root, ctx, events, cleanups);
266
+ // 3. when directive
267
+ bindWhen(root, ctx, cleanups);
268
+ // 4. match directive
269
+ bindMatch(root, ctx, cleanups);
270
+ });
271
+ // Bindings complete: remove loading state and mark as ready
272
+ // Use microtask to ensure all effects have run at least once
273
+ queueMicrotask(() => {
274
+ htmlRoot.removeAttribute('d-loading');
275
+ htmlRoot.setAttribute('d-ready', '');
276
+ });
277
+ // Return dispose function
278
+ return () => {
279
+ // Run manual cleanups (event listeners)
280
+ for (const cleanup of cleanups) {
281
+ if (typeof cleanup === 'function') {
282
+ try {
283
+ cleanup();
284
+ }
285
+ catch (e) {
286
+ if (isInDevMode()) {
287
+ console.warn('[Dalila] Cleanup error:', e);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ cleanups.length = 0;
293
+ // Dispose template scope (stops all effects)
294
+ try {
295
+ templateScope.dispose();
296
+ }
297
+ catch (e) {
298
+ if (isInDevMode()) {
299
+ console.warn('[Dalila] Scope dispose error:', e);
300
+ }
301
+ }
302
+ };
303
+ }
304
+ // ============================================================================
305
+ // Convenience: Auto-bind on DOMContentLoaded
306
+ // ============================================================================
307
+ /**
308
+ * Automatically bind when DOM is ready.
309
+ * Useful for simple pages without a build step.
310
+ *
311
+ * @example
312
+ * ```html
313
+ * <script type="module">
314
+ * import { autoBind } from 'dalila/runtime';
315
+ * autoBind('#app', { count: signal(0) });
316
+ * </script>
317
+ * ```
318
+ */
319
+ export function autoBind(selector, ctx, options) {
320
+ return new Promise((resolve, reject) => {
321
+ const doBind = () => {
322
+ const root = document.querySelector(selector);
323
+ if (!root) {
324
+ reject(new Error(`[Dalila] Element not found: ${selector}`));
325
+ return;
326
+ }
327
+ resolve(bind(root, ctx, options));
328
+ };
329
+ if (document.readyState === 'loading') {
330
+ document.addEventListener('DOMContentLoaded', doBind, { once: true });
331
+ }
332
+ else {
333
+ doBind();
334
+ }
335
+ });
336
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Dalila Runtime Module
3
+ *
4
+ * Provides declarative DOM binding with reactive updates.
5
+ * No eval, no inline JS - only identifier resolution.
6
+ *
7
+ * @module dalila/runtime
8
+ */
9
+ export { bind, autoBind } from './bind.js';
10
+ export type { BindOptions, BindContext, DisposeFunction } from './bind.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Dalila Runtime Module
3
+ *
4
+ * Provides declarative DOM binding with reactive updates.
5
+ * No eval, no inline JS - only identifier resolution.
6
+ *
7
+ * @module dalila/runtime
8
+ */
9
+ export { bind, autoBind } from './bind.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,6 +9,22 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
+ },
13
+ "./core": {
14
+ "types": "./dist/core/index.d.ts",
15
+ "default": "./dist/core/index.js"
16
+ },
17
+ "./context": {
18
+ "types": "./dist/context/index.d.ts",
19
+ "default": "./dist/context/index.js"
20
+ },
21
+ "./context/raw": {
22
+ "types": "./dist/context/raw.d.ts",
23
+ "default": "./dist/context/raw.js"
24
+ },
25
+ "./runtime": {
26
+ "types": "./dist/runtime/index.d.ts",
27
+ "default": "./dist/runtime/index.js"
12
28
  }
13
29
  },
14
30
  "scripts": {
@@ -34,6 +50,7 @@
34
50
  "@playwright/test": "^1.57.0",
35
51
  "@types/jest": "^29.5.0",
36
52
  "@types/node": "^20.0.0",
53
+ "chokidar": "^3.6.0",
37
54
  "jest": "^29.5.0",
38
55
  "jest-environment-jsdom": "^29.5.0",
39
56
  "typescript": "^5.9.3"
@@ -1,85 +0,0 @@
1
- export interface DalilaTemplate {
2
- path: string;
3
- content: string;
4
- componentName: string;
5
- }
6
- export interface CompilationResult {
7
- source: string;
8
- typeDefinition: string;
9
- diagnostics: Diagnostic[];
10
- }
11
- export interface Diagnostic {
12
- message: string;
13
- line: number;
14
- column: number;
15
- severity: 'error' | 'warning' | 'info';
16
- }
17
- export interface ElementNode {
18
- type: 'element';
19
- tagName: string;
20
- attributes: Attribute[];
21
- children: Node[];
22
- selfClosing: boolean;
23
- location: Location;
24
- }
25
- export interface TextNode {
26
- type: 'text';
27
- content: string;
28
- location: Location;
29
- }
30
- export interface InterpolationNode {
31
- type: 'interpolation';
32
- identifier: string;
33
- location: Location;
34
- }
35
- export interface Attribute {
36
- name: string;
37
- value: string | InterpolationNode | null;
38
- location: Location;
39
- }
40
- export interface Location {
41
- line: number;
42
- column: number;
43
- offset: number;
44
- }
45
- export type Node = ElementNode | TextNode | InterpolationNode;
46
- export declare function isElementNode(node: Node): node is ElementNode;
47
- export declare function isTextNode(node: Node): node is TextNode;
48
- export declare function isInterpolationNode(node: Node): node is InterpolationNode;
49
- export declare class DalilaParser {
50
- private input;
51
- private position;
52
- private line;
53
- private column;
54
- private rawTextTags;
55
- constructor(input: string);
56
- parse(): {
57
- ast: Node[];
58
- diagnostics: Diagnostic[];
59
- };
60
- private parseElement;
61
- private parseTagName;
62
- private parseAttributes;
63
- private parseAttribute;
64
- private parseInterpolation;
65
- private parseText;
66
- private skipWhitespace;
67
- private match;
68
- private lookingAt;
69
- private peek;
70
- private advance;
71
- private backup;
72
- private isAtEnd;
73
- private currentLocation;
74
- }
75
- export declare class DalilaCodeGenerator {
76
- private indentLevel;
77
- private output;
78
- generate(ast: Node[], componentName: string): CompilationResult;
79
- private generateElement;
80
- private generateAttribute;
81
- private generateTextInterpolation;
82
- private generateTypeDefinition;
83
- private emit;
84
- }
85
- export declare function compileDalilaTemplate(template: DalilaTemplate): CompilationResult;