658 lines
26 KiB
Rust
658 lines
26 KiB
Rust
|
|
pub mod opcodes;
|
|
pub mod reactive;
|
|
pub mod reader;
|
|
pub mod rhei;
|
|
pub mod style;
|
|
pub mod types;
|
|
|
|
use std::borrow::Cow;
|
|
pub use rhei::RheiContext;
|
|
use style::StyleSheet as SS;
|
|
pub use types::{ComponentDef, Document, Element, Interner, InterpError};
|
|
pub use reactive::{ElementId, ReactiveTracker};
|
|
|
|
use compact_str::CompactString;
|
|
use opcodes::*;
|
|
use reader::Reader;
|
|
use rhei::RHEI_PREFIX;
|
|
use style::{AncestorInfo, ComputedStyle, StructuralContext};
|
|
pub use types::Value;
|
|
use std::collections::{HashMap, HashSet};
|
|
|
|
pub struct Interpreter;
|
|
|
|
impl Interpreter {
|
|
pub fn run<'a>(bytecode: &'a [u8]) -> Result<Document<'a>, InterpError> {
|
|
if bytecode.len() < 4 { return Err(InterpError::UnexpectedEof); }
|
|
if &bytecode[..4] != MAGIC { return Err(InterpError::BadMagic); }
|
|
|
|
let mut r = Reader::new(bytecode);
|
|
r.pos = 4;
|
|
let mut variables = HashMap::new();
|
|
let mut components = HashMap::new();
|
|
let mut rhei_scripts: Vec<String> = Vec::new();
|
|
let mut stylesheet = SS::new();
|
|
let mut tracker = ReactiveTracker::new();
|
|
|
|
let roots = Self::parse_block_elements(
|
|
&mut r,
|
|
&mut variables,
|
|
&mut components,
|
|
&mut rhei_scripts,
|
|
&mut stylesheet,
|
|
&mut tracker,
|
|
true,
|
|
)?;
|
|
|
|
//let rhei_ctx = RheiContext::new(&rhei_scripts);
|
|
//rhei_ctx.initialize(&mut variables);
|
|
|
|
Ok(Document { roots, components, variables, rhei_scripts, stylesheet, interner: Interner::new(), tracker })
|
|
}
|
|
|
|
fn parse_block_elements<'a>(
|
|
r: &mut Reader<'a>,
|
|
variables: &mut HashMap<String, Value>,
|
|
components: &mut HashMap<String, ComponentDef<'a>>,
|
|
rhei_scripts: &mut Vec<String>,
|
|
stylesheet: &mut SS,
|
|
tracker: &mut ReactiveTracker,
|
|
is_root: bool,
|
|
) -> Result<Vec<Element<'a>>, InterpError> {
|
|
let mut roots: Vec<Element> = Vec::new();
|
|
let mut stack: Vec<Element> = Vec::new();
|
|
|
|
while r.remaining() > 0 {
|
|
let op = r.read_byte()?;
|
|
if !is_root && op == OP_END_BLOCK { break; }
|
|
|
|
match op {
|
|
OP_ELEM_PUSH => {
|
|
let elem_id = tracker.alloc_id();
|
|
let mut el = Element::new(r.read_str_ref()?);
|
|
el.element_id = elem_id;
|
|
stack.push(el);
|
|
}
|
|
OP_ELEM_POP => {
|
|
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
|
|
Self::attach(&mut stack, &mut roots, finished);
|
|
}
|
|
|
|
OP_GLOBAL | OP_LET => {
|
|
let name = r.read_string()?;
|
|
let vop = r.read_byte()?;
|
|
if let Some(value) = r.read_value(vop)? {
|
|
variables.insert(name, value);
|
|
}
|
|
}
|
|
|
|
OP_SINGLETON => {
|
|
let _name = r.read_string()?;
|
|
let count = r.read_u32()?;
|
|
for _ in 0..count {
|
|
r.read_string()?;
|
|
let vop = r.read_byte()?;
|
|
r.skip_value(vop)?;
|
|
}
|
|
}
|
|
|
|
OP_CONTENT => {
|
|
let vop = r.read_byte()?;
|
|
if vop == OP_PROP_RHEI {
|
|
let expr_ref = r.read_str_ref()?;
|
|
let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len());
|
|
val.push_str(RHEI_PREFIX);
|
|
val.push_str(expr_ref);
|
|
if let Some(el) = stack.last_mut() {
|
|
tracker.scan_value(el.element_id, &val);
|
|
el.push_prop("text".to_string(), val);
|
|
}
|
|
} else if let Some(value) = r.read_value_as_string(vop)? {
|
|
if let Some(el) = stack.last_mut() {
|
|
tracker.scan_value(el.element_id, &value);
|
|
el.push_prop("text".to_string(), value);
|
|
}
|
|
}
|
|
}
|
|
|
|
OP_PROP_STR => {
|
|
let key = r.read_str_ref()?;
|
|
let val = r.read_str_ref()?;
|
|
if let Some(el) = stack.last_mut() {
|
|
tracker.scan_value(el.element_id, val);
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
OP_PROP_VAR => {
|
|
let key = r.read_str_ref()?;
|
|
let val_ref = r.read_str_ref()?;
|
|
let mut val = String::with_capacity(val_ref.len() + 1);
|
|
val.push('$');
|
|
val.push_str(val_ref);
|
|
if let Some(el) = stack.last_mut() {
|
|
tracker.scan_value(el.element_id, &val);
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
OP_PROP_INT => {
|
|
let key = r.read_str_ref()?;
|
|
let val = r.read_i64()?.to_string();
|
|
if let Some(el) = stack.last_mut() {
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
OP_PROP_FLOAT => {
|
|
let key = r.read_str_ref()?;
|
|
let val = r.read_f64()?.to_string();
|
|
if let Some(el) = stack.last_mut() {
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
OP_PROP_BOOL => {
|
|
let key = r.read_str_ref()?;
|
|
let val = (r.read_byte()? != 0).to_string();
|
|
if let Some(el) = stack.last_mut() {
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
OP_PROP_RHEI => {
|
|
let key = r.read_str_ref()?;
|
|
let expr_ref = r.read_str_ref()?;
|
|
let mut val = String::with_capacity(RHEI_PREFIX.len() + expr_ref.len());
|
|
val.push_str(RHEI_PREFIX);
|
|
val.push_str(expr_ref);
|
|
if let Some(el) = stack.last_mut() {
|
|
tracker.scan_value(el.element_id, &val);
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
|
|
let key = r.read_str_ref()?;
|
|
if let Some(val) = r.read_value_as_string(op)? {
|
|
if let Some(el) = stack.last_mut() {
|
|
el.push_prop(key, val);
|
|
}
|
|
}
|
|
}
|
|
|
|
OP_RHEI_BLK => {
|
|
let script = r.read_string()?;
|
|
if is_root && stack.is_empty() {
|
|
rhei_scripts.push(script);
|
|
} else {
|
|
let mut text_el = Element::new("#text");
|
|
text_el.element_id = tracker.alloc_id();
|
|
let full_val = format!("{RHEI_PREFIX}{script}");
|
|
tracker.scan_value(text_el.element_id, &full_val);
|
|
text_el.push_prop("text".to_string(), full_val);
|
|
Self::attach(&mut stack, &mut roots, text_el);
|
|
}
|
|
}
|
|
|
|
OP_COMPONENT => {
|
|
let name = r.read_string()?;
|
|
let param_count = r.read_u32()?;
|
|
let params = (0..param_count)
|
|
.map(|_| Ok((r.read_string()?, r.read_string()?)))
|
|
.collect::<Result<Vec<_>, InterpError>>()?;
|
|
let children = Self::parse_block_elements(
|
|
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
|
)?;
|
|
components.insert(name.clone(), ComponentDef { name, params, children });
|
|
}
|
|
|
|
OP_IF => {
|
|
let vop = r.read_byte()?;
|
|
let raw = r.read_value_as_string(vop)?.unwrap_or_default();
|
|
let cond_val = if vop == OP_PROP_RHEI {
|
|
format!("{RHEI_PREFIX}{raw}")
|
|
} else {
|
|
raw
|
|
};
|
|
|
|
let true_children = Self::parse_block_elements(
|
|
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
|
)?;
|
|
let has_else = r.read_byte()? == 1;
|
|
let false_children = if has_else {
|
|
Self::parse_block_elements(
|
|
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
|
)?
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
let mut if_el = Element::new("@if");
|
|
if_el.element_id = tracker.alloc_id();
|
|
tracker.scan_value(if_el.element_id, &cond_val);
|
|
if_el.push_prop("condition", cond_val);
|
|
if_el.children = true_children;
|
|
|
|
if !false_children.is_empty() {
|
|
let mut else_el = Element::new("@else");
|
|
else_el.element_id = tracker.alloc_id();
|
|
else_el.children = false_children;
|
|
if_el.children.push(else_el);
|
|
}
|
|
|
|
Self::attach(&mut stack, &mut roots, if_el);
|
|
}
|
|
|
|
OP_EACH => {
|
|
let var_name = r.read_string()?;
|
|
let vop = r.read_byte()?;
|
|
let source_val = match vop {
|
|
OP_PROP_VAR => format!("${}", r.read_string()?),
|
|
OP_PROP_ARRAY => r.read_array_as_strings()?.join(","),
|
|
OP_PROP_RHEI => format!("{RHEI_PREFIX}{}", r.read_string()?),
|
|
_ => { r.skip_value(vop)?; String::new() }
|
|
};
|
|
|
|
let block_children = Self::parse_block_elements(
|
|
r, variables, components, rhei_scripts, stylesheet, tracker, false,
|
|
)?;
|
|
|
|
let mut each_el = Element::new("@each");
|
|
each_el.element_id = tracker.alloc_id();
|
|
tracker.scan_value(each_el.element_id, &source_val);
|
|
each_el.push_prop("var_name".to_string(), var_name);
|
|
each_el.push_prop("source".to_string(), source_val);
|
|
each_el.children = block_children;
|
|
Self::attach(&mut stack, &mut roots, each_el);
|
|
}
|
|
|
|
OP_ON => {
|
|
let event_name = r.read_string()?;
|
|
let arg_count = r.read_u32()?;
|
|
let mut _args: Vec<(String, String)> = Vec::with_capacity(arg_count as usize);
|
|
for _ in 0..arg_count {
|
|
let k = r.read_string()?;
|
|
let vop = r.read_byte()?;
|
|
if let Some(v) = r.read_value_as_string(vop)? {
|
|
_args.push((k, v));
|
|
}
|
|
}
|
|
|
|
let mut handler_script = String::new();
|
|
loop {
|
|
let inner_op = r.read_byte()?;
|
|
if inner_op == OP_END_BLOCK { break; }
|
|
if inner_op == OP_RHEI_BLK {
|
|
handler_script = r.read_string()?;
|
|
} else {
|
|
r.skip_opcode(inner_op)?;
|
|
}
|
|
}
|
|
|
|
if let Some(el) = stack.last_mut() {
|
|
el.push_prop(
|
|
format!("__on:{event_name}"),
|
|
handler_script,
|
|
);
|
|
}
|
|
}
|
|
|
|
OP_STYLE_RULE => {
|
|
let selector = r.read_string()?;
|
|
let prop_count = r.read_u32()?;
|
|
let mut props = HashMap::with_capacity(prop_count as usize);
|
|
|
|
for _ in 0..prop_count {
|
|
let key = r.read_string()?;
|
|
let type_op = r.read_byte()?;
|
|
if let Some(val) = r.read_value_as_string(type_op)? {
|
|
props.insert(key, val);
|
|
}
|
|
}
|
|
stylesheet.add_rule(selector, props);
|
|
}
|
|
|
|
OP_STYLE_ANIM => {
|
|
r.skip_opcode(OP_STYLE_ANIM)?;
|
|
}
|
|
|
|
other => r.skip_opcode(other)?,
|
|
}
|
|
}
|
|
|
|
Ok(roots)
|
|
}
|
|
|
|
pub fn evaluate_vdom<'a>(
|
|
templates: &[Element<'a>],
|
|
variables: &mut HashMap<String, Value>,
|
|
components: &HashMap<String, ComponentDef<'a>>,
|
|
rhei: &RheiContext,
|
|
stylesheet: &SS,
|
|
ancestors: &[AncestorInfo],
|
|
) -> Vec<Element<'a>> {
|
|
Self::evaluate_vdom_incr(templates, variables, components, rhei, stylesheet, ancestors, &HashSet::new())
|
|
}
|
|
|
|
pub fn evaluate_vdom_incr<'a>(
|
|
templates: &[Element<'a>],
|
|
variables: &mut HashMap<String, Value>,
|
|
components: &HashMap<String, ComponentDef<'a>>,
|
|
rhei: &RheiContext,
|
|
stylesheet: &SS,
|
|
ancestors: &[AncestorInfo],
|
|
dirty_set: &HashSet<ElementId>,
|
|
) -> Vec<Element<'a>> {
|
|
let mut output = Vec::with_capacity(templates.len());
|
|
|
|
// Precompute sibling info for structural pseudo-classes and sibling combinators
|
|
let sibling_infos: Vec<AncestorInfo> = templates.iter().map(|el| {
|
|
AncestorInfo::new_with_id(
|
|
el.type_name,
|
|
el.id().map(String::from),
|
|
el.get_prop("class")
|
|
.map(|s| s.split_whitespace().map(String::from).collect())
|
|
.unwrap_or_default(),
|
|
)
|
|
}).collect();
|
|
|
|
// Precompute type totals for structural pseudo-class context
|
|
let mut type_counts: HashMap<&str, usize> = HashMap::new();
|
|
for el in templates {
|
|
*type_counts.entry(el.type_name).or_insert(0) += 1;
|
|
}
|
|
let mut type_seen: HashMap<&str, usize> = HashMap::new();
|
|
|
|
for (i, el) in templates.iter().enumerate() {
|
|
let type_idx = type_seen.entry(el.type_name).or_insert(0);
|
|
let type_total = *type_counts.get(el.type_name).unwrap_or(&0);
|
|
|
|
let structural = StructuralContext {
|
|
sibling_index: i,
|
|
sibling_total: templates.len(),
|
|
type_index: *type_idx,
|
|
type_total,
|
|
has_children: !el.children.is_empty()
|
|
|| el.properties.iter().any(|(k, v)| k == "text" && !v.is_empty()),
|
|
is_root: ancestors.is_empty(),
|
|
};
|
|
|
|
*type_idx += 1;
|
|
|
|
match el.type_name {
|
|
"@if" => {
|
|
let cond = el.get_prop("condition").unwrap_or_default();
|
|
let is_true = Self::evaluate_condition(cond, variables, rhei);
|
|
|
|
let mut active_branch = Vec::new();
|
|
for child in &el.children {
|
|
if child.type_name == "@else" {
|
|
if !is_true { active_branch.extend(child.children.clone()); }
|
|
} else if is_true {
|
|
active_branch.push(child.clone());
|
|
}
|
|
}
|
|
output.extend(Self::evaluate_vdom_incr(
|
|
&active_branch, variables, components, rhei, stylesheet, ancestors, dirty_set,
|
|
));
|
|
}
|
|
|
|
"@each" => {
|
|
let var_name = el.get_prop("var_name").unwrap_or_default();
|
|
let source_expr = el.get_prop("source").unwrap_or_default();
|
|
|
|
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
|
|
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables).to_owned_string())
|
|
} else {
|
|
Self::resolve_string(source_expr, variables).into_owned()
|
|
};
|
|
|
|
let items: Vec<String> = if resolved_source.is_empty() {
|
|
vec![]
|
|
} else {
|
|
resolved_source.split(',').map(str::trim).map(str::to_string).collect()
|
|
};
|
|
|
|
for item in items {
|
|
let old_val = variables.insert(var_name.to_string(), Value::Str(CompactString::new(item)));
|
|
|
|
output.extend(Self::evaluate_vdom_incr(
|
|
&el.children, variables, components, rhei, stylesheet, ancestors, dirty_set,
|
|
));
|
|
|
|
if let Some(old) = old_val {
|
|
variables.insert(var_name.to_string(), old);
|
|
} else {
|
|
variables.remove(var_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
_ => {
|
|
if let Some(comp) = components.get(el.type_name) {
|
|
let mut new_args = Vec::with_capacity(comp.params.len());
|
|
for (param, _) in &comp.params {
|
|
if let Some(arg) = el.get_prop(param) {
|
|
new_args.push((
|
|
param.clone(),
|
|
Self::resolve_prop(arg, variables, rhei),
|
|
));
|
|
}
|
|
}
|
|
|
|
let mut old_vals = Vec::with_capacity(new_args.len());
|
|
for (k, v) in new_args {
|
|
old_vals.push((k.clone(), variables.insert(k, Value::from(v))));
|
|
}
|
|
|
|
let mut vcomp = Element::new(el.type_name);
|
|
for (k, v) in &el.properties {
|
|
if k.starts_with("__on:") {
|
|
vcomp.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
|
|
} else {
|
|
vcomp.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
|
|
}
|
|
}
|
|
|
|
let matched_sheets = Self::collect_matching_styles(&vcomp, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
|
|
vcomp.computed_style = ComputedStyle::compute(&vcomp.properties, &matched_sheets);
|
|
|
|
let child_ancestors = Self::build_ancestor_chain(ancestors, &vcomp);
|
|
vcomp.children = Self::evaluate_vdom_incr(
|
|
&comp.children, variables, components, rhei, stylesheet, &child_ancestors, dirty_set,
|
|
);
|
|
output.push(vcomp);
|
|
|
|
for (k, old) in old_vals.into_iter().rev() {
|
|
if let Some(o) = old {
|
|
variables.insert(k, o);
|
|
} else {
|
|
variables.remove(&k);
|
|
}
|
|
}
|
|
} else {
|
|
let mut vnode = Element::new(el.type_name);
|
|
|
|
for (k, v) in &el.properties {
|
|
if k.starts_with("__on:") {
|
|
vnode.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
|
|
continue;
|
|
}
|
|
if v.starts_with('$')
|
|
&& !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
|
{
|
|
vnode.set_prop(format!("__bind:{k}"), v[1..].to_string());
|
|
}
|
|
vnode.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
|
|
}
|
|
|
|
let matched_sheets = Self::collect_matching_styles(&vnode, stylesheet, &[], &structural, ancestors, &sibling_infos[0..i]);
|
|
vnode.computed_style = ComputedStyle::compute(&vnode.properties, &matched_sheets);
|
|
|
|
let child_ancestors = Self::build_ancestor_chain(ancestors, &vnode);
|
|
vnode.children = Self::evaluate_vdom_incr(
|
|
&el.children, variables, components, rhei, stylesheet, &child_ancestors, dirty_set,
|
|
);
|
|
output.push(vnode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
fn build_ancestor_chain<'a>(ancestors: &[AncestorInfo], el: &Element) -> Vec<AncestorInfo> {
|
|
let mut chain = ancestors.to_vec();
|
|
let id = el.id().map(String::from);
|
|
let classes: Vec<String> = el
|
|
.get_prop("class")
|
|
.map(|s| s.split_whitespace().map(String::from).collect())
|
|
.unwrap_or_default();
|
|
chain.push(AncestorInfo::new_with_id(el.type_name, id, classes));
|
|
chain
|
|
}
|
|
|
|
fn collect_matching_styles<'a>(
|
|
el: &Element,
|
|
stylesheet: &'a SS,
|
|
active_pseudo: &[&str],
|
|
structural: &StructuralContext,
|
|
ancestors: &[AncestorInfo],
|
|
preceding_siblings: &[AncestorInfo],
|
|
) -> Vec<&'a HashMap<String, String>> {
|
|
let el_id = el.id();
|
|
let classes: Vec<&str> = el
|
|
.get_prop("class")
|
|
.map(|s| s.split_whitespace().collect())
|
|
.unwrap_or_default();
|
|
|
|
let el_attributes: HashMap<String, String> = el.properties.iter()
|
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
.collect();
|
|
|
|
stylesheet.matching_rules(el.type_name, el_id, &classes, active_pseudo, structural, ancestors, preceding_siblings, &el_attributes)
|
|
}
|
|
|
|
fn resolve_prop(v: &str, variables: &HashMap<String, Value>, rhei: &RheiContext) -> String {
|
|
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
|
|
rhei.eval_expr(expr, variables).to_owned_string().into()
|
|
} else {
|
|
Self::resolve_string(v, variables).into_owned()
|
|
}
|
|
}
|
|
|
|
fn evaluate_condition(
|
|
cond: &str,
|
|
variables: &HashMap<String, Value>,
|
|
rhei: &RheiContext,
|
|
) -> bool {
|
|
if let Some(expr) = cond.strip_prefix(RHEI_PREFIX) {
|
|
return rhei.eval_condition(expr, variables);
|
|
}
|
|
|
|
let mut clean = cond.to_string();
|
|
if let Some(start) = clean.find('{') {
|
|
if let Some(end) = clean.rfind('}') {
|
|
clean = clean[start + 1..end].trim().to_string();
|
|
}
|
|
}
|
|
|
|
let resolved = Self::resolve_string(&clean, variables).trim().to_string();
|
|
|
|
if Self::is_truthy_str(&resolved) { return true; }
|
|
if resolved == "false" || resolved == "0" || resolved.is_empty() { return false; }
|
|
|
|
let operators: [(&str, fn(f64, f64) -> bool); 6] = [
|
|
(">=", |a, b| a >= b),
|
|
("<=", |a, b| a <= b),
|
|
(">", |a, b| a > b),
|
|
("<", |a, b| a < b),
|
|
("==", |a, b| a == b),
|
|
("!=", |a, b| a != b),
|
|
];
|
|
|
|
for (op, compare) in operators {
|
|
if let Some((left, right)) = resolved.split_once(op) {
|
|
let l = left.trim();
|
|
let r = right.trim();
|
|
if let (Ok(ln), Ok(rn)) = (l.parse::<f64>(), r.parse::<f64>()) {
|
|
return compare(ln, rn);
|
|
}
|
|
if op == "==" { return l == r; }
|
|
if op == "!=" { return l != r; }
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[inline]
|
|
fn is_truthy(v: &Value) -> bool {
|
|
match v {
|
|
Value::Bool(b) => *b,
|
|
Value::Int(i) => *i != 0,
|
|
Value::Float(f) => *f != 0.0,
|
|
Value::Str(s) => Self::is_truthy_str(s),
|
|
Value::None => false,
|
|
Value::Array(a) => !a.is_empty(),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn is_truthy_str(s: &str) -> bool {
|
|
match s.trim() {
|
|
"" | "false" | "0" | "null" => false,
|
|
"true" | "1" => true,
|
|
other => other.parse::<f64>().map(|n| n != 0.0).unwrap_or(true),
|
|
}
|
|
}
|
|
|
|
fn normalize_rhai_array(s: &str) -> String {
|
|
let trimmed = s.trim();
|
|
if trimmed.starts_with('[') && trimmed.ends_with(']') {
|
|
trimmed[1..trimmed.len() - 1]
|
|
.split(',')
|
|
.map(|item| item.trim().to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(",")
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, Value>) -> Cow<'a, str> {
|
|
if !val.contains('$') {
|
|
return Cow::Borrowed(val);
|
|
}
|
|
|
|
let mut result = String::with_capacity(val.len() + 16);
|
|
let mut chars = val.char_indices().peekable();
|
|
|
|
while let Some((_, c)) = chars.next() {
|
|
if c == '$' {
|
|
let mut var_name = String::new();
|
|
while let Some(&(_, next_c)) = chars.peek() {
|
|
if next_c.is_ascii_alphanumeric() || next_c == '_' {
|
|
var_name.push(chars.next().unwrap().1);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if let Some(resolved) = scope.get(&var_name) {
|
|
let formatted = resolved.to_owned_string();
|
|
result.push_str(&formatted);
|
|
} else {
|
|
result.push('$');
|
|
result.push_str(&var_name);
|
|
}
|
|
} else {
|
|
result.push(c);
|
|
}
|
|
}
|
|
Cow::Owned(result)
|
|
}
|
|
|
|
fn attach<'a>(stack: &mut Vec<Element<'a>>, roots: &mut Vec<Element<'a>>, el: Element<'a>) {
|
|
match stack.last_mut() {
|
|
Some(parent) => parent.children.push(el),
|
|
None => roots.push(el),
|
|
}
|
|
}
|
|
}
|