@ape-egg/vibe 2.3.0 → 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.
Files changed (57) hide show
  1. package/README.md +14 -4
  2. package/boot.js +4 -4
  3. package/component.js +27 -29
  4. package/hot-module-refresh.js +4 -4
  5. package/index.js +10 -15
  6. package/llms.txt +8 -6
  7. package/package.json +19 -14
  8. package/runtime/affected.js +159 -36
  9. package/runtime/cleanup.js +45 -1
  10. package/runtime/component.js +312 -99
  11. package/runtime/conditionals.js +111 -14
  12. package/runtime/debug.js +24 -0
  13. package/runtime/dispatch.js +172 -0
  14. package/runtime/hydrate.js +251 -111
  15. package/runtime/index.js +180 -71
  16. package/runtime/iterate.js +125 -50
  17. package/runtime/iteration-utils.js +59 -8
  18. package/runtime/manifest.js +77 -2
  19. package/runtime/parse.js +69 -5
  20. package/runtime/pre-compiled-iterations.js +19 -6
  21. package/runtime/pre-compiled-manifest.js +13 -4
  22. package/runtime/staging.js +153 -0
  23. package/runtime/state.js +31 -0
  24. package/runtime/tracking.js +173 -0
  25. package/runtime/utils.js +155 -78
  26. package/spa.js +77 -14
  27. package/vibe.css +8 -4
  28. package/CHANGELOG.md +0 -1196
  29. package/ROADMAP.md +0 -397
  30. package/compiler/bin/vibe-compile.js +0 -121
  31. package/compiler/native/.gitkeep +0 -0
  32. package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
  33. package/compiler/native/vibe-compiler-linux-x64 +0 -0
  34. package/compiler/src/Cargo.lock +0 -2023
  35. package/compiler/src/Cargo.toml +0 -38
  36. package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
  37. package/compiler/src/compiler/binding_case.rs +0 -88
  38. package/compiler/src/compiler/compile.rs +0 -2880
  39. package/compiler/src/compiler/component_tagger.rs +0 -469
  40. package/compiler/src/compiler/iteration_optimizer.rs +0 -455
  41. package/compiler/src/compiler/js_analyzer.rs +0 -715
  42. package/compiler/src/compiler/manifest_builder.rs +0 -693
  43. package/compiler/src/compiler/mod.rs +0 -16
  44. package/compiler/src/compiler/name_binding_protect.rs +0 -207
  45. package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
  46. package/compiler/src/compiler/spa.rs +0 -477
  47. package/compiler/src/compiler/state_extractor.rs +0 -263
  48. package/compiler/src/compiler/value_stamper.rs +0 -921
  49. package/compiler/src/compiler/watcher.rs +0 -1278
  50. package/compiler/src/config.rs +0 -279
  51. package/compiler/src/main.rs +0 -358
  52. package/compiler/src/parser/element.rs +0 -96
  53. package/compiler/src/parser/html.rs +0 -1004
  54. package/compiler/src/parser/mod.rs +0 -8
  55. package/runtime/pre-compiled-manifest.test.mjs +0 -58
  56. package/runtime/scope.js +0 -50
  57. package/test-results/.last-run.json +0 -4
