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),
}
}
}

View File

@@ -0,0 +1,41 @@
pub const MAGIC: &[u8; 4] = b"GLBC";
pub const MAGIC_HEADER: [u8; 4] = *b"GLBC";
// Control directives
pub const OP_VERSION: u8 = 0x01;
pub const OP_STYLE: u8 = 0x02;
pub const OP_GLOBAL: u8 = 0x03;
pub const OP_SINGLETON: u8 = 0x04;
pub const OP_COMPONENT: u8 = 0x05;
pub const OP_LET: u8 = 0x06;
pub const OP_IF: u8 = 0x07;
pub const OP_EACH: u8 = 0x08;
pub const OP_ON: u8 = 0x09;
pub const OP_RHEI_BLK: u8 = 0x0A;
pub const OP_STYLE_RULE: u8 = 0x0B;
pub const OP_STYLE_ANIM: u8 = 0x0C;
// Element tree
pub const OP_ELEM_PUSH: u8 = 0x10;
pub const OP_ELEM_POP: u8 = 0x11;
pub const OP_CONTENT: u8 = 0x12;
// Value type tags
pub const OP_PROP_STR: u8 = 0x20; // UTF-8 string literal
pub const OP_PROP_INT: u8 = 0x21; // i64 LE
pub const OP_PROP_FLOAT: u8 = 0x22; // f64 LE
pub const OP_PROP_BOOL: u8 = 0x23; // u8 (0 or 1)
pub const OP_PROP_COLOR: u8 = 0x24; // "#RRGGBB" string
pub const OP_PROP_FSPATH: u8 = 0x25; // filesystem path string
pub const OP_PROP_VAR: u8 = 0x26; // variable name string (without "$")
pub const OP_PROP_RHEI: u8 = 0x27; // Rhai expression string
pub const OP_PROP_NULL: u8 = 0x28; // no data follows
pub const OP_PROP_ARRAY: u8 = 0x29; // count:u32 + count * (type_op + data)
pub const OP_PROP_CALL: u8 = 0x2A;
pub const OP_PROP_UNIT: u8 = 0x2B;
pub const OP_PROP_IDENT: u8 = 0x2C;
// Block terminator
pub const OP_END_BLOCK: u8 = 0xFF;

280
src/interpreter/reader.rs Normal file
View File

