feat: new styles

This commit is contained in:
faynot
2026-07-08 23:45:37 +03:00
parent 9c279015dc
commit 75af23930f
18 changed files with 3556 additions and 816 deletions

View File

@@ -1,9 +1,11 @@
pub mod opcodes;
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, InterpError};
@@ -11,7 +13,7 @@ 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 style::{AncestorInfo, ComputedStyle, StructuralContext};
use regex::Regex;
use std::collections::HashMap;
use std::sync::OnceLock;
@@ -21,8 +23,8 @@ 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); }
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);
@@ -41,20 +43,20 @@ impl Interpreter {
true,
)?;
let rhei_ctx = RheiContext::new(&rhei_scripts);
rhei_ctx.initialize(&mut variables);
//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> {
fn parse_block_elements<'a>(
r: &mut Reader<'a>,
variables: &mut HashMap<String, String>,
components: &mut HashMap<String, ComponentDef<'a>>,
rhei_scripts: &mut Vec<String>,
stylesheet: &mut SS,
is_root: bool,
) -> Result<Vec<Element<'a>>, InterpError> {
let mut roots: Vec<Element> = Vec::new();
let mut stack: Vec<Element> = Vec::new();
@@ -63,7 +65,7 @@ impl Interpreter {
if !is_root && op == OP_END_BLOCK { break; }
match op {
OP_ELEM_PUSH => stack.push(Element::new(r.read_string()?)),
OP_ELEM_PUSH => stack.push(Element::new(r.read_str_ref()?)),
OP_ELEM_POP => {
let finished = stack.pop().ok_or(InterpError::UnexpectedPop)?;
@@ -90,53 +92,63 @@ impl Interpreter {
OP_CONTENT => {
let vop = r.read_byte()?;
if let Some(value) = r.read_value_as_string(vop)? {
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() {
let stored = if vop == OP_PROP_RHEI {
format!("{RHEI_PREFIX}{value}")
} else {
value
};
el.properties.insert("text".to_string(), stored);
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() {
el.push_prop("text".to_string(), value);
}
}
}
OP_PROP_STR => {
let (key, val) = (r.read_string()?, r.read_string()?);
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
let key = r.read_str_ref()?;
let val = r.read_str_ref()?;
if let Some(el) = stack.last_mut() { el.push_prop(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); }
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() { el.push_prop(key, val); }
}
OP_PROP_INT => {
let key = r.read_string()?;
let key = r.read_str_ref()?;
let val = r.read_i64()?.to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
}
OP_PROP_FLOAT => {
let key = r.read_string()?;
let key = r.read_str_ref()?;
let val = r.read_f64()?.to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
}
OP_PROP_BOOL => {
let key = r.read_string()?;
let key = r.read_str_ref()?;
let val = (r.read_byte()? != 0).to_string();
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
}
OP_PROP_RHEI => {
let key = r.read_string()?;
let expr = r.read_string()?;
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() {
el.properties.insert(key, format!("{RHEI_PREFIX}{expr}"));
el.push_prop(key, val);
}
}
OP_PROP_UNIT | OP_PROP_CALL | OP_PROP_IDENT | OP_PROP_FSPATH | OP_PROP_COLOR => {
let key = r.read_string()?;
let key = r.read_str_ref()?;
if let Some(val) = r.read_value_as_string(op)? {
if let Some(el) = stack.last_mut() { el.properties.insert(key, val); }
if let Some(el) = stack.last_mut() { el.push_prop(key, val); }
}
}
@@ -145,8 +157,8 @@ impl Interpreter {
if is_root && stack.is_empty() {
rhei_scripts.push(script);
} else {
let mut text_el = Element::new("#text".to_string());
text_el.properties.insert(
let mut text_el = Element::new("#text");
text_el.push_prop(
"text".to_string(),
format!("{RHEI_PREFIX}{script}"),
);
@@ -187,12 +199,12 @@ impl Interpreter {
Vec::new()
};
let mut if_el = Element::new("@if".to_string());
if_el.properties.insert("condition".to_string(), cond_val);
let mut if_el = Element::new("@if");
if_el.push_prop("condition", cond_val);
if_el.children = true_children;
if !false_children.is_empty() {
let mut else_el = Element::new("@else".to_string());
let mut else_el = Element::new("@else");
else_el.children = false_children;
if_el.children.push(else_el);
}
@@ -214,9 +226,9 @@ impl Interpreter {
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);
let mut each_el = Element::new("@each");
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);
}
@@ -245,7 +257,7 @@ impl Interpreter {
}
if let Some(el) = stack.last_mut() {
el.properties.insert(
el.push_prop(
format!("__on:{event_name}"),
handler_script,
);
@@ -278,20 +290,54 @@ impl Interpreter {
Ok(roots)
}
pub fn evaluate_vdom(
templates: &[Element],
variables: &HashMap<String, String>,
components: &HashMap<String, ComponentDef>,
rhei: &RheiContext,
stylesheet: &SS,
) -> Vec<Element> {
pub fn evaluate_vdom<'a>(
templates: &[Element<'a>],
variables: &mut HashMap<String, String>,
components: &HashMap<String, ComponentDef<'a>>,
rhei: &RheiContext,
stylesheet: &SS,
ancestors: &[AncestorInfo],
) -> 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 {
match el.type_name.as_str() {
*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.properties.get("condition").cloned().unwrap_or_default();
let is_true = Self::evaluate_condition(&cond, variables, rhei);
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 {
@@ -302,21 +348,19 @@ impl Interpreter {
}
}
output.extend(Self::evaluate_vdom(
&active_branch, variables, components, rhei, stylesheet,
&active_branch, variables, components, rhei, stylesheet, ancestors,
));
}
"@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 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) {
rhei.eval_expr(expr, variables)
.map(|s| Self::normalize_rhai_array(&s))
.unwrap_or_default()
} else {
Self::resolve_string(&source_expr, variables)
};
Self::normalize_rhai_array(&rhei.eval_expr(expr, variables))
} else {
Self::resolve_string(source_expr, variables).into_owned()
};
let items: Vec<String> = if resolved_source.is_empty() {
vec![]
@@ -325,73 +369,84 @@ impl Interpreter {
};
for item in items {
let mut local_vars = variables.clone();
local_vars.insert(var_name.clone(), item);
let old_val = variables.insert(var_name.to_string(), item);
output.extend(Self::evaluate_vdom(
&el.children, &local_vars, components, rhei, stylesheet,
&el.children, variables, components, rhei, stylesheet, ancestors,
));
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) {
// Expand custom component
let mut comp_scope = variables.clone();
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.properties.get(param) {
comp_scope.insert(
if let Some(arg) = el.get_prop(param) {
new_args.push((
param.clone(),
Self::resolve_prop(arg, variables, rhei),
);
));
}
}
let mut vcomp = Element::new(el.type_name.clone());
let mut old_vals = Vec::with_capacity(new_args.len());
for (k, v) in new_args {
old_vals.push((k.clone(), variables.insert(k, v)));
}
let mut vcomp = Element::new(el.type_name);
for (k, v) in &el.properties {
if k.starts_with("__on:") {
vcomp.properties.insert(k.clone(), Self::resolve_string(v, variables));
vcomp.set_prop(k.clone(), Self::resolve_string(v, variables).into_owned());
} else {
vcomp.properties.insert(k.clone(), Self::resolve_prop(v, variables, rhei));
vcomp.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
}
}
vcomp.computed_style = ComputedStyle::compute(
&vcomp.properties,
stylesheet.resolve(&vcomp.type_name),
);
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(
&comp.children, &comp_scope, components, rhei, stylesheet,
&comp.children, variables, components, rhei, stylesheet, &child_ancestors,
);
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.clone());
let mut vnode = Element::new(el.type_name);
for (k, v) in &el.properties {
if k.starts_with("__on:") {
vnode.properties.insert(k.clone(), Self::resolve_string(v, variables));
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.properties.insert(
format!("__bind:{k}"),
v[1..].to_string(),
);
vnode.set_prop(format!("__bind:{k}"), v[1..].to_string());
}
vnode.properties.insert(
k.clone(),
Self::resolve_prop(v, variables, rhei),
);
vnode.set_prop(k.clone(), Self::resolve_prop(v, variables, rhei));
}
vnode.computed_style = ComputedStyle::compute(
&vnode.properties,
stylesheet.resolve(&el.type_name),
);
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(
&el.children, variables, components, rhei, stylesheet,
&el.children, variables, components, rhei, stylesheet, &child_ancestors,
);
output.push(vnode);
}
@@ -402,14 +457,46 @@ impl Interpreter {
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 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, String>, rhei: &RheiContext) -> String {
if let Some(expr) = v.strip_prefix(RHEI_PREFIX) {
rhei.eval_expr(expr, variables)
} else {
Self::resolve_string(v, variables).into_owned()
}
}
fn evaluate_condition(
cond: &str,
variables: &HashMap<String, String>,
@@ -476,18 +563,38 @@ impl Interpreter {
}
}
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()
pub fn resolve_string<'a>(val: &'a str, scope: &HashMap<String, String>) -> Cow<'a, str> {
if !val.contains('$') {
return Cow::Borrowed(val);
}
fn attach(stack: &mut Vec<Element>, roots: &mut Vec<Element>, el: Element) {
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) {
result.push_str(resolved);
} 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),

View File

@@ -7,81 +7,115 @@ pub struct Reader<'a> {
}
impl<'a> Reader<'a> {
#[inline(always)]
pub fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
#[inline(always)]
pub fn remaining(&self) -> usize {
self.data.len() - self.pos
}
#[inline(always)]
fn require(&self, n: usize) -> Result<(), InterpError> {
if self.pos + n > self.data.len() {
Err(InterpError::UnexpectedEof)
} else {
Ok(())
}
}
#[inline(always)]
pub fn read_byte(&mut self) -> Result<u8, InterpError> {
self.require(1)?;
let b = self.data[self.pos];
let b = unsafe { *self.data.get_unchecked(self.pos) };
self.pos += 1;
Ok(b)
}
#[inline(always)]
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());
let v = unsafe {
let ptr = self.data.as_ptr().add(self.pos) as *const [u8; 4];
u32::from_le_bytes(std::ptr::read_unaligned(ptr))
};
self.pos += 4;
Ok(v)
}
#[inline(always)]
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());
let v = unsafe {
let ptr = self.data.as_ptr().add(self.pos) as *const [u8; 8];
i64::from_le_bytes(std::ptr::read_unaligned(ptr))
};
self.pos += 8;
Ok(v)
}
#[inline(always)]
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());
let v = unsafe {
let ptr = self.data.as_ptr().add(self.pos) as *const [u8; 8];
f64::from_le_bytes(std::ptr::read_unaligned(ptr))
};
self.pos += 8;
Ok(v)
}
#[inline(always)]
pub fn read_str_ref(&mut self) -> Result<&'a str, InterpError> {
let len = self.read_u32()? as usize;
self.require(len)?;
let slice = &self.data[self.pos .. self.pos + len];
self.pos += len;
unsafe {
Ok(std::str::from_utf8_unchecked(slice))
}
}
#[inline(always)]
pub fn read_string(&mut self) -> Result<String, InterpError> {
self.read_str_ref().map(|s| s.to_owned())
}
#[inline(always)]
pub fn skip_string(&mut self) -> Result<(), 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)
Ok(())
}
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 => {
OP_PROP_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_RHEI | 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())
let name = self.read_str_ref()?;
let mut s = String::with_capacity(name.len() + 1);
s.push('$');
s.push_str(name);
Some(s)
}
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()?;
let num = self.read_f64()?;
let unit = self.read_str_ref()?;
if num.fract() == 0.0 {
Some(format!("{}{}", num as i64, unit))
} else {
@@ -89,13 +123,12 @@ impl<'a> Reader<'a> {
}
}
OP_PROP_CALL => {
let name = self.read_string()?;
let name = self.read_string()?;
let arg_count = self.read_u32()? as usize;
let mut args = Vec::with_capacity(arg_count);
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();
let op = self.read_byte()?;
let val = self.read_value_as_string(op)?.unwrap_or_default();
args.push(val);
}
Some(format!("{}({})", name, args.join(",")))
@@ -105,7 +138,6 @@ impl<'a> Reader<'a> {
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);
@@ -118,21 +150,15 @@ impl<'a> Reader<'a> {
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_STR | OP_PROP_COLOR | OP_PROP_FSPATH | OP_PROP_VAR | OP_PROP_RHEI | OP_PROP_IDENT => {
self.skip_string()?;
}
OP_PROP_INT => { self.read_i64()?; }
OP_PROP_INT => { self.read_i64()?; }
OP_PROP_FLOAT => { self.read_f64()?; }
OP_PROP_BOOL => { self.read_byte()?; }
OP_PROP_NULL => {}
OP_PROP_BOOL => { self.read_byte()?; }
OP_PROP_NULL => {}
OP_PROP_ARRAY => {
let count = self.read_u32()?;
for _ in 0..count {
@@ -141,11 +167,11 @@ impl<'a> Reader<'a> {
}
}
OP_PROP_UNIT => {
self.read_f64()?; // number
self.read_string()?; // unit suffix
self.read_f64()?;
self.skip_string()?;
}
OP_PROP_CALL => {
self.read_string()?; // function name
self.skip_string()?;
let arg_count = self.read_u32()?;
for _ in 0..arg_count {
let op = self.read_byte()?;
@@ -157,49 +183,38 @@ impl<'a> Reader<'a> {
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_VERSION => { self.read_i64()?; }
OP_STYLE | OP_RHEI_BLK => { self.skip_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_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.skip_string()?;
self.skip_value(op)?;
}
OP_GLOBAL | OP_LET => {
self.read_string()?;
self.skip_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
OP_SINGLETON => {
self.read_string()?;
self.skip_string()?;
let count = self.read_u32()?;
for _ in 0..count {
self.read_string()?;
self.skip_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
}
OP_COMPONENT => {
self.read_string()?;
self.skip_string()?;
let params = self.read_u32()?;
for _ in 0..params {
self.read_string()?;
self.read_string()?;
self.skip_string()?;
self.skip_string()?;
}
self.skip_block()?;
}
@@ -210,50 +225,48 @@ impl<'a> Reader<'a> {
if self.read_byte()? == 1 { self.skip_block()?; }
}
OP_EACH => {
self.read_string()?;
self.skip_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
self.skip_block()?;
}
OP_ON => {
self.read_string()?;
self.skip_string()?;
let args = self.read_u32()?;
for _ in 0..args {
self.read_string()?;
self.skip_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
self.skip_block()?;
}
OP_ELEM_PUSH => { self.read_string()?; }
OP_CONTENT => {
OP_ELEM_PUSH => { self.skip_string()?; }
OP_CONTENT => {
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
OP_STYLE_RULE => {
self.read_string()?; // selector
self.skip_string()?;
let count = self.read_u32()?;
for _ in 0..count {
self.read_string()?; // property key
self.skip_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?; // property value
self.skip_value(vop)?;
}
}
OP_STYLE_ANIM => {
self.read_string()?; // animation name
self.skip_string()?;
let frame_count = self.read_u32()?;
for _ in 0..frame_count {
self.read_string()?; // step ("from", "to", "50%", …)
self.skip_string()?;
let prop_count = self.read_u32()?;
for _ in 0..prop_count {
self.read_string()?;
self.skip_string()?;
let vop = self.read_byte()?;
self.skip_value(vop)?;
}
}
}
_ => {}
}
Ok(())
@@ -266,15 +279,4 @@ impl<'a> Reader<'a> {
self.skip_opcode(op)?;
}
}
#[inline]
fn require(&self, n: usize) -> Result<(), InterpError> {
if self.remaining() < n {
Err(InterpError::UnexpectedEof)
} else {
Ok(())
}
}
}

View File

@@ -1,12 +1,13 @@
use rhai::{Dynamic, Engine, Scope, AST};
use rhai::{Dynamic, Engine, Scope, AST, Module};
use std::collections::HashMap;
use std::cell::RefCell;
pub const RHEI_PREFIX: &str = "__rhei:";
pub struct RheiContext {
engine: Engine,
init_ast: AST,
fn_ast: AST,
scope: RefCell<Scope<'static>>,
}
impl RheiContext {
@@ -29,102 +30,88 @@ impl RheiContext {
let mut fn_ast = combined.clone();
fn_ast.clear_statements();
Self {
engine,
init_ast: combined,
fn_ast
match Module::eval_ast_as_new(Scope::new(), &fn_ast, &engine) {
Ok(module) => {
engine.register_global_module(module.into());
}
Err(e) => {
eprintln!("⚠️ Rhei module creation error: {e}");
}
}
Self {
engine,
init_ast: combined,
scope: RefCell::new(Scope::new()),
}
}
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
let mut scope = Scope::new();
pub fn sync_scope(&self, variables: &HashMap<String, String>) {
let mut scope = self.scope.borrow_mut();
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));
if scope.contains(k) {
if let Some(old_val) = scope.get_value::<Dynamic>(k) {
if dyn_to_str(&old_val) == *v {
continue;
}
}
scope.set_value(k, str_to_dyn(v));
} else {
scope.push_dynamic(k.clone(), str_to_dyn(v));
}
}
}
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));
pub fn initialize(&self, variables: &mut HashMap<String, String>) {
self.sync_scope(variables);
let mut scope = self.scope.borrow_mut();
if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &self.init_ast) {
eprintln!("⚠️ Rhei initialization error: {e}");
}
let expr_ast = self.engine
.compile_expression(expr)
.or_else(|_| self.engine.compile(expr))
.ok()?;
for (name, _, val) in scope.iter_raw() {
let s_val = dyn_to_str(&val);
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
variables.insert(name.to_string(), s_val);
}
}
}
let full = self.fn_ast.merge(&expr_ast);
pub fn eval_expr(&self, expr: &str, variables: &HashMap<String, String>) -> String {
self.sync_scope(variables);
let mut scope = self.scope.borrow_mut();
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
match self.engine.eval_expression_with_scope::<Dynamic>(&mut *scope, expr) {
Ok(val) => dyn_to_str(&val),
Err(e) => {
eprintln!("⚠️ Rhei eval_expr error: {e}");
String::new()
}
}
}
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));
}
self.sync_scope(variables);
let mut scope = self.scope.borrow_mut();
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()
}
match self.engine.eval_expression_with_scope::<bool>(&mut *scope, expr) {
Ok(b) => b,
Err(e) => {
eprintln!("⚠️ Rhei condition eval `{expr}`: {e}");
eprintln!("⚠️ Rhei eval_condition error: {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));
}
self.sync_scope(variables);
let mut scope = self.scope.borrow_mut();
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) {
if let Err(e) = self.engine.run_ast_with_scope(&mut *scope, &action_ast) {
eprintln!("⚠️ Rhei action execution error: {e}");
}
}
@@ -133,12 +120,10 @@ impl RheiContext {
}
}
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));
for (name, _, val) in scope.iter_raw() {
let s_val = dyn_to_str(&val);
if variables.get(name).map(|v| v != &s_val).unwrap_or(true) {
variables.insert(name.to_string(), s_val);
}
}
}
@@ -150,7 +135,6 @@ impl Default for RheiContext {
}
}
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); }
@@ -158,12 +142,9 @@ pub fn str_to_dyn(s: &str) -> Dynamic {
Dynamic::from(s.to_owned())
}
pub fn dyn_to_str(val: &Dynamic) -> String {
if val.is_string() {
return val.clone().cast::<String>();
pub fn dyn_to_str(d: &Dynamic) -> String {
if d.is_string() {
return d.clone().into_string().unwrap_or_default();
}
if val.is_unit() {
return String::new();
}
val.to_string()
d.to_string()
}

View File

@@ -1,8 +1,465 @@
use std::collections::HashMap;
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub enum AttributeSelector {
Exists(String),
Equals(String, String),
}
fn parse_attribute(input: &str) -> AttributeSelector {
let input = input.trim();
if let Some((name, val)) = input.split_once('=') {
let name = name.trim().to_string();
let val = val.trim().trim_matches('"').trim_matches('\'').to_string();
AttributeSelector::Equals(name, val)
} else {
AttributeSelector::Exists(input.to_string())
}
}
#[derive(Debug, Clone)]
pub struct CompoundSelector {
pub tag: Option<String>,
pub id: Option<String>,
pub classes: Vec<String>,
pub pseudo_classes: Vec<String>,
pub attributes: Vec<AttributeSelector>,
}
impl CompoundSelector {
fn parse(input: &str) -> Self {
let input = input.trim();
let mut tag = None;
let mut id = None;
let mut classes = Vec::new();
let mut pseudo_classes = Vec::new();
let mut attributes = Vec::new();
let mut current = String::new();
let mut delim: Option<char> = None;
for ch in input.chars() {
match ch {
'#' | '.' | ':' | '[' => {
if !current.is_empty() {
match (delim, ch) {
(Some('.'), _) => classes.push(std::mem::take(&mut current)),
(Some(':'), _) => pseudo_classes.push(std::mem::take(&mut current)),
(Some('#'), _) => id = Some(std::mem::take(&mut current)),
(Some('['), _) => {
attributes.push(parse_attribute(&std::mem::take(&mut current)));
}
_ if tag.is_none() && id.is_none()
&& classes.is_empty() && pseudo_classes.is_empty()
&& attributes.is_empty() =>
{
tag = Some(std::mem::take(&mut current));
}
_ => current.clear(),
}
}
delim = Some(ch);
}
']' => {
if delim == Some('[') && !current.is_empty() {
attributes.push(parse_attribute(&std::mem::take(&mut current)));
delim = None;
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
match delim {
Some('#') => id = Some(current),
Some('.') => classes.push(current),
Some(':') => pseudo_classes.push(current),
Some('[') => attributes.push(parse_attribute(&current)),
None => tag = Some(current),
_ => {}
}
}
Self { tag, id, classes, pseudo_classes, attributes }
}
fn specificity(&self) -> (u32, u32, u32) {
(
if self.id.is_some() { 1 } else { 0 },
self.classes.len() as u32 + self.attributes.len() as u32 + self.pseudo_classes.len() as u32,
if self.tag.is_some() { 1 } else { 0 },
)
}
pub fn matches_element(
&self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
el_attributes: &HashMap<String, String>,
) -> bool {
if let Some(ref t) = self.tag {
if t != type_name && t != "*" {
return false;
}
}
if let Some(ref i) = self.id {
match el_id {
Some(eid) if eid == i => {}
_ => return false,
}
}
for cls in &self.classes {
if !el_classes.contains(&cls.as_str()) {
return false;
}
}
for attr in &self.attributes {
match attr {
AttributeSelector::Exists(name) => {
if !el_attributes.contains_key(name) {
return false;
}
}
AttributeSelector::Equals(name, val) => {
match el_attributes.get(name) {
Some(v) if v == val => {}
_ => return false,
}
}
}
}
for pc in &self.pseudo_classes {
match pc.as_str() {
"first-child" => {
if structural.sibling_index != 0 { return false; }
}
"last-child" => {
if structural.sibling_index + 1 != structural.sibling_total { return false; }
}
"first-of-type" => {
if structural.type_index != 0 { return false; }
}
"empty" => {
if structural.has_children { return false; }
}
"root" => {
if !structural.is_root { return false; }
}
s if s.starts_with("nth-child(") => {
let inner = &s[10..s.len().saturating_sub(1)];
if !nth_matches(inner, structural.sibling_index + 1) {
return false;
}
}
_ => {
if !active_pseudo.contains(&pc.as_str()) {
return false;
}
}
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combinator {
Descendant, // Space
Child, // >
NextSibling, // +
Subsequent, // ~
}
#[derive(Debug, Clone)]
pub struct ComplexSelector {
pub compounds: Vec<CompoundSelector>,
pub combinators: Vec<Combinator>,
}
impl ComplexSelector {
pub fn parse(input: &str) -> Self {
let input = input.trim();
if input.is_empty() {
return Self { compounds: vec![], combinators: vec![] };
}
let mut parts: Vec<(String, Combinator)> = Vec::new();
let mut buf = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'>' | '+' | '~' => {
let right = buf.trim().to_string();
let combinator = match ch {
'>' => Combinator::Child,
'+' => Combinator::NextSibling,
'~' => Combinator::Subsequent,
_ => unreachable!(),
};
if !right.is_empty() {
parts.push((right, combinator));
}
buf.clear();
while let Some(&c) = chars.peek() {
if c.is_ascii_whitespace() { chars.next(); } else { break; }
}
}
'#' | '.' | ':' => {
buf.push(ch);
}
c if c.is_ascii_whitespace() => {
let candidate = buf.trim().to_string();
if !candidate.is_empty() {
let mut peek_pos = chars.clone();
let next_nonws = peek_pos.find(|c| !c.is_ascii_whitespace());
match next_nonws {
Some('>') | Some('+') | Some('~') => {
buf.push(' ');
}
_ => {
parts.push((candidate, Combinator::Descendant));
buf.clear();
}
}
}
}
_ => buf.push(ch),
}
}
let last = buf.trim().to_string();
if !last.is_empty() {
if parts.is_empty() {
parts.push((last, Combinator::Descendant));
} else {
parts.push((last, Combinator::Descendant));
}
}
let compounds: Vec<CompoundSelector> = parts.iter().map(|(s, _)| CompoundSelector::parse(s)).collect();
let combinators: Vec<Combinator> = parts.iter().rev().skip(1).rev().map(|(_, c)| *c).collect();
Self { compounds, combinators }
}
fn check_compound_against(&self, i: usize, info: &AncestorInfo) -> bool {
let compound = &self.compounds[i];
let tag_ok = compound.tag.as_ref().map_or(true, |t| {
t == &info.type_name || t == "*"
});
let id_ok = compound.id.as_ref().map_or(true, |id| {
info.id.as_ref().map_or(false, |sid| sid == id)
});
let classes_ok = compound.classes.iter()
.all(|c| info.classes.contains(c));
tag_ok && id_ok && classes_ok
}
pub fn matches(
&self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> bool {
if self.compounds.is_empty() {
return false;
}
let target = self.compounds.last().unwrap();
if !target.matches_element(type_name, el_id, el_classes, active_pseudo, structural, el_attributes) {
return false;
}
if self.compounds.len() == 1 {
return true;
}
let mut ai = 0;
let mut si = preceding_siblings.len();
for i in (0..self.compounds.len() - 1).rev() {
let combinator = self.combinators[i];
match combinator {
Combinator::Descendant => {
let mut found = false;
for a in &ancestors[ai..] {
if self.check_compound_against(i, a) {
found = true;
break;
}
}
if !found { return false; }
}
Combinator::Child => {
if ai >= ancestors.len() { return false; }
let a = &ancestors[ai];
if !self.check_compound_against(i, a) { return false; }
ai += 1;
}
Combinator::NextSibling => {
if si == 0 { return false; }
let sib = &preceding_siblings[si - 1];
if !self.check_compound_against(i, sib) { return false; }
si -= 1;
}
Combinator::Subsequent => {
let mut found = false;
for sib in preceding_siblings[..si].iter().rev() {
if self.check_compound_against(i, sib) {
found = true;
break;
}
}
if !found { return false; }
}
}
}
true
}
pub fn specificity(&self) -> (u32, u32, u32) {
let mut a = 0u32;
let mut b = 0u32;
let mut c = 0u32;
for compound in &self.compounds {
let (sa, sb, sc) = compound.specificity();
a += sa;
b += sb;
c += sc;
}
(a, b, c)
}
pub fn as_simple(&self) -> Option<&CompoundSelector> {
if self.compounds.len() == 1 {
self.compounds.first()
} else {
None
}
}
pub fn has_pseudo_class(&self, pc: &str) -> bool {
self.compounds.iter().any(|c| c.pseudo_classes.iter().any(|p| p == pc))
}
}
#[derive(Debug, Clone)]
pub struct AncestorInfo {
pub type_name: String,
pub id: Option<String>,
pub classes: Vec<String>,
}
impl AncestorInfo {
pub fn new(type_name: &str, classes: Vec<String>) -> Self {
Self { type_name: type_name.to_string(), id: None, classes }
}
pub fn new_with_id(type_name: &str, id: Option<String>, classes: Vec<String>) -> Self {
Self { type_name: type_name.to_string(), id, classes }
}
}
#[derive(Debug, Clone, Default)]
pub struct StructuralContext {
pub sibling_index: usize,
pub sibling_total: usize,
pub type_index: usize,
pub type_total: usize,
pub has_children: bool,
pub is_root: bool,
}
fn nth_matches(expr: &str, n: usize) -> bool {
let expr = expr.trim();
if expr.eq_ignore_ascii_case("odd") {
return n % 2 == 1;
}
if expr.eq_ignore_ascii_case("even") {
return n % 2 == 0;
}
if let Ok(num) = expr.parse::<i32>() {
return n == num as usize;
}
let expr_lower = expr.to_lowercase();
if let Some(n_pos) = expr_lower.find('n') {
let a_str = expr_lower[..n_pos].trim();
let b_str = expr_lower[n_pos + 1..].trim();
let a = if a_str.is_empty() || a_str == "+" {
1
} else if a_str == "-" {
-1
} else {
a_str.parse::<i32>().unwrap_or(0)
};
let b = if b_str.is_empty() {
0
} else if b_str.starts_with('+') {
b_str[1..].trim().parse::<i32>().unwrap_or(0)
} else if b_str.starts_with('-') {
b_str.parse::<i32>().unwrap_or(0)
} else {
b_str.parse::<i32>().unwrap_or(0)
};
if a == 0 {
return n == b as usize;
}
let n_i32 = n as i32;
if a > 0 {
let k = n_i32 - b;
k >= 0 && k % a == 0
} else {
let diff = b - n_i32;
diff >= 0 && diff % a.abs() == 0
}
} else {
false
}
}
#[derive(Debug, Clone)]
pub struct StyleRule {
pub selector: ComplexSelector,
pub properties: HashMap<String, String>,
}
impl StyleRule {
pub fn build(selector_str: String, properties: HashMap<String, String>) -> Self {
Self {
selector: ComplexSelector::parse(&selector_str),
properties,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StyleSheet {
index: HashMap<String, HashMap<String, String>>,
rules: Vec<StyleRule>,
}
impl StyleSheet {
@@ -11,80 +468,374 @@ impl StyleSheet {
}
pub fn add_rule(&mut self, selector: String, properties: HashMap<String, String>) {
self.index
.entry(selector.trim().to_string())
.or_default()
.extend(properties);
for part in split_selectors(&selector) {
self.rules.push(StyleRule::build(part, properties.clone()));
}
}
#[inline]
pub fn resolve(&self, element_type: &str) -> Option<&HashMap<String, String>> {
self.index.get(element_type.trim())
pub fn matching_rules<'a>(
&'a self,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
active_pseudo: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> Vec<&'a HashMap<String, String>> {
let mut matched: Vec<(usize, &StyleRule)> = self
.rules
.iter()
.enumerate()
.filter(|(_, rule)| {
rule.selector
.matches(type_name, el_id, el_classes, active_pseudo, structural, ancestors, preceding_siblings, el_attributes)
})
.collect();
matched.sort_by(|(i, a), (j, b)| {
a.selector
.specificity()
.cmp(&b.selector.specificity())
.then_with(|| i.cmp(j))
});
matched.into_iter().map(|(_, rule)| &rule.properties).collect()
}
pub fn matching_pseudo_rules(
&self,
pseudo: &str,
type_name: &str,
el_id: Option<&str>,
el_classes: &[&str],
structural: &StructuralContext,
ancestors: &[AncestorInfo],
preceding_siblings: &[AncestorInfo],
el_attributes: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut props = HashMap::new();
for rule in &self.rules {
if !rule.selector.has_pseudo_class(pseudo) {
continue;
}
if !rule.selector.matches(type_name, el_id, el_classes, &[pseudo], structural, ancestors, preceding_siblings, el_attributes) {
continue;
}
for (k, v) in &rule.properties {
props.insert(k.clone(), v.clone());
}
}
props
}
pub fn is_empty(&self) -> bool {
self.index.is_empty()
self.rules.is_empty()
}
}
fn split_selectors(input: &str) -> Vec<String> {
let mut parts = Vec::new();
let mut depth = 0u32;
let mut start = 0usize;
for (i, ch) in input.char_indices() {
match ch {
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = input[start..i].trim();
if !part.is_empty() {
parts.push(part.to_string());
}
start = i + 1;
}
_ => {}
}
}
let last = input[start..].trim();
if !last.is_empty() {
parts.push(last.to_string());
}
parts
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Overflow {
#[default]
Visible,
Hidden,
Scroll,
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Position {
#[default]
Static,
Relative,
Absolute,
Sticky,
Fixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Display {
#[default]
Block,
Flex,
Grid,
Inline,
None,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ComputedStyle {
pub font_size: Option<f32>,
pub color: Option<iced::Color>,
pub padding: Option<f32>,
pub padding_top: Option<f32>,
pub padding_right: Option<f32>,
pub padding_bottom:Option<f32>,
pub padding_left: Option<f32>,
pub margin: Option<f32>,
pub margin_top: Option<f32>,
pub margin_right: Option<f32>,
pub margin_bottom: Option<f32>,
pub margin_left: 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 width: Option<iced::Length>,
pub height: Option<iced::Length>,
pub min_width: Option<f32>,
pub max_width: Option<f32>,
pub min_height: Option<f32>,
pub max_height: Option<f32>,
pub direction: Option<LayoutDirection>,
pub align_items: Option<iced::Alignment>,
pub content_align: Option<ContentAlign>,
pub flex_grow: Option<u16>,
pub position: Option<Position>,
pub top: Option<f32>,
pub right: Option<f32>,
pub bottom: Option<f32>,
pub left: Option<f32>,
pub overflow_x: Option<Overflow>,
pub overflow_y: Option<Overflow>,
pub display: Option<Display>,
pub opacity: Option<f32>,
pub font_weight: Option<u16>,
pub line_height: Option<f32>,
pub text_align: Option<TextAlign>,
}
impl ComputedStyle {
pub fn compute(
inline: &HashMap<String, String>,
sheet: Option<&HashMap<String, String>>,
pub fn compute(
inline: &[(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&HashMap<String, String>],
) -> Self {
let overflow = lookup("overflow", inline, matched_sheets).and_then(parse_overflow);
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),
font_size: lookup("font-size", inline, matched_sheets).and_then(parse_size),
color: lookup("color", inline, matched_sheets).and_then(parse_color),
padding: lookup("padding", inline, matched_sheets).and_then(parse_size),
padding_top: lookup("padding-top", inline, matched_sheets).and_then(parse_size),
padding_right: lookup("padding-right", inline, matched_sheets).and_then(parse_size),
padding_bottom: lookup("padding-bottom", inline, matched_sheets).and_then(parse_size),
padding_left: lookup("padding-left", inline, matched_sheets).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),
margin: lookup("margin", inline, matched_sheets).and_then(parse_size),
margin_top: lookup("margin-top", inline, matched_sheets).and_then(parse_size),
margin_right: lookup("margin-right", inline, matched_sheets).and_then(parse_size),
margin_bottom: lookup("margin-bottom", inline, matched_sheets).and_then(parse_size),
margin_left: lookup("margin-left", inline, matched_sheets).and_then(parse_size),
background: lookup("background", inline, matched_sheets)
.or_else(|| lookup("background-color", inline, matched_sheets))
.and_then(parse_color),
spacing: lookup("spacing", inline, matched_sheets)
.or_else(|| lookup("gap", inline, matched_sheets))
.and_then(parse_size),
border_radius: lookup("border-radius", inline, matched_sheets).and_then(parse_size),
border_width: lookup("border-width", inline, matched_sheets).and_then(parse_size),
border_color: lookup("border-color", inline, matched_sheets).and_then(parse_color),
width: lookup("width", inline, matched_sheets).and_then(parse_length),
height: lookup("height", inline, matched_sheets).and_then(parse_length),
min_width: lookup("min-width", inline, matched_sheets).and_then(parse_size),
max_width: lookup("max-width", inline, matched_sheets).and_then(parse_size),
min_height: lookup("min-height", inline, matched_sheets).and_then(parse_size),
max_height: lookup("max-height", inline, matched_sheets).and_then(parse_size),
direction: lookup("direction", inline, matched_sheets).and_then(parse_direction),
align_items: lookup("align-items", inline, matched_sheets).and_then(parse_alignment),
content_align: lookup("content-align", inline, matched_sheets).and_then(parse_content_align),
flex_grow: lookup("flex-grow", inline, matched_sheets)
.and_then(|s| s.trim().parse::<f32>().ok())
.map(|v| v as u16),
position: lookup("position", inline, matched_sheets).and_then(parse_position),
top: lookup("top", inline, matched_sheets).and_then(parse_size),
right: lookup("right", inline, matched_sheets).and_then(parse_size),
bottom: lookup("bottom", inline, matched_sheets).and_then(parse_size),
left: lookup("left", inline, matched_sheets).and_then(parse_size),
overflow_x: lookup("overflow-x", inline, matched_sheets)
.and_then(parse_overflow)
.or(overflow),
overflow_y: lookup("overflow-y", inline, matched_sheets)
.and_then(parse_overflow)
.or(overflow),
display: lookup("display", inline, matched_sheets).and_then(parse_display),
opacity: lookup("opacity", inline, matched_sheets).and_then(parse_opacity),
font_weight: lookup("font-weight", inline, matched_sheets).and_then(parse_font_weight),
line_height: lookup("line-height", inline, matched_sheets).and_then(parse_size),
text_align: lookup("text-align", inline, matched_sheets).and_then(parse_text_align),
}
}
pub fn apply_overrides(&mut self, sheet: &HashMap<String, String>) {
struct Override<'a>(&'a HashMap<String, String>);
impl<'a> Override<'a> {
fn get(&self, key: &str) -> Option<&'a str> { self.0.get(key).map(|s| s.as_str()) }
}
let ov = Override(sheet);
if let Some(v) = ov.get("font-size") { self.font_size = parse_size(v); }
if let Some(v) = ov.get("color") { self.color = parse_color(v); }
if let Some(v) = ov.get("padding") { self.padding = parse_size(v); }
if let Some(v) = ov.get("padding-top") { self.padding_top = parse_size(v); }
if let Some(v) = ov.get("padding-right") { self.padding_right = parse_size(v); }
if let Some(v) = ov.get("padding-bottom") { self.padding_bottom = parse_size(v); }
if let Some(v) = ov.get("padding-left") { self.padding_left = parse_size(v); }
if let Some(v) = ov.get("margin") { self.margin = parse_size(v); }
if let Some(v) = ov.get("margin-top") { self.margin_top = parse_size(v); }
if let Some(v) = ov.get("margin-right") { self.margin_right = parse_size(v); }
if let Some(v) = ov.get("margin-bottom") { self.margin_bottom = parse_size(v); }
if let Some(v) = ov.get("margin-left") { self.margin_left = parse_size(v); }
if let Some(v) = ov.get("background").or_else(|| ov.get("background-color")) {
self.background = parse_color(v);
}
if let Some(v) = ov.get("spacing").or_else(|| ov.get("gap")) {
self.spacing = parse_size(v);
}
if let Some(v) = ov.get("border-radius") { self.border_radius = parse_size(v); }
if let Some(v) = ov.get("border-width") { self.border_width = parse_size(v); }
if let Some(v) = ov.get("border-color") { self.border_color = parse_color(v); }
if let Some(v) = ov.get("width") { self.width = parse_length(v); }
if let Some(v) = ov.get("height") { self.height = parse_length(v); }
if let Some(v) = ov.get("direction") { self.direction = parse_direction(v); }
if let Some(v) = ov.get("align-items") { self.align_items = parse_alignment(v); }
if let Some(v) = ov.get("content-align") { self.content_align = parse_content_align(v); }
if let Some(v) = ov.get("flex-grow") { self.flex_grow = v.trim().parse::<f32>().ok().map(|x| x as u16); }
if let Some(v) = ov.get("position") { self.position = parse_position(v); }
if let Some(v) = ov.get("top") { self.top = parse_size(v); }
if let Some(v) = ov.get("right") { self.right = parse_size(v); }
if let Some(v) = ov.get("bottom") { self.bottom = parse_size(v); }
if let Some(v) = ov.get("left") { self.left = parse_size(v); }
if let Some(v) = ov.get("overflow") { let o = parse_overflow(v);
if o.is_some() { self.overflow_x = o; self.overflow_y = o; } }
if let Some(v) = ov.get("overflow-x") { self.overflow_x = parse_overflow(v); }
if let Some(v) = ov.get("overflow-y") { self.overflow_y = parse_overflow(v); }
if let Some(v) = ov.get("display") { self.display = parse_display(v); }
if let Some(v) = ov.get("opacity") { self.opacity = parse_opacity(v); }
if let Some(v) = ov.get("font-weight") { self.font_weight = parse_font_weight(v); }
if let Some(v) = ov.get("line-height") { self.line_height = parse_size(v); }
if let Some(v) = ov.get("text-align") { self.text_align = parse_text_align(v); }
}
}
#[inline]
fn lookup<'a>(
key: &str,
inline: &'a HashMap<String, String>,
sheet: Option<&'a HashMap<String, String>>,
inline: &'a [(Cow<'_, str>, Cow<'_, str>)],
matched_sheets: &[&'a HashMap<String, String>],
) -> Option<&'a str> {
if let Some(v) = inline.get(key) {
return Some(v.as_str());
for (k, v) in inline {
if **k == *key { return Some(v.as_ref()); }
if k.len() == key.len() + 6
&& k.starts_with("style:")
&& &k[6..] == key
{
return Some(v.as_ref());
}
}
if let Some(v) = inline.get(&format!("style:{key}")) {
return Some(v.as_str());
for sheet in matched_sheets.iter().rev() {
if let Some(v) = sheet.get(key) {
return Some(v.as_str());
}
}
sheet.and_then(|s| s.get(key)).map(String::as_str)
None
}
pub fn parse_overflow(s: &str) -> Option<Overflow> {
let s = s.trim();
if s.eq_ignore_ascii_case("visible") { Some(Overflow::Visible) }
else if s.eq_ignore_ascii_case("hidden") { Some(Overflow::Hidden) }
else if s.eq_ignore_ascii_case("scroll") { Some(Overflow::Scroll) }
else if s.eq_ignore_ascii_case("auto") { Some(Overflow::Auto) }
else { None }
}
pub fn parse_position(s: &str) -> Option<Position> {
let s = s.trim();
if s.eq_ignore_ascii_case("fixed") { Some(Position::Fixed) }
else if s.eq_ignore_ascii_case("sticky") { Some(Position::Sticky) }
else if s.eq_ignore_ascii_case("absolute") { Some(Position::Absolute) }
else if s.eq_ignore_ascii_case("relative") { Some(Position::Relative) }
else if s.eq_ignore_ascii_case("static") { Some(Position::Static) }
else { None }
}
pub fn parse_length(s: &str) -> Option<iced::Length> {
let s = s.trim();
if s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("100%") ||
s.eq_ignore_ascii_case("100vw") || s.eq_ignore_ascii_case("100vh") ||
s.eq_ignore_ascii_case("stretch") {
return Some(iced::Length::Fill);
}
if s.eq_ignore_ascii_case("shrink") || s.eq_ignore_ascii_case("fit-content") || s.eq_ignore_ascii_case("auto") {
return Some(iced::Length::Shrink);
}
let val = s.trim_end_matches(|c: char| c.is_alphabetic() || c == '%').parse::<f32>().ok()?;
if s.ends_with('%') {
if val >= 100.0 {
Some(iced::Length::Fill)
} else {
Some(iced::Length::FillPortion(val as u16))
}
} else {
Some(iced::Length::Fixed(val))
}
}
pub fn parse_color(s: &str) -> Option<iced::Color> {
let s = s.trim();
@@ -148,36 +899,91 @@ pub enum ContentAlign {
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,
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
impl From<TextAlign> for iced::alignment::Horizontal {
fn from(ta: TextAlign) -> Self {
match ta {
TextAlign::Left => iced::alignment::Horizontal::Left,
TextAlign::Center => iced::alignment::Horizontal::Center,
TextAlign::Right => iced::alignment::Horizontal::Right,
}
}
}
pub fn parse_direction(s: &str) -> Option<LayoutDirection> {
let s = s.trim();
if s.eq_ignore_ascii_case("horizontal") || s.eq_ignore_ascii_case("row") { Some(LayoutDirection::Row) }
else if s.eq_ignore_ascii_case("vertical") || s.eq_ignore_ascii_case("column") { Some(LayoutDirection::Column) }
else if s.eq_ignore_ascii_case("grid") { Some(LayoutDirection::Grid) }
else { 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,
}
let s = s.trim();
if s.eq_ignore_ascii_case("start") { Some(iced::Alignment::Start) }
else if s.eq_ignore_ascii_case("center") { Some(iced::Alignment::Center) }
else if s.eq_ignore_ascii_case("end") { Some(iced::Alignment::End) }
else { 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,
let s = s.trim();
if s.eq_ignore_ascii_case("start") || s.eq_ignore_ascii_case("left") || s.eq_ignore_ascii_case("top") { Some(ContentAlign::Start) }
else if s.eq_ignore_ascii_case("center") { Some(ContentAlign::Center) }
else if s.eq_ignore_ascii_case("end") || s.eq_ignore_ascii_case("right") || s.eq_ignore_ascii_case("bottom") { Some(ContentAlign::End) }
else { None }
}
pub fn parse_display(s: &str) -> Option<Display> {
let s = s.trim();
if s.eq_ignore_ascii_case("none") { Some(Display::None) }
else if s.eq_ignore_ascii_case("block") { Some(Display::Block) }
else if s.eq_ignore_ascii_case("flex") { Some(Display::Flex) }
else if s.eq_ignore_ascii_case("grid") { Some(Display::Grid) }
else if s.eq_ignore_ascii_case("inline") { Some(Display::Inline) }
else { None }
}
pub fn parse_opacity(s: &str) -> Option<f32> {
let v = s.trim().parse::<f32>().ok()?;
Some(v.clamp(0.0, 1.0))
}
pub fn parse_font_weight(s: &str) -> Option<u16> {
let s = s.trim();
match s {
"normal" => Some(400),
"bold" => Some(700),
"lighter" => Some(300),
"bolder" => Some(900),
_ => s.parse::<f32>().ok().map(|v| v as u16),
}
}
pub fn parse_text_align(s: &str) -> Option<TextAlign> {
let s = s.trim();
if s.eq_ignore_ascii_case("left") { Some(TextAlign::Left) }
else if s.eq_ignore_ascii_case("center") { Some(TextAlign::Center) }
else if s.eq_ignore_ascii_case("right") { Some(TextAlign::Right) }
else { None }
}
pub fn parse_size(s: &str) -> Option<f32> {
s.trim()
.trim_end_matches(|c: char| c.is_alphabetic() || c == '%')
let s = s.trim();
if s.ends_with('%') || s.eq_ignore_ascii_case("auto") ||
s.eq_ignore_ascii_case("fill") || s.eq_ignore_ascii_case("stretch") {
return None;
}
if s.eq_ignore_ascii_case("vw") || s.eq_ignore_ascii_case("vh") {
return None;
}
s.trim_end_matches(|c: char| c.is_alphabetic())
.parse::<f32>()
.ok()
}

View File

@@ -1,38 +1,45 @@
use std::borrow::Cow;
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 struct Element<'a> {
pub type_name: &'a str,
pub properties: Vec<(Cow<'a, str>, Cow<'a, str>)>,
pub children: Vec<Element<'a>>,
pub computed_style: ComputedStyle,
}
impl Element {
pub fn new(type_name: String) -> Self {
impl<'a> Element<'a> {
pub fn id(&self) -> Option<&str> {
self.properties.iter().find_map(|(k, v)| {
if **k == *"id" { Some(v.as_ref()) } else { None }
})
}
pub fn new(type_name: &'a str) -> Self {
Self {
type_name,
properties: HashMap::new(),
children: Vec::new(),
properties: Vec::with_capacity(8),
children: Vec::with_capacity(4),
computed_style: ComputedStyle::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct ComponentDef {
pub struct ComponentDef<'a> {
pub name: String,
pub params: Vec<(String, String)>,
pub children: Vec<Element>,
pub children: Vec<Element<'a>>,
}
#[derive(Debug, Clone)]
pub struct Document {
pub roots: Vec<Element>,
pub components: HashMap<String, ComponentDef>,
pub struct Document<'a> {
pub roots: Vec<Element<'a>>,
pub components: HashMap<String, ComponentDef<'a>>,
pub variables: HashMap<String, String>,
pub rhei_scripts: Vec<String>,
pub stylesheet: StyleSheet,