@@ -1,693 +0,0 @@
1
- use html5ever::parse_document;
2
- use html5ever::tendril::TendrilSink;
3
- use markup5ever_rcdom::{RcDom, NodeData, Handle};
4
- use regex::Regex;
5
- use serde::{Serialize, Serializer};
6
- use serde_json::Value;
7
- use std::cell::RefCell;
8
- use std::collections::{HashMap, BTreeMap};
9
- use crate::compiler::iteration_optimizer::build_iteration_optimizations;
10
-
11
- pub struct ManifestBuilder {
12
- binding_regex: Regex,
13
- name_binding_fix_regex: Regex,
14
- /// Original-cased `@[...]` bindings captured from the pre-parse HTML, keyed by
15
- /// their lowercased form — used to undo html5ever's attribute-name lowercasing
16
- /// of name bindings on every string this builder serializes back out.
17
- binding_cases: RefCell<HashMap<String, String>>,
18
- }
19
-
20
- impl ManifestBuilder {
21
- pub fn new() -> Self {
22
- Self {
23
- binding_regex: Regex::new(r"@\[((?:[^\[\]]|\[[^\]]*\])+)\]").unwrap(),
24
- name_binding_fix_regex: Regex::new(r#"(@\[[^\]]+\])="+"#).unwrap(),
25
- binding_cases: RefCell::new(HashMap::new()),
26
- }
27
- }
28
-
29
- pub fn build_from_html(&self, html: &str, _state: &Value, iterations_as_is: bool) -> Result<ManifestNode, String> {
30
- // Snapshot original-cased bindings before html5ever lowercases name
31
- // bindings (attr-name position). Restored on serialized templates and on
32
- // the captured name_bindings array.
33
- *self.binding_cases.borrow_mut() = crate::compiler::binding_case::capture(html);
34
-
35
- // Parse HTML
36
- let dom = parse_document(RcDom::default(), Default::default())
37
- .from_utf8()
38
- .read_from(&mut html.as_bytes())
39
- .map_err(|e| format!("Failed to parse HTML: {:?}", e))?;
40
-
41
- // Walk tree starting from document root
42
- let mut root_node = self.walk_node(&dom.document, &mut 0);
43
-
44
- // Inject compiled batch functions directly into iteration nodes (unless disabled)
45
- if !iterations_as_is {
46
- let compiled_functions = build_iteration_optimizations(html);
47
- if let Some(optimizations) = compiled_functions {
48
- self.inject_compiled_functions(&mut root_node, &optimizations.iterations);
49
- }
50
- }
51
-
52
- Ok(root_node)
53
- }
54
-
55
- /// Recursively inject compiled batch functions into iteration nodes
56
- fn inject_compiled_functions(&self, node: &mut ManifestNode, compiled_map: &HashMap<String, crate::compiler::iteration_optimizer::CompiledIteration>) {
57
- // Check if this is an iteration node with a template
58
- if node.node_type.as_deref() == Some("iteration") {
59
- if let Some(ref mut compiled_data) = node.compiled {
60
- if let Some(ref restoration) = compiled_data.restoration {
61
- if let Some(ref template) = restoration.template {
62
- // Generate hash for this template (same logic as optimizer)
63
- let template_hash = self.generate_template_hash(template);
64
-
65
- // Look up compiled function
66
- if let Some(compiled) = compiled_map.get(&template_hash) {
67
- compiled_data.iterations = Some(IterationData {
68
- batch_fn: Some(compiled.batch_fn.clone()),
69
- item_alias: Some(compiled.item_alias.clone()),
70
- index_alias: Some(compiled.index_alias.clone()),
71
- });
72
- }
73
- }
74
- }
75
- }
76
- }
77
-
78
- // Recursively process children
79
- for child in node.children.values_mut() {
80
- self.inject_compiled_functions(child, compiled_map);
81
- }
82
- }
83
-
84
- /// Generate stable hash for template (matches iteration_optimizer logic)
85
- fn generate_template_hash(&self, template: &str) -> String {
86
- use std::collections::hash_map::DefaultHasher;
87
- use std::hash::{Hash, Hasher};
88
-
89
- let mut hasher = DefaultHasher::new();
90
- template.trim().hash(&mut hasher);
91
- let hash = hasher.finish();
92
- format!("iter_{:x}", hash)
93
- }
94
-
95
- fn walk_node(&self, node: &Handle, _counter: &mut usize) -> ManifestNode {
96
- self.walk_node_impl(node, &node.children.borrow())
97
- }
98
-
99
- fn walk_node_impl(&self, node: &Handle, siblings: &[Handle]) -> ManifestNode {
100
- let mut manifest_node = ManifestNode {
101
- element: None,
102
- parsed: vec![],
103
- children: BTreeMap::new(),
104
- compiled: None,
105
- node_type: None,
106
- attributes: None,
107
- name_bindings: None,
108
- meta: None,
109
- runtime: None,
110
- };
111
-
112
- match &node.data {
113
- NodeData::Text { contents } => {
114
- let text = contents.borrow().to_string();
115
-
116
- // Check for @[...] bindings
117
- if self.binding_regex.is_match(&text) {
118
- let parsed = self.split_by_bindings(&text);
119
- manifest_node.compiled = Some(CompiledData {
120
- restoration: Some(RestorationData {
121
- parsed: Some(parsed),
122
- ..Default::default()
123
- }),
124
- ..Default::default()
125
- });
126
- }
127
- }
128
-
129
- NodeData::Element { name, attrs, .. } => {
130
- let _tag_name = name.local.to_string();
131
-
132
- // Check attribute bindings (both value and name bindings)
133
- let mut attr_bindings = BTreeMap::new();
134
- let mut name_bindings: Vec<Option<String>> = Vec::new();
135
- let borrowed_attrs = attrs.borrow();
136
-
137
- for (idx, attr) in borrowed_attrs.iter().enumerate() {
138
- let attr_name = attr.name.local.to_string();
139
- let attr_value = attr.value.to_string();
140
-
141
- // A relocated name-binding: its expression(s) live verbatim in the
142
- // VALUE of data-vibe-namebind because whitespace barred them from
143
- // attribute-name position. Treat the value as the name-binding — no
144
- // case restoration needed (value attrs keep their case).
145
- if attr_name == crate::compiler::name_binding_protect::NAME_BIND_ATTR {
146
- while name_bindings.len() <= idx {
147
- name_bindings.push(None);
148
- }
149
- name_bindings[idx] = Some(attr_value);
150
- }
151
- // Check for name bindings (binding in attribute NAME)
152
- else if self.binding_regex.is_match(&attr_name) {
153
- // Extend vec to include this index
154
- while name_bindings.len() <= idx {
155
- name_bindings.push(None);
156
- }
157
- // html5ever lowercased the attr name; restore original casing.
158
- let restored = crate::compiler::binding_case::restore(
159
- &attr_name,
160
- &self.binding_cases.borrow(),
161
- );
162
- name_bindings[idx] = Some(restored);
163
- }
164
- // Check for attribute value bindings
165
- else if self.binding_regex.is_match(&attr_value) {
166
- attr_bindings.insert(attr_name, attr_value);
167
- }
168
- }
169
-
170
- // Store if we have any bindings
171
- let has_name_bindings = name_bindings.iter().any(|b| b.is_some());
172
-
173
- // Set runtime-compatible fields at node level
174
- if !attr_bindings.is_empty() {
175
- manifest_node.attributes = Some(attr_bindings.clone());
176
- }
177
- if has_name_bindings {
178
- manifest_node.name_bindings = Some(name_bindings.clone());
179
- }
180
-
181
- // Also store in compiled.restoration for restoration purposes
182
- if !attr_bindings.is_empty() || has_name_bindings {
183
- manifest_node.compiled = Some(CompiledData {
184
- restoration: Some(RestorationData {
185
- attributes: if !attr_bindings.is_empty() { Some(attr_bindings) } else { None },
186
- name_bindings: if has_name_bindings { Some(name_bindings) } else { None },
187
- ..Default::default()
188
- }),
189
- ..Default::default()
190
- });
191
- }
192
- }
193
-
194
- NodeData::Comment { contents } => {
195
- let comment = contents.to_string();
196
- let trimmed = comment.trim();
197
-
198
- // Check for iteration: <!-- each items as item -->
199
- if trimmed.starts_with("each ") {
200
- manifest_node.node_type = Some("iteration".to_string());
201
-
202
- // Extract expression: "each items as item" -> "items as item"
203
- let expression = trimmed.strip_prefix("each ").unwrap_or("").to_string();
204
-
205
- // Parse expression to extract parts: "items as item, index" or "items as item".
206
- // The optional (key) expression may sit before or after the index — strip it
207
- // first so the item/index split isn't polluted by it. The key itself isn't
208
- // stored here; the runtime re-derives it from the preserved comment.
209
- let parts: Vec<&str> = expression.split(" as ").collect();
210
- let array_path = parts.get(0).unwrap_or(&"").trim().to_string();
211
- let alias_part = parts.get(1).unwrap_or(&"").trim();
212
- let alias_without_key = match (alias_part.find('('), alias_part.rfind(')')) {
213
- (Some(open), Some(close)) if close > open => {
214
- format!("{}{}", &alias_part[..open], &alias_part[close + 1..])
215
- }
216
- _ => alias_part.to_string(),
217
- };
218
- let alias_without_key = alias_without_key.trim();
219
- let (item_alias, index_alias) = if let Some(comma_pos) = alias_without_key.find(',') {
220
- let item = alias_without_key[..comma_pos].trim().to_string();
221
- let index = alias_without_key[comma_pos + 1..].trim().to_string();
222
- (item, index)
223
- } else {
224
- (alias_without_key.to_string(), "index".to_string())
225
- };
226
-
227
- // Find node index in siblings
228
- let node_idx = siblings.iter().position(|n| std::ptr::eq(n as *const _, node as *const _));
229
-
230
- // Extract template if we can find this node in siblings
231
- let template = if let Some(idx) = node_idx {
232
- self.extract_template_from_siblings(siblings, idx, "each ", "/each")
233
- } else {
234
- String::new()
235
- };
236
-
237
- // Create minimal meta structure - runtime will populate template after parsing restored DOM
238
- manifest_node.meta = Some(serde_json::json!({
239
- "arrayPath": array_path,
240
- "itemAlias": item_alias,
241
- "indexAlias": index_alias
242
- }));
243
-
244
- // Runtime state
245
- manifest_node.runtime = Some(serde_json::json!({
246
- "instances": [],
247
- "templateRemoved": false
248
- }));
249
-
250
- // Compiled restoration data
251
- manifest_node.compiled = Some(CompiledData {
252
- restoration: Some(RestorationData {
253
- template: Some(template),
254
- expression: Some(expression),
255
- ..Default::default()
256
- }),
257
- ..Default::default()
258
- });
259
- }
260
-
261
- // Check for conditionals: <!-- if condition -->
262
- if trimmed.starts_with("if ") {
263
- manifest_node.node_type = Some("conditional".to_string());
264
-
265
- let expression = trimmed.strip_prefix("if ").unwrap_or("").to_string();
266
- let node_idx = siblings.iter().position(|n| std::ptr::eq(n as *const _, node as *const _));
267
-
268
- let template = if let Some(idx) = node_idx {
269
- self.extract_template_from_siblings(siblings, idx, "if ", "/if")
270
- } else {
271
- String::new()
272
- };
273
-
274
- // Create minimal meta structure - runtime will populate branches after parsing restored DOM
275
- manifest_node.meta = Some(serde_json::json!({
276
- "expression": expression
277
- }));
278
-
279
- // Runtime state
280
- manifest_node.runtime = Some(serde_json::json!({
281
- "activeBranch": null,
282
- "activeInstance": null,
283
- "templateRemoved": false
284
- }));
285
-
286
- // Compiled restoration data
287
- manifest_node.compiled = Some(CompiledData {
288
- restoration: Some(RestorationData {
289
- template: Some(template),
290
- ..Default::default()
291
- }),
292
- ..Default::default()
293
- });
294
- }
295
- }
296
-
297
- _ => {}
298
- }
299
-
300
- // Recursively process children using local indices (to match runtime key generation)
301
- let children = node.children.borrow();
302
- let mut skip_indices = std::collections::HashSet::new();
303
-
304
- // First pass: identify indices to skip (iteration/conditional template content)
305
- for (i, child) in children.iter().enumerate() {
306
- if let NodeData::Comment { contents } = &child.data {
307
- let trimmed = contents.trim();
308
-
309
- // Find iteration blocks and mark template indices to skip
310
- if trimmed.starts_with("each ") {
311
- if let Some(end_idx) = self.find_matching_end(&children, i, "each ", "/each") {
312
- // Skip all nodes between start and end comments (including end comment)
313
- for j in (i + 1)..=end_idx {
314
- skip_indices.insert(j);
315
- }
316
- }
317
- }
318
-
319
- // Find conditional blocks and mark template indices to skip
320
- if trimmed.starts_with("if ") {
321
- if let Some(end_idx) = self.find_matching_end(&children, i, "if ", "/if") {
322
- for j in (i + 1)..=end_idx {
323
- skip_indices.insert(j);
324
- }
325
- }
326
- }
327
- }
328
- }
329
-
330
- // Second pass: process children that aren't skipped
331
- for (local_index, child) in children.iter().enumerate() {
332
- if skip_indices.contains(&local_index) {
333
- continue;
334
- }
335
-
336
- let child_key = self.generate_child_key(child, &local_index);
337
- manifest_node.children.insert(
338
- child_key.clone(),
339
- self.walk_node_impl(child, &children)
340
- );
341
- }
342
-
343
- manifest_node
344
- }
345
-
346
- /// Split text by @[...] bindings: "Hello @[name]!" -> ["Hello ", "@[name]", "!"]
347
- fn split_by_bindings(&self, text: &str) -> Vec<String> {
348
- let mut result = vec![];
349
- let mut last_end = 0;
350
-
351
- for mat in self.binding_regex.find_iter(text) {
352
- // Add text before match
353
- if mat.start() > last_end {
354
- result.push(text[last_end..mat.start()].to_string());
355
- }
356
- // Add binding
357
- result.push(mat.as_str().to_string());
358
- last_end = mat.end();
359
- }
360
-
361
- // Add remaining text
362
- if last_end < text.len() {
363
- result.push(text[last_end..].to_string());
364
- }
365
-
366
- result
367
- }
368
-
369
- /// Generate child key: "text_0", "div_1", etc.
370
- fn generate_child_key(&self, node: &Handle, index: &usize) -> String {
371
- match &node.data {
372
- NodeData::Text { .. } => format!("text_{}", index),
373
- NodeData::Element { name, .. } => {
374
- format!("{}_{}", name.local.to_lowercase(), index)
375
- }
376
- NodeData::Comment { contents } => {
377
- let trimmed = contents.trim();
378
- if trimmed.starts_with("each ") {
379
- format!("iteration_{}", index)
380
- } else if trimmed.starts_with("if ") {
381
- format!("conditional_{}", index)
382
- } else {
383
- format!("comment_{}", index)
384
- }
385
- }
386
- _ => format!("node_{}", index),
387
- }
388
- }
389
-
390
- /// Find the index of matching end comment
391
- fn find_matching_end(&self, siblings: &[Handle], start_idx: usize, start_marker: &str, end_marker: &str) -> Option<usize> {
392
- let mut depth = 0;
393
-
394
- for (idx, sibling) in siblings.iter().enumerate().skip(start_idx + 1) {
395
- if let NodeData::Comment { contents } = &sibling.data {
396
- let trimmed = contents.trim();
397
- if trimmed.starts_with(start_marker) {
398
- depth += 1;
399
- } else if trimmed == end_marker {
400
- if depth == 0 {
401
- return Some(idx);
402
- } else {
403
- depth -= 1;
404
- }
405
- }
406
- }
407
- }
408
-
409
- None
410
- }
411
-
412
- /// Extract template between start and end comments (generic for iterations and conditionals)
413
- fn extract_template_from_siblings(&self, siblings: &[Handle], start_idx: usize, start_marker: &str, end_marker: &str) -> String {
414
- // Find matching end comment
415
- let mut depth = 0;
416
- let mut end_idx = None;
417
-
418
- for (idx, sibling) in siblings.iter().enumerate().skip(start_idx + 1) {
419
- if let NodeData::Comment { contents } = &sibling.data {
420
- let trimmed = contents.trim();
421
- if trimmed.starts_with(start_marker) {
422
- depth += 1;
423
- } else if trimmed == end_marker {
424
- if depth == 0 {
425
- end_idx = Some(idx);
426
- break;
427
- } else {
428
- depth -= 1;
429
- }
430
- }
431
- }
432
- }
433
-
434
- // Extract template HTML between start and end — VERBATIM, including
435
- // whitespace-only text nodes. The manifest's child keys are childNodes
436
- // indices computed on the pre-stamp DOM; restoration re-inserts this
437
- // template, and only an exact node-count round-trip keeps those
438
- // indices valid for every sibling that follows the restored region.
439
- if let Some(end) = end_idx {
440
- let template_nodes: Vec<_> = siblings.iter()
441
- .skip(start_idx + 1)
442
- .take(end - start_idx - 1)
443
- .collect();
444
-
445
- return self.serialize_nodes(&template_nodes);
446
- }
447
-
448
- String::new()
449
- }
450
-
451
- /// Serialize nodes to HTML string
452
- fn serialize_nodes(&self, nodes: &[&Handle]) -> String {
453
- use html5ever::serialize::{serialize, SerializeOpts, TraversalScope};
454
- use markup5ever_rcdom::SerializableHandle;
455
-
456
- let mut html = String::new();
457
-
458
- for node in nodes.iter() {
459
- let mut bytes = Vec::new();
460
-
461
- // Serialize with IncludeNode to include the element tags
462
- let opts = SerializeOpts {
463
- traversal_scope: TraversalScope::IncludeNode,
464
- ..Default::default()
465
- };
466
-
467
- let serializable = SerializableHandle::from((*node).clone());
468
- serialize(&mut bytes, &serializable, opts).ok();
469
-
470
- if let Ok(node_html) = String::from_utf8(bytes) {
471
- html.push_str(&node_html);
472
- }
473
- }
474
-
475
- // Fix name bindings: Remove ="" added by html5ever serializer
476
- // Name bindings like <icon @[section.icon]> get serialized as <icon @[section.icon]="">
477
- // which is invalid HTML that browsers reject
478
- html = self.name_binding_fix_regex.replace_all(&html, "$1").to_string();
479
-
480
- // Restore original casing of any name-binding expressions html5ever
481
- // lowercased while parsing (attr-name position).
482
- html = crate::compiler::binding_case::restore(&html, &self.binding_cases.borrow());
483
-
484
- html
485
- }
486
- }
487
-
488
- #[derive(Debug)]
489
- pub struct ManifestNode {
490
- element: Option<()>, // Always null in static manifest
491
- parsed: Vec<String>,
492
- children: BTreeMap<String, ManifestNode>,
493
-
494
- #[allow(dead_code)]
495
- compiled: Option<CompiledData>,
496
-
497
- #[allow(dead_code)]
498
- node_type: Option<String>,
499
-
500
- // Runtime-compatible fields
501
- #[allow(dead_code)]
502
- attributes: Option<BTreeMap<String, String>>,
503
-
504
- #[allow(dead_code)]
505
- name_bindings: Option<Vec<Option<String>>>,
506
-
507
- #[allow(dead_code)]
508
- meta: Option<Value>, // For conditionals and iterations
509
-
510
- #[allow(dead_code)]
511
- runtime: Option<Value>, // Runtime state
512
- }
513
-
514
- // Custom Serialize implementation to control output format
515
- impl Serialize for ManifestNode {
516
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
517
- where
518
- S: Serializer,
519
- {
520
- use serde::ser::SerializeMap;
521
-
522
- let mut map = serializer.serialize_map(None)?;
523
-
524
- // Always include element (null)
525
- map.serialize_entry("element", &self.element)?;
526
-
527
- // Always include parsed (empty array if no data)
528
- map.serialize_entry("parsed", &self.parsed)?;
529
-
530
- // Always include children
531
- map.serialize_entry("children", &self.children)?;
532
-
533
- // Optional: attributes (runtime field)
534
- if let Some(ref attributes) = self.attributes {
535
- map.serialize_entry("attributes", attributes)?;
536
- }
537
-
538
- // Optional: nameBindings (runtime field)
539
- if let Some(ref name_bindings) = self.name_bindings {
540
- map.serialize_entry("nameBindings", name_bindings)?;
541
- }
542
-
543
- // Optional: meta (runtime field for conditionals/iterations)
544
- if let Some(ref meta) = self.meta {
545
- map.serialize_entry("meta", meta)?;
546
- }
547
-
548
- // Optional: runtime (runtime state)
549
- if let Some(ref runtime) = self.runtime {
550
- map.serialize_entry("runtime", runtime)?;
551
- }
552
-
553
- // Optional: compiled (contains restoration and iterations data)
554
- if let Some(ref compiled) = self.compiled {
555
- map.serialize_entry("compiled", compiled)?;
556
- }
557
-
558
- // Optional: type
559
- if let Some(ref node_type) = self.node_type {
560
- map.serialize_entry("type", node_type)?;
561
- }
562
-
563
- map.end()
564
- }
565
- }
566
-
567
- #[derive(Debug, Serialize, Default)]
568
- pub struct RestorationData {
569
- #[serde(skip_serializing_if = "Option::is_none")]
570
- pub parsed: Option<Vec<String>>,
571
-
572
- #[serde(skip_serializing_if = "Option::is_none")]
573
- pub attributes: Option<BTreeMap<String, String>>,
574
-
575
- #[serde(skip_serializing_if = "Option::is_none")]
576
- pub template: Option<String>,
577
-
578
- #[serde(skip_serializing_if = "Option::is_none")]
579
- pub expression: Option<String>,
580
-
581
- #[serde(skip_serializing_if = "Option::is_none", rename = "nameBindings")]
582
- pub name_bindings: Option<Vec<Option<String>>>,
583
- }
584
-
585
- #[derive(Debug, Serialize, Default)]
586
- pub struct IterationData {
587
- #[serde(skip_serializing_if = "Option::is_none", rename = "batchFn")]
588
- pub batch_fn: Option<String>,
589
-
590
- #[serde(skip_serializing_if = "Option::is_none", rename = "itemAlias")]
591
- pub item_alias: Option<String>,
592
-
593
- #[serde(skip_serializing_if = "Option::is_none", rename = "indexAlias")]
594
- pub index_alias: Option<String>,
595
- }
596
-
597
- #[derive(Debug, Serialize, Default)]
598
- pub struct CompiledData {
599
- #[serde(skip_serializing_if = "Option::is_none")]
600
- pub restoration: Option<RestorationData>,
601
-
602
- #[serde(skip_serializing_if = "Option::is_none")]
603
- pub iterations: Option<IterationData>,
604
- }
605
-
606
- #[cfg(test)]
607
- mod tests {
608
- use super::*;
609
- use serde_json::json;
610
-
611
- #[test]
612
- fn build_simple_text_binding() {
613
- let html = r#"<div>Hello @[name]</div>"#;
614
- let state = json!({ "name": "World" });
615
- let builder = ManifestBuilder::new();
616
- let manifest = builder.build_from_html(html, &state, false).unwrap();
617
-
618
- // Should have children
619
- assert!(!manifest.children.is_empty());
620
- }
621
-
622
- #[test]
623
- fn build_attribute_binding() {
624
- let html = r#"<input value="@[firstName]">"#;
625
- let state = json!({ "firstName": "John" });
626
- let builder = ManifestBuilder::new();
627
- let manifest = builder.build_from_html(html, &state, false).unwrap();
628
-
629
- // Should have children
630
- assert!(!manifest.children.is_empty());
631
- }
632
-
633
- #[test]
634
- fn build_iteration_with_template() {
635
- let html = r#"<body><!-- each items as item --><div>@[item]</div><!-- /each --></body>"#;
636
- let state = json!({ "items": [1, 2, 3] });
637
- let builder = ManifestBuilder::new();
638
- let manifest = builder.build_from_html(html, &state, false).unwrap();
639
-
640
- // Serialize to JSON to inspect
641
- let json = serde_json::to_string_pretty(&manifest).unwrap();
642
- eprintln!("Manifest JSON:\n{}", json);
643
-
644
- // Find iteration node
645
- // Note: might be nested under body
646
- assert!(!manifest.children.is_empty());
647
- }
648
-
649
- #[test]
650
- fn split_bindings_simple() {
651
- let builder = ManifestBuilder::new();
652
- let result = builder.split_by_bindings("Hello @[name]!");
653
- assert_eq!(result, vec!["Hello ", "@[name]", "!"]);
654
- }
655
-
656
- #[test]
657
- fn split_bindings_multiple() {
658
- let builder = ManifestBuilder::new();
659
- let result = builder.split_by_bindings("@[firstName] @[lastName]");
660
- assert_eq!(result, vec!["@[firstName]", " ", "@[lastName]"]);
661
- }
662
-
663
- #[test]
664
- fn split_bindings_no_match() {
665
- let builder = ManifestBuilder::new();
666
- let result = builder.split_by_bindings("Hello World");
667
- assert_eq!(result, vec!["Hello World"]);
668
- }
669
-
670
- #[test]
671
- fn build_multiple_name_bindings() {
672
- let html = r#"<div test @[theme] @[size]></div>"#;
673
- let state = json!({ "theme": "light", "size": "large" });
674
- let builder = ManifestBuilder::new();
675
- let manifest = builder.build_from_html(html, &state, false).unwrap();
676
-
677
- // Navigate to div
678
- let html_node = manifest.children.get("html_0").unwrap();
679
- let body_node = html_node.children.get("body_1").unwrap();
680
- let div_node = body_node.children.get("div_0").unwrap();
681
-
682
- // Check name bindings (sparse array indexed by position)
683
- let compiled = div_node.compiled.as_ref().unwrap();
684
- let restoration = compiled.restoration.as_ref().unwrap();
685
- let name_bindings = restoration.name_bindings.as_ref().unwrap();
686
-
687
- // Attributes: [0] test, [1] @[theme], [2] @[size]
688
- assert_eq!(name_bindings.len(), 3);
689
- assert_eq!(name_bindings[0], None); // "test" is not a binding
690
- assert_eq!(name_bindings[1], Some("@[theme]".to_string()));
691
- assert_eq!(name_bindings[2], Some("@[size]".to_string()));
692
- }
693
- }