@ape-egg/vibe 1.3.2 → 1.6.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 +177 -0
- package/README.md +97 -0
- package/boot.js +0 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/Cargo.lock +719 -40
- package/compiler/src/Cargo.toml +11 -2
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +241 -0
- package/compiler/src/compiler/compile.rs +552 -175
- package/compiler/src/compiler/component_tagger.rs +234 -0
- package/compiler/src/compiler/iteration_optimizer.rs +351 -0
- package/compiler/src/compiler/js_analyzer.rs +572 -0
- package/compiler/src/compiler/manifest_builder.rs +251 -26
- package/compiler/src/compiler/mod.rs +5 -1
- package/compiler/src/compiler/state_extractor.rs +140 -25
- package/compiler/src/compiler/value_stamper.rs +579 -88
- package/compiler/src/compiler/watcher.rs +579 -0
- package/compiler/src/config.rs +51 -8
- package/compiler/src/main.rs +41 -28
- package/compiler/src/parser/html.rs +229 -118
- package/component.js +23 -11
- package/package.json +1 -1
- package/runtime/cleanup.js +4 -4
- package/runtime/component.js +98 -21
- package/runtime/conditionals.js +2 -2
- package/runtime/constants.js +2 -1
- package/runtime/index.js +152 -30
- package/runtime/iterate.js +27 -5
- package/runtime/parse.js +2 -1
- package/runtime/pre-compiled-iterations.js +153 -0
- package/runtime/{hyperspeed.js → pre-compiled-manifest.js} +204 -132
- package/runtime/utils.js +2 -1
- package/test-results/.last-run.json +4 -0
- package/vibe.css +19 -0
- package/runtime/component-state.js +0 -63
|
@@ -82,25 +82,25 @@ impl HtmlParser {
|
|
|
82
82
|
&self.cache
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/// Process HTML content: transform custom tags to <component>, optionally inline,
|
|
85
|
+
/// Process HTML content: transform custom tags to <component>, optionally inline, transform custom elements
|
|
86
86
|
pub fn process_html(
|
|
87
87
|
&self,
|
|
88
88
|
content: &str,
|
|
89
|
-
accessibility: bool,
|
|
90
|
-
exclude_tags: &[String],
|
|
91
89
|
elements_as_is: bool,
|
|
90
|
+
reserved_elements: &[String],
|
|
91
|
+
components_as_is: bool,
|
|
92
92
|
components_dir: &str,
|
|
93
93
|
) -> String {
|
|
94
|
-
self.process_html_with_cache(content,
|
|
94
|
+
self.process_html_with_cache(content, elements_as_is, reserved_elements, components_as_is, components_dir, &HashMap::new())
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
pub fn process_html_with_cache(
|
|
98
98
|
&self,
|
|
99
99
|
content: &str,
|
|
100
|
-
accessibility: bool,
|
|
101
|
-
exclude_tags: &[String],
|
|
102
100
|
elements_as_is: bool,
|
|
103
|
-
|
|
101
|
+
reserved_elements: &[String],
|
|
102
|
+
components_as_is: bool,
|
|
103
|
+
_components_dir: &str,
|
|
104
104
|
external_cache: &HashMap<String, String>,
|
|
105
105
|
) -> String {
|
|
106
106
|
// Extract and preserve DOCTYPE declaration if present
|
|
@@ -109,17 +109,22 @@ impl HtmlParser {
|
|
|
109
109
|
|
|
110
110
|
let mut result = content.to_string();
|
|
111
111
|
|
|
112
|
-
// Step 1:
|
|
113
|
-
|
|
112
|
+
// Step 1: Inline custom elements directly (ALWAYS, even with components_as_is)
|
|
113
|
+
// Custom elements like <card> are always inlined because runtime doesn't know about /components directory
|
|
114
|
+
result = self.inline_custom_elements(&result);
|
|
114
115
|
|
|
115
|
-
// Step 2:
|
|
116
|
-
|
|
116
|
+
// Step 2: Handle explicit <component src="..."> elements
|
|
117
|
+
// If components_as_is is false, recursively inline all <component> elements
|
|
118
|
+
if !components_as_is {
|
|
117
119
|
result = self.inline_component_elements(&result, external_cache);
|
|
120
|
+
|
|
121
|
+
// NOTE: Don't run inline_custom_elements again here - it causes infinite recursion
|
|
122
|
+
// Custom elements inside components are already processed when the component was cached
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
// Step 3:
|
|
121
|
-
if
|
|
122
|
-
result = transform_custom_tags_to_divs(&result,
|
|
125
|
+
// Step 3: Transform custom elements to divs if elements_as_is is false (accessible by default)
|
|
126
|
+
if !elements_as_is {
|
|
127
|
+
result = transform_custom_tags_to_divs(&result, reserved_elements);
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
// Step 4: Restore DOCTYPE if it was present
|
|
@@ -133,134 +138,237 @@ impl HtmlParser {
|
|
|
133
138
|
result
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
///
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for tag_name in self.cache.keys() {
|
|
142
|
-
// Match opening and closing tags with any attributes and children
|
|
143
|
-
let tag_pattern = format!(r"<{}(\s[^>]*)?>", regex::escape(tag_name));
|
|
144
|
-
let tag_re = regex::Regex::new(&tag_pattern).unwrap();
|
|
145
|
-
let closing_pattern = format!(r"</{}>", regex::escape(tag_name));
|
|
146
|
-
|
|
147
|
-
// Find all occurrences and transform them
|
|
148
|
-
let mut matches: Vec<(usize, usize, String, Option<String>)> = Vec::new();
|
|
149
|
-
|
|
150
|
-
// Find opening tags
|
|
151
|
-
for cap in tag_re.find_iter(&result) {
|
|
152
|
-
let start = cap.start();
|
|
153
|
-
let tag_with_attrs = cap.as_str();
|
|
154
|
-
|
|
155
|
-
// Extract attributes (everything between tag name and >)
|
|
156
|
-
let attrs = if tag_with_attrs.ends_with('>') {
|
|
157
|
-
let inner = &tag_with_attrs[tag_name.len() + 1..tag_with_attrs.len() - 1];
|
|
158
|
-
if inner.trim().is_empty() {
|
|
159
|
-
None
|
|
160
|
-
} else {
|
|
161
|
-
Some(inner.to_string())
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
None
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Find corresponding closing tag
|
|
168
|
-
if let Some(closing_pos) = result[cap.end()..].find(&closing_pattern) {
|
|
169
|
-
let closing_start = cap.end() + closing_pos;
|
|
170
|
-
let closing_end = closing_start + closing_pattern.len();
|
|
171
|
-
let children = result[cap.end()..closing_start].to_string();
|
|
172
|
-
|
|
173
|
-
matches.push((start, closing_end, children, attrs));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Replace from end to start to maintain indices
|
|
178
|
-
matches.reverse();
|
|
179
|
-
for (start, end, children, attrs) in matches {
|
|
180
|
-
let attrs_str = attrs.map(|a| format!(" {}", a)).unwrap_or_default();
|
|
181
|
-
let replacement = format!("<component src=\"/{}/{}.html\"{}>{}</component>", components_dir, tag_name, attrs_str, children);
|
|
182
|
-
result.replace_range(start..end, &replacement);
|
|
183
|
-
}
|
|
141
|
+
/// Inline custom elements directly with their HTML content (ALWAYS, even with components_as_is)
|
|
142
|
+
/// Custom elements like <card> are always inlined because runtime doesn't know about /components directory
|
|
143
|
+
fn inline_custom_elements(&self, content: &str) -> String {
|
|
144
|
+
if self.cache.is_empty() {
|
|
145
|
+
return content.to_string(); // No custom elements to inline
|
|
184
146
|
}
|
|
185
147
|
|
|
186
|
-
result
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/// Recursively inline all <component> elements with their HTML content
|
|
190
|
-
fn inline_component_elements(&self, content: &str, external_cache: &HashMap<String, String>) -> String {
|
|
191
148
|
let mut result = content.to_string();
|
|
192
149
|
let mut changed = true;
|
|
193
150
|
let mut iterations = 0;
|
|
194
|
-
const MAX_ITERATIONS: usize =
|
|
151
|
+
const MAX_ITERATIONS: usize = 10; // Reasonable limit for deeply nested custom elements
|
|
195
152
|
|
|
196
153
|
while changed && iterations < MAX_ITERATIONS {
|
|
197
154
|
changed = false;
|
|
198
155
|
iterations += 1;
|
|
199
156
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
157
|
+
// Find all tags that match loaded elements
|
|
158
|
+
for (tag_name, element) in &self.cache {
|
|
159
|
+
// Match opening and closing tags with any attributes and children
|
|
160
|
+
let tag_pattern = format!(r"<{}(\s[^>]*)?>", regex::escape(tag_name));
|
|
161
|
+
let tag_re = regex::Regex::new(&tag_pattern).unwrap();
|
|
162
|
+
let closing_pattern = format!(r"</{}>", regex::escape(tag_name));
|
|
163
|
+
|
|
164
|
+
// Find all occurrences
|
|
165
|
+
let mut matches: Vec<(usize, usize, String, HashMap<String, String>)> = Vec::new();
|
|
166
|
+
|
|
167
|
+
// Find opening tags
|
|
168
|
+
for cap in tag_re.find_iter(&result) {
|
|
169
|
+
let start = cap.start();
|
|
170
|
+
let tag_with_attrs = cap.as_str();
|
|
171
|
+
|
|
172
|
+
// Extract attributes and parse as props
|
|
173
|
+
let attrs_str = if tag_with_attrs.ends_with('>') {
|
|
174
|
+
let inner = &tag_with_attrs[tag_name.len() + 1..tag_with_attrs.len() - 1];
|
|
175
|
+
inner.to_string()
|
|
176
|
+
} else {
|
|
177
|
+
String::new()
|
|
178
|
+
};
|
|
179
|
+
let props = Self::parse_props(&attrs_str);
|
|
206
180
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
181
|
+
// Find corresponding closing tag
|
|
182
|
+
if let Some(closing_pos) = result[cap.end()..].find(&closing_pattern) {
|
|
183
|
+
let closing_start = cap.end() + closing_pos;
|
|
184
|
+
let closing_end = closing_start + closing_pattern.len();
|
|
185
|
+
let slot_content = result[cap.end()..closing_start].to_string();
|
|
212
186
|
|
|
213
|
-
|
|
214
|
-
|
|
187
|
+
matches.push((start, closing_end, slot_content, props));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
215
190
|
|
|
216
|
-
|
|
217
|
-
|
|
191
|
+
if !matches.is_empty() {
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
218
194
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
195
|
+
// Replace from end to start to maintain indices
|
|
196
|
+
matches.reverse();
|
|
197
|
+
for (start, end, slot_content, props) in matches {
|
|
198
|
+
let mut replacement = element.content.clone();
|
|
222
199
|
|
|
223
|
-
// Replace from end to start
|
|
224
|
-
for (start, end, src, props, slot_content) in matches.iter().rev() {
|
|
225
|
-
// Check if it's an external URL
|
|
226
|
-
let replacement_content = if src.starts_with("http://") || src.starts_with("https://") {
|
|
227
|
-
// External component - get from cache
|
|
228
|
-
external_cache.get(src).cloned()
|
|
229
|
-
} else {
|
|
230
|
-
// Internal component - get element name from filename (e.g., "card.html" -> "card")
|
|
231
|
-
let element_name = src
|
|
232
|
-
.trim_start_matches("./")
|
|
233
|
-
.trim_start_matches('/')
|
|
234
|
-
.split('/')
|
|
235
|
-
.last()
|
|
236
|
-
.unwrap_or(src)
|
|
237
|
-
.trim_end_matches(".html");
|
|
238
|
-
|
|
239
|
-
self.cache.get(element_name).map(|e| e.content.clone())
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if let Some(mut replacement) = replacement_content {
|
|
243
200
|
// Replace props: for each prop like headline="@[pageTitle]",
|
|
244
201
|
// replace @[headline] in content with @[pageTitle]
|
|
245
202
|
for (prop_name, prop_value) in props {
|
|
246
203
|
let prop_binding = format!("@[{}]", prop_name);
|
|
247
|
-
replacement = replacement.replace(&prop_binding, prop_value);
|
|
204
|
+
replacement = replacement.replace(&prop_binding, &prop_value);
|
|
248
205
|
}
|
|
249
206
|
|
|
250
207
|
// Replace <slot> tags with content, or remove if empty/missing
|
|
251
|
-
let slot_replacement = slot_content
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
208
|
+
let slot_replacement = if slot_content.trim().is_empty() {
|
|
209
|
+
""
|
|
210
|
+
} else {
|
|
211
|
+
&slot_content
|
|
212
|
+
};
|
|
257
213
|
replacement = replacement.replace("<slot></slot>", slot_replacement);
|
|
258
214
|
replacement = replacement.replace("<slot/>", slot_replacement);
|
|
259
215
|
replacement = replacement.replace("<slot />", slot_replacement);
|
|
260
216
|
|
|
261
|
-
|
|
217
|
+
// Keep the wrapper for consistency with runtime (using generic <component> wrapper)
|
|
218
|
+
// No src attribute = wrapper won't be re-processed
|
|
219
|
+
let wrapper = format!("<component>{}</component>", replacement);
|
|
220
|
+
result.replace_range(start..end, &wrapper);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Recursively inline all <component> elements with their HTML content
|
|
229
|
+
/// Inline a single component (one pass, no looping)
|
|
230
|
+
/// Used during recursive component fetching to resolve nested components
|
|
231
|
+
pub fn inline_single_component(&self, content: &str, component_src: &str, component_content: &str) -> String {
|
|
232
|
+
let mut result = content.to_string();
|
|
233
|
+
|
|
234
|
+
// Normalize the src for matching
|
|
235
|
+
let normalized_src = if component_src.starts_with("http://") || component_src.starts_with("https://") {
|
|
236
|
+
component_src.to_string()
|
|
237
|
+
} else {
|
|
238
|
+
let without_prefix = component_src.trim_start_matches("./");
|
|
239
|
+
if without_prefix.starts_with('/') {
|
|
240
|
+
without_prefix.to_string()
|
|
241
|
+
} else {
|
|
242
|
+
format!("/{}", without_prefix)
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Match <component src="..." attrs...>children</component>
|
|
247
|
+
// OR <div class="component" src="..." attrs...>children</div>
|
|
248
|
+
let pattern = format!(r#"(?s)<(component|div)\s+([^>]*)\bsrc="{}"\s*([^>]*)>(.*?)</(component|div)>"#, regex::escape(&normalized_src));
|
|
249
|
+
let component_re = regex::Regex::new(&pattern).unwrap();
|
|
250
|
+
|
|
251
|
+
let matches: Vec<_> = component_re.captures_iter(&result).map(|cap| {
|
|
252
|
+
let full_match = cap.get(0).unwrap();
|
|
253
|
+
let tag_name = cap.get(1).unwrap().as_str();
|
|
254
|
+
let attrs_before = cap.get(2).map(|m| m.as_str()).unwrap_or("");
|
|
255
|
+
let attrs_after = cap.get(3).map(|m| m.as_str()).unwrap_or("");
|
|
256
|
+
let slot_content = cap.get(4).map(|m| m.as_str().to_string());
|
|
257
|
+
|
|
258
|
+
// For <div>, verify it has class="component"
|
|
259
|
+
if tag_name == "div" {
|
|
260
|
+
let combined_attrs = format!("{} {}", attrs_before, attrs_after);
|
|
261
|
+
if !combined_attrs.contains("class=") || !combined_attrs.contains("component") {
|
|
262
|
+
return None; // Not a component div, skip
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Combine all attributes
|
|
267
|
+
let attrs_str = format!("{} {}", attrs_before, attrs_after);
|
|
268
|
+
let props = Self::parse_props(&attrs_str);
|
|
269
|
+
|
|
270
|
+
Some((full_match.start(), full_match.end(), props, slot_content))
|
|
271
|
+
}).flatten().collect();
|
|
272
|
+
|
|
273
|
+
// Replace from end to start
|
|
274
|
+
for (start, end, props, slot_content) in matches.iter().rev() {
|
|
275
|
+
let mut replacement = component_content.to_string();
|
|
276
|
+
|
|
277
|
+
// Replace props
|
|
278
|
+
for (prop_name, prop_value) in props {
|
|
279
|
+
let prop_binding = format!("@[{}]", prop_name);
|
|
280
|
+
replacement = replacement.replace(&prop_binding, prop_value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Replace slots
|
|
284
|
+
let slot_replacement = slot_content
|
|
285
|
+
.as_ref()
|
|
286
|
+
.filter(|s| !s.trim().is_empty())
|
|
287
|
+
.map(|s| s.as_str())
|
|
288
|
+
.unwrap_or("");
|
|
289
|
+
|
|
290
|
+
replacement = replacement.replace("<slot></slot>", slot_replacement);
|
|
291
|
+
replacement = replacement.replace("<slot/>", slot_replacement);
|
|
292
|
+
replacement = replacement.replace("<slot />", slot_replacement);
|
|
293
|
+
|
|
294
|
+
// Keep the <component> wrapper
|
|
295
|
+
let wrapper = format!("<component>{}</component>", replacement);
|
|
296
|
+
result.replace_range(*start..*end, &wrapper);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
result
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn inline_component_elements(&self, content: &str, external_cache: &HashMap<String, String>) -> String {
|
|
303
|
+
let mut result = content.to_string();
|
|
304
|
+
|
|
305
|
+
// Single pass - all components in cache are already fully resolved
|
|
306
|
+
// Match <component src="..." attrs...>children</component>
|
|
307
|
+
// OR <div class="component" src="..." attrs...>children</div> (after accessibility transformation)
|
|
308
|
+
// Both signatures require src attribute (wrappers without src are already processed)
|
|
309
|
+
let component_re = regex::Regex::new(
|
|
310
|
+
r#"(?s)<(component|div)\s+([^>]*)\bsrc="([^"]+)"([^>]*)>(.*?)</(component|div)>"#
|
|
311
|
+
).unwrap();
|
|
312
|
+
|
|
313
|
+
let matches: Vec<_> = component_re.captures_iter(&result).map(|cap| {
|
|
314
|
+
let full_match = cap.get(0).unwrap();
|
|
315
|
+
let tag_name = cap.get(1).unwrap().as_str();
|
|
316
|
+
let attrs_before = cap.get(2).map(|m| m.as_str()).unwrap_or("");
|
|
317
|
+
let src = cap.get(3).unwrap().as_str();
|
|
318
|
+
let attrs_after = cap.get(4).map(|m| m.as_str()).unwrap_or("");
|
|
319
|
+
let slot_content = cap.get(5).map(|m| m.as_str().to_string());
|
|
320
|
+
|
|
321
|
+
// For <div>, verify it has class="component" (or class="component other-classes")
|
|
322
|
+
if tag_name == "div" {
|
|
323
|
+
let combined_attrs = format!("{} {}", attrs_before, attrs_after);
|
|
324
|
+
if !combined_attrs.contains("class=") || !combined_attrs.contains("component") {
|
|
325
|
+
return None; // Not a component div, skip
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Combine all attributes (excluding src which we already extracted)
|
|
330
|
+
let attrs_str = format!("{} {}", attrs_before, attrs_after);
|
|
331
|
+
let props = Self::parse_props(&attrs_str);
|
|
332
|
+
|
|
333
|
+
Some((full_match.start(), full_match.end(), src.to_string(), props, slot_content))
|
|
334
|
+
}).flatten().collect();
|
|
335
|
+
|
|
336
|
+
// Replace from end to start
|
|
337
|
+
for (start, end, src, props, slot_content) in matches.iter().rev() {
|
|
338
|
+
// Normalize path
|
|
339
|
+
let normalized_src = if src.starts_with("http://") || src.starts_with("https://") {
|
|
340
|
+
src.to_string()
|
|
341
|
+
} else {
|
|
342
|
+
let without_prefix = src.trim_start_matches("./");
|
|
343
|
+
if without_prefix.starts_with('/') {
|
|
344
|
+
without_prefix.to_string()
|
|
345
|
+
} else {
|
|
346
|
+
format!("/{}", without_prefix)
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Get fully resolved component from cache
|
|
351
|
+
if let Some(mut replacement) = external_cache.get(&normalized_src).cloned() {
|
|
352
|
+
// Replace props
|
|
353
|
+
for (prop_name, prop_value) in props {
|
|
354
|
+
let prop_binding = format!("@[{}]", prop_name);
|
|
355
|
+
replacement = replacement.replace(&prop_binding, prop_value);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Replace slots
|
|
359
|
+
let slot_replacement = slot_content
|
|
360
|
+
.as_ref()
|
|
361
|
+
.filter(|s| !s.trim().is_empty())
|
|
362
|
+
.map(|s| s.as_str())
|
|
363
|
+
.unwrap_or("");
|
|
364
|
+
|
|
365
|
+
replacement = replacement.replace("<slot></slot>", slot_replacement);
|
|
366
|
+
replacement = replacement.replace("<slot/>", slot_replacement);
|
|
367
|
+
replacement = replacement.replace("<slot />", slot_replacement);
|
|
368
|
+
|
|
369
|
+
// Keep the <component> wrapper
|
|
370
|
+
let wrapper = format!("<component>{}</component>", replacement);
|
|
371
|
+
result.replace_range(*start..*end, &wrapper);
|
|
264
372
|
}
|
|
265
373
|
}
|
|
266
374
|
|
|
@@ -286,7 +394,7 @@ impl HtmlParser {
|
|
|
286
394
|
}
|
|
287
395
|
|
|
288
396
|
/// Transform custom HTML elements to divs with classes
|
|
289
|
-
fn transform_custom_tags_to_divs(content: &str,
|
|
397
|
+
fn transform_custom_tags_to_divs(content: &str, reserved_elements: &[String]) -> String {
|
|
290
398
|
let mut result = content.to_string();
|
|
291
399
|
|
|
292
400
|
// Standard HTML5 elements (should not be transformed)
|
|
@@ -318,13 +426,13 @@ fn transform_custom_tags_to_divs(content: &str, exclude_tags: &[String]) -> Stri
|
|
|
318
426
|
let tag_pattern = regex::Regex::new(r"<([a-z][a-z0-9-]*)([^>]*)>").unwrap();
|
|
319
427
|
let _closing_pattern = regex::Regex::new(r"</([a-z][a-z0-9-]*)>").unwrap();
|
|
320
428
|
|
|
321
|
-
// Collect unique custom tags
|
|
429
|
+
// Collect unique custom tags (excluding standard HTML tags and reserved elements)
|
|
322
430
|
let mut custom_tags: Vec<String> = Vec::new();
|
|
323
431
|
for cap in tag_pattern.captures_iter(&result.clone()) {
|
|
324
432
|
if let Some(m) = cap.get(1) {
|
|
325
433
|
let tag = m.as_str().to_string();
|
|
326
434
|
if !standard_tags.contains(tag.as_str())
|
|
327
|
-
&& !
|
|
435
|
+
&& !reserved_elements.contains(&tag)
|
|
328
436
|
&& !custom_tags.contains(&tag)
|
|
329
437
|
{
|
|
330
438
|
custom_tags.push(tag);
|
|
@@ -345,5 +453,8 @@ fn transform_custom_tags_to_divs(content: &str, exclude_tags: &[String]) -> Stri
|
|
|
345
453
|
result = close_re.replace_all(&result, "</div>").to_string();
|
|
346
454
|
}
|
|
347
455
|
|
|
456
|
+
// Note: <component> tags are NOT transformed - they are a framework element, not a custom element
|
|
457
|
+
// Only user-defined custom elements are transformed to divs
|
|
458
|
+
|
|
348
459
|
result
|
|
349
460
|
}
|
package/component.js
CHANGED
|
@@ -11,33 +11,45 @@
|
|
|
11
11
|
// Or with class:
|
|
12
12
|
// <div class="component">...</div>
|
|
13
13
|
|
|
14
|
-
import { generateComponentId } from './runtime/component
|
|
14
|
+
import { generateComponentId } from './runtime/component.js';
|
|
15
15
|
import { ensureBoot } from './boot.js';
|
|
16
16
|
|
|
17
17
|
const component = (state = {}, config, targetSelector) => {
|
|
18
|
+
// Initialize component registry
|
|
19
|
+
if (!window.__vibeComponents) {
|
|
20
|
+
window.__vibeComponents = {};
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
// Find the first unprocessed INLINE component wrapper
|
|
19
24
|
// Skip <component src=""> (fetched components) - they don't need state tagging
|
|
20
25
|
// Scripts execute in DOM order, so we claim wrappers in DOM order too
|
|
21
26
|
// Supports: <component> or <div class="component">
|
|
22
27
|
const allWrappers = Array.from(document.querySelectorAll('component:not([src]), div.component:not([src])'));
|
|
23
|
-
|
|
28
|
+
|
|
29
|
+
// Try to find unprocessed component (runtime mode)
|
|
30
|
+
let wrapper = allWrappers.find(el => !el.hasAttribute('data-vibe-component-id'));
|
|
31
|
+
|
|
32
|
+
// If not found, try to find pre-compiled component that hasn't registered state yet (compiled mode)
|
|
33
|
+
if (!wrapper) {
|
|
34
|
+
wrapper = allWrappers.find(el => {
|
|
35
|
+
const existingId = el.getAttribute('data-vibe-component-id');
|
|
36
|
+
return existingId && !window.__vibeComponents[existingId];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
24
39
|
|
|
25
40
|
if (!wrapper) {
|
|
26
41
|
console.warn('[vibe] component() must be called inside <component> or <div class="component">');
|
|
27
42
|
return;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
// Get or generate component ID
|
|
46
|
+
let componentId = wrapper.getAttribute('data-vibe-component-id');
|
|
47
|
+
if (!componentId) {
|
|
48
|
+
componentId = generateComponentId();
|
|
49
|
+
wrapper.setAttribute('data-vibe-component-id', componentId);
|
|
50
|
+
}
|
|
36
51
|
|
|
37
52
|
// Register component state in shared registry
|
|
38
|
-
if (!window.__vibeComponents) {
|
|
39
|
-
window.__vibeComponents = {};
|
|
40
|
-
}
|
|
41
53
|
window.__vibeComponents[componentId] = state;
|
|
42
54
|
|
|
43
55
|
// Store config (first caller wins)
|
package/package.json
CHANGED
package/runtime/cleanup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { debugLog } from './debug.js';
|
|
2
|
-
import { PHASE_READY, FOUC_CLASS_OR_ATTR } from './constants.js';
|
|
2
|
+
import { PHASE_READY, FOUC_CLASS_OR_ATTR, DEHYDRATE_CLASS_OR_ATTR } from './constants.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Check if all Vibe processing is complete and cleanup can run
|
|
@@ -20,7 +20,7 @@ export const shouldCleanup = (rootElement) => {
|
|
|
20
20
|
// Check if this text node is inside a dehydrated element
|
|
21
21
|
let parent = node.parentElement;
|
|
22
22
|
while (parent && parent !== rootElement) {
|
|
23
|
-
if (parent.hasAttribute(
|
|
23
|
+
if (parent.hasAttribute(DEHYDRATE_CLASS_OR_ATTR) || parent.classList?.contains(DEHYDRATE_CLASS_OR_ATTR)) {
|
|
24
24
|
return NodeFilter.FILTER_REJECT; // Skip dehydrated content
|
|
25
25
|
}
|
|
26
26
|
parent = parent.parentElement;
|
|
@@ -57,14 +57,14 @@ export const cleanup = (rootElement, debug = false) => {
|
|
|
57
57
|
// Remove class from all elements in document that have it
|
|
58
58
|
const elements = document.querySelectorAll(`.${cleanName}`);
|
|
59
59
|
elements.forEach((el) => el.classList.remove(cleanName));
|
|
60
|
-
debugLog(PHASE_READY, `
|
|
60
|
+
debugLog(PHASE_READY, `Removing .${cleanName} class from ${elements.length} ${elements.length === 1 ? 'element' : 'elements'}`, debug);
|
|
61
61
|
} else {
|
|
62
62
|
// Remove attribute from all elements in document that have it
|
|
63
63
|
const elements = document.querySelectorAll(`[${cleanName}]`);
|
|
64
64
|
elements.forEach((el) => el.removeAttribute(cleanName));
|
|
65
65
|
debugLog(
|
|
66
66
|
PHASE_READY,
|
|
67
|
-
`
|
|
67
|
+
`Removing [${cleanName}] attribute from ${elements.length} ${elements.length === 1 ? 'element' : 'elements'}`,
|
|
68
68
|
debug,
|
|
69
69
|
);
|
|
70
70
|
}
|