use crate::ir::{ConditionalBranch, Element, IRNode, Props, Value};
use crate::reactive::Binding;
pub(crate) fn navigate_item_path<'a>(
item: &'a serde_json::Value,
path: &str,
) -> Option<&'a serde_json::Value> {
let mut current = item;
for segment in path.split('.') {
if let Ok(index) = segment.parse::<usize>() {
current = current.get(index)?;
} else {
current = current.get(segment)?;
}
}
Some(current)
}
#[derive(Debug)]
pub(super) struct Replacement {
pub start: usize,
pub end: usize,
pub text: String,
}
pub(super) fn apply_replacements(s: &str, mut replacements: Vec<Replacement>) -> String {
if replacements.is_empty() {
return s.to_string();
}
replacements.sort_by(|a, b| a.start.cmp(&b.start));
let valid_replacements: Vec<_> = replacements
.into_iter()
.filter(|r| r.start <= s.len() && r.end <= s.len() && r.start <= r.end)
.collect();
if valid_replacements.is_empty() {
return s.to_string();
}
let removed: usize = valid_replacements.iter().map(|r| r.end - r.start).sum();
let added: usize = valid_replacements.iter().map(|r| r.text.len()).sum();
let capacity = s.len() - removed + added;
let mut result = String::with_capacity(capacity);
let mut pos = 0;
for r in valid_replacements {
if r.start > pos {
result.push_str(&s[pos..r.start]);
}
result.push_str(&r.text);
pos = r.end;
}
if pos < s.len() {
result.push_str(&s[pos..]);
}
result
}
fn find_path_end(s: &str, start: usize) -> usize {
let substring = &s[start..];
let mut end = start;
let mut char_iter = substring.char_indices().peekable();
while let Some((byte_offset, ch)) = char_iter.next() {
if ch.is_alphanumeric() || ch == '_' {
end = start + byte_offset + ch.len_utf8();
} else if ch == '.' {
if let Some(&(_, next)) = char_iter.peek() {
if next.is_alphanumeric() || next == '_' {
end = start + byte_offset + ch.len_utf8();
continue;
}
}
break;
} else {
break;
}
}
end
}
fn format_value_for_replacement(val: &serde_json::Value, quote_strings: bool) -> String {
match val {
serde_json::Value::String(s) => {
if quote_strings {
format!("'{}'", s)
} else {
s.clone()
}
}
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
_ => serde_json::to_string(val).unwrap_or_default(),
}
}
pub fn replace_item_bindings(element: &Element, item: &serde_json::Value, index: usize) -> Element {
replace_item_bindings_with_name(element, item, index, "item")
}
pub fn replace_item_bindings_with_name(
element: &Element,
item: &serde_json::Value,
index: usize,
item_name: &str,
) -> Element {
let mut new_props = Props::new();
for (key, value) in &element.props {
new_props.insert(
key.clone(),
replace_value_item_bindings(value, item, item_name),
);
}
let key = item
.get("id")
.and_then(|v| {
v.as_str()
.map(|s| s.to_string())
.or_else(|| v.as_i64().map(|n| n.to_string()))
})
.or_else(|| {
item.get("key")
.and_then(|v| v.as_str().map(|s| s.to_string()))
})
.map(|id| format!("{}-{}", item_name, id))
.unwrap_or_else(|| format!("{}-{}", item_name, index));
let child_key = format!("{}-{}", item_name, index);
let ir_children = element
.ir_children
.iter()
.map(|child_ir| {
replace_ir_node_item_bindings(child_ir, item, index, item_name, &child_key)
})
.collect();
Element {
element_type: element.element_type.clone(),
props: new_props,
ir_children,
key: Some(key),
module_scope: element.module_scope.clone(),
}
}
pub(crate) fn replace_ir_node_item_bindings(
node: &IRNode,
item: &serde_json::Value,
index: usize,
item_name: &str,
item_key: &str,
) -> IRNode {
match node {
IRNode::Element(element) => {
let mut new_element = replace_item_bindings_with_name(element, item, index, item_name);
new_element.key = Some(item_key.to_string());
IRNode::Element(new_element)
}
IRNode::ForEach {
source,
item_name: inner_item_name,
key_path,
template,
props,
module_scope,
} => {
let new_props = replace_props_item_bindings(props, item, item_name);
let new_template: Vec<IRNode> = template
.iter()
.map(|child| replace_ir_node_item_bindings(child, item, index, item_name, item_key))
.collect();
IRNode::ForEach {
source: source.clone(),
item_name: inner_item_name.clone(),
key_path: key_path.clone(),
template: new_template,
props: new_props,
module_scope: module_scope.clone(),
}
}
IRNode::Conditional {
value,
branches,
fallback,
module_scope,
} => {
let new_value = replace_value_item_bindings(value, item, item_name);
let new_branches: Vec<ConditionalBranch> = branches
.iter()
.map(|branch| {
let new_pattern = replace_value_item_bindings(&branch.pattern, item, item_name);
let new_children: Vec<IRNode> = branch
.children
.iter()
.map(|child| {
replace_ir_node_item_bindings(child, item, index, item_name, item_key)
})
.collect();
ConditionalBranch::new(new_pattern, new_children)
})
.collect();
let new_fallback = fallback.as_ref().map(|f| {
f.iter()
.map(|child| {
replace_ir_node_item_bindings(child, item, index, item_name, item_key)
})
.collect()
});
IRNode::Conditional {
value: new_value,
branches: new_branches,
fallback: new_fallback,
module_scope: module_scope.clone(),
}
}
IRNode::Router {
location,
routes,
fallback,
module_scope,
} => {
let new_location = replace_value_item_bindings(location, item, item_name);
let new_routes: Vec<crate::ir::RouterRoute> = routes
.iter()
.map(|route| crate::ir::RouterRoute {
path: route.path.clone(),
children: route
.children
.iter()
.map(|child| {
replace_ir_node_item_bindings(child, item, index, item_name, item_key)
})
.collect(),
})
.collect();
let new_fallback = fallback.as_ref().map(|f| {
f.iter()
.map(|child| {
replace_ir_node_item_bindings(child, item, index, item_name, item_key)
})
.collect()
});
IRNode::Router {
location: new_location,
routes: new_routes,
fallback: new_fallback,
module_scope: module_scope.clone(),
}
}
}
}
fn replace_props_item_bindings(props: &Props, item: &serde_json::Value, item_name: &str) -> Props {
let mut new_props = Props::new();
for (key, value) in props {
new_props.insert(
key.clone(),
replace_value_item_bindings(value, item, item_name),
);
}
new_props
}
fn replace_value_item_bindings(value: &Value, item: &serde_json::Value, item_name: &str) -> Value {
match value {
Value::Binding(binding) => {
if binding.is_item() {
if binding.path.is_empty() {
Value::Static(item.clone())
} else {
let path = binding.full_path();
if let Some(val) = navigate_item_path(item, &path) {
Value::Static(val.clone())
} else {
value.clone()
}
}
} else {
value.clone()
}
}
Value::TemplateString { template, bindings } => {
replace_template_string_item_bindings(template, bindings, item, item_name, value)
}
Value::Static(serde_json::Value::String(s))
if s.contains(&format!("@{{{}.", item_name))
|| s.contains(&format!("@{{{}}}", item_name)) =>
{
replace_static_item_bindings_with_name(s, item, item_name)
}
_ => value.clone(),
}
}
fn replace_template_string_item_bindings(
template: &str,
bindings: &[Binding],
item: &serde_json::Value,
item_name: &str,
original: &Value,
) -> Value {
use crate::reactive::{build_evaluator, evaluate_template_string};
let has_item_bindings = bindings.iter().any(|b| b.is_item());
if !has_item_bindings {
return original.clone();
}
let mut replacements = Vec::new();
for binding in bindings {
if binding.is_item() {
let pattern = format!("@{{{}}}", binding.full_path_with_source());
if let Some(start) = template.find(&pattern) {
let replacement = if binding.path.is_empty() {
format_value_for_replacement(item, false)
} else if let Some(val) = navigate_item_path(item, &binding.full_path()) {
format_value_for_replacement(val, false)
} else {
continue;
};
replacements.push(Replacement {
start,
end: start + pattern.len(),
text: replacement,
});
}
}
}
let mut result = apply_replacements(template, replacements);
let expr_replacements = collect_item_replacements_with_name(&result, item, true, item_name);
result = apply_replacements(&result, expr_replacements);
let remaining_bindings: Vec<_> = bindings.iter().filter(|b| b.is_state()).cloned().collect();
if remaining_bindings.is_empty() {
if result.contains("@{") {
let evaluator = build_evaluator(&serde_json::Value::Null, None, None);
match evaluate_template_string(&result, &evaluator) {
Ok(evaluated) => Value::Static(serde_json::Value::String(evaluated)),
Err(_) => Value::Static(serde_json::Value::String(result)),
}
} else {
Value::Static(serde_json::Value::String(result))
}
} else {
Value::TemplateString {
template: result,
bindings: remaining_bindings,
}
}
}
fn collect_item_replacements_with_name(
s: &str,
item: &serde_json::Value,
quote_strings: bool,
item_name: &str,
) -> Vec<Replacement> {
let mut replacements = Vec::new();
let mut pos = 0;
while pos < s.len() {
if let Some(rel_start) = s[pos..].find(item_name) {
let abs_start = pos + rel_start;
let after_item = abs_start + item_name.len();
if after_item < s.len() && s.as_bytes()[after_item] == b'.' {
let path_start = after_item + 1;
let path_end = find_path_end(s, path_start);
if path_end > path_start {
let path = &s[path_start..path_end];
if let Some(val) = navigate_item_path(item, path) {
let replacement = format_value_for_replacement(val, quote_strings);
replacements.push(Replacement {
start: abs_start,
end: path_end,
text: replacement,
});
}
}
pos = path_end.max(after_item + 1);
} else {
pos = after_item;
}
} else {
break;
}
}
replacements
}
fn replace_static_item_bindings_with_name(
s: &str,
item: &serde_json::Value,
item_name: &str,
) -> Value {
use crate::reactive::{build_evaluator, evaluate_template_string};
let mut replacements = Vec::new();
let mut pos = 0;
while let Some(start) = s[pos..].find("@{") {
let abs_start = pos + start;
if let Some(end) = s[abs_start..].find('}') {
let abs_end = abs_start + end;
let content = &s[abs_start + 2..abs_end];
if content == item_name {
replacements.push(Replacement {
start: abs_start,
end: abs_end + 1,
text: format_value_for_replacement(item, false),
});
pos = abs_end + 1;
} else if content.starts_with(&format!("{}.", item_name))
&& is_simple_path_with_name(content, item_name)
{
let path = &content[item_name.len() + 1..];
if let Some(val) = navigate_item_path(item, path) {
replacements.push(Replacement {
start: abs_start,
end: abs_end + 1,
text: format_value_for_replacement(val, false),
});
}
pos = abs_end + 1;
} else if content.contains(&format!("{}.", item_name))
|| content.contains(&format!("{} ", item_name))
{
let expr_replacements =
collect_item_replacements_with_name(content, item, true, item_name);
let substituted_content = apply_replacements(content, expr_replacements);
let new_expr = format!("@{{{}}}", substituted_content);
let evaluator = build_evaluator(&serde_json::Value::Null, None, None);
if let Ok(evaluated) = evaluate_template_string(&new_expr, &evaluator) {
replacements.push(Replacement {
start: abs_start,
end: abs_end + 1,
text: evaluated,
});
}
pos = abs_end + 1;
} else {
pos = abs_end + 1;
}
} else {
break;
}
}
let result = apply_replacements(s, replacements);
Value::Static(serde_json::Value::String(result))
}
fn is_simple_path_with_name(s: &str, item_name: &str) -> bool {
if !s.starts_with(item_name) {
return false;
}
let after_item = &s[item_name.len()..];
if after_item.is_empty() {
return true;
}
if !after_item.starts_with('.') {
return false;
}
after_item[1..]
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '.')
}