@ape-egg/vibe-compiler 3.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.
@@ -0,0 +1,482 @@
1
+ use html5ever::parse_document;
2
+ use html5ever::tendril::TendrilSink;
3
+ use html5ever::serialize::{serialize, SerializeOpts};
4
+ use markup5ever_rcdom::{RcDom, NodeData, Handle, Node, SerializableHandle};
5
+ use markup5ever::{QualName, LocalName, Namespace};
6
+ use regex::Regex;
7
+ use serde_json::Value;
8
+ use std::collections::HashSet;
9
+ use std::path::PathBuf;
10
+ use std::cell::RefCell;
11
+ use std::sync::OnceLock;
12
+ use crate::compiler::state_extractor::StateExtractor;
13
+
14
+ /// Matches a `component(...)` state-registration call in a component's inline
15
+ /// script. The `\b` keeps it from firing on `subcomponent(`; resolvable or not,
16
+ /// its presence is what tells the tagger this wrapper owns component-local state.
17
+ fn component_call_regex() -> &'static Regex {
18
+ static RE: OnceLock<Regex> = OnceLock::new();
19
+ RE.get_or_init(|| Regex::new(r"\bcomponent\s*\(").unwrap())
20
+ }
21
+
22
+ pub struct ComponentTagger;
23
+
24
+ pub struct TaggedResult {
25
+ pub html: String,
26
+ pub state: Value,
27
+ /// One past the last id assigned — `tag_components_from(start)` callers use
28
+ /// it to verify a compile unit stayed inside its allotted id range.
29
+ pub next_id: usize,
30
+ }
31
+
32
+ impl ComponentTagger {
33
+ /// Find component wrappers, add deterministic IDs, and structure state
34
+ pub fn tag_components(html: &str, base_path: &PathBuf) -> Result<TaggedResult, String> {
35
+ Self::tag_components_from(html, base_path, 0)
36
+ }
37
+
38
+ /// Same, but ids start at `start_id`. SPA compiles tag several units that
39
+ /// share one live document (the shell owns its chrome for the app's
40
+ /// lifetime; fragments mount into it) — each unit gets a disjoint id range
41
+ /// so a fragment's `_cN` never claims a shell component's state.
42
+ pub fn tag_components_from(html: &str, base_path: &PathBuf, start_id: usize) -> Result<TaggedResult, String> {
43
+ // Extract ALL <template> content to protect it from HTML parser
44
+ // HTML parsers can strip content from <template> tags during serialization
45
+ let template_regex = regex::Regex::new(r"(?s)<template[^>]*>.*?</template>").unwrap();
46
+ let mut template_placeholders: Vec<String> = Vec::new();
47
+ let mut html_with_placeholders = html.to_string();
48
+
49
+ for (i, mat) in template_regex.find_iter(html).enumerate() {
50
+ let content = mat.as_str();
51
+ let placeholder = format!("<!--VIBE_TEMPLATE_PLACEHOLDER_{}-->", i);
52
+ template_placeholders.push(content.to_string());
53
+ html_with_placeholders = html_with_placeholders.replace(content, &placeholder);
54
+ }
55
+
56
+ // Snapshot original-cased `@[...]` bindings before html5ever lowercases
57
+ // any that live in attribute-name position (name bindings); restored after
58
+ // serialization below.
59
+ let binding_cases = crate::compiler::binding_case::capture(&html_with_placeholders);
60
+
61
+ // Parse HTML (with placeholders instead of actual vibe-dehydrate content)
62
+ let dom = parse_document(RcDom::default(), Default::default())
63
+ .from_utf8()
64
+ .read_from(&mut html_with_placeholders.as_bytes())
65
+ .map_err(|e| format!("Failed to parse HTML: {:?}", e))?;
66
+
67
+ // Find all component wrappers and extract their state
68
+ let component_counter = RefCell::new(start_id);
69
+ let component_states = RefCell::new(Vec::new());
70
+ Self::walk_tag_and_extract(&dom.document, &component_counter, &component_states, html);
71
+
72
+ // Serialize back to HTML
73
+ let mut modified_html_bytes = Vec::new();
74
+ serialize(
75
+ &mut modified_html_bytes,
76
+ &SerializableHandle::from(dom.document.clone()),
77
+ SerializeOpts::default()
78
+ ).map_err(|e| format!("Failed to serialize HTML: {:?}", e))?;
79
+
80
+ let mut modified_html = String::from_utf8(modified_html_bytes)
81
+ .map_err(|e| format!("Failed to convert HTML to UTF-8: {}", e))?;
82
+
83
+ // Restore original casing of name-binding expressions html5ever lowercased.
84
+ modified_html = crate::compiler::binding_case::restore(&modified_html, &binding_cases);
85
+
86
+ // Restore template content from placeholders
87
+ for (i, content) in template_placeholders.iter().enumerate() {
88
+ let placeholder = format!("<!--VIBE_TEMPLATE_PLACEHOLDER_{}-->", i);
89
+ modified_html = modified_html.replace(&placeholder, content);
90
+ }
91
+
92
+ // Extract all state from HTML
93
+ let all_state = StateExtractor::extract_from_html(html, base_path)?;
94
+
95
+ // Build a set of all keys that are in component states
96
+ let mut component_keys = HashSet::new();
97
+ for (_, comp_state) in component_states.borrow().iter() {
98
+ if let Value::Object(comp_map) = comp_state {
99
+ for key in comp_map.keys() {
100
+ component_keys.insert(key.clone());
101
+ }
102
+ }
103
+ }
104
+
105
+ // Filter global state to only include keys NOT in component states
106
+ let mut merged_state = if let Value::Object(all_map) = all_state {
107
+ let mut global_only = serde_json::Map::new();
108
+ for (key, value) in all_map {
109
+ // Only include in global if it's not a component state key
110
+ if !component_keys.contains(&key) {
111
+ global_only.insert(key, value);
112
+ }
113
+ }
114
+ global_only
115
+ } else {
116
+ serde_json::Map::new()
117
+ };
118
+
119
+ // Add component states under their IDs
120
+ for (component_id, comp_state) in component_states.borrow().iter() {
121
+ if let Value::Object(comp_map) = comp_state {
122
+ // Only add if component has non-empty state
123
+ if !comp_map.is_empty() {
124
+ merged_state.insert(component_id.clone(), comp_state.clone());
125
+ }
126
+ }
127
+ }
128
+
129
+ let merged_state = Value::Object(merged_state);
130
+
131
+ let next_id = *component_counter.borrow();
132
+ Ok(TaggedResult {
133
+ html: modified_html,
134
+ state: merged_state,
135
+ next_id,
136
+ })
137
+ }
138
+
139
+ /// Recursively walk DOM, add data-vibe-component-id, and extract component state
140
+ fn walk_tag_and_extract(
141
+ node: &Handle,
142
+ counter: &RefCell<usize>,
143
+ component_states: &RefCell<Vec<(String, Value)>>,
144
+ original_html: &str,
145
+ ) {
146
+ // Check if this is a component wrapper
147
+ if let NodeData::Element { name, attrs, .. } = &node.data {
148
+ let tag_name = name.local.as_ref();
149
+ let borrowed_attrs = attrs.borrow();
150
+
151
+ // Check if it's <component> (without src) or <div class="component"> (without src)
152
+ let is_component = tag_name == "component" && !Self::has_src_attr(&borrowed_attrs);
153
+ let is_div_component = tag_name == "div"
154
+ && Self::has_class_component(&borrowed_attrs)
155
+ && !Self::has_src_attr(&borrowed_attrs);
156
+
157
+ if is_component || is_div_component {
158
+ drop(borrowed_attrs); // Release borrow before checking innerHTML
159
+
160
+ // Only extract state from DIRECT <script> children — not the full subtree.
161
+ // Using the full subtree caused the Layout wrapper (a <component> created by
162
+ // inline_component_elements that contains all nested component scripts) to be
163
+ // treated as a stateful component with the merged state of all its descendants.
164
+ // That made rewrite_this_to_component_id rewrite every @[this.xxx] in the page
165
+ // to @[_c0.xxx] before child components could claim their own bindings.
166
+ let direct_scripts_html = Self::direct_scripts_html(node);
167
+
168
+ // A component must be tagged whenever it REGISTERS local state via a
169
+ // `component(...)` call — that is what makes `this.X` in its markup
170
+ // resolvable at runtime. Whether the initial state VALUES are statically
171
+ // resolvable is a separate concern (it only governs FOUC value-stamping).
172
+ // A state built from a runtime expression — e.g.
173
+ // `component({ levels: Array.from({ length: 25 }, ...) })` — resolves to
174
+ // an empty object here, but the component still needs its id + `this.`
175
+ // rewrite, or a compiled `<!-- each this.levels -->` ships a raw `this.`
176
+ // the runtime can't resolve when a restored conditional branch re-parses
177
+ // the template (no wrapper-tag ancestor to fall back to).
178
+ let registers_state = component_call_regex().is_match(&direct_scripts_html);
179
+
180
+ if registers_state {
181
+ let comp_state = match StateExtractor::extract_from_html(
182
+ &direct_scripts_html,
183
+ &PathBuf::from(".")
184
+ ) {
185
+ Ok(Value::Object(state)) => state,
186
+ _ => serde_json::Map::new(),
187
+ };
188
+
189
+ let component_id = format!("_c{}", *counter.borrow());
190
+ *counter.borrow_mut() += 1;
191
+
192
+ // Add data-vibe-component-id attribute
193
+ let mut attrs_mut = attrs.borrow_mut();
194
+ attrs_mut.push(markup5ever::Attribute {
195
+ name: QualName::new(
196
+ None,
197
+ Namespace::from(""),
198
+ LocalName::from("data-vibe-component-id"),
199
+ ),
200
+ value: component_id.clone().into(),
201
+ });
202
+ drop(attrs_mut);
203
+
204
+ // Rewrite this.property to componentId.property in the node's children
205
+ // This allows ValueStamper to properly evaluate component-scoped expressions
206
+ Self::rewrite_this_to_component_id(node, &component_id);
207
+
208
+ // Register any statically-resolvable initial state under the id. May be
209
+ // empty (runtime-built state); the runtime fills it in when the inlined
210
+ // `component(...)` call runs on mount.
211
+ component_states.borrow_mut().push((component_id, Value::Object(comp_state)));
212
+ }
213
+ }
214
+ }
215
+
216
+ // Recurse into children
217
+ for child in node.children.borrow().iter() {
218
+ Self::walk_tag_and_extract(child, counter, component_states, original_html);
219
+ }
220
+ }
221
+
222
+ /// Check if element has src attribute
223
+ fn has_src_attr(attrs: &[markup5ever::Attribute]) -> bool {
224
+ attrs.iter().any(|attr| attr.name.local.as_ref() == "src")
225
+ }
226
+
227
+ /// Check if element has class="component"
228
+ fn has_class_component(attrs: &[markup5ever::Attribute]) -> bool {
229
+ attrs.iter().any(|attr| {
230
+ attr.name.local.as_ref() == "class"
231
+ && attr.value.as_ref().split_whitespace().any(|c| c == "component")
232
+ })
233
+ }
234
+
235
+ /// Concatenate a node's DIRECT `<script>` children as `<script>…</script>`.
236
+ /// Reads raw text (not serialized) so JS operators (`>`, `&&`) aren't
237
+ /// HTML-escaped — the escaped source would fail the AST parse and drop a
238
+ /// `component(stateVar)` call to the regex fallback. Used both to extract a
239
+ /// wrapper's own state and to detect nested component boundaries.
240
+ fn direct_scripts_html(node: &Handle) -> String {
241
+ let mut out = String::new();
242
+ for child in node.children.borrow().iter() {
243
+ if let NodeData::Element { name: ref child_name, .. } = child.data {
244
+ if child_name.local.as_ref() == "script" {
245
+ let mut script_text = String::new();
246
+ for grandchild in child.children.borrow().iter() {
247
+ if let NodeData::Text { ref contents } = grandchild.data {
248
+ script_text.push_str(&contents.borrow());
249
+ }
250
+ }
251
+ out.push_str("<script>");
252
+ out.push_str(&script_text);
253
+ out.push_str("</script>");
254
+ }
255
+ }
256
+ }
257
+ out
258
+ }
259
+
260
+ /// Is this node a component wrapper that registers its OWN local state? Such
261
+ /// a node gets its own `_cN` id and `this.`→id pass when `walk_tag_and_extract`
262
+ /// reaches it, so an ancestor's rewrite must stop here — descending would let
263
+ /// the ancestor claim the nested component's bindings with the wrong id (the
264
+ /// DebugContent→ScalingModal empty-legend bug).
265
+ fn is_state_registering_component(node: &Handle) -> bool {
266
+ if let NodeData::Element { name, attrs, .. } = &node.data {
267
+ let tag_name = name.local.as_ref();
268
+ let borrowed = attrs.borrow();
269
+ let is_component = tag_name == "component" && !Self::has_src_attr(&borrowed);
270
+ let is_div_component = tag_name == "div"
271
+ && Self::has_class_component(&borrowed)
272
+ && !Self::has_src_attr(&borrowed);
273
+ if !(is_component || is_div_component) {
274
+ return false;
275
+ }
276
+ drop(borrowed);
277
+ return component_call_regex().is_match(&Self::direct_scripts_html(node));
278
+ }
279
+ false
280
+ }
281
+
282
+ /// Rewrite `this.X` to `componentId.X` throughout a component's subtree:
283
+ /// inside `@[...]` bindings (text + attributes, nested paths and multi-ref
284
+ /// expressions) and inside if/each/else-if directive comments. Resolving the
285
+ /// directives at build time is what lets a component-local `<!-- if this.x -->`
286
+ /// work on compiled (manifest-restored) pages, where the runtime can't fall
287
+ /// back to a `data-vibe-component-id` ancestor lookup.
288
+ fn rewrite_this_to_component_id(node: &Handle, component_id: &str) {
289
+ use regex::Regex;
290
+ // The whole `@[...]` binding; `this.` is resolved within it so nested
291
+ // paths (this.x.y) and expressions (this.a + this.b) are all covered.
292
+ let binding_regex = Regex::new(r"@\[[^\]]*\]").unwrap();
293
+ let this_prop = Regex::new(r"\bthis\.").unwrap();
294
+ // `$.this.X` writes that live in event-handler bodies (onclick="$.this.mode
295
+ // = 'edit'"), outside any `@[...]`. The runtime lowers these via its
296
+ // STATE_THIS_PROP_REGEX pass (runtime/component.js); the compiler must do
297
+ // the same or compiled pages ship a literal `$.this.X` that resolves to
298
+ // undefined when the native handler fires.
299
+ let state_this_prop = Regex::new(r"\$\.this\.(\w+)").unwrap();
300
+ Self::rewrite_node_recursive(node, &binding_regex, &this_prop, &state_this_prop, component_id);
301
+ }
302
+
303
+ /// Resolve `this.` to `componentId.` inside every `@[...]` binding in a string.
304
+ fn rewrite_bindings(s: &str, binding_regex: &Regex, this_prop: &Regex, component_id: &str) -> String {
305
+ let replacement = format!("{}.", component_id);
306
+ binding_regex
307
+ .replace_all(s, |caps: &regex::Captures| {
308
+ this_prop.replace_all(&caps[0], replacement.as_str()).to_string()
309
+ })
310
+ .to_string()
311
+ }
312
+
313
+ /// Recursively rewrite this.property in text, attributes, and directive comments
314
+ fn rewrite_node_recursive(node: &Handle, binding_regex: &Regex, this_prop: &Regex, state_this_prop: &Regex, component_id: &str) {
315
+ // Rewrite text content bindings
316
+ if let NodeData::Text { ref contents } = node.data {
317
+ let mut text = contents.borrow_mut();
318
+ let new_text = Self::rewrite_bindings(&text, binding_regex, this_prop, component_id);
319
+ *text = new_text.into();
320
+ }
321
+
322
+ // Rewrite attribute bindings (`@[...]`) and event-handler `$.this.X` writes.
323
+ if let NodeData::Element { ref attrs, .. } = node.data {
324
+ let mut attrs_mut = attrs.borrow_mut();
325
+ for attr in attrs_mut.iter_mut() {
326
+ let mut new_value = Self::rewrite_bindings(&attr.value, binding_regex, this_prop, component_id);
327
+ if new_value.contains("$.this.") {
328
+ new_value = state_this_prop
329
+ .replace_all(&new_value, |c: &regex::Captures| format!("$.{}.{}", component_id, &c[1]))
330
+ .to_string();
331
+ }
332
+ attr.value = new_value.into();
333
+ }
334
+ }
335
+
336
+ // Directive comments are immutable in the RcDom, so swap any if/each/else-if
337
+ // comment carrying a `this.` expression for a freshly-built comment node.
338
+ {
339
+ let mut children = node.children.borrow_mut();
340
+ for child in children.iter_mut() {
341
+ if let NodeData::Comment { ref contents } = child.data {
342
+ let text = contents.to_string();
343
+ let trimmed = text.trim_start();
344
+ let is_directive = trimmed.starts_with("if ")
345
+ || trimmed.starts_with("each ")
346
+ || trimmed.starts_with("else if ");
347
+ if is_directive && text.contains("this.") {
348
+ let new_text = this_prop
349
+ .replace_all(&text, format!("{}.", component_id).as_str())
350
+ .to_string();
351
+ *child = Node::new(NodeData::Comment { contents: new_text.into() });
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ // Recurse into children — but STOP at a nested component that registers
358
+ // its own state. It owns the `this.` inside it and gets its own id +
359
+ // rewrite when walk_tag_and_extract reaches it; descending here would
360
+ // claim its bindings with this ancestor's id (the empty-legend bug).
361
+ for child in node.children.borrow().iter() {
362
+ if Self::is_state_registering_component(child) {
363
+ continue;
364
+ }
365
+ Self::rewrite_node_recursive(child, binding_regex, this_prop, state_this_prop, component_id);
366
+ }
367
+ }
368
+ }
369
+
370
+ #[cfg(test)]
371
+ mod tests {
372
+ use super::*;
373
+
374
+
375
+
376
+
377
+
378
+ #[test]
379
+ fn tag_single_component() {
380
+ let html = r#"<!DOCTYPE html><html><body><component><script>component({ count: 0 })</script><div>@[this.count]</div></component></body></html>"#;
381
+
382
+ let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
383
+
384
+ // Should have data-vibe-component-id in output
385
+ assert!(result.html.contains("data-vibe-component-id=\"_c0\""));
386
+ }
387
+
388
+ #[test]
389
+ fn rewrites_this_in_directive_comments_and_nested_bindings() {
390
+ // Conditionals/iterations keep `this.` in their directive comments and
391
+ // bindings can be nested (this.x.y). Compiled (manifest-restored) pages
392
+ // can't resolve `this.` via a runtime wrapper-tag lookup, so the compiler
393
+ // must resolve it to the component id at build time.
394
+ let html = r#"<!DOCTYPE html><html><body><component><script>const s = { x: null }; component(s);</script><page-content><!-- if this.x --><span>@[this.x.name]</span><!-- /if --><!-- each this.items as it --><b>@[it]</b><!-- /each --></page-content></component></body></html>"#;
395
+
396
+ let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
397
+
398
+ assert!(result.html.contains("if _c0.x"), "if comment not rewritten: {}", result.html);
399
+ assert!(result.html.contains("@[_c0.x.name]"), "nested binding not rewritten: {}", result.html);
400
+ assert!(result.html.contains("each _c0.items as it"), "each comment not rewritten: {}", result.html);
401
+ // A non-this global expression must be left alone.
402
+ assert!(!result.html.contains("_c0.items as _c0"), "over-rewrote loop alias: {}", result.html);
403
+ }
404
+
405
+ #[test]
406
+ fn tag_component_called_with_variable() {
407
+ // BrawlerDetailContent shape: state held in a `const`, passed to
408
+ // component(state) as a bare identifier, with a top-level
409
+ // `<!-- if this.X -->`. The wrapper must still be tagged so the
410
+ // conditional can resolve `this.` at runtime.
411
+ let html = r#"<!DOCTYPE html><html><body><component><script>const state = { currentCharacter: null }; component(state);</script><page-content><!-- if this.currentCharacter --><span>@[this.currentCharacter]</span><!-- /if --></page-content></component></body></html>"#;
412
+
413
+ let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
414
+
415
+ assert!(
416
+ result.html.contains("data-vibe-component-id=\"_c0\""),
417
+ "wrapper not tagged; html: {}",
418
+ result.html
419
+ );
420
+ }
421
+
422
+ #[test]
423
+ fn tags_component_with_runtime_built_state() {
424
+ // AccountProgression shape: the ONLY state key is built from a runtime
425
+ // expression (`Array.from`), so static extraction resolves to an empty
426
+ // object. The wrapper must still be tagged and its `<!-- each this.X -->`
427
+ // rewritten — otherwise a restored conditional branch re-parses a raw
428
+ // `this.levels` the runtime can't resolve, and the loop renders nothing.
429
+ let html = r#"<!DOCTYPE html><html><body><component><script>component({ levels: Array.from({ length: 25 }, (_, i) => i + 1) });</script><slides><!-- each this.levels as lvl --><slide>@[lvl]</slide><!-- /each --></slides></component></body></html>"#;
430
+
431
+ let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
432
+
433
+ assert!(
434
+ result.html.contains("data-vibe-component-id=\"_c0\""),
435
+ "runtime-built-state component not tagged: {}",
436
+ result.html
437
+ );
438
+ assert!(
439
+ result.html.contains("each _c0.levels as lvl"),
440
+ "each comment not rewritten: {}",
441
+ result.html
442
+ );
443
+ }
444
+
445
+ #[test]
446
+ fn nested_component_this_resolves_to_own_id() {
447
+ // DebugContent → ScalingModal shape: a stateful component nested inside
448
+ // another stateful component, each owning its own `this.`. The outer's
449
+ // this.→id rewrite must STOP at the inner component boundary — otherwise
450
+ // it claims the inner's bindings/directives with the OUTER id before the
451
+ // inner is reached, and the inner's live state (registered under its own
452
+ // runtime id) never reaches the markup → empty each-loops, dead bindings.
453
+ let html = r#"<!DOCTYPE html><html><body><component><script>component({ outer: 1 })</script><outer-mark>@[this.outer]</outer-mark><component><script>component({ inner: 2, items: [] })</script><modal-root open="@[this.open]"><!-- each this.items as it --><inner-mark>@[this.inner]</inner-mark><!-- /each --></modal-root></component></component></body></html>"#;
454
+
455
+ let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
456
+
457
+ // Outer is _c0 (visited first), inner is _c1 (reached on recursion).
458
+ assert!(result.html.contains("@[_c0.outer]"), "outer binding wrong: {}", result.html);
459
+ assert!(result.html.contains("@[_c1.inner]"), "inner binding not rewritten to own id: {}", result.html);
460
+ assert!(result.html.contains("each _c1.items as it"), "inner each not rewritten to own id: {}", result.html);
461
+ assert!(result.html.contains("open=\"@[_c1.open]\""), "inner attr binding not rewritten to own id: {}", result.html);
462
+ // The outer must NOT have claimed any of the inner's bindings/directives.
463
+ assert!(!result.html.contains("@[_c0.inner]"), "outer claimed inner text binding: {}", result.html);
464
+ assert!(!result.html.contains("each _c0.items"), "outer claimed inner each: {}", result.html);
465
+ assert!(!result.html.contains("@[_c0.open]"), "outer claimed inner attr binding: {}", result.html);
466
+ }
467
+
468
+ #[test]
469
+ fn tag_multiple_components() {
470
+ // Each wrapper registers component-local state, so all three are tagged
471
+ // with sequential ids. (A bare `<component></component>` with no state
472
+ // call is intentionally NOT tagged — it owns no `this.` to resolve.)
473
+ let html = r#"<!DOCTYPE html><html><body><component><script>component({ a: 1 })</script></component><component><script>component({ b: 2 })</script></component><component><script>component({ c: 3 })</script></component></body></html>"#;
474
+
475
+ let result = ComponentTagger::tag_components(html, &PathBuf::from(".")).unwrap();
476
+
477
+ // Should have _c0, _c1, _c2
478
+ assert!(result.html.contains("data-vibe-component-id=\"_c0\""));
479
+ assert!(result.html.contains("data-vibe-component-id=\"_c1\""));
480
+ assert!(result.html.contains("data-vibe-component-id=\"_c2\""));
481
+ }
482
+ }