feat: reactivityl; vdom; rhei; styles; fs; image element

This commit is contained in:
Faynot
2026-06-02 12:12:20 +03:00
commit f956b750e3
16 changed files with 7713 additions and 0 deletions

496
src/interpreter/mod.rs Normal file
View File

@@ -0,0 +1,496 @@
pub mod opcodes;
pub mod reader;
pub mod rhei;
pub mod style;
pub mod types;
pub use rhei::RheiContext;
use style::StyleSheet as SS;
pub use types::{ComponentDef, Document, Element, InterpError};
use opcodes::*;
use reader::Reader;
use rhei::{dyn_to_str, str_to_dyn, RHEI_PREFIX};
use style::ComputedStyle;
use regex::Regex;
use std::collections::HashMap;
use std::sync::OnceLock;
static RE_VAR: OnceLock<Regex> = OnceLock::new();
pub struct Interpreter;
impl Interpreter {
pub fn run(bytecode: &[u8]) -> Result<Document, 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 roots = Self::parse_block_elements(
&mut r,
&mut variables,
&mut components,
&mut rhei_scripts,
&mut stylesheet,
true,
)?;
let rhei_ctx = RheiContext::new(&rhei_scripts);
rhei_ctx.initialize(&mut variables);
Ok(Document { roots, components, variables, rhei_scripts, stylesheet })
}
fn parse_block_elements(
r: &mut Reader,
variables: &mut HashMap<String, String>,
components: &mut HashMap<String, ComponentDef>,
rhei_scripts: &mut Vec<String>,
stylesheet: &mut SS,
is_root: bool,
) -> Result<Vec<Element>, 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 => stack.push(Element::new(r.read_string()?)),
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_as_string(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 let Some(value) = r.read_value_as_string(vop)? {
if let Some(el) = stack.last_mut() {
let stored = if vop == OP_PROP_RHEI {
format!("{RHEI_PREFIX}{value}")
} else {
value
};
el.properties.insert("text".to_string(), stored);
}
}
}
OP_PROP_STR => {
let (key, val) = (r.read_string()?, r.read_string()?);
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
}
OP_PROP_VAR => {
let key = r.read_string()?;
let val = format!("${}", r.read_string()?);
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
}
OP_PROP_INT => {
let key = r.read_string()?;
let val = r.read_i64()?.to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
}
OP_PROP_FLOAT => {
let key = r.read_string()?;
let val = r.read_f64()?.to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
}
OP_PROP_BOOL => {
let key = r.read_string()?;
let val = (r.read_byte()? != 0).to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
}
OP_PROP_RHEI => {
let key = r.read_string()?;
let expr = r.read_string()?;
if let Some(el) = stack.last_mut() {
el.properties.insert(key, format!("{RHEI_PREFIX}{expr}"));
}
}
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
let key = r.read_string()?;
if let Some(val) = r.read_value_as_string(op)? {
if let Some(el) = stack.last_mut() { el.properties.insert(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".to_string());
text_el.properties.insert(
"text".to_string(),
format!("{RHEI_PREFIX}{script}"),
);
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, 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, false,
)?;
let has_else = r.read_byte()? == 1;
let false_children = if has_else {
Self::parse_block_elements(
r, variables, components, rhei_scripts, stylesheet, false,
)?
} else {
Vec::new()
};
let mut if_el = Element::new("@if".to_string());
if_el.properties.insert("condition".to_string(), cond_val);
if_el.children = true_children;
if !false_children.is_empty() {
let mut else_el = Element::new("@else".to_string());
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, false,
)?;
let mut each_el = Element::new("@each".to_string());
each_el.properties.insert("var_name".to_string(), var_name);
each_el.properties.insert("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.properties.insert(
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(
templates: &[Element],
variables: &HashMap<String, String>,
components: &HashMap<String, ComponentDef>,
rhei: &RheiContext,
stylesheet: &SS,
) -> Vec<Element> {
let mut output = Vec::with_capacity(templates.len());
for el in templates {
match el.type_name.as_str() {
"@if" => {
let cond = el.properties.get("condition").cloned().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(
&active_branch, variables, components, rhei, stylesheet,
));
}
"@each" => {
let var_name = el.properties.get("var_name").cloned().unwrap_or_default();
let source_expr = el.properties.get("source").cloned().unwrap_or_default();
let resolved_source = if let Some(expr) = source_expr.strip_prefix(RHEI_PREFIX) {
rhei.eval_expr(expr, variables)
.map(|s| Self::normalize_rhai_array(&s))
.unwrap_or_default()
} else {
Self::resolve_string(&source_expr, variables)
};
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 mut local_vars = variables.clone();
local_vars.insert(var_name.clone(), item);
output.extend(Self::evaluate_vdom(
&el.children, &local_vars, components, rhei, stylesheet,
));
}
}
_ => {
if let Some(comp) = components.get(&el.type_name) {
// Expand custom component
let mut comp_scope = variables.clone();
for (param, _) in &comp.params {
if let Some(arg) = el.properties.get(param) {
comp_scope.insert(
param.clone(),
Self::resolve_prop(arg, variables, rhei),
);
}
}
let mut vcomp = Element::new(el.type_name.clone());
for (k, v) in &el.properties {
if k.starts_with("__on:") {
vcomp.properties.insert(k.clone(), Self::resolve_string(v, variables));
} else {
vcomp.properties.insert(k.clone(), Self::resolve_prop(v, variables, rhei));
}
}
vcomp.computed_style = ComputedStyle::compute(
&vcomp.properties,
stylesheet.resolve(&vcomp.type_name),
);
vcomp.children = Self::evaluate_vdom(
&comp.children, &comp_scope, components, rhei, stylesheet,
);
output.push(vcomp);
} else {
let mut vnode = Element::new(el.type_name.clone());
for (k, v) in &el.properties {
if k.starts_with("__on:") {
vnode.properties.insert(k.clone(), Self::resolve_string(v, variables));
continue;
}
if v.starts_with('$')
&& !v[1..].contains(|c: char| !c.is_ascii_alphanumeric() && c != '_')
{
vnode.properties.insert(
format!("__bind:{k}"),
v[1..].to_string(),
);
}
vnode.properties.insert(
k.clone(),
Self::resolve_prop(v, variables, rhei),
);
}
vnode.computed_style = ComputedStyle::compute(
&vnode.properties,
stylesheet.resolve(&el.type_name),
);
vnode.children = Self::evaluate_vdom(
&el.children, variables, components, rhei, stylesheet,
);
output.push(vnode);
}
}
}
}
output
}
fn resolve_prop(v: &str, variables: &HashMap<String, String>, rhei: &RheiContext) -> String {
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
rhei.eval_expr(expr, variables).unwrap_or_default()
} else {
Self::resolve_string(v, variables)
}
}
fn evaluate_condition(
cond: &str,
variables: &HashMap<String, String>,
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(&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(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(val: &str, scope: &HashMap<String, String>) -> String {
let re = RE_VAR.get_or_init(|| Regex::new(r"\$([a-zA-Z0-9_]+)").unwrap());
re.replace_all(val, |caps: &regex::Captures| {
scope.get(&caps[1])
.map(|s| s.as_str())
.unwrap_or(&caps[0])
.to_string()
})
.to_string()
}
fn attach(stack: &mut Vec<Element>, roots: &mut Vec<Element>, el: Element) {
match stack.last_mut() {
Some(parent) => parent.children.push(el),
None => roots.push(el),
}
}
}