gonia 0.1.2 → 0.2.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.
@@ -4,8 +4,8 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  import { directive, DirectivePriority, Mode } from '../types.js';
7
- import { effect, createEffectScope, createScope } from '../reactivity.js';
8
- import { createContext } from '../context.js';
7
+ import { effect, createEffectScope } from '../reactivity.js';
8
+ import { processElementTree } from '../process.js';
9
9
  /**
10
10
  * Parse a g-for expression.
11
11
  *
@@ -31,77 +31,10 @@ function parseForExpression(expr) {
31
31
  export const FOR_PROCESSED_ATTR = 'data-g-for-processed';
32
32
  /** Attribute used to mark template content that should be skipped during SSR */
33
33
  export const FOR_TEMPLATE_ATTR = 'data-g-for-template';
34
- /**
35
- * Process directives on a cloned element within a child scope.
36
- */
37
- function processClonedElement(el, parentState, scopeAdditions, mode) {
38
- // Mark this element as processed by g-for so hydrate skips it
39
- el.setAttribute(FOR_PROCESSED_ATTR, '');
40
- const childScope = createScope(parentState, scopeAdditions);
41
- const childCtx = createContext(mode, childScope);
42
- // Process g-text directives
43
- const textAttr = el.getAttribute('g-text');
44
- if (textAttr) {
45
- const value = childCtx.eval(textAttr);
46
- el.textContent = String(value ?? '');
47
- }
48
- // Process g-class directives
49
- const classAttr = el.getAttribute('g-class');
50
- if (classAttr) {
51
- const classObj = childCtx.eval(classAttr);
52
- if (classObj && typeof classObj === 'object') {
53
- for (const [className, shouldAdd] of Object.entries(classObj)) {
54
- if (shouldAdd) {
55
- el.classList.add(className);
56
- }
57
- else {
58
- el.classList.remove(className);
59
- }
60
- }
61
- }
62
- }
63
- // Process g-show directives
64
- const showAttr = el.getAttribute('g-show');
65
- if (showAttr) {
66
- const value = childCtx.eval(showAttr);
67
- el.style.display = value ? '' : 'none';
68
- }
69
- // Process g-on directives (format: "event: handler") - client only
70
- if (mode === Mode.CLIENT) {
71
- const onAttr = el.getAttribute('g-on');
72
- if (onAttr) {
73
- setupEventHandler(el, onAttr, childCtx, childScope);
74
- }
75
- }
76
- // Process children recursively
77
- for (const child of el.children) {
78
- processClonedElement(child, childScope, {}, mode);
79
- }
80
- }
81
- /**
82
- * Set up an event handler on an element.
83
- * Expression format: "event: handler"
84
- */
85
- function setupEventHandler(el, expr, ctx, state) {
86
- const colonIdx = expr.indexOf(':');
87
- if (colonIdx === -1) {
88
- console.error(`Invalid g-on expression: ${expr}. Expected "event: handler"`);
89
- return;
90
- }
91
- const eventName = expr.slice(0, colonIdx).trim();
92
- const handlerExpr = expr.slice(colonIdx + 1).trim();
93
- const handler = (event) => {
94
- const result = ctx.eval(handlerExpr);
95
- if (typeof result === 'function') {
96
- result.call(state, event);
97
- }
98
- };
99
- el.addEventListener(eventName, handler);
100
- }
101
34
  /**
102
35
  * Render loop items (used by both server and client).
103
36
  */
