feat: reactivityl; vdom; rhei; styles; fs; image element
This commit is contained in:
496
src/interpreter/mod.rs
Normal file
496
src/interpreter/mod.rs
Normal 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: ®ex::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),
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/interpreter/opcodes.rs
Normal file
41
src/interpreter/opcodes.rs
Normal 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
280
src/interpreter/reader.rs
Normal 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
169
src/interpreter/rhei.rs
Normal 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
183
src/interpreter/style.rs
Normal 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
60
src/interpreter/types.rs
Normal 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 {}
|
||||
Reference in New Issue
Block a user