use std::collections::HashMap;
use crate::{
engine_core::EngineCore,
ir::{IRNode, NodeId},
reconcile::{create_ir_node_tree_impl, node_id_str, reconcile_ir_node_impl, Patch, ReconcileCtx},
};
#[derive(Default)]
pub(crate) struct NodeIdIndex {
map: HashMap<String, NodeId>,
}
impl NodeIdIndex {
pub fn new() -> Self {
Self::default()
}
pub fn lookup(&self, id: &str) -> Option<NodeId> {
self.map.get(id).copied()
}
pub fn index_creates(&mut self, patches: &[Patch], core: &EngineCore) {
for patch in patches {
if let Patch::Create { id, .. } = patch {
if !self.map.contains_key(id) {
for (node_id, _) in core.tree.iter() {
if node_id_str(node_id) == *id {
self.map.insert(id.clone(), node_id);
break;
}
}
}
}
}
}
}
pub(crate) fn render_subtree_into(
core: &mut EngineCore,
parent_id: NodeId,
expanded: &IRNode,
state: &serde_json::Value,
patches: &mut Vec<Patch>,
) {
let first_child_id = core
.tree
.get(parent_id)
.and_then(|node| node.children.front().copied());
let ds = (!core.data_sources.is_empty()).then_some(&core.data_sources);
let mods = (!core.modules.is_empty()).then_some(&core.modules);
match first_child_id {
Some(first_child_id) => {
let mut ctx = ReconcileCtx {
tree: &mut core.tree,
state,
patches,
dependencies: &mut core.dependencies,
data_sources: ds,
modules: mods,
};
reconcile_ir_node_impl(&mut ctx, first_child_id, expanded);
}
None => {
let mut ctx = ReconcileCtx {
tree: &mut core.tree,
state,
patches,
dependencies: &mut core.dependencies,
data_sources: ds,
modules: mods,
};
create_ir_node_tree_impl(&mut ctx, expanded, Some(parent_id), false);
}
}
core.revision += 1;
}
pub(crate) fn format_parse_errors(errors: &[hypen_parser::error::Rich<char>]) -> String {
errors
.iter()
.map(hypen_parser::error::format_error_simple)
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Element;
use crate::reconcile::tree::ResolvedProps;
use indexmap::IndexMap;
use std::sync::Arc;
#[test]
fn node_id_index_create_patch_tracking() {
let mut core = EngineCore::new();
let mut index = NodeIdIndex::new();
let elem_a = Element::new("Text");
let elem_b = Element::new("Column");
let id_a = core.tree.create_node(&elem_a, &serde_json::Value::Null);
let id_b = core.tree.create_node(&elem_b, &serde_json::Value::Null);
let str_a = crate::reconcile::node_id_str(id_a);
let str_b = crate::reconcile::node_id_str(id_b);
assert!(index.lookup(&str_a).is_none(), "fresh index must be empty");
let empty_props: ResolvedProps = Arc::new(IndexMap::new());
let patches = vec![
Patch::Create {
id: str_a.clone(),
element_type: "Text".into(),
props: empty_props.clone(),
},
Patch::Create {
id: str_a.clone(),
element_type: "Text".into(),
props: empty_props.clone(),
},
Patch::Create {
id: str_b.clone(),
element_type: "Column".into(),
props: empty_props.clone(),
},
Patch::Remove { id: str_a.clone() },
];
index.index_creates(&patches, &core);
assert_eq!(index.lookup(&str_a), Some(id_a));
assert_eq!(index.lookup(&str_b), Some(id_b));
assert!(index.lookup("nonexistent").is_none());
}
#[test]
fn render_subtree_into_fresh_subtree_path() {
let mut core = EngineCore::new();
let parent = core
.tree
.create_node(&Element::new("Column"), &serde_json::Value::Null);
core.tree.set_root(parent);
let rev_before = core.revision;
let mut patches = Vec::new();
let child_ir = IRNode::Element(Element::new("Text"));
render_subtree_into(
&mut core,
parent,
&child_ir,
&serde_json::Value::Null,
&mut patches,
);
assert!(
patches.iter().any(|p| matches!(p, Patch::Create { .. })),
"fresh subtree must emit at least one Create patch; got: {:?}",
patches
);
assert_eq!(
core.revision,
rev_before + 1,
"render_subtree_into must bump revision"
);
}
#[test]
fn format_parse_errors_handles_empty_batch() {
assert_eq!(format_parse_errors(&[]), "");
}
}