104
- function renderItems(template, parent, insertAfterNode, parsed, $eval, $state, mode) {
37
+ function renderItems(template, parent, insertAfterNode, parsed, $eval, $scope, mode) {
105
38
  const { itemName, indexName, iterableName } = parsed;
106
39
  const iterable = $eval(iterableName);
107
40
  const renderedElements = [];
@@ -136,7 +69,10 @@ function renderItems(template, parent, insertAfterNode, parsed, $eval, $state, m
136
69
  if (indexName) {
137
70
  scopeAdditions[indexName] = key;
138
71
  }
139
- processClonedElement(clone, $state, scopeAdditions, mode);
72
+ // Mark as processed
73
+ clone.setAttribute(FOR_PROCESSED_ATTR, '');
74
+ // Process with shared utility
75
+ processElementTree(clone, $scope, mode, { scopeAdditions });
140
76
  if (insertAfter.nextSibling) {
141
77
  parent.insertBefore(clone, insertAfter.nextSibling);
142
78
  }
@@ -177,7 +113,7 @@ function removeSSRItems(templateEl) {
177
113
  * <div g-for="(value, key) in object" g-text="key + ': ' + value"></div>
178
114
  * ```
179
115
  */
180
- export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
116
+ export const cfor = function cfor($expr, $element, $eval, $scope, $mode) {
181
117
  const parsed = parseForExpression($expr);
182
118
  if (!parsed) {
183
119
  console.error(`Invalid g-for expression: ${$expr}`);
@@ -204,7 +140,7 @@ export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
204
140
  // Replace original with template wrapper
205
141
  parent.replaceChild(templateWrapper, $element);
206
142
  // Render items after the template
207
- renderItems(templateContent, parent, templateWrapper, parsed, $eval, $state, $mode);
143
+ renderItems(templateContent, parent, templateWrapper, parsed, $eval, $scope, $mode);
208
144
  return;
209
145
  }
210
146
  // Client-side: check if hydrating from SSR or fresh render
@@ -230,7 +166,7 @@ export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
230
166
  }
231
167
  scope = createEffectScope();
232
168
  scope.run(() => {
233
- renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $state, Mode.CLIENT);
169
+ renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $scope, Mode.CLIENT);
234
170
  });
235
171
  });
236
172
  }
@@ -255,11 +191,11 @@ export const cfor = function cfor($expr, $element, $eval, $state, $mode) {
255
191
  }
256
192
  scope = createEffectScope();
257
193
  scope.run(() => {
258
- renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $state, Mode.CLIENT);
194
+ renderedElements = renderItems(templateContent, parent, templateWrapper, parsed, $eval, $scope, Mode.CLIENT);
259
195
  });
260
196
  });
261
197
  }
262
198
  };
263
- cfor.$inject = ['$expr', '$element', '$eval', '$state', '$mode'];
199
+ cfor.$inject = ['$expr', '$element', '$eval', '$scope', '$mode'];
264
200
  cfor.priority = DirectivePriority.STRUCTURAL;
265
201
  directive('g-for', cfor);
@@ -13,6 +13,9 @@ export declare const IF_PROCESSED_ATTR = "data-g-if-processed";
13
13
  * Unlike g-show which uses display:none, g-if completely removes
14
14
  * the element from the DOM when the condition is falsy.
15
15
  *
16
+ * State within the conditional block is preserved across toggles.
17
+ * The scope is anchored to the placeholder, not the rendered element.
18
+ *
16
19
  * On server: evaluates once and removes element if false.
17
20
  * On client: sets up reactive effect to toggle element.
18
21
  *
@@ -22,4 +25,4 @@ export declare const IF_PROCESSED_ATTR = "data-g-if-processed";
22
25
  * <div g-if="items.length > 0">Has items</div>
23
26
  * ```
24
27
  */
25
- export declare const cif: Directive<['$expr', '$element', '$eval', '$state', '$mode']>;
28
+ export declare const cif: Directive<['$expr', '$element', '$eval', '$scope', '$mode']>;
@@ -4,66 +4,30 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
  import { directive, DirectivePriority, Mode } from '../types.js';
7
- import { effect } from '../reactivity.js';
8
- import { createContext } from '../context.js';
9
- import { createScope } from '../reactivity.js';
7
+ import { effect, createScope } from '../reactivity.js';
8
+ import { processElementTree } from '../process.js';
10
9
  /** Attribute used to mark elements processed by g-if */
11
10
  export const IF_PROCESSED_ATTR = 'data-g-if-processed';
11
+ /** WeakMap to store persistent scopes for g-if placeholders */
12
+ const placeholderScopes = new WeakMap();
12
13
  /**
13
- * Process directives on a conditionally rendered element.
14
+ * Get or create a persistent scope for a g-if placeholder.
15
+ *
16
+ * @remarks
17
+ * The scope is anchored to the placeholder element, not the rendered content.
18
+ * This allows state to persist across condition toggles.
19
+ *
20
+ * @param placeholder - The template placeholder element
21
+ * @param parentState - The parent scope to inherit from
22
+ * @returns The persistent scope for this g-if block
14
23
  */
15
- function processConditionalElement(el, parentState, mode) {
16
- el.setAttribute(IF_PROCESSED_ATTR, '');
17
- const childScope = createScope(parentState, {});
18
- const childCtx = createContext(mode, childScope);
19
- // Process g-text directives
20
- const textAttr = el.getAttribute('g-text');
21
- if (textAttr) {
22
- const value = childCtx.eval(textAttr);
23
- el.textContent = String(value ?? '');
24
- }
25
- // Process g-class directives
26
- const classAttr = el.getAttribute('g-class');
27
- if (classAttr) {
28
- const classObj = childCtx.eval(classAttr);
29
- if (classObj && typeof classObj === 'object') {
30
- for (const [className, shouldAdd] of Object.entries(classObj)) {
31
- if (shouldAdd) {
32
- el.classList.add(className);
33
- }
34
- else {
35
- el.classList.remove(className);
36
- }
37
- }
38
- }
39
- }
40
- // Process g-show directives
41
- const showAttr = el.getAttribute('g-show');
42
- if (showAttr) {
43
- const value = childCtx.eval(showAttr);
44
- el.style.display = value ? '' : 'none';
45
- }
46
- // Process g-on directives (format: "event: handler") - client only
47
- if (mode === Mode.CLIENT) {
48
- const onAttr = el.getAttribute('g-on');
49
- if (onAttr) {
50
- const colonIdx = onAttr.indexOf(':');
51
- if (colonIdx !== -1) {
52
- const eventName = onAttr.slice(0, colonIdx).trim();
53
- const handlerExpr = onAttr.slice(colonIdx + 1).trim();
54
- el.addEventListener(eventName, (event) => {
55
- const result = childCtx.eval(handlerExpr);
56
- if (typeof result === 'function') {
57
- result.call(childScope, event);
58
- }
59
- });
60
- }
61
- }
62
- }
63
- // Process children recursively
64
- for (const child of el.children) {
65
- processConditionalElement(child, childScope, mode);
24
+ function getOrCreateScope(placeholder, parentState) {
25
+ let scope = placeholderScopes.get(placeholder);
26
+ if (!scope) {
27
+ scope = createScope(parentState, {});
28
+ placeholderScopes.set(placeholder, scope);
66
29
  }
30
+ return scope;
67
31
  }
68
32
  /**
69
33
  * Conditionally render an element.
@@ -72,6 +36,9 @@ function processConditionalElement(el, parentState, mode) {
72
36
  * Unlike g-show which uses display:none, g-if completely removes
73
37
  * the element from the DOM when the condition is falsy.
74
38
  *
39
+ * State within the conditional block is preserved across toggles.
40
+ * The scope is anchored to the placeholder, not the rendered element.
41
+ *
75
42
  * On server: evaluates once and removes element if false.
76
43
  * On client: sets up reactive effect to toggle element.
77
44
  *
@@ -81,37 +48,72 @@ function processConditionalElement(el, parentState, mode) {
81
48
  * <div g-if="items.length > 0">Has items</div>
82
49
  * ```
83
50
  */
84
- export const cif = function cif($expr, $element, $eval, $state, $mode) {
51
+ export const cif = function cif($expr, $element, $eval, $scope, $mode) {
85
52
  const parent = $element.parentNode;
86
53
  if (!parent) {
87
54
  return;
88
55
  }
89
- // Server-side: evaluate once and remove if false
56
+ // Server-side: evaluate once and leave template placeholder if false
90
57
  if ($mode === Mode.SERVER) {
91
58
  const condition = $eval($expr);
92
59
  if (!condition) {
60
+ // Leave a template marker so client hydration knows where to insert
61
+ const placeholder = $element.ownerDocument.createElement('template');
62
+ placeholder.setAttribute('data-g-if', String($expr));
63
+ // Store original element inside template for hydration
64
+ placeholder.innerHTML = $element.outerHTML;
65
+ parent.insertBefore(placeholder, $element);
93
66
  $element.remove();
94
67
  }
95
68
  else {
96
69
  // Process child directives
97
70
  $element.removeAttribute('g-if');
98
- processConditionalElement($element, $state, $mode);
71
+ $element.setAttribute(IF_PROCESSED_ATTR, '');
72
+ processElementTree($element, $scope, $mode);
99
73
  }
100
74
  return;
101
75
  }
102
- // Client-side: set up reactive effect
103
- const placeholder = $element.ownerDocument.createComment(` g-if: ${$expr} `);
104
- parent.insertBefore(placeholder, $element);
105
- const template = $element.cloneNode(true);
106
- template.removeAttribute('g-if');
107
- $element.remove();
76
+ // Client-side: check if this is a template placeholder (from SSR)
77
+ const isTemplatePlaceholder = $element.tagName === 'TEMPLATE' && $element.hasAttribute('data-g-if');
78
+ let placeholder;
79
+ let template;
80
+ if (isTemplatePlaceholder) {
81
+ // Hydrating SSR output - template is the placeholder, content is inside
82
+ placeholder = $element;
83
+ const content = $element.content.firstElementChild
84
+ || $element.innerHTML;
85
+ if (typeof content === 'string') {
86
+ const temp = $element.ownerDocument.createElement('div');
87
+ temp.innerHTML = content;
88
+ template = temp.firstElementChild;
89
+ }
90
+ else {
91
+ template = content.cloneNode(true);
92
+ }
93
+ template.removeAttribute('g-if');
94
+ }
95
+ else {
96
+ // Pure client-side - create template placeholder
97
+ placeholder = $element.ownerDocument.createElement('template');
98
+ placeholder.setAttribute('data-g-if', String($expr));
99
+ parent.insertBefore(placeholder, $element);
100
+ template = $element.cloneNode(true);
101
+ template.removeAttribute('g-if');
102
+ $element.remove();
103
+ }
104
+ // Create persistent scope anchored to the placeholder
105
+ const persistentScope = getOrCreateScope(placeholder, $scope);
108
106
  let renderedElement = null;
109
107
  effect(() => {
110
108
  const condition = $eval($expr);
111
109
  if (condition) {
112
110
  if (!renderedElement) {
113
111
  renderedElement = template.cloneNode(true);
114
- processConditionalElement(renderedElement, $state, Mode.CLIENT);
112
+ renderedElement.setAttribute(IF_PROCESSED_ATTR, '');
113
+ // Process with the persistent scope - state survives across toggles
114
+ processElementTree(renderedElement, $scope, Mode.CLIENT, {
115
+ existingScope: persistentScope
116
+ });
115
117
  if (placeholder.nextSibling) {
116
118
  parent.insertBefore(renderedElement, placeholder.nextSibling);
117
119
  }
@@ -124,10 +126,11 @@ export const cif = function cif($expr, $element, $eval, $state, $mode) {
124
126
  if (renderedElement) {
125
127
  renderedElement.remove();
126
128
  renderedElement = null;
129
+ // Scope survives in persistentScope - ready for next render
127
130
  }
128
131
  }
129
132
  });
130
133
  };
131
- cif.$inject = ['$expr', '$element', '$eval', '$state', '$mode'];
134
+ cif.$inject = ['$expr', '$element', '$eval', '$scope', '$mode'];
132
135
  cif.priority = DirectivePriority.STRUCTURAL;
133
136
  directive('g-if', cif);
@@ -9,12 +9,14 @@
9
9
  * @packageDocumentation
10
10
  */
11
11
  import { Directive } from '../types.js';
12
+ import { SlotContentContext } from './template.js';
12
13
  /**
13
14
  * Slot directive for content transclusion.
14
15
  *
15
16
  * @remarks
16
17
  * Finds the nearest template ancestor and transcludes the
17
- * matching slot content into itself.
18
+ * matching slot content into itself. The SlotContentContext
19
+ * is automatically injected via DI.
18
20
  *
19
21
  * If the slot name is an expression, wraps in an effect
20
22
  * for reactivity.
@@ -35,7 +37,7 @@ import { Directive } from '../types.js';
35
37
  * <slot></slot>
36
38
  * ```
37
39
  */
38
- export declare const slot: Directive<['$expr', '$element', '$eval']>;
40
+ export declare const slot: Directive<['$expr', '$element', '$eval', typeof SlotContentContext]>;
39
41
  /**
40
42
  * Process native <slot> elements.
41
43
  *
@@ -10,13 +10,15 @@
10
10
  */
11
11
  import { directive } from '../types.js';
12
12
  import { effect } from '../reactivity.js';
13
- import { findTemplateAncestor, getSavedContent } from './template.js';
13
+ import { resolveContext } from '../context-registry.js';
14
+ import { SlotContentContext } from './template.js';
14
15
  /**
15
16
  * Slot directive for content transclusion.
16
17
  *
17
18
  * @remarks
18
19
  * Finds the nearest template ancestor and transcludes the
19
- * matching slot content into itself.
20
+ * matching slot content into itself. The SlotContentContext
21
+ * is automatically injected via DI.
20
22
  *
21
23
  * If the slot name is an expression, wraps in an effect
22
24
  * for reactivity.
@@ -37,7 +39,7 @@ import { findTemplateAncestor, getSavedContent } from './template.js';
37
39
  * <slot></slot>
38
40
  * ```
39
41
  */
40
- export const slot = function slot($expr, $element, $eval) {
42
+ export const slot = function slot($expr, $element, $eval, $slotContent) {
41
43
  // Determine slot name
42
44
  // If expr is empty, check for name attribute, otherwise use 'default'
43
45
  const getName = () => {
@@ -50,16 +52,11 @@ export const slot = function slot($expr, $element, $eval) {
50
52
  };
51
53
  const transclude = () => {
52
54
  const name = getName();
53
- const templateEl = findTemplateAncestor($element);
54
- if (!templateEl) {
55
- // No template ancestor - leave slot as-is or clear it
55
+ // SlotContentContext is injected via DI
56
+ if (!$slotContent) {
56
57
  return;
57
58
  }
58
- const content = getSavedContent(templateEl);
59
- if (!content) {
60
- return;
61
- }
62
- const slotContent = content.slots.get(name);
59
+ const slotContent = $slotContent.slots.get(name);
63
60
  if (slotContent) {
64
61
  $element.innerHTML = slotContent;
65
62
  }
@@ -73,6 +70,7 @@ export const slot = function slot($expr, $element, $eval) {
73
70
  transclude();
74
71
  }
75
72
  };
73
+ slot.$inject = ['$expr', '$element', '$eval', SlotContentContext];
76
74
  directive('g-slot', slot);
77
75
  /**
78
76
  * Process native <slot> elements.
@@ -85,11 +83,8 @@ directive('g-slot', slot);
85
83
  */
86
84
  export function processNativeSlot(el) {
87
85
  const name = el.getAttribute('name') ?? 'default';
88
- const templateEl = findTemplateAncestor(el);
89
- if (!templateEl) {
90
- return;
91
- }
92
- const content = getSavedContent(templateEl);
86
+ // Resolve slot content from nearest template ancestor
87
+ const content = resolveContext(el, SlotContentContext);
93
88
  if (!content) {
94
89
  return;
95
90
  }
@@ -10,15 +10,22 @@
10
10
  */
11
11
  import { Directive } from '../types.js';
12
12
  import { EffectScope } from '../reactivity.js';
13
+ import { ContextKey } from '../context-registry.js';
13
14
  /**
14
15
  * Saved slot content for an element.
15
- *
16
- * @internal
17
16
  */
18
17
  export interface SlotContent {
19
18
  /** Content by slot name. 'default' for unnamed content. */
20
19
  slots: Map<string, string>;
21
20
  }
21
+ /**
22
+ * Context key for slot content.
23
+ *
24
+ * @remarks
25
+ * Templates register their slot content using this context, and
26
+ * slot directives resolve it to find their content.
27
+ */
28
+ export declare const SlotContentContext: ContextKey<SlotContent>;
22
29
  /**
23
30
  * Get saved slot content for an element.
24
31
  *
@@ -10,8 +10,16 @@
10
10
  */
11
11
  import { directive, DirectivePriority } from '../types.js';
12
12
  import { createEffectScope } from '../reactivity.js';
13
- /** WeakMap storing saved children per element. */
14
- const savedContent = new WeakMap();
13
+ import { findAncestor } from '../dom.js';
14
+ import { createContextKey, registerContext, resolveContext, hasContext } from '../context-registry.js';
15
+ /**
16
+ * Context key for slot content.
17
+ *
18
+ * @remarks
19
+ * Templates register their slot content using this context, and
20
+ * slot directives resolve it to find their content.
21
+ */
22
+ export const SlotContentContext = createContextKey('SlotContent');
15
23
  /** WeakMap storing effect scopes per element for cleanup. */
16
24
  const elementScopes = new WeakMap();
17
25
  /** Set tracking which templates are currently rendering (cycle detection). */
@@ -22,7 +30,7 @@ const renderingChain = new WeakMap();
22
30
  * @internal
23
31
  */
24
32
  export function getSavedContent(el) {
25
- return savedContent.get(el);
33
+ return resolveContext(el, SlotContentContext, true);
26
34
  }
27
35
  /**
28
36
  * Find the nearest ancestor with saved content (the template element).
@@ -30,14 +38,7 @@ export function getSavedContent(el) {
30
38
  * @internal
31
39
  */
32
40
  export function findTemplateAncestor(el) {
33
- let current = el.parentElement;
34
- while (current) {
35
- if (savedContent.has(current)) {
36
- return current;
37
- }
38
- current = current.parentElement;
39
- }
40
- return null;
41
+ return findAncestor(el, (e) => hasContext(e, SlotContentContext) ? e : undefined) ?? null;
41
42
  }
42
43
  /**
43
44
  * Check if a node is an Element.
@@ -116,11 +117,11 @@ export const template = async function template($expr, $element, $templates) {
116
117
  console.error(`Cycle detected: template "${templateName}" is already being rendered`);
117
118
  return;
118
119
  }
119
- // Save children for slots
120
+ // Save children for slots using typed context
120
121
  const slotContent = {
121
122
  slots: extractSlotContent($element)
122
123
  };
123
- savedContent.set($element, slotContent);
124
+ registerContext($element, SlotContentContext, slotContent);
124
125
  // Track this template in the chain
125
126
  const newChain = new Set(chain);
126
127
  newChain.add(templateName);
package/dist/dom.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * DOM traversal utilities.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ /**
7
+ * Find an ancestor element matching a predicate.
8
+ *
9
+ * @remarks
10
+ * Walks up the DOM tree starting from the element's parent (or the element
11
+ * itself if `includeSelf` is true), calling the predicate on each ancestor.
12
+ * Returns the first non-undefined result from the predicate.
13
+ *
14
+ * @typeParam T - The type returned by the predicate
15
+ * @param el - The element to start from
16
+ * @param predicate - Function called on each ancestor, returns a value or undefined
17
+ * @param includeSelf - Whether to check the element itself (default: false)
18
+ * @returns The first non-undefined result from the predicate, or undefined
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // Find ancestor with a specific attribute
23
+ * const ancestor = findAncestor(el, (e) => e.hasAttribute('data-scope') ? e : undefined);
24
+ *
25
+ * // Find value from a WeakMap on an ancestor
26
+ * const value = findAncestor(el, (e) => myWeakMap.get(e));
27
+ * ```
28
+ */
29
+ export declare function findAncestor<T>(el: Element, predicate: (el: Element) => T | undefined, includeSelf?: boolean): T | undefined;
package/dist/dom.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * DOM traversal utilities.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ /**
7
+ * Find an ancestor element matching a predicate.
8
+ *
9
+ * @remarks
10
+ * Walks up the DOM tree starting from the element's parent (or the element
11
+ * itself if `includeSelf` is true), calling the predicate on each ancestor.
12
+ * Returns the first non-undefined result from the predicate.
13
+ *
14
+ * @typeParam T - The type returned by the predicate
15
+ * @param el - The element to start from
16
+ * @param predicate - Function called on each ancestor, returns a value or undefined
17
+ * @param includeSelf - Whether to check the element itself (default: false)
18
+ * @returns The first non-undefined result from the predicate, or undefined
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // Find ancestor with a specific attribute
23
+ * const ancestor = findAncestor(el, (e) => e.hasAttribute('data-scope') ? e : undefined);
24
+ *
25
+ * // Find value from a WeakMap on an ancestor
26
+ * const value = findAncestor(el, (e) => myWeakMap.get(e));
27
+ * ```
28
+ */
29
+ export function findAncestor(el, predicate, includeSelf = false) {
30
+ let current = includeSelf ? el : el.parentElement;
31
+ while (current) {
32
+ const result = predicate(current);
33
+ if (result !== undefined) {
34
+ return result;
35
+ }
36
+ current = current.parentElement;
37
+ }
38
+ return undefined;
39
+ }
@@ -43,8 +43,23 @@ export function findRoots(expr) {
43
43
  .replace(/'(?:[^'\\]|\\.)*'/g, '""')
44
44
  .replace(/"(?:[^"\\]|\\.)*"/g, '""')
45
45
  .replace(/`(?:[^`\\$]|\\.|\$(?!\{))*`/g, '""');
46
- const matches = cleaned.match(/(?<![.\w$])\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g) || [];
47
- const roots = [...new Set(matches.filter(m => !JS_KEYWORDS.has(m)))];
46
+ // Match identifiers that are not preceded by a dot
47
+ // Use simpler approach: match all identifiers, then filter out property accesses
48
+ const allIdentifiers = cleaned.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [];
49
+ // Filter to keep only root identifiers (not after a dot)
50
+ const roots = [];
51
+ let lastEnd = 0;
52
+ for (const id of allIdentifiers) {
53
+ const pos = cleaned.indexOf(id, lastEnd);
54
+ // Check if preceded by a dot (with optional whitespace)
55
+ const before = cleaned.slice(0, pos).trimEnd();
56
+ if (!before.endsWith('.')) {
57
+ if (!JS_KEYWORDS.has(id) && !roots.includes(id)) {
58
+ roots.push(id);
59
+ }
60
+ }
61
+ lastEnd = pos + id.length;
62
+ }
48
63
  return roots;
49
64
  }
50
65
  /**
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @packageDocumentation
9
9
  */
10
- export { Mode, Expression, Context, Directive, directive, getDirective, getDirectiveNames, clearDirectives } from './types.js';
10
+ export { Mode, Expression, Context, Directive, directive, getDirective, getDirectiveNames, clearDirectives, configureDirective } from './types.js';
11
11
  export type { DirectiveMeta } from './types.js';
12
12
  export { createContext, createChildContext } from './context.js';
13
13
  export { reactive, effect, createScope, createEffectScope } from './reactivity.js';
@@ -15,6 +15,12 @@ export type { EffectScope } from './reactivity.js';
15
15
  export { createTemplateRegistry, createMemoryRegistry, createServerRegistry } from './templates.js';
16
16
  export type { TemplateRegistry } from './templates.js';
17
17
  export { findRoots, parseInterpolation } from './expression.js';
18
- export { getInjectables } from './inject.js';
18
+ export { getInjectables, isContextKey } from './inject.js';
19
+ export type { Injectable } from './inject.js';
19
20
  export { getRootScope, clearRootScope } from './scope.js';
21
+ export { findAncestor } from './dom.js';
22
+ export { processElementDirectives, processElementTree, PROCESSED_ATTR } from './process.js';
23
+ export type { ProcessOptions } from './process.js';
24
+ export { createContextKey, registerContext, resolveContext, hasContext, removeContext, clearContexts } from './context-registry.js';
25
+ export type { ContextKey } from './context-registry.js';
20
26
  export * as directives from './directives/index.js';
package/dist/index.js CHANGED
@@ -7,11 +7,14 @@
7
7
  *
8
8
  * @packageDocumentation
9
9
  */
10
- export { Mode, directive, getDirective, getDirectiveNames, clearDirectives } from './types.js';
10
+ export { Mode, directive, getDirective, getDirectiveNames, clearDirectives, configureDirective } from './types.js';
11
11
  export { createContext, createChildContext } from './context.js';
12
12
  export { reactive, effect, createScope, createEffectScope } from './reactivity.js';
13
13
  export { createTemplateRegistry, createMemoryRegistry, createServerRegistry } from './templates.js';
14
14
  export { findRoots, parseInterpolation } from './expression.js';
15
- export { getInjectables } from './inject.js';
15
+ export { getInjectables, isContextKey } from './inject.js';
16
16
  export { getRootScope, clearRootScope } from './scope.js';
17
+ export { findAncestor } from './dom.js';
18
+ export { processElementDirectives, processElementTree, PROCESSED_ATTR } from './process.js';
19
+ export { createContextKey, registerContext, resolveContext, hasContext, removeContext, clearContexts } from './context-registry.js';
17
20
  export * as directives from './directives/index.js';