@@ -0,0 +1,280 @@
use super::opcodes::*;
use super::types::InterpError;
pub struct Reader<'a> {
pub data: &'a [u8],
pub pos: usize,
}
impl<'a> Reader<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
pub fn remaining(&self) -> usize {
self.data.len() - self.pos
}
pub fn read_byte(&mut self) -> Result<u8, InterpError> {
self.require(1)?;
let b = self.data[self.pos];
self.pos += 1;
Ok(b)
}
pub fn read_u32(&mut self) -> Result<u32, InterpError> {
self.require(4)?;
let v = u32::from_le_bytes(self.data[self.pos..self.pos + 4].try_into().unwrap());
self.pos += 4;
Ok(v)
}
pub fn read_i64(&mut self) -> Result<i64, InterpError> {
self.require(8)?;
let v = i64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
self.pos += 8;
Ok(v)
}
pub fn read_f64(&mut self) -> Result<f64, InterpError> {
self.require(8)?;
let v = f64::from_le_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
self.pos += 8;
Ok(v)
}
pub fn read_string(&mut self) -> Result<String, InterpError> {
let len = self.read_u32()? as usize;
self.require(len)?;
let s = std::str::from_utf8(&self.data[self.pos..self.pos + len])
.map_err(|_| InterpError::InvalidUtf8)?
.to_string();
self.pos += len;
Ok(s)
}
pub fn read_value_as_string(&mut self, type_op: u8) -> Result<Option<String>, InterpError> {
let s = match type_op {
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI => {
Some(self.read_string()?)
}
OP_PROP_IDENT => {
Some(self.read_string()?)
}
OP_PROP_VAR => {
Some(format!("${}", self.read_string()?))
}
OP_PROP_INT => {
Some(self.read_i64()?.to_string())
}
OP_PROP_FLOAT => {
Some(self.read_f64()?.to_string())
}
OP_PROP_BOOL => {
Some((self.read_byte()? != 0).to_string())
}
OP_PROP_NULL => None,
OP_PROP_ARRAY => {
let items = self.read_array_as_strings()?;
Some(items.join(","))
}
OP_PROP_UNIT => {
let num = self.read_f64()?;
let unit = self.read_string()?;
if num.fract() == 0.0 {
Some(format!("{}{}", num as i64, unit))
} else {
Some(format!("{}{}", num, unit))
}
}
OP_PROP_CALL => {
let name = self.read_string()?;
let arg_count = self.read_u32()? as usize;
let mut args = Vec::with_capacity(arg_count);
for _ in 0..arg_count {
let op = self.read_byte()?;
let val = self.read_value_as_string(op)?
.unwrap_or_default();
args.push(val);
}
Some(format!("{}({})", name, args.join(",")))
}
_ => None,
};
Ok(s)
}
/// Read `OP_PROP_ARRAY` (opcode already consumed) and return elements as strings.
pub fn read_array_as_strings(&mut self) -> Result<Vec<String>, InterpError> {
let count = self.read_u32()? as usize;
let mut items = Vec::with_capacity(count);
for _ in 0..count {
let elem_op = self.read_byte()?;
if let Some(s) = self.read_value_as_string(elem_op)? {
items.push(s);
}
}
Ok(items)
}
pub fn skip_value(&mut self, type_op: u8) -> Result<(), InterpError> {
match type_op {
OP_PROP_STR
| OP_PROP_COLOR
| OP_PROP_FSPATH
| OP_PROP_VAR
| OP_PROP_RHEI
| OP_PROP_IDENT => {
self.read_string()?;
}
OP_PROP_INT => { self.read_i64()?; }
OP_PROP_FLOAT => { self.read_f64()?; }
OP_PROP_BOOL => { self.read_byte()?; }
OP_PROP_NULL => {}
OP_PROP_ARRAY => {
let count = self.read_u32()?;
for _ in 0..count {
let elem_op = self.read_byte()?;
self.skip_value(elem_op)?;
}
}
OP_PROP_UNIT => {
self.read_f64()?; // number
self.read_string()?; // unit suffix
}
OP_PROP_CALL => {
self.read_string()?; // function name
let arg_count = self.read_u32()?;
for _ in 0..arg_count {
let op = self.read_byte()?;
self.skip_value(op)?;
}
}
_ => {}
}
Ok(())
}
/// Skip a full opcode + its payload without interpreting it.
pub fn skip_opcode(&mut self, op: u8) -> Result<(), InterpError> {
match op {
OP_VERSION => { self.read_i64()?; }
OP_STYLE | OP_RHEI_BLK => { self.read_string()?; }
OP_PROP_STR
| OP_PROP_COLOR
| OP_PROP_FSPATH
| OP_PROP_VAR
| OP_PROP_RHEI
| OP_PROP_INT
| OP_PROP_FLOAT
| OP_PROP_BOOL
| OP_PROP_NULL
| OP_PROP_ARRAY
| OP_PROP_CALL
| OP_PROP_UNIT
| OP_PROP_IDENT => {
self.read_string()?; // key
self.skip_value(op)?; // value
}
OP_GLOBAL | OP_LET => {
self.read_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
OP_SINGLETON => {
self.read_string()?;
let count = self.read_u32()?;
for _ in 0..count {
self.read_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
}
OP_COMPONENT => {
self.read_string()?;
let params = self.read_u32()?;
for _ in 0..params {
self.read_string()?;
self.read_string()?;
}
self.skip_block()?;
}
OP_IF => {
let vop = self.read_byte()?;
self.skip_value(vop)?;
self.skip_block()?;
if self.read_byte()? == 1 { self.skip_block()?; }
}
OP_EACH => {
self.read_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
self.skip_block()?;
}
OP_ON => {
self.read_string()?;
let args = self.read_u32()?;
for _ in 0..args {
self.read_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
self.skip_block()?;
}
OP_ELEM_PUSH => { self.read_string()?; }
OP_CONTENT => {
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
OP_STYLE_RULE => {
self.read_string()?; // selector
let count = self.read_u32()?;
for _ in 0..count {
self.read_string()?; // property key
let vop = self.read_byte()?;
self.skip_value(vop)?; // property value
}
}
OP_STYLE_ANIM => {
self.read_string()?; // animation name
let frame_count = self.read_u32()?;
for _ in 0..frame_count {
self.read_string()?; // step ("from", "to", "50%", …)
let prop_count = self.read_u32()?;
for _ in 0..prop_count {
self.read_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
}
}
_ => {}
}
Ok(())
}
pub fn skip_block(&mut self) -> Result<(), InterpError> {
loop {
let op = self.read_byte()?;
if op == OP_END_BLOCK { return Ok(()); }
self.skip_opcode(op)?;
}
}
#[inline]
fn require(&self, n: usize) -> Result<(), InterpError> {
if self.remaining() < n {
Err(InterpError::UnexpectedEof)
} else {
Ok(())
}
}
}

169
src/interpreter/rhei.rs Normal file
View File

@@ -0,0 +1,169 @@
use rhai::{Dynamic, Engine, Scope, AST};
use std::collections::HashMap;
pub const RHEI_PREFIX: &str = "__rhei:";
pub struct RheiContext {
engine: Engine,
init_ast: AST,
fn_ast: AST,
}
impl RheiContext {
pub fn new(scripts: &[String]) -> Self {
let mut engine = Engine::new();
engine.on_print(|s| println!("[rhei] {s}"));
engine.on_debug(|s, src, pos| {
eprintln!("[rhei debug @ {src:?}:{pos}] {s}");
});
let mut combined = AST::empty();
for (i, script) in scripts.iter().enumerate() {
match engine.compile(script) {
Ok(ast) => combined = combined.merge(&ast),
Err(e) => eprintln!("⚠️ Rhei compile error (block #{i}): {e}"),
}
}
let mut fn_ast = combined.clone();
fn_ast.clear_statements();
Self {
engine,
init_ast: combined,
fn_ast
}
}
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
let mut scope = Scope::new();
for (k, v) in variables.iter() {
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &self.init_ast) {
eprintln!("⚠️ Rhei init error: {e}");
}
let names: Vec<String> = scope.iter_raw()
.map(|(name, _, _)| name.to_string())
.collect();
for name in &names {
if let Some(val) = scope.get_value::<Dynamic>(name) {
variables.insert(name.clone(), dyn_to_str(&val));
}
}
}
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> Option<String> {
let mut scope = Scope::new();
for (k, v) in variables {
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
let expr_ast = self.engine
.compile_expression(expr)
.or_else(|_| self.engine.compile(expr))
.ok()?;
let full = self.fn_ast.merge(&expr_ast);
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
Ok(val) => Some(dyn_to_str(&val)),
Err(e) => {
eprintln!("⚠️ Rhei eval `{expr}`: {e}");
None
}
}
}
pub fn eval_condition(&self, expr: &str, variables: &HashMap<String, String>) -> bool {
let mut scope = Scope::new();
for (k, v) in variables {
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
let ast = match self.engine.compile_expression(expr) {
Ok(a) => a,
Err(_) => match self.engine.compile(expr) {
Ok(a) => a,
Err(e) => {
eprintln!("⚠️ Rhei condition compile `{expr}`: {e}");
return false;
}
},
};
let full = self.fn_ast.merge(&ast);
match self.engine.eval_ast_with_scope::<Dynamic>(&mut scope, &full) {
Ok(val) => {
if val.is_bool() { return val.cast::<bool>(); }
if val.is_int() { return val.cast::<i64>() != 0; }
if val.is_float(){ return val.cast::<f64>() != 0.0; }
if val.is_string(){
let s = val.cast::<String>();
return !matches!(s.trim(), "" | "false" | "0" | "null");
}
!val.is_unit()
}
Err(e) => {
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}");
false
}
}
}
pub fn execute_action(&self, script: &str, variables: &mut HashMap<String, String>) {
let mut scope = Scope::new();
for (k, v) in variables.iter() {
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
match self.engine.compile(script) {
Ok(action_ast) => {
let full = self.fn_ast.merge(&action_ast);
if let Err(e) = self.engine.run_ast_with_scope(&mut scope, &full) {
eprintln!("⚠️ Rhei action execution error: {e}");
}
}
Err(e) => {
eprintln!("⚠️ Rhei action compilation error: {e}");
}
}
let names: Vec<String> = scope.iter_raw()
.map(|(name, _, _)| name.to_string())
.collect();
for name in &names {
if let Some(val) = scope.get_value::<Dynamic>(name) {
variables.insert(name.clone(), dyn_to_str(&val));
}
}
}
}
impl Default for RheiContext {
fn default() -> Self {
Self::new(&[])
}
}
pub fn str_to_dyn(s: &str) -> Dynamic {
if let Ok(i) = s.parse::<i64>() { return Dynamic::from(i); }
if let Ok(f) = s.parse::<f64>() { return Dynamic::from(f); }
if let Ok(b) = s.parse::<bool>() { return Dynamic::from(b); }
Dynamic::from(s.to_owned())
}
pub fn dyn_to_str(val: &Dynamic) -> String {
if val.is_string() {
return val.clone().cast::<String>();
}
if val.is_unit() {
return String::new();
}
val.to_string()
}

183
src/interpreter/style.rs Normal file
View File

@@ -0,0 +1,183 @@
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct StyleSheet {
index: HashMap<String, HashMap<String, String>>,
}
impl StyleSheet {
pub fn new() -> Self {
Self::default()
}
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
self.index
.entry(selector.trim().to_string())
.or_default()
.extend(properties);
}
#[inline]
pub fn resolve(&self, element_type: &str) -> Option<&HashMap<String, String>> {
self.index.get(element_type.trim())
}
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ComputedStyle {
pub font_size: Option<f32>,
pub color: Option<iced::Color>,
pub padding: Option<f32>,
pub background: Option<iced::Color>,
pub spacing: Option<f32>,
pub border_radius: Option<f32>,
pub border_width: Option<f32>,
pub border_color: Option<iced::Color>,
pub width: Option<f32>,
pub direction: Option<LayoutDirection>,
pub align_items: Option<iced::Alignment>,
pub content_align: Option<ContentAlign>,
}
impl ComputedStyle {
pub fn compute(
inline: &HashMap<String, String>,
sheet: Option<&HashMap<String, String>>,
) -> Self {
Self {
font_size: lookup("font-size", inline, sheet).and_then(parse_size),
color: lookup("color", inline, sheet).and_then(parse_color),
padding: lookup("padding", inline, sheet).and_then(parse_size),
background: lookup("background", inline, sheet)
.or_else(|| lookup("background-color", inline, sheet))
.and_then(parse_color),
spacing: lookup("spacing", inline, sheet)
.or_else(|| lookup("gap", inline, sheet))
.and_then(parse_size),
border_radius: lookup("border-radius", inline, sheet).and_then(parse_size),
border_width: lookup("border-width", inline, sheet).and_then(parse_size),
border_color: lookup("border-color", inline, sheet).and_then(parse_color),
width: lookup("width", inline, sheet).and_then(parse_size),
direction: lookup("direction", inline, sheet).and_then(parse_direction),
align_items: lookup("align-items", inline, sheet).and_then(parse_alignment),
content_align: lookup("content-align", inline, sheet).and_then(parse_content_align),
}
}
}
#[inline]
fn lookup<'a>(
key: &str,
inline: &'a HashMap<String, String>,
sheet: Option<&'a HashMap<String, String>>,
) -> Option<&'a str> {
if let Some(v) = inline.get(key) {
return Some(v.as_str());
}
if let Some(v) = inline.get(&format!("style:{key}")) {
return Some(v.as_str());
}
sheet.and_then(|s| s.get(key)).map(String::as_str)
}
pub fn parse_color(s: &str) -> Option<iced::Color> {
let s = s.trim();
if let Some(hex) = s.strip_prefix('#') {
return match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
Some(iced::Color::from_rgb(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
))
}
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(iced::Color::from_rgb(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some(iced::Color::from_rgba(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
))
}
_ => None,
};
}
match s {
"white" => Some(iced::Color::WHITE),
"black" => Some(iced::Color::BLACK),
"transparent" => Some(iced::Color::TRANSPARENT),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutDirection {
Column,
Row,
Grid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentAlign {
Start,
Center,
End,
}
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
match s.trim().to_lowercase().as_str() {
"horizontal" | "row" => Some(LayoutDirection::Row),
"vertical" | "column" => Some(LayoutDirection::Column),
"grid" => Some(LayoutDirection::Grid),
_ => None,
}
}
pub fn parse_alignment(s: &str) -> Option<iced::Alignment> {
match s.trim().to_lowercase().as_str() {
"start" => Some(iced::Alignment::Start),
"center" => Some(iced::Alignment::Center),
"end" => Some(iced::Alignment::End),
_ => None,
}
}
pub fn parse_content_align(s: &str) -> Option<ContentAlign> {
match s.trim().to_lowercase().as_str() {
"start" | "left" | "top" => Some(ContentAlign::Start),
"center" => Some(ContentAlign::Center),
"end" | "right" | "bottom" => Some(ContentAlign::End),
_ => None,
}
}
pub fn parse_size(s: &str) -> Option<f32> {
s.trim()
.trim_end_matches(|c: char| c.is_alphabetic() || c == '%')
.parse::<f32>()
.ok()
}

60
src/interpreter/types.rs Normal file
View File

@@ -0,0 +1,60 @@
use std::collections::HashMap;
use std::fmt;
use super::style::{ComputedStyle, StyleSheet};
#[derive(Debug, Clone)]
pub struct Element {
pub type_name: String,
pub properties: HashMap<String, String>,
pub children: Vec<Element>,
pub computed_style: ComputedStyle,
}
impl Element {
pub fn new(type_name: String) -> Self {
Self {
type_name,
properties: HashMap::new(),
children: Vec::new(),
computed_style: ComputedStyle::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct ComponentDef {
pub name: String,
pub params: Vec<(String, String)>,
pub children: Vec<Element>,
}
#[derive(Debug, Clone)]
pub struct Document {
pub roots: Vec<Element>,
pub components: HashMap<String, ComponentDef>,
pub variables: HashMap<String, String>,
pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet,
}
#[derive(Debug)]
pub enum InterpError {
BadMagic,
UnexpectedEof,
InvalidUtf8,
UnexpectedPop,
}
impl fmt::Display for InterpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadMagic => write!(f, "Bad magic bytes — not a .glbc file"),
Self::UnexpectedEof => write!(f, "Unexpected end of bytecode"),
Self::InvalidUtf8 => write!(f, "String is not valid UTF-8"),
Self::UnexpectedPop => write!(f, "OP_ELEM_POP without matching OP_ELEM_PUSH"),
}
}
}
impl std::error::Error for InterpError {}