@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/link.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const recursive = (tree, results, tagChain) => {
|
|
2
|
+
results[tagChain.join(".")] = tree.element;
|
|
3
|
+
|
|
4
|
+
if (tree.children && Object.keys(tree.children).length > 0) {
|
|
5
|
+
Object.keys(tree.children).forEach((tag) => {
|
|
6
|
+
results = recursive(tree.children[tag], results, [...tagChain, tag]);
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return results;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default (tree) => recursive(tree, {}, []);
|
package/llms.txt
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Vibe - Complete Documentation
|
|
2
|
+
|
|
3
|
+
> Runtime-first reactivity. No virtual DOM. No build step.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Vibe is a lightweight reactive library that uses Proxy-based state and MutationObserver for fine-grained DOM updates. It works directly in the browser with zero compilation required.
|
|
8
|
+
|
|
9
|
+
**Key characteristics:**
|
|
10
|
+
- Proxy-based reactive state (`window.$`)
|
|
11
|
+
- Surgical DOM updates (only affected elements re-render)
|
|
12
|
+
- MutationObserver for dynamic element tracking
|
|
13
|
+
- Works with vanilla HTML - no special file format
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @ape-egg/vibe
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<script type="module">
|
|
25
|
+
import state from "@ape-egg/vibe";
|
|
26
|
+
window.$ = state({ name: "World", count: 0 });
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<h1>Hello, @[name]!</h1>
|
|
30
|
+
<button onclick="$.count++">Clicked @[count] times</button>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Core Syntax
|
|
34
|
+
|
|
35
|
+
### Reactive Bindings
|
|
36
|
+
|
|
37
|
+
Use `@[property]` syntax anywhere in HTML or CSS:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<!-- Text content -->
|
|
41
|
+
<div>@[firstName]</div>
|
|
42
|
+
|
|
43
|
+
<!-- Expressions -->
|
|
44
|
+
<div>@[firstName + ' ' + lastName]</div>
|
|
45
|
+
<div>@[count * 2]</div>
|
|
46
|
+
|
|
47
|
+
<!-- Attributes -->
|
|
48
|
+
<input value="@[inputValue]">
|
|
49
|
+
<button disabled="@[isLoading]">Submit</button>
|
|
50
|
+
|
|
51
|
+
<!-- CSS -->
|
|
52
|
+
<style>
|
|
53
|
+
.box { background: @[themeColor]; }
|
|
54
|
+
</style>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
⚠️ **Important**: Bindings are evaluated using `new Function()`. Do not bind untrusted user input.
|
|
58
|
+
|
|
59
|
+
### State Access
|
|
60
|
+
|
|
61
|
+
State is accessed globally via `window.$`:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
// Read
|
|
65
|
+
console.log($.firstName);
|
|
66
|
+
|
|
67
|
+
// Write (triggers re-render)
|
|
68
|
+
$.firstName = "John";
|
|
69
|
+
|
|
70
|
+
// Increment
|
|
71
|
+
$.count++;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Control Flow
|
|
75
|
+
|
|
76
|
+
### Iteration
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<!-- each items as item -->
|
|
80
|
+
<li>@[item]</li>
|
|
81
|
+
<!-- /each -->
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
With index:
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!-- each items as item, index -->
|
|
88
|
+
<li>@[index]: @[item]</li>
|
|
89
|
+
<!-- /each -->
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Nested Iteration
|
|
93
|
+
|
|
94
|
+
Use dot paths for nested arrays:
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<!-- each categories as category -->
|
|
98
|
+
<h2>@[category.name]</h2>
|
|
99
|
+
<!-- each category.items as item -->
|
|
100
|
+
<span>@[item.name]</span>
|
|
101
|
+
<!-- /each -->
|
|
102
|
+
<!-- /each -->
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Conditionals
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<!-- if isLoggedIn -->
|
|
109
|
+
<span>Welcome, @[username]!</span>
|
|
110
|
+
<!-- else -->
|
|
111
|
+
<span>Please log in</span>
|
|
112
|
+
<!-- /if -->
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Conditionals can be nested inside iterations and vice versa.
|
|
116
|
+
|
|
117
|
+
## Special Attributes
|
|
118
|
+
|
|
119
|
+
### Dehydrate
|
|
120
|
+
|
|
121
|
+
Skip reactive processing for an element and its children:
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<code dehydrate>@[this] displays literally, not parsed</code>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use cases:
|
|
128
|
+
- Displaying `@[...]` syntax in documentation
|
|
129
|
+
- Static content that shouldn't be reactive
|
|
130
|
+
- Performance optimization for large static sections
|
|
131
|
+
|
|
132
|
+
### Boolean Attributes
|
|
133
|
+
|
|
134
|
+
Attributes not in the value whitelist are removed when falsy:
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<button disabled="@[isLoading]">Submit</button>
|
|
138
|
+
<!-- When isLoading is false, disabled attribute is removed entirely -->
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Events
|
|
142
|
+
|
|
143
|
+
Use standard inline event handlers:
|
|
144
|
+
|
|
145
|
+
```html
|
|
146
|
+
<button onclick="$.count++">Increment</button>
|
|
147
|
+
<input oninput="$.text = this.value">
|
|
148
|
+
<form onsubmit="event.preventDefault(); handleSubmit()">
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Styling
|
|
152
|
+
|
|
153
|
+
### CSS Bindings
|
|
154
|
+
|
|
155
|
+
Reactive values work inside `<style>` tags:
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<style>
|
|
159
|
+
.box {
|
|
160
|
+
background: @[backgroundColor];
|
|
161
|
+
color: @[textColor];
|
|
162
|
+
width: @[width]px;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Preventing FOUC
|
|
168
|
+
|
|
169
|
+
Hide content until hydration completes:
|
|
170
|
+
|
|
171
|
+
```html
|
|
172
|
+
<body style="visibility: hidden;">
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Or use the included CSS:
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<link rel="stylesheet" href="@ape-egg/vibe/vibe.css">
|
|
179
|
+
<body vibe>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Dynamic Elements
|
|
183
|
+
|
|
184
|
+
Elements added via JavaScript are automatically hydrated through MutationObserver:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
const div = document.createElement('div');
|
|
188
|
+
div.innerHTML = '<span>Hello, @[name]!</span>';
|
|
189
|
+
document.body.appendChild(div);
|
|
190
|
+
// Automatically becomes reactive
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## API Reference
|
|
194
|
+
|
|
195
|
+
### `state(initialState, afterUpdate?)`
|
|
196
|
+
|
|
197
|
+
Creates reactive state and initializes the framework.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
import state from "@ape-egg/vibe";
|
|
201
|
+
|
|
202
|
+
window.$ = state(
|
|
203
|
+
{ count: 0, user: { name: "Alice" } },
|
|
204
|
+
(newState, oldState) => {
|
|
205
|
+
console.log("State updated:", newState);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Parameters:**
|
|
211
|
+
- `initialState` - Object containing initial state values
|
|
212
|
+
- `afterUpdate` - Optional callback after each state change (receives read-only snapshots)
|
|
213
|
+
|
|
214
|
+
**Returns:** Proxy object for reactive state access
|
|
215
|
+
|
|
216
|
+
## Scoped Variables
|
|
217
|
+
|
|
218
|
+
Inside `<!-- each -->` blocks, these variables are available:
|
|
219
|
+
- `item` (or custom name) - current array element
|
|
220
|
+
- `index` (or custom name) - current index
|
|
221
|
+
- Parent state remains accessible via `$`
|
|
222
|
+
|
|
223
|
+
```html
|
|
224
|
+
<!-- each users as user, i -->
|
|
225
|
+
<div>@[i]: @[user.name] (total: @[users.length])</div>
|
|
226
|
+
<!-- /each -->
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Architecture
|
|
230
|
+
|
|
231
|
+
Vibe consists of these core modules:
|
|
232
|
+
|
|
233
|
+
- **state.js** - Proxy-based reactive state container
|
|
234
|
+
- **parse.js** - DOM parser that finds `@[...]` bindings
|
|
235
|
+
- **link.js** - Maps elements to parsed tree nodes
|
|
236
|
+
- **hydrate.js** - Updates DOM with current state values
|
|
237
|
+
- **affected.js** - Determines which elements need updating
|
|
238
|
+
- **iterate.js** - Array rendering with efficient diffing
|
|
239
|
+
- **conditionals.js** - Conditional block rendering
|
|
240
|
+
|
|
241
|
+
## How It Works
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
1. state() initializes the Proxy and framework
|
|
245
|
+
2. parse.js scans DOM for @[...], <!-- each -->, <!-- if -->
|
|
246
|
+
3. link.js maps elements to the parsed tree
|
|
247
|
+
4. hydrate.js replaces bindings with values
|
|
248
|
+
5. iterate.js renders <!-- each --> loops
|
|
249
|
+
6. conditionals.js renders <!-- if --> blocks
|
|
250
|
+
7. MutationObserver watches for new elements
|
|
251
|
+
8. On state change: affected.js finds changed elements → hydrate.js updates them
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Current Limitations
|
|
255
|
+
|
|
256
|
+
- **Top-level reactivity only**: `$.nested.prop = value` doesn't trigger updates (must replace parent object)
|
|
257
|
+
- **No computed values**: Derived state must be calculated manually
|
|
258
|
+
- **No two-way binding sugar**: Must wire input events manually
|
|
259
|
+
- **Expression security**: `new Function()` evaluation - don't bind untrusted input
|
|
260
|
+
|
|
261
|
+
## Best Practices
|
|
262
|
+
|
|
263
|
+
1. **Initialize state before DOM**: Place `<script>` in `<head>` or before reactive elements
|
|
264
|
+
2. **Use dehydrate for docs**: When showing `@[...]` syntax examples
|
|
265
|
+
3. **Prevent FOUC**: Use `visibility: hidden` on body until hydration
|
|
266
|
+
4. **Keep expressions simple**: Complex logic belongs in JavaScript, not templates
|
|
267
|
+
5. **Replace objects for deep updates**: `$.user = { ...$.user, name: "New" }`
|
|
268
|
+
|
|
269
|
+
## Browser Support
|
|
270
|
+
|
|
271
|
+
Modern browsers with:
|
|
272
|
+
- Proxy (ES6)
|
|
273
|
+
- MutationObserver
|
|
274
|
+
- ES Modules
|
|
275
|
+
|
|
276
|
+
## Resources
|
|
277
|
+
|
|
278
|
+
- **Homepage**: https://vibe.korte.kim
|
|
279
|
+
- **npm**: https://www.npmjs.com/package/@ape-egg/vibe
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ape-egg/vibe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Runtime-first reactivity",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"homepage": "https://vibe.korte.kim",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"reactive",
|
|
10
|
+
"framework",
|
|
11
|
+
"frontend",
|
|
12
|
+
"ui",
|
|
13
|
+
"mutation-observer",
|
|
14
|
+
"proxy",
|
|
15
|
+
"minimalistic"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
19
|
+
},
|
|
20
|
+
"author": "Kim Korte",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {}
|
|
26
|
+
}
|
package/parse.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { hash } from './utils.js'
|
|
2
|
+
import { findEndComment, findConditionalEnd } from './iteration-utils.js'
|
|
3
|
+
import { NON_REACTIVE_ELEMENTS, BINDING_REGEX, ITERATION_REGEX, CONDITIONAL_REGEX } from './constants.js'
|
|
4
|
+
|
|
5
|
+
const parseHTML = (children, rootKey = undefined) =>
|
|
6
|
+
children.reduce((s, element, i) => {
|
|
7
|
+
const { nodeName, textContent } = element
|
|
8
|
+
if (['#comment'].includes(nodeName)) {
|
|
9
|
+
return `${s}${nodeName === '#comment' ? `asd` : textContent}`
|
|
10
|
+
}
|
|
11
|
+
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName
|
|
12
|
+
const innerNodeIdentifier = `${name}_${i}`.toLowerCase()
|
|
13
|
+
|
|
14
|
+
return rootKey || `${s}${`\$[${innerNodeIdentifier}]`}`
|
|
15
|
+
}, '')
|
|
16
|
+
|
|
17
|
+
const recursive = (children, rootKey = undefined, skipIndices = new Set()) => {
|
|
18
|
+
let result = {};
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < children.length; i++) {
|
|
21
|
+
// Skip if this index was part of an iteration template
|
|
22
|
+
if (skipIndices.has(i)) continue;
|
|
23
|
+
|
|
24
|
+
const element = children[i];
|
|
25
|
+
const { nodeName, childNodes, outerHTML, innerHTML, textContent } = element;
|
|
26
|
+
|
|
27
|
+
// Skip non-reactive elements and dehydrated elements
|
|
28
|
+
if (NON_REACTIVE_ELEMENTS.includes(nodeName)) continue;
|
|
29
|
+
if (element.hasAttribute?.('dehydrate')) continue;
|
|
30
|
+
|
|
31
|
+
// Handle iteration comments
|
|
32
|
+
if (nodeName === '#comment') {
|
|
33
|
+
const iterationMatch = textContent.trim().match(ITERATION_REGEX);
|
|
34
|
+
|
|
35
|
+
if (iterationMatch) {
|
|
36
|
+
const [_, arrayPath, itemAlias, indexAlias] = iterationMatch;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Find matching end comment
|
|
40
|
+
const endIndex = findEndComment(children, i + 1);
|
|
41
|
+
|
|
42
|
+
// Extract template nodes (between start and end comments)
|
|
43
|
+
const templateNodes = Array.from(children).slice(i + 1, endIndex);
|
|
44
|
+
|
|
45
|
+
// Create a temporary container for the template
|
|
46
|
+
const templateContainer = document.createElement('div');
|
|
47
|
+
templateNodes.forEach(node => {
|
|
48
|
+
templateContainer.appendChild(node.cloneNode(true));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Parse the template recursively
|
|
52
|
+
const templateParsed = recursive([...templateContainer.childNodes]);
|
|
53
|
+
|
|
54
|
+
// Store iteration metadata
|
|
55
|
+
const iterationKey = `iteration_${hash()}`;
|
|
56
|
+
result[iterationKey] = {
|
|
57
|
+
type: 'iteration',
|
|
58
|
+
meta: {
|
|
59
|
+
arrayPath,
|
|
60
|
+
itemAlias,
|
|
61
|
+
indexAlias: indexAlias || 'index',
|
|
62
|
+
startComment: element,
|
|
63
|
+
endComment: children[endIndex],
|
|
64
|
+
template: {
|
|
65
|
+
parsed: parseHTML([...templateContainer.childNodes]),
|
|
66
|
+
element: templateContainer,
|
|
67
|
+
children: templateParsed
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
runtime: {
|
|
71
|
+
instances: [],
|
|
72
|
+
templateRemoved: false
|
|
73
|
+
},
|
|
74
|
+
children: {}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Mark template indices as processed
|
|
78
|
+
for (let j = i + 1; j < endIndex; j++) {
|
|
79
|
+
skipIndices.add(j);
|
|
80
|
+
}
|
|
81
|
+
skipIndices.add(endIndex); // Also skip the end comment
|
|
82
|
+
|
|
83
|
+
// Skip past this iteration block
|
|
84
|
+
i = endIndex;
|
|
85
|
+
continue;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('Error parsing iteration block:', e);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle conditional comments
|
|
93
|
+
const conditionalMatch = textContent.trim().match(CONDITIONAL_REGEX);
|
|
94
|
+
|
|
95
|
+
if (conditionalMatch) {
|
|
96
|
+
const [_, expression] = conditionalMatch;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Find matching end comment and optional else
|
|
100
|
+
const { elseIndex, endIndex } = findConditionalEnd(children, i + 1);
|
|
101
|
+
|
|
102
|
+
// Extract true branch nodes (between if and else/endif)
|
|
103
|
+
const trueBranchEnd = elseIndex !== null ? elseIndex : endIndex;
|
|
104
|
+
const trueBranchNodes = Array.from(children).slice(i + 1, trueBranchEnd);
|
|
105
|
+
|
|
106
|
+
// Create temporary container for true branch
|
|
107
|
+
const trueBranchContainer = document.createElement('div');
|
|
108
|
+
trueBranchNodes.forEach(node => {
|
|
109
|
+
trueBranchContainer.appendChild(node.cloneNode(true));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Parse the true branch recursively
|
|
113
|
+
const trueBranchParsed = recursive([...trueBranchContainer.childNodes]);
|
|
114
|
+
|
|
115
|
+
// Extract false branch nodes if else exists
|
|
116
|
+
let falseBranchParsed = null;
|
|
117
|
+
let falseBranchContainer = null;
|
|
118
|
+
if (elseIndex !== null) {
|
|
119
|
+
const falseBranchNodes = Array.from(children).slice(elseIndex + 1, endIndex);
|
|
120
|
+
falseBranchContainer = document.createElement('div');
|
|
121
|
+
falseBranchNodes.forEach(node => {
|
|
122
|
+
falseBranchContainer.appendChild(node.cloneNode(true));
|
|
123
|
+
});
|
|
124
|
+
falseBranchParsed = recursive([...falseBranchContainer.childNodes]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Store conditional metadata
|
|
128
|
+
const conditionalKey = `conditional_${hash()}`;
|
|
129
|
+
result[conditionalKey] = {
|
|
130
|
+
type: 'conditional',
|
|
131
|
+
meta: {
|
|
132
|
+
expression,
|
|
133
|
+
startComment: element,
|
|
134
|
+
elseComment: elseIndex !== null ? children[elseIndex] : null,
|
|
135
|
+
endComment: children[endIndex],
|
|
136
|
+
branches: {
|
|
137
|
+
if: {
|
|
138
|
+
parsed: parseHTML([...trueBranchContainer.childNodes]),
|
|
139
|
+
element: trueBranchContainer,
|
|
140
|
+
children: trueBranchParsed
|
|
141
|
+
},
|
|
142
|
+
else: falseBranchContainer ? {
|
|
143
|
+
parsed: parseHTML([...falseBranchContainer.childNodes]),
|
|
144
|
+
element: falseBranchContainer,
|
|
145
|
+
children: falseBranchParsed
|
|
146
|
+
} : null
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
runtime: {
|
|
150
|
+
activeBranch: undefined,
|
|
151
|
+
activeInstance: null,
|
|
152
|
+
templateRemoved: false
|
|
153
|
+
},
|
|
154
|
+
children: {}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Mark template indices as processed
|
|
158
|
+
for (let j = i + 1; j <= endIndex; j++) {
|
|
159
|
+
skipIndices.add(j);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Skip past this conditional block
|
|
163
|
+
i = endIndex;
|
|
164
|
+
continue;
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error('Error parsing conditional block:', e);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Skip other comments
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
|
|
176
|
+
const nodeIdentifier = `${name}_${i}`.toLowerCase();
|
|
177
|
+
|
|
178
|
+
// Check for attribute bindings
|
|
179
|
+
const attributes = {};
|
|
180
|
+
if (element.attributes) {
|
|
181
|
+
for (let j = 0; j < element.attributes.length; j++) {
|
|
182
|
+
const attr = element.attributes[j];
|
|
183
|
+
// Reset lastIndex before test - BINDING_REGEX has 'g' flag which persists state
|
|
184
|
+
BINDING_REGEX.lastIndex = 0;
|
|
185
|
+
if (BINDING_REGEX.test(attr.value)) {
|
|
186
|
+
attributes[attr.name] = attr.value;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const hasAttributeBindings = Object.keys(attributes).length > 0;
|
|
191
|
+
|
|
192
|
+
const hasChildren = childNodes.length;
|
|
193
|
+
|
|
194
|
+
if (hasChildren) {
|
|
195
|
+
const iteratableChildren = [...childNodes];
|
|
196
|
+
const parsed = parseHTML(iteratableChildren);
|
|
197
|
+
|
|
198
|
+
result[rootKey || nodeIdentifier] = {
|
|
199
|
+
parsed,
|
|
200
|
+
element,
|
|
201
|
+
children: recursive(iteratableChildren),
|
|
202
|
+
...(hasAttributeBindings && { attributes })
|
|
203
|
+
};
|
|
204
|
+
} else {
|
|
205
|
+
result[nodeIdentifier] = {
|
|
206
|
+
parsed: innerHTML || textContent,
|
|
207
|
+
element,
|
|
208
|
+
children: {},
|
|
209
|
+
...(hasAttributeBindings && { attributes })
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default (root, rootKey = undefined) => {
|
|
218
|
+
const { childNodes } = root
|
|
219
|
+
return {
|
|
220
|
+
// html: root.outerHTML,
|
|
221
|
+
parsed: parseHTML([...childNodes], rootKey),
|
|
222
|
+
element: root,
|
|
223
|
+
children: recursive(Array.from(childNodes), rootKey)
|
|
224
|
+
}
|
|
225
|
+
}
|
package/state.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default (state, rerender) =>
|
|
2
|
+
new Proxy(state, {
|
|
3
|
+
set(obj, prop, value) {
|
|
4
|
+
const ref = Reflect.set(...arguments)
|
|
5
|
+
rerender({
|
|
6
|
+
[prop]: value,
|
|
7
|
+
})
|
|
8
|
+
// console.info("state change", { obj, prop, value });
|
|
9
|
+
return ref
|
|
10
|
+
},
|
|
11
|
+
get(target, prop) {
|
|
12
|
+
// if (typeof target[prop] === 'function') {
|
|
13
|
+
// const inputString = target[prop].toString();
|
|
14
|
+
// const regex = /\$\.\w+/g;
|
|
15
|
+
// let match;
|
|
16
|
+
// while ((match = regex.exec(inputString))) {
|
|
17
|
+
// const varName = match[0].split('.')[1];
|
|
18
|
+
// console.log(varName, state[varName]);
|
|
19
|
+
// }
|
|
20
|
+
// console.log(arguments)
|
|
21
|
+
|
|
22
|
+
// }
|
|
23
|
+
|
|
24
|
+
return Reflect.get(...arguments)
|
|
25
|
+
},
|
|
26
|
+
})
|
package/utils.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Fast incrementing counter instead of expensive random hash
|
|
2
|
+
let hashCounter = 0;
|
|
3
|
+
export const hash = () => `_${hashCounter++}`;
|
|
4
|
+
|
|
5
|
+
// Instead of using lodash-es as a dependency, we run our own deepMerge (mergeWith in lodash)
|
|
6
|
+
export const deepMerge = (target, source) => {
|
|
7
|
+
// Handle null/undefined
|
|
8
|
+
if (source == null) return target;
|
|
9
|
+
if (target == null) return source;
|
|
10
|
+
|
|
11
|
+
// If source is not an object, return it
|
|
12
|
+
if (typeof source !== "object") return source;
|
|
13
|
+
|
|
14
|
+
// If source is an array, replace target array (don't merge arrays)
|
|
15
|
+
if (Array.isArray(source)) return source;
|
|
16
|
+
|
|
17
|
+
// Clone target to avoid mutation
|
|
18
|
+
const result = { ...target };
|
|
19
|
+
|
|
20
|
+
// Merge each property from source
|
|
21
|
+
for (const key in source) {
|
|
22
|
+
if (source.hasOwnProperty(key)) {
|
|
23
|
+
const targetValue = result[key];
|
|
24
|
+
const sourceValue = source[key];
|
|
25
|
+
|
|
26
|
+
// If both are objects (but not arrays), merge recursively
|
|
27
|
+
if (
|
|
28
|
+
targetValue != null &&
|
|
29
|
+
sourceValue != null &&
|
|
30
|
+
typeof targetValue === "object" &&
|
|
31
|
+
typeof sourceValue === "object" &&
|
|
32
|
+
!Array.isArray(targetValue) &&
|
|
33
|
+
!Array.isArray(sourceValue)
|
|
34
|
+
) {
|
|
35
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
36
|
+
} else {
|
|
37
|
+
// Otherwise, replace with source value (handles arrays and primitives)
|
|
38
|
+
result[key] = sourceValue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
|
package/vibe.css
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* Vibe Framework - Hydration Styles
|
|
2
|
+
* Elements with [vibe] attribute are hidden until framework removes it after hydration.
|
|
3
|
+
* This prevents flash of unprocessed content and disables transitions during init.
|
|
4
|
+
*/
|
|
5
|
+
[vibe] {
|
|
6
|
+
visibility: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
[vibe] * {
|
|
10
|
+
transition: none !important;
|
|
11
|
+
}
|