@ape-egg/vibe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +108 -0
- package/_vibe-compiled-iteration-batch.js +161 -0
- package/affected.js +149 -0
- package/conditionals.js +182 -0
- package/constants.js +76 -0
- package/hydrate.js +100 -0
- package/index.js +250 -0
- package/iterate.js +451 -0
- package/iteration-utils.js +261 -0
- package/link.js +13 -0
- package/llms.txt +279 -0
- package/package.json +26 -0
- package/parse.js +225 -0
- package/state.js +26 -0
- package/utils.js +45 -0
- package/vibe.css +11 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-01-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial public release
|
|
8
|
+
- Proxy-based reactive state (`window.$`)
|
|
9
|
+
- MutationObserver for automatic DOM tracking
|
|
10
|
+
- Fine-grained updates (no virtual DOM)
|
|
11
|
+
|
|
12
|
+
### Core Features
|
|
13
|
+
|
|
14
|
+
- **Reactive bindings**: `@[property]` syntax in HTML and CSS
|
|
15
|
+
- **Iteration**: `<!-- each items as item, i -->` with efficient diffing
|
|
16
|
+
- **Nested iteration**: `<!-- each category.items as item -->`
|
|
17
|
+
- **Conditionals**: `<!-- if condition -->...<!-- else -->...<!-- /if -->`
|
|
18
|
+
- **Dehydrate**: `<div dehydrate>` to skip reactive processing
|
|
19
|
+
- **Expression evaluation**: `@[count * 2]`, `@[firstName + ' ' + lastName]`
|
|
20
|
+
|
|
21
|
+
### Performance
|
|
22
|
+
|
|
23
|
+
- Surgical DOM updates - only affected elements re-render
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [0.0.5] - 2026-01-15
|
|
28
|
+
|
|
29
|
+
- Heavy optimizations for iteration rendering
|
|
30
|
+
- Added benchmark tooling
|
|
31
|
+
- Improved diffing algorithm for array changes
|
|
32
|
+
|
|
33
|
+
## [0.0.4] - 2026-01-10
|
|
34
|
+
|
|
35
|
+
- Renamed from "Soulbound" to "Vibe"
|
|
36
|
+
- Published to npm as `@ape-egg/vibe`
|
|
37
|
+
|
|
38
|
+
## [0.0.1-0.0.3] - 2026-01-06 to 2026-01-09
|
|
39
|
+
|
|
40
|
+
- Initial development and prototyping
|
|
41
|
+
- Core architecture: parse, link, hydrate, affected, state
|
|
42
|
+
- Basic iteration and conditional support
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Vibe
|
|
2
|
+
|
|
3
|
+
A runtime-first reactive library.
|
|
4
|
+
|
|
5
|
+
No virtual DOM. No build step. Just modern JavaScript.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @ape-egg/vibe
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<script type="module">
|
|
17
|
+
import state from "@ape-egg/vibe";
|
|
18
|
+
window.$ = state({ name: "World", count: 0 });
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<h1>Hello, @[name]!</h1>
|
|
22
|
+
<button onclick="$.count++">Clicked @[count] times</button>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
### Reactive Bindings
|
|
28
|
+
|
|
29
|
+
Use `@[property]` anywhere in HTML or CSS:
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<div>@[firstName] @[lastName]</div>
|
|
33
|
+
<div>@[firstName + ' ' + lastName]</div>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.box { background: @[themeColor]; }
|
|
37
|
+
</style>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Iteration
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<!-- each items as item, index -->
|
|
44
|
+
<li>@[index]: @[item]</li>
|
|
45
|
+
<!-- /each -->
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Nested iteration with dot paths:
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<!-- each categories as category -->
|
|
52
|
+
<!-- each category.items as item -->
|
|
53
|
+
<span>@[item.name]</span>
|
|
54
|
+
<!-- /each -->
|
|
55
|
+
<!-- /each -->
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Conditionals
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<!-- if user.isAdmin -->
|
|
62
|
+
<admin-badge>Admin</admin-badge>
|
|
63
|
+
<!-- else -->
|
|
64
|
+
<span>User</span>
|
|
65
|
+
<!-- /if -->
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Dehydrate
|
|
69
|
+
|
|
70
|
+
Skip reactive processing for an element:
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<code dehydrate>@[this] displays literally</code>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Dynamic Elements
|
|
77
|
+
|
|
78
|
+
Elements added via JavaScript are automatically hydrated through MutationObserver.
|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
1. **Proxy-based state** — `window.$` intercepts property changes
|
|
83
|
+
2. **DOM parsing** — Finds all `@[...]` bindings on load
|
|
84
|
+
3. **Surgical updates** — Only affected elements re-render
|
|
85
|
+
4. **MutationObserver** — Tracks dynamically added elements
|
|
86
|
+
|
|
87
|
+
## Prevent FOUC
|
|
88
|
+
|
|
89
|
+
Hide content until hydration completes:
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<body style="visibility: hidden;">
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or use the included CSS:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
|
|
99
|
+
<body vibe>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Browser Support
|
|
103
|
+
|
|
104
|
+
Modern browsers with Proxy and MutationObserver support.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
ISC
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _vibe-compiled-iteration-batch.js
|
|
3
|
+
*
|
|
4
|
+
* EXPERIMENTAL: Fast path for iteration rendering using compiled batch functions.
|
|
5
|
+
*
|
|
6
|
+
* This is a preview/prototype of what Vibe Compiled (Phase 2) will do automatically.
|
|
7
|
+
* Currently opt-in via: window.__VIBE_FAST_ITERATION__ = true
|
|
8
|
+
*
|
|
9
|
+
* HOW IT WORKS:
|
|
10
|
+
* Instead of cloning DOM nodes and hydrating each item individually (slow),
|
|
11
|
+
* this compiles the template to a JavaScript function that builds HTML strings
|
|
12
|
+
* in a loop, then parses once with innerHTML (fast).
|
|
13
|
+
*
|
|
14
|
+
* Template: <item-card><item-text>@[item.name]</item-text></item-card>
|
|
15
|
+
* Compiles to: (arr) => { let html=''; for(...) html += `<item-card>...${item.name}...`; return html; }
|
|
16
|
+
*
|
|
17
|
+
* LIMITATIONS:
|
|
18
|
+
* - Cannot handle nested <!-- each --> or <!-- if --> (those need DOM-based rendering)
|
|
19
|
+
* - Cannot do incremental updates for small changes (rebuilds entire list)
|
|
20
|
+
* - Template structure is "frozen" at compile time
|
|
21
|
+
*
|
|
22
|
+
* WHEN VIBE COMPILED EXISTS:
|
|
23
|
+
* This logic will move to build-time compilation, producing optimized JavaScript
|
|
24
|
+
* that gets shipped to the browser. The runtime will just execute the compiled code.
|
|
25
|
+
*
|
|
26
|
+
* See: .claude/phase-2-compiler.md for full compiler plans
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { BINDING_REGEX } from './constants.js';
|
|
30
|
+
|
|
31
|
+
// Reusable template element for HTML parsing
|
|
32
|
+
const parseTemplate = document.createElement('template');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a template can use the fast path (no nested iterations/conditionals)
|
|
36
|
+
*/
|
|
37
|
+
export const canUseFastPath = (template) => {
|
|
38
|
+
return !hasNestedStructures(template);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Recursively check if tree has nested iterations or conditionals
|
|
43
|
+
*/
|
|
44
|
+
const hasNestedStructures = (tree) => {
|
|
45
|
+
if (!tree || !tree.children) return false;
|
|
46
|
+
for (const key in tree.children) {
|
|
47
|
+
const child = tree.children[key];
|
|
48
|
+
if (!child) continue;
|
|
49
|
+
if (child.type === 'iteration' || child.type === 'conditional') return true;
|
|
50
|
+
if (hasNestedStructures(child)) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compile a batch function for an iteration template.
|
|
57
|
+
* Returns a function: (array, ...stateValues) => htmlString
|
|
58
|
+
*/
|
|
59
|
+
export const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
|
|
60
|
+
const templateHtml = template.element.innerHTML.trim();
|
|
61
|
+
const escaped = templateHtml
|
|
62
|
+
.replace(/\\/g, '\\\\')
|
|
63
|
+
.replace(/`/g, '\\`')
|
|
64
|
+
.replace(/\$\{/g, '\\${');
|
|
65
|
+
const code = escaped.replace(BINDING_REGEX, (_, expr) => '${' + expr + '}');
|
|
66
|
+
|
|
67
|
+
return new Function('arr', ...stateKeys, `
|
|
68
|
+
let html = '';
|
|
69
|
+
const len = arr.length;
|
|
70
|
+
for (let ${indexAlias} = 0; ${indexAlias} < len; ${indexAlias}++) {
|
|
71
|
+
const ${itemAlias} = arr[${indexAlias}];
|
|
72
|
+
html += \`${code}\`;
|
|
73
|
+
}
|
|
74
|
+
return html;
|
|
75
|
+
`);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fast render: compile template to batch function and render via innerHTML
|
|
80
|
+
*/
|
|
81
|
+
export const renderFast = (iterationNode, array, state, parent, endComment) => {
|
|
82
|
+
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
83
|
+
|
|
84
|
+
// Compile batch function if not cached
|
|
85
|
+
if (!iterationNode.runtime.batchFn) {
|
|
86
|
+
const stateKeys = Object.keys(state);
|
|
87
|
+
iterationNode.runtime.batchFn = compileBatchFn(template, itemAlias, indexAlias, stateKeys);
|
|
88
|
+
iterationNode.runtime.stateKeys = stateKeys;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const batchFn = iterationNode.runtime.batchFn;
|
|
92
|
+
const stateKeys = iterationNode.runtime.stateKeys;
|
|
93
|
+
|
|
94
|
+
// Build HTML string using batch function
|
|
95
|
+
const stateValues = stateKeys.map(k => state[k]);
|
|
96
|
+
const html = batchFn(array, ...stateValues);
|
|
97
|
+
|
|
98
|
+
// Parse with reusable template element
|
|
99
|
+
parseTemplate.innerHTML = html;
|
|
100
|
+
const frag = parseTemplate.content;
|
|
101
|
+
const kids = frag.children;
|
|
102
|
+
|
|
103
|
+
// Track instances
|
|
104
|
+
const arrayLen = array.length;
|
|
105
|
+
const instances = new Array(arrayLen);
|
|
106
|
+
for (let i = 0; i < arrayLen; i++) {
|
|
107
|
+
instances[i] = { element: kids[i], item: array[i], index: i };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
parent.insertBefore(frag, endComment);
|
|
111
|
+
iterationNode.runtime.instances = instances;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fast update: clear and rebuild for bulk operations
|
|
116
|
+
*/
|
|
117
|
+
export const updateFast = (iterationNode, newArray, state, startComment, endComment) => {
|
|
118
|
+
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
119
|
+
const parent = startComment.parentNode;
|
|
120
|
+
|
|
121
|
+
// Fast clear using Range
|
|
122
|
+
if (iterationNode.runtime.instances.length > 0) {
|
|
123
|
+
const range = document.createRange();
|
|
124
|
+
range.setStartAfter(startComment);
|
|
125
|
+
range.setEndBefore(endComment);
|
|
126
|
+
range.deleteContents();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (newArray.length === 0) {
|
|
130
|
+
iterationNode.runtime.instances = [];
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Compile batch function if not cached (e.g., initial array was empty)
|
|
135
|
+
if (!iterationNode.runtime.batchFn) {
|
|
136
|
+
const stateKeys = Object.keys(state);
|
|
137
|
+
iterationNode.runtime.batchFn = compileBatchFn(template, itemAlias, indexAlias, stateKeys);
|
|
138
|
+
iterationNode.runtime.stateKeys = stateKeys;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build HTML string using cached batch function
|
|
142
|
+
const batchFn = iterationNode.runtime.batchFn;
|
|
143
|
+
const stateKeys = iterationNode.runtime.stateKeys;
|
|
144
|
+
const stateValues = stateKeys.map(k => state[k]);
|
|
145
|
+
const html = batchFn(newArray, ...stateValues);
|
|
146
|
+
|
|
147
|
+
// Parse and insert
|
|
148
|
+
parseTemplate.innerHTML = html;
|
|
149
|
+
const frag = parseTemplate.content;
|
|
150
|
+
const kids = frag.children;
|
|
151
|
+
|
|
152
|
+
// Build instance tracking
|
|
153
|
+
const arrayLen = newArray.length;
|
|
154
|
+
const instances = new Array(arrayLen);
|
|
155
|
+
for (let i = 0; i < arrayLen; i++) {
|
|
156
|
+
instances[i] = { element: kids[i], item: newArray[i], index: i };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
parent.insertBefore(frag, endComment);
|
|
160
|
+
iterationNode.runtime.instances = instances;
|
|
161
|
+
};
|
package/affected.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { resolvePath, deepEqual } from './iteration-utils.js'
|
|
2
|
+
import { extractDependencies } from './conditionals.js'
|
|
3
|
+
import { BINDING_REGEX } from './constants.js'
|
|
4
|
+
|
|
5
|
+
// Evaluate conditional expression
|
|
6
|
+
const evaluateCondition = (expression, state) => {
|
|
7
|
+
try {
|
|
8
|
+
const keys = Object.keys(state);
|
|
9
|
+
const values = Object.values(state);
|
|
10
|
+
const result = new Function(...keys, `'use strict'; return !!(${expression})`)(...values);
|
|
11
|
+
return !!result;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Helper function to check if a match references a specific key
|
|
18
|
+
const matchesKey = (matchStr, key) => matchStr === key || matchStr.startsWith(key + '.');
|
|
19
|
+
|
|
20
|
+
const recursive = (tree, state, newState, affected) => {
|
|
21
|
+
// Handle iteration nodes specially
|
|
22
|
+
if (tree.type === 'iteration') {
|
|
23
|
+
const oldArray = resolvePath(state, tree.meta.arrayPath);
|
|
24
|
+
const newArray = resolvePath(newState, tree.meta.arrayPath);
|
|
25
|
+
|
|
26
|
+
// Fast path: reference comparison (arrays are typically replaced, not mutated)
|
|
27
|
+
// This avoids expensive O(n) deepEqual for large arrays
|
|
28
|
+
if (oldArray !== newArray) {
|
|
29
|
+
affected.push({
|
|
30
|
+
type: 'iteration',
|
|
31
|
+
node: tree,
|
|
32
|
+
changeType: 'array'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return affected;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Handle conditional nodes specially
|
|
39
|
+
if (tree.type === 'conditional') {
|
|
40
|
+
const oldValue = evaluateCondition(tree.meta.expression, state);
|
|
41
|
+
const newValue = evaluateCondition(tree.meta.expression, newState);
|
|
42
|
+
|
|
43
|
+
// Check if condition result changed
|
|
44
|
+
if (oldValue !== newValue) {
|
|
45
|
+
affected.push({
|
|
46
|
+
type: 'conditional',
|
|
47
|
+
node: tree,
|
|
48
|
+
changeType: 'expression'
|
|
49
|
+
});
|
|
50
|
+
return affected;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Condition didn't change, check for affected elements inside active branch
|
|
54
|
+
if (tree.runtime.activeInstance && tree.runtime.activeInstance.parsedTree) {
|
|
55
|
+
return recursive(tree.runtime.activeInstance.parsedTree, state, newState, affected);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return affected;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reset regex state for reuse
|
|
62
|
+
BINDING_REGEX.lastIndex = 0;
|
|
63
|
+
|
|
64
|
+
const matches = [];
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = BINDING_REGEX.exec(tree.parsed))) {
|
|
67
|
+
matches.push({ outer: match[0], inner: match[1], input: match.input });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (matches.length) {
|
|
71
|
+
const shallowState = Object.keys(state);
|
|
72
|
+
const shallowNewState = Object.keys(newState);
|
|
73
|
+
|
|
74
|
+
let hasAffected = false;
|
|
75
|
+
const checkedMatches = [];
|
|
76
|
+
|
|
77
|
+
for (const m of matches) {
|
|
78
|
+
const noMatch = !shallowState.some(key => matchesKey(m.inner, key));
|
|
79
|
+
const newMatches = shallowNewState.filter(key => matchesKey(m.inner, key));
|
|
80
|
+
|
|
81
|
+
if (noMatch || newMatches.length) {
|
|
82
|
+
hasAffected = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
checkedMatches.push({
|
|
86
|
+
...m,
|
|
87
|
+
matches: newMatches.length ? newMatches : shallowState.filter(key => matchesKey(m.inner, key))
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (hasAffected) {
|
|
92
|
+
for (const m of checkedMatches) {
|
|
93
|
+
affected.push({
|
|
94
|
+
matchOuter: m.outer,
|
|
95
|
+
matchInner: m.inner,
|
|
96
|
+
input: m.input,
|
|
97
|
+
matches: m.matches,
|
|
98
|
+
element: tree.element
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check attribute bindings
|
|
105
|
+
if (tree.attributes) {
|
|
106
|
+
const shallowState = Object.keys(state);
|
|
107
|
+
const shallowNewState = Object.keys(newState);
|
|
108
|
+
|
|
109
|
+
for (const [attrName, attrValue] of Object.entries(tree.attributes)) {
|
|
110
|
+
BINDING_REGEX.lastIndex = 0;
|
|
111
|
+
const attrMatches = [];
|
|
112
|
+
let attrMatch;
|
|
113
|
+
while ((attrMatch = BINDING_REGEX.exec(attrValue))) {
|
|
114
|
+
attrMatches.push({ outer: attrMatch[0], inner: attrMatch[1] });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const m of attrMatches) {
|
|
118
|
+
const noMatch = !shallowState.some(key => matchesKey(m.inner, key));
|
|
119
|
+
const newMatches = shallowNewState.filter(key => matchesKey(m.inner, key));
|
|
120
|
+
|
|
121
|
+
if (noMatch || newMatches.length) {
|
|
122
|
+
affected.push({
|
|
123
|
+
type: 'attribute',
|
|
124
|
+
attrName,
|
|
125
|
+
attrValue,
|
|
126
|
+
matchOuter: m.outer,
|
|
127
|
+
matchInner: m.inner,
|
|
128
|
+
element: tree.element
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Process children
|
|
136
|
+
const children = tree.children;
|
|
137
|
+
if (children) {
|
|
138
|
+
for (const key in children) {
|
|
139
|
+
const child = children[key];
|
|
140
|
+
if (child && typeof child === 'object') {
|
|
141
|
+
recursive(child, state, newState, affected);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return affected;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default (tree, state, newState) => recursive(tree, state, newState, [])
|
package/conditionals.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import parse from './parse.js';
|
|
2
|
+
import affected from './affected.js';
|
|
3
|
+
import hydrate from './hydrate.js';
|
|
4
|
+
import { createScopedState, renderAllIterations, initializeBlock } from './iterate.js';
|
|
5
|
+
|
|
6
|
+
// Evaluate conditional expression in state context
|
|
7
|
+
const evaluateCondition = (expression, state) => {
|
|
8
|
+
try {
|
|
9
|
+
const keys = Object.keys(state);
|
|
10
|
+
const values = Object.values(state);
|
|
11
|
+
// Create a function with state keys as parameters and evaluate the expression
|
|
12
|
+
const result = new Function(...keys, `'use strict'; return !!(${expression})`)(...values);
|
|
13
|
+
return !!result; // Coerce to boolean
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.warn(`Error evaluating condition "${expression}":`, e);
|
|
16
|
+
return false; // Default to false on error
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Extract state dependencies from an expression
|
|
21
|
+
// e.g., "count > 5" → ["count"]
|
|
22
|
+
// e.g., "isLoggedIn && isAdmin" → ["isLoggedIn", "isAdmin"]
|
|
23
|
+
export const extractDependencies = (expression) => {
|
|
24
|
+
const regex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
|
|
25
|
+
const matches = [];
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = regex.exec(expression))) {
|
|
28
|
+
const identifier = match[1];
|
|
29
|
+
// Filter out JavaScript keywords and common literals
|
|
30
|
+
if (!['true', 'false', 'null', 'undefined', 'this', 'return'].includes(identifier)) {
|
|
31
|
+
matches.push(identifier);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...new Set(matches)]; // Unique values
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Render all conditionals in the parsed tree
|
|
38
|
+
export const renderAllConditionals = (tree, state, linkList, parentScope = {}) => {
|
|
39
|
+
// If this is a conditional node, render it
|
|
40
|
+
if (tree.type === 'conditional') {
|
|
41
|
+
renderConditional(tree, state, linkList, parentScope);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Recursively render conditionals in child nodes
|
|
46
|
+
if (tree.children) {
|
|
47
|
+
Object.keys(tree.children).forEach(key => {
|
|
48
|
+
const child = tree.children[key];
|
|
49
|
+
if (typeof child === 'object' && child !== null) {
|
|
50
|
+
renderAllConditionals(child, state, linkList, parentScope);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Initial render of a conditional block
|
|
57
|
+
export const renderConditional = (node, state, linkList, parentScope = {}) => {
|
|
58
|
+
const { expression, startComment, endComment, branches } = node.meta;
|
|
59
|
+
|
|
60
|
+
// Remove original template nodes from DOM (between start and end comments)
|
|
61
|
+
// Only do this on first render (when activeBranch is undefined)
|
|
62
|
+
if (node.runtime.activeBranch === undefined) {
|
|
63
|
+
let currentNode = startComment.nextSibling;
|
|
64
|
+
while (currentNode && currentNode !== endComment) {
|
|
65
|
+
const nextNode = currentNode.nextSibling;
|
|
66
|
+
if (currentNode.parentNode) {
|
|
67
|
+
currentNode.parentNode.removeChild(currentNode);
|
|
68
|
+
}
|
|
69
|
+
currentNode = nextNode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Mark that template has been removed
|
|
73
|
+
node.runtime.templateRemoved = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Evaluate condition with current state
|
|
77
|
+
const conditionResult = evaluateCondition(expression, state);
|
|
78
|
+
|
|
79
|
+
// Determine which branch to mount
|
|
80
|
+
const branchToMount = conditionResult ? branches.if : branches.else;
|
|
81
|
+
|
|
82
|
+
// Mount the appropriate branch
|
|
83
|
+
mountBranch(node, branchToMount, state, linkList, parentScope);
|
|
84
|
+
|
|
85
|
+
// Store active branch reference
|
|
86
|
+
node.runtime.activeBranch = branchToMount;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Mount a specific branch
|
|
90
|
+
const mountBranch = (node, branchData, state, linkList, parentScope) => {
|
|
91
|
+
const { startComment, endComment } = node.meta;
|
|
92
|
+
|
|
93
|
+
// If branch doesn't exist (no else clause), just unmount current
|
|
94
|
+
if (!branchData) {
|
|
95
|
+
unmountBranch(node);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Unmount current branch first (if any)
|
|
100
|
+
unmountBranch(node);
|
|
101
|
+
|
|
102
|
+
// Clone the template element
|
|
103
|
+
const templateContent = branchData.element.childNodes;
|
|
104
|
+
const parent = startComment.parentNode;
|
|
105
|
+
|
|
106
|
+
// Create scoped state (with parent scope if inside iteration)
|
|
107
|
+
const scopedState = Object.keys(parentScope).length > 0
|
|
108
|
+
? createScopedState(state, parentScope)
|
|
109
|
+
: state;
|
|
110
|
+
|
|
111
|
+
// Initialize block (clone, parse, hydrate)
|
|
112
|
+
const { element: firstElement, tree: branchTree, clonedNodes } = initializeBlock(templateContent, scopedState);
|
|
113
|
+
|
|
114
|
+
// Insert cloned nodes into DOM
|
|
115
|
+
clonedNodes.forEach(clonedNode => parent.insertBefore(clonedNode, endComment));
|
|
116
|
+
|
|
117
|
+
// Recursively render any nested iterations and conditionals
|
|
118
|
+
if (branchTree) {
|
|
119
|
+
renderAllIterations(branchTree, scopedState, linkList);
|
|
120
|
+
renderAllConditionals(branchTree, scopedState, linkList, parentScope);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Store active instance
|
|
124
|
+
node.runtime.activeInstance = {
|
|
125
|
+
branch: branchData,
|
|
126
|
+
nodes: clonedNodes,
|
|
127
|
+
parsedTree: branchTree
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Unmount currently active branch
|
|
132
|
+
const unmountBranch = (node) => {
|
|
133
|
+
const { activeInstance } = node.runtime;
|
|
134
|
+
|
|
135
|
+
if (!activeInstance) return;
|
|
136
|
+
|
|
137
|
+
// Remove all nodes from DOM
|
|
138
|
+
activeInstance.nodes.forEach(domNode => {
|
|
139
|
+
if (domNode.parentNode) {
|
|
140
|
+
domNode.parentNode.removeChild(domNode);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Clear active instance
|
|
145
|
+
node.runtime.activeInstance = null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Update conditional when dependencies change
|
|
149
|
+
export const updateConditional = (node, newState, oldState, linkList, parentScope = {}) => {
|
|
150
|
+
const { expression, branches } = node.meta;
|
|
151
|
+
|
|
152
|
+
// If not yet rendered, skip (renderConditional handles initial render)
|
|
153
|
+
if (!node.runtime.templateRemoved) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Evaluate expression with new state
|
|
158
|
+
const newConditionResult = evaluateCondition(expression, newState);
|
|
159
|
+
const newBranchData = newConditionResult ? branches.if : branches.else;
|
|
160
|
+
|
|
161
|
+
// Check if branch changed (compare references)
|
|
162
|
+
const branchChanged = node.runtime.activeBranch !== newBranchData;
|
|
163
|
+
|
|
164
|
+
if (branchChanged) {
|
|
165
|
+
// Switch branches
|
|
166
|
+
mountBranch(node, newBranchData, newState, linkList, parentScope);
|
|
167
|
+
node.runtime.activeBranch = newBranchData;
|
|
168
|
+
} else {
|
|
169
|
+
// Same branch, but state might have changed - rehydrate
|
|
170
|
+
const { activeInstance } = node.runtime;
|
|
171
|
+
|
|
172
|
+
if (activeInstance && activeInstance.parsedTree) {
|
|
173
|
+
const scopedState = Object.keys(parentScope).length > 0
|
|
174
|
+
? createScopedState(newState, parentScope)
|
|
175
|
+
: newState;
|
|
176
|
+
|
|
177
|
+
const affectedElements = affected(activeInstance.parsedTree, oldState, scopedState);
|
|
178
|
+
hydrate(affectedElements, scopedState);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|