@ape-egg/vibe 4.0.1 → 4.1.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.
@@ -9,20 +9,10 @@ import { evalInScope } from './utils.js';
9
9
  import { collectComponentIds, releaseOrphanedComponentState, executeCompiledComponentScriptsIn } from './component.js';
10
10
  import { isOutgoing } from './staging.js';
11
11
 
12
- // Registry of DOM nodes owned by conditional branches.
13
- // Maps a DOM node to { nodes: array_ref, index: number } so that
14
- // processMutations can update the reference when processComponent
15
- // replaces the node (el.replaceWith). Keeps conditional state in
16
- // sync with actual DOM without sweeps or special properties.
17
12
  export const branchNodeRegistry = new WeakMap();
18
13
 
19
- // Nodes that have been processed by mountBranch or renderIteration.
20
- // processMutations checks this to avoid re-processing already-handled content.
21
14
  export const managedNodes = new WeakSet();
22
15
 
23
- // Find the dot path of a conditional node in the manifest.
24
- // Resolves the parent element through the reverse index, then appends the
25
- // conditional's key.
26
16
  const findConditionalPath = (node, manifest) => {
27
17
  const parentPath = manifestPathOf(manifest, node.meta.startComment.parentNode);
28
18
  if (parentPath === null) return null;
@@ -30,7 +20,6 @@ const findConditionalPath = (node, manifest) => {
30
20
  return `${parentPath}.${node._key}`;
31
21
  };
32
22
 
33
- // Register branch tree nodes in the manifest (recursive)
34
23
  const addBranchToManifest = (tree, manifest, basePath) => {
35
24
  if (tree.element) {
36
25
  setManifestEntry(manifest, basePath, tree.element);
@@ -42,7 +31,6 @@ const addBranchToManifest = (tree, manifest, basePath) => {
42
31
  }
43
32
  };
44
33
 
45
- // Remove branch tree nodes from the manifest (recursive)
46
34
  const removeBranchFromManifest = (tree, manifest, basePath) => {
47
35
  delete manifest[basePath];
48
36
  if (tree.children) {
@@ -52,39 +40,29 @@ const removeBranchFromManifest = (tree, manifest, basePath) => {
52
40
  }
53
41
  };
54
42
 
55
- // Evaluate conditional expression in state context
56
43
  const evaluateCondition = (expression, state, element = null) => !!evalInScope(expression, state, element);
57
44
 
58
- // Extract state dependencies from an expression
59
- // e.g., "count > 5" → ["count"]
60
- // e.g., "isLoggedIn && isAdmin" → ["isLoggedIn", "isAdmin"]
61
45
  export const extractDependencies = (expression) => {
62
46
  const regex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
63
47
  const matches = [];
64
48
  let match;
65
49
  while ((match = regex.exec(expression))) {
66
50
  const identifier = match[1];
67
- // Filter out JavaScript keywords and common literals
68
51
  if (!['true', 'false', 'null', 'undefined', 'this', 'return'].includes(identifier)) {
69
52
  matches.push(identifier);
70
53
  }
71
54
  }
72
- return [...new Set(matches)]; // Unique values
55
+ return [...new Set(matches)];
73
56
  };
74
57
 
75
-
76
- // Render all conditionals in the parsed tree
77
58
  export const renderAllConditionals = (tree, state, manifest, parentScope = {}) => {
78
59
  let count = 0;
79
60
 
80
- // If this is a conditional node, render it
81
61
  if (tree.type === 'conditional') {
82
62
  renderConditional(tree, state, manifest, parentScope);
83
63
  return 1;
84
64
  }
85
65
 
86
- // Recursively render conditionals in child nodes — except under an
87
- // outgoing wrapper, whose content the pending swap will replace wholesale.
88
66
  if (tree.children && !(tree.element && isOutgoing(tree.element))) {
89
67
  Object.keys(tree.children).forEach((key) => {
90
68
  const child = tree.children[key];
@@ -97,21 +75,9 @@ export const renderAllConditionals = (tree, state, manifest, parentScope = {}) =
97
75
  return count;
98
76
  };
99
77
 
100
- // Re-settle every ALREADY-RENDERED conditional against the current world.
101
- // The boot's initial pass can run before module scripts have evaluated
102
- // (compiled shell: boot.js rides the vibe-module chain), so a gate on a
103
- // module-provided global — `<!-- if window.isDev -->` — first evaluates
104
- // against the pre-module world and carries no reactive dependency that would
105
- // ever re-check it. Walks like renderAllConditionals but routes through
106
- // updateConditional: value-unchanged branches are a no-op, changed ones
107
- // mount/unmount through the normal branch machinery.
108
78
  export const settleConditionals = (tree, state, manifest, parentScope = {}) => {
109
79
  if (tree.type === 'conditional') {
110
80
  updateConditional(tree, state, state, manifest, parentScope);
111
- // A COMPILED pre-rendered conditional keeps its live content in
112
- // `children` (never mountBranch'd) — nested conditionals there need
113
- // settling too. Connected-root check skips a runtime conditional's
114
- // detached template nodes.
115
81
  if (tree.children) {
116
82
  Object.keys(tree.children).forEach((key) => {
117
83
  const child = tree.children[key];
@@ -133,35 +99,17 @@ export const settleConditionals = (tree, state, manifest, parentScope = {}) => {
133
99
  }
134
100
  };
135
101
 
136
- // Initial render of a conditional block
137
102
  export const renderConditional = (node, state, manifest, parentScope = {}) => {
138
103
  const { expression, startComment, endComment, branches } = node.meta;
139
104
 
140
- // Mark conditionals that live inside an iteration row so update-time branch
141
- // flips can route `<component src>` props through the iteration-prop
142
- // registry. Skipping this for top-level conditionals keeps their props as
143
- // live `@[stateKey]` bindings — which is what global state-change reactivity
144
- // depends on (the registry path snapshots a value and doesn't react to
145
- // global state changes on its own).
146
- // `parentScope` is populated on the initial-render path, but a conditional
147
- // mounted via the update path (a parent conditional flipping true after load)
148
- // — or any deeper-nested conditional — arrives with an empty parentScope, the
149
- // iteration's scoped state flowing in through `state` instead. `scopeAliases`
150
- // is set at parse time and persists, so it reliably marks a conditional that
151
- // lexically lives inside an iteration regardless of which path mounts it — the
152
- // Brawling loader, whose conditional flips true only once combat starts.
153
105
  if (Object.keys(parentScope).length > 0 || node.meta.scopeAliases?.length) {
154
106
  node.runtime.inIteration = true;
155
107
  }
156
108
 
157
- // Check if already rendered (using marker on comment node)
158
- // @ts-ignore - adding custom property to comment node
159
109
  if (startComment.__vibeRendered) {
160
110
  return;
161
111
  }
162
112
 
163
- // Remove original template nodes from DOM (between start and end comments)
164
- // Only do this on first render (when template hasn't been removed yet)
165
113
  if (!node.runtime.templateRemoved) {
166
114
  let currentNode = startComment.nextSibling;
167
115
  while (currentNode && currentNode !== endComment) {
@@ -172,58 +120,40 @@ export const renderConditional = (node, state, manifest, parentScope = {}) => {
172
120
  currentNode = nextNode;
173
121
  }
174
122
 
175
- // Mark that template has been removed
176
123
  node.runtime.templateRemoved = true;
177
124
  }
178
125
 
179
- // Mark comment as rendered (survives re-parsing)
180
- // @ts-ignore - adding custom property to comment node
181
126
  startComment.__vibeRendered = true;
182
127
 
183
- // Evaluate condition with current state — inside a tracking window, so the
184
- // conditional subscribes to what its expression read from first render on.
185
128
  const trackSub = nodeSubscriberOf(node, 'conditional');
186
129
  trackSub.lastScope = state;
187
130
  beginTracking(trackSub, overlayKeysOf(state));
188
131
  const conditionResult = evaluateCondition(expression, state, startComment.parentElement);
189
132
  endTracking();
190
133
 
191
- // Determine which branch to mount
192
134
  const branchToMount = conditionResult ? branches.if : branches.else;
193
135
 
194
- // Mount the appropriate branch
195
136
  mountBranch(node, branchToMount, state, manifest, parentScope);
196
137
 
197
- // Store active branch reference
198
138
  node.runtime.activeBranch = branchToMount;
199
139
  };
200
140
 
201
- // Mount a specific branch
202
141
  const mountBranch = (node, branchData, state, manifest, parentScope) => {
203
142
  const { startComment, endComment } = node.meta;
204
143
 
205
- // If branch doesn't exist (no else clause), just unmount current
206
144
  if (!branchData) {
207
145
  unmountBranch(node, manifest);
208
146
  return;
209
147
  }
210
148
 
211
- // Unmount current branch first (if any)
212
149
  unmountBranch(node, manifest);
213
150
 
214
- // Clone the template element
215
151
  const templateContent = branchData.element.childNodes;
216
152
  const parent = startComment.parentNode;
217
153
 
218
- // Create scoped state (with parent scope if inside iteration)
219
154
  const scopedState =
220
155
  Object.keys(parentScope).length > 0 ? createScopedState(state, parentScope) : state;
221
156
 
222
- // Initialize block (clone, parse, hydrate). Pass the enclosing loop aliases so
223
- // the re-parse rewrites loop-scoped handlers in this branch — including ones
224
- // nested deeper in further conditionals, which the parser reaches by carrying
225
- // the alias set down. `scopeAliases` is set at parse time and persists, so this
226
- // works on both the initial-render and update (hydrate) mount paths.
227
157
  const aliasSet = node.meta.scopeAliases?.length ? new Set(node.meta.scopeAliases) : undefined;
228
158
  const {
229
159
  element: firstElement,
@@ -231,44 +161,22 @@ const mountBranch = (node, branchData, state, manifest, parentScope) => {
231
161
  clonedNodes,
232
162
  } = initializeBlock(templateContent, scopedState, null, null, aliasSet);
233
163
 
234
- // For conditionals living inside an iteration, route any `<component src>`
235
- // props through the iteration-prop registry against the active scopedState
236
- // (which carries the iteration's local vars). Without this, processSingle
237
- // would inline `@[item.x]` bindings into the component template, where
238
- // `item` isn't reachable in global scope and props resolve to undefined.
239
- // Skipped for top-level conditionals because their props reference live
240
- // global-state bindings — going through the registry would snapshot the
241
- // value and break reactivity.
242
164
  if (node.runtime.inIteration) {
243
165
  resolveIterationComponentProps(clonedNodes, scopedState, aliasSet);
244
166
  }
245
167
 
246
- // Insert cloned nodes into DOM and register in branch registry
247
168
  clonedNodes.forEach((clonedNode, i) => {
248
169
  parent.insertBefore(clonedNode, endComment);
249
170
  branchNodeRegistry.set(clonedNode, { nodes: clonedNodes, index: i });
250
171
  if (clonedNode.nodeType === 1) managedNodes.add(clonedNode);
251
172
  });
252
173
 
253
- // Branch content is mounted outside the enclosing iteration instance's
254
- // clonedNodes, so it doesn't inherit the instance's `__vibeScope` stamp by DOM
255
- // ancestry. When this conditional lives inside a loop, stamp `scopedState` —
256
- // the same scope that hydrates the branch's `@[alias.x]` bindings — onto the
257
- // branch's root elements so loop-scoped `$scope(this,'alias')` handlers resolve.
258
- // Gate on `scopeAliases` (set at parse time, persists) rather than the runtime
259
- // parentScope/inIteration, because the update path (hydrate -> updateConditional)
260
- // and deeper-nested conditionals mount with an empty parentScope yet still flow
261
- // the iteration's scoped state in as `state`.
262
174
  if (node.meta.scopeAliases?.length) {
263
175
  for (let i = 0; i < clonedNodes.length; i++) {
264
176
  if (clonedNodes[i].nodeType === 1) clonedNodes[i].__vibeScope = scopedState;
265
177
  }
266
178
  }
267
179
 
268
- // Integrate branch tree into the conditional node's children and manifest.
269
- // This makes branch content visible to the main update loop (hydrate,
270
- // renderAllConditionals, renderAllIterations) and to MutationObserver
271
- // (which looks up parents in the manifest).
272
180
  if (branchTree?.children) {
273
181
  for (const key in branchTree.children) {
274
182
  node.children[key] = branchTree.children[key];
@@ -282,42 +190,19 @@ const mountBranch = (node, branchData, state, manifest, parentScope) => {
282
190
  }
283
191
  }
284
192
 
285
- // Compiled pages inline each component's setup script as type="vibe-module".
286
- // The boot-time pass only runs scripts present at boot, so a branch that
287
- // mounts (or re-mounts after unmount) must run its own scripts here to
288
- // re-register component-local state. Must happen BEFORE rendering nested
289
- // iterations/conditionals so `<!-- each _cN.x -->` sees the registered state.
290
193
  const scriptsPending = executeCompiledComponentScriptsIn(clonedNodes);
291
194
 
292
- // Recursively render any nested iterations and conditionals
293
195
  if (branchTree) {
294
196
  renderAllIterations(branchTree, scopedState, manifest, parentScope);
295
197
  renderAllConditionals(branchTree, scopedState, manifest, parentScope);
296
198
  }
297
199
 
298
- // Store active instance
299
200
  node.runtime.activeInstance = {
300
201
  branch: branchData,
301
202
  nodes: clonedNodes,
302
203
  parsedTree: branchTree,
303
204
  };
304
205
 
305
- // Import-bearing branch scripts run async through the script chain, so the
306
- // eager render above may evaluate expressions before the window helpers those
307
- // scripts define exist — and defining a global is not a state write, so no
308
- // flush ever re-dispatches them (the fetch path never has this gap: it mounts
309
- // DOM only after its scripts ran). Finish the mount when the chain settles:
310
- // re-render this branch's directives against the fully-scripted world.
311
- // Idempotent — rendered iterations early-return, value-unchanged conditionals
312
- // are a no-op — and skipped when a remount superseded this instance.
313
- //
314
- // The settle evaluates CURRENT state (the live `$`), never the mount-time
315
- // snapshot: state that legitimately changed while the imports were pending
316
- // (a nested gate flipping true off a socket flush) already dispatched and
317
- // mounted — settling against the stale snapshot would unmount it again.
318
- // Loop-scoped branches keep their mount scope (the row's aliases aren't
319
- // reachable from the root proxy); a row update supersedes activeInstance
320
- // and the guard skips the settle entirely.
321
206
  if (scriptsPending && branchTree) {
322
207
  scriptsPending.then(() => {
323
208
  if (node.runtime.activeInstance?.nodes !== clonedNodes) return;
@@ -328,13 +213,11 @@ const mountBranch = (node, branchData, state, manifest, parentScope) => {
328
213
  }
329
214
  };
330
215
 
331
- // Unmount currently active branch
332
216
  const unmountBranch = (node, manifest) => {
333
217
  const { activeInstance } = node.runtime;
334
218
 
335
219
  if (!activeInstance) return;
336
220
 
337
- // Remove branch children from the conditional node's tree and manifest
338
221
  if (activeInstance.parsedTree?.children) {
339
222
  const condPath = manifest ? findConditionalPath(node, manifest) : null;
340
223
 
@@ -346,13 +229,6 @@ const unmountBranch = (node, manifest) => {
346
229
  }
347
230
  }
348
231
 
349
- // The conditional owns everything between its comments. That's more than
350
- // activeInstance.nodes: nested directives at the branch's top level (each
351
- // rows, deeper if branches) insert their rendered output between their own
352
- // comments after the branch mounts, so it never appears in the original
353
- // clonedNodes list. Sweep the live range (same pattern as iteration
354
- // teardown), collecting componentIds BEFORE detaching so we can check
355
- // after removal whether any live DOM still holds them.
356
232
  const { startComment, endComment } = node.meta;
357
233
  const ids = new Set();
358
234
  let current = startComment.nextSibling;
@@ -366,22 +242,12 @@ const unmountBranch = (node, manifest) => {
366
242
  current = next;
367
243
  }
368
244
 
369
- // CLEANUP OF CURRENT STATE
370
245
  releaseOrphanedComponentState(ids);
371
- // The swept range's binding subscribers are dead — their anchors just left
372
- // the document. (The conditional's own subscriber survives: its anchor is
373
- // the start comment, which stays.)
374
246
  pruneDisconnected();
375
247
 
376
- // Clear active instance
377
248
  node.runtime.activeInstance = null;
378
249
  };
379
250
 
380
- // Subscription dispatch: flip the branch if the expression's value moved,
381
- // nothing else. Branch CONTENT updates arrive through the content's own
382
- // subscribers — re-walking the branch here (updateConditional's else path)
383
- // would reintroduce a partial walk per dirty conditional. A mounting branch
384
- // registers its fresh subscribers through initializeBlock's hydrate pass.
385
251
  export const dispatchConditional = (node, newState, manifest, parentScope = {}) => {
386
252
  if (!node.runtime.templateRemoved) return;
387
253
  const sub = nodeSubscriberOf(node, 'conditional');
@@ -396,17 +262,13 @@ export const dispatchConditional = (node, newState, manifest, parentScope = {})
396
262
  }
397
263
  };
398
264
 
399
- // Update conditional when dependencies change
400
265
  export const updateConditional = (node, newState, oldState, manifest, parentScope = {}) => {
401
266
  const { expression, branches, startComment } = node.meta;
402
267
 
403
- // If not yet rendered, skip (renderConditional handles initial render)
404
268
  if (!node.runtime.templateRemoved) {
405
269
  return;
406
270
  }
407
271
 
408
- // Evaluate expression with new state — re-records the subscription every
409
- // update, so branch-dependent reads self-heal.
410
272
  const trackSub = nodeSubscriberOf(node, 'conditional');
411
273
  trackSub.lastScope = newState;
412
274
  beginTracking(trackSub, overlayKeysOf(newState));
@@ -414,16 +276,12 @@ export const updateConditional = (node, newState, oldState, manifest, parentScop
414
276
  endTracking();
415
277
  const newBranchData = newConditionResult ? branches.if : branches.else;
416
278
 
417
- // Check if branch changed (compare references)
418
279
  const branchChanged = node.runtime.activeBranch !== newBranchData;
419
280
 
420
281
  if (branchChanged) {
421
- // Switch branches
422
282
  mountBranch(node, newBranchData, newState, manifest, parentScope);
423
283
  node.runtime.activeBranch = newBranchData;
424
284
  } else {
425
- // Same branch, but state might have changed — rehydrate bindings
426
- // and update nested conditionals/iterations
427
285
  const { activeInstance } = node.runtime;
428
286
 
429
287
  if (activeInstance && activeInstance.parsedTree) {
@@ -1,49 +1,31 @@
1
- // The released version — check-release.js holds this in lockstep with
2
- // package.json, the READMEs and the CHANGELOG.
3
- export const VERSION = '4.0.1';
1
+ export const VERSION = '4.1.1';
4
2
 
5
- // Debug logger name
6
3
  export const DEBUGGER_NAME = '[vibe-debug]:';
7
- export const FOUC_CLASS_OR_ATTR = 'vibe-fouc'; // Class or attribute used to prevent FOUC (default: [vibe])
8
- export const DEHYDRATE_CLASS_OR_ATTR = 'vibe-dehydrate'; // Class or attribute used to skip reactive processing
9
-
10
- // Lifecycle phase names for debug logging
11
- // ONE-OFF operations (run once during initialization)
12
- export const PHASE_ATTACH = 'Attached'; // Latches onto DOM element (index.js)
13
- export const PHASE_MANIFEST = 'Manifested'; // Creates DOM manifest (manifest.js)
14
- export const PHASE_OBSERVE = 'Observer'; // Starts MutationObserver (index.js)
15
- export const PHASE_READY = 'Ready'; // Removes [vibe-fouc] attribute (or .vibe-fouc class) and dispatches vibe:ready event (cleanup.js)
16
-
17
- // REPEATED operations (run during init + can repeat during runtime)
18
- export const PHASE_PARSE = 'Parsed'; // Reads DOM structure (parse.js)
19
- export const PHASE_HYDRATE = 'Hydrated'; // Replaces @[...] with values (hydrate.js)
20
- export const PHASE_ITERATE = 'Iterated'; // Renders <!-- each --> blocks (iterate.js)
21
- export const PHASE_CONDITION = 'Evaluated'; // Renders <!-- if --> blocks (conditionals.js)
22
- export const PHASE_FETCH = 'Fetched'; // Loads <component> content over the network (component.js)
23
- export const PHASE_FETCH_CACHED = 'Fet(ca)ched'; // Loads <component> content from the runtime template cache — a Fetch served from memory (component.js)
24
- export const PHASE_UPDATE = 'Proxy'; // State changes trigger re-hydration (index.js)
25
- export const PHASE_MUTATE = 'Mutation'; // DOM mutations detected by observer (index.js)
26
- export const PHASE_HYPERSPEED = 'Hyperspeed'; // Restores @[...] markers from pre-compiled manifest (pre-compiled-manifest.js)
27
-
28
- // Elements that should not have reactive bindings
29
- // Note: COMPONENT is NOT in this list - inline component wrappers need to be parsed
30
- // Fetched components (<component src="">) are handled separately by processComponent()
4
+ export const FOUC_CLASS_OR_ATTR = 'vibe-fouc';
5
+ export const DEHYDRATE_CLASS_OR_ATTR = 'vibe-dehydrate';
6
+
7
+ export const PHASE_ATTACH = 'Attached';
8
+ export const PHASE_MANIFEST = 'Manifested';
9
+ export const PHASE_OBSERVE = 'Observer';
10
+ export const PHASE_READY = 'Ready';
11
+
12
+ export const PHASE_PARSE = 'Parsed';
13
+ export const PHASE_HYDRATE = 'Hydrated';
14
+ export const PHASE_ITERATE = 'Iterated';
15
+ export const PHASE_CONDITION = 'Evaluated';
16
+ export const PHASE_FETCH = 'Fetched';
17
+ export const PHASE_FETCH_CACHED = 'Fet(ca)ched';
18
+ export const PHASE_UPDATE = 'Proxy';
19
+ export const PHASE_MUTATE = 'Mutation';
20
+ export const PHASE_HYPERSPEED = 'Hyperspeed';
21
+
31
22
  export const NON_REACTIVE_ELEMENTS = ['SCRIPT', 'HEAD', 'PRE'];
32
23
 
33
- // Elements whose src-family attributes the browser fetches the moment they are
34
- // set — a raw `src="@[binding]"` reaching one of these fires a network request
35
- // for the literal binding text before hydration can stamp the real value.
36
- // Bindings on them are parked on data-vibe-<attr> (the established transport)
37
- // until hydration writes the evaluated URL. <component src> is NOT here — its
38
- // src is consumed by processComponent, never by the browser.
39
24
  export const FETCH_SRC_ATTRS = ['src', 'srcset', 'poster'];
40
25
  export const FETCH_SRC_SELECTOR = 'img, source, iframe, video, audio, embed, track';
41
26
  export const FETCH_SRC_ELEMENTS = ['IMG', 'SOURCE', 'IFRAME', 'VIDEO', 'AUDIO', 'EMBED', 'TRACK'];
42
27
 
43
- // Attributes where the string value is meaningful (should NOT be removed when falsy)
44
- // All other attributes are treated as boolean-like (removed when falsy, present when truthy)
45
28
  export const VALUE_ATTRS = [
46
- // Global attributes
47
29
  'class',
48
30
  'style',
49
31
  'id',
@@ -59,7 +41,6 @@ export const VALUE_ATTRS = [
59
41
  'popover',
60
42
  'anchor',
61
43
 
62
- // Enumerated (take specific string values, not truly boolean)
63
44
  'contenteditable',
64
45
  'draggable',
65
46
  'spellcheck',
@@ -69,7 +50,6 @@ export const VALUE_ATTRS = [
69
50
  'enterkeyhint',
70
51
  'virtualkeyboardpolicy',
71
52
 
72
- // URLs and sources
73
53
  'href',
74
54
  'src',
75
55
  'action',
@@ -84,7 +64,6 @@ export const VALUE_ATTRS = [
84
64
  'manifest',
85
65
  'codebase',
86
66
 
87
- // Form attributes
88
67
  'name',
89
68
  'type',
90
69
  'value',
@@ -109,13 +88,11 @@ export const VALUE_ATTRS = [
109
88
  'for',
110
89
  'dirname',
111
90
 
112
- // Text/accessibility
113
91
  'alt',
114
92
  'label',
115
93
  'summary',
116
94
  'abbr',
117
95
 
118
- // Dimensions and layout
119
96
  'width',
120
97
  'height',
121
98
  'cols',
@@ -127,7 +104,6 @@ export const VALUE_ATTRS = [
127
104
  'high',
128
105
  'optimum',
129
106
 
130
- // Link/resource hints
131
107
  'target',
132
108
  'rel',
133
109
  'hreflang',
@@ -146,81 +122,42 @@ export const VALUE_ATTRS = [
146
122
  'imagesizes',
147
123
  'sizes',
148
124
 
149
- // Media
150
125
  'preload',
151
126
  'kind',
152
127
  'srclang',
153
128
 
154
- // Meta
155
129
  'content',
156
130
  'http-equiv',
157
131
 
158
- // iframe/embed
159
132
  'sandbox',
160
133
  'allow',
161
134
  'srcdoc',
162
135
  'credentialless',
163
136
 
164
- // Table
165
137
  'headers',
166
138
  'scope',
167
139
 
168
- // Datetime
169
140
  'datetime',
170
141
 
171
- // Object/embed legacy
172
142
  'coords',
173
143
  'shape',
174
144
  ];
175
145
 
176
- // Properties that should be set directly on the DOM element (not as attributes)
177
146
  export const DOM_PROPERTIES = ['value', 'checked', 'selected'];
178
147
 
179
-
180
- // Regex for matching reactive bindings (@[expression])
181
- // Supports nested brackets, single-quoted and double-quoted strings inside expressions:
182
- // @[items[0]], @[obj[key]], @[x.replace('.png', '-mugshot.png')]
183
148
  const BINDING_INNER = String.raw`(?:[^\[\]'"]|\[[^\]]*\]|'[^']*'|"[^"]*")+`;
184
149
  export const BINDING_REGEX = new RegExp(String.raw`\@\[(${BINDING_INNER})\]`, 'g');
185
150
 
186
- // Regex for detecting a pure binding (entire value is just @[expression])
187
151
  export const PURE_BINDING_REGEX = new RegExp(String.raw`^\@\[(${BINDING_INNER})\]$`);
188
152
 
189
- // Regex for parsing iteration comment syntax. Supported forms — the index and
190
- // the key are each optional and may appear in EITHER order:
191
- // <!-- each items as item -->
192
- // <!-- each items as item, index -->
193
- // <!-- each items as item (item.id) --> // explicit key
194
- // <!-- each items as item (item.id), index --> // key, then index
195
- // <!-- each items as item, index (item.id) --> // index, then key
196
- // Capture groups: arrayPath, itemAlias, keyBeforeIndex (optional), indexAlias
197
- // (optional), keyAfterIndex (optional). The key lands in group 3 when written
198
- // before the index and in group 5 when written after; parseIterationHeader
199
- // coalesces the two — prefer that helper over destructuring the raw match.
200
- // The array expression can be any JS: a state path, a window global, a method
201
- // call, or an inline literal. The optional key expression is evaluated per
202
- // item against scoped state to produce a stable identity for diffing — this
203
- // keeps survivors stable when earlier items are removed (otherwise the
204
- // fallback hash key embeds the index and triggers bulk re-render).
205
153
  export const ITERATION_REGEX = /^each\s+(.+)\s+as\s+(\w+)(?:\s*\(\s*([^)]+?)\s*\))?(?:\s*,\s*(\w+))?(?:\s*\(\s*([^)]+?)\s*\))?\s*$/;
206
154
 
207
- // Regex for detecting start of iteration comment
208
155
  export const ITERATION_START_REGEX = /^each\s+/;
209
156
 
210
- // Regex for parsing conditional comment syntax (<!-- if expression -->)
211
157
  export const CONDITIONAL_REGEX = /^if\s+(.+)$/;
212
158
 
213
- // Regex for detecting start of conditional comment
214
159
  export const CONDITIONAL_START_REGEX = /^if\s+/;
215
160
 
216
- // Regex for rewriting component-local `this.X` references to the component's
217
- // state path. Captures the leading identifier only — any trailing `.Y.Z`
218
- // chain stays attached after replacement, so `this.user.name` becomes
219
- // `<componentId>.user.name`. Used in expression bodies (bindings, event
220
- // handlers, conditional/iteration directives).
221
161
  export const THIS_PROP_REGEX = /\bthis\.(\w+)/g;
222
162
 
223
- // Regex for rewriting `$.this.X` writes (proxy assignment from event handlers)
224
- // to the component's write path. Same prefix-only semantics as THIS_PROP_REGEX
225
- // — `$.this.user.name = x` becomes `$.<componentId>.user.name = x`.
226
163
  export const STATE_THIS_PROP_REGEX = /\$\.this\.(\w+)/g;