feat: add styles
This commit is contained in:
312
src/style_parser.rs
Normal file
312
src/style_parser.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
use crate::ast::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct StyleParser<'a> {
|
||||
src: &'a [u8],
|
||||
pos: usize,
|
||||
pub module: &'a mut ModuleSoA,
|
||||
variables: HashMap<String, Value>,
|
||||
mixins: HashMap<String, Vec<(String, Value)>>,
|
||||
}
|
||||
|
||||
impl<'a> StyleParser<'a> {
|
||||
pub fn new(src: &'a str, module: &'a mut ModuleSoA) -> Self {
|
||||
Self {
|
||||
src: src.as_bytes(),
|
||||
pos: 0,
|
||||
module,
|
||||
variables: HashMap::new(),
|
||||
mixins: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<u8> {
|
||||
self.src.get(self.pos).copied()
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.pos += 1;
|
||||
}
|
||||
|
||||
fn consume_if(&mut self, expected: u8) -> bool {
|
||||
if self.peek() == Some(expected) {
|
||||
self.pos += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
let len = self.src.len();
|
||||
while self.pos < len {
|
||||
let c = self.src[self.pos];
|
||||
if c.is_ascii_whitespace() {
|
||||
self.pos += 1;
|
||||
} else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' {
|
||||
self.pos += 2;
|
||||
while self.pos < len && self.src[self.pos] != b'\n' {
|
||||
self.pos += 1;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ident(&mut self) -> String {
|
||||
let start = self.pos;
|
||||
while self.pos < self.src.len() {
|
||||
let c = self.src[self.pos];
|
||||
if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_') {
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() }
|
||||
}
|
||||
|
||||
fn parse_selector(&mut self) -> String {
|
||||
let start = self.pos;
|
||||
while self.pos < self.src.len() && self.src[self.pos] != b'{' {
|
||||
self.pos += 1;
|
||||
}
|
||||
unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).trim().to_string() }
|
||||
}
|
||||
|
||||
fn parse_single_value(&self, s: &str) -> Value {
|
||||
let s = s.trim_end_matches(',');
|
||||
|
||||
if let Some(stripped) = s.strip_prefix('#') {
|
||||
return Value::Color(format!("#{}", stripped));
|
||||
}
|
||||
|
||||
if let Some(var_name) = s.strip_prefix('$') {
|
||||
if let Some(val) = self.variables.get(var_name) {
|
||||
return val.clone();
|
||||
}
|
||||
return Value::Variable(var_name.to_string());
|
||||
}
|
||||
|
||||
if s.contains('(') && s.ends_with(')') {
|
||||
let parts: Vec<&str> = s.splitn(2, '(').collect();
|
||||
let name = parts[0].trim().to_string();
|
||||
let args_str = parts[1].trim_end_matches(')');
|
||||
let args = args_str.split(',')
|
||||
.filter(|a| !a.trim().is_empty())
|
||||
.map(|a| self.parse_single_value(a.trim()))
|
||||
.collect();
|
||||
return Value::Call(name, args);
|
||||
}
|
||||
|
||||
let mut num_end = 0;
|
||||
for (i, c) in s.char_indices() {
|
||||
if c.is_ascii_digit() || c == '.' || c == '-' {
|
||||
num_end = i + c.len_utf8();
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
if num_end > 0 && num_end < s.len() {
|
||||
if let Ok(num) = s[..num_end].parse::<f64>() {
|
||||
return Value::Unit(num, s[num_end..].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(i) = s.parse::<i64>() { return Value::Int(i); }
|
||||
if let Ok(f) = s.parse::<f64>() { return Value::Float(f); }
|
||||
if s == "true" { return Value::Bool(true); }
|
||||
if s == "false" { return Value::Bool(false); }
|
||||
if s == "null" { return Value::Null; }
|
||||
|
||||
Value::Ident(s.to_string())
|
||||
}
|
||||
|
||||
fn parse_value_line(&mut self) -> Value {
|
||||
let start = self.pos;
|
||||
let mut parens = 0;
|
||||
|
||||
while self.pos < self.src.len() {
|
||||
let c = self.src[self.pos];
|
||||
if c == b'(' { parens += 1; }
|
||||
else if c == b')' { parens -= 1; }
|
||||
else if parens == 0 && matches!(c, b'\n' | b'\r' | b';' | b'}') {
|
||||
break;
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
|
||||
let line = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.trim();
|
||||
if self.peek() == Some(b';') { self.advance(); }
|
||||
|
||||
let mut tokens = Vec::new();
|
||||
let mut current = String::new();
|
||||
let mut p = 0;
|
||||
for c in line.chars() {
|
||||
match c {
|
||||
'(' => { p += 1; current.push(c); }
|
||||
')' => { p -= 1; current.push(c); }
|
||||
' ' | '\t' if p == 0 => {
|
||||
if !current.is_empty() {
|
||||
tokens.push(current.clone());
|
||||
current.clear();
|
||||
}
|
||||
}
|
||||
_ => current.push(c),
|
||||
}
|
||||
}
|
||||
if !current.is_empty() { tokens.push(current); }
|
||||
|
||||
if tokens.len() == 1 {
|
||||
self.parse_single_value(&tokens[0])
|
||||
} else {
|
||||
Value::Array(tokens.into_iter().map(|t| self.parse_single_value(&t)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_properties(&mut self, target: &mut Vec<(String, Value)>) -> Result<(), String> {
|
||||
self.consume_if(b'{');
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.consume_if(b'}') { break; }
|
||||
|
||||
if self.peek() == Some(b'@') {
|
||||
self.advance();
|
||||
let ident = self.parse_ident();
|
||||
if ident == "use" {
|
||||
self.skip_whitespace();
|
||||
let mixin_name = self.parse_ident();
|
||||
if let Some(props) = self.mixins.get(&mixin_name) {
|
||||
target.extend(props.clone());
|
||||
} else {
|
||||
return Err(format!("Unknown mixin: {}", mixin_name));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = self.parse_ident();
|
||||
if key.is_empty() { return Err("Expected property key".into()); }
|
||||
|
||||
self.skip_whitespace();
|
||||
if !self.consume_if(b':') { return Err("Expected ':' after property".into()); }
|
||||
self.skip_whitespace();
|
||||
|
||||
let val = self.parse_value_line();
|
||||
target.push((key, val));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_top_level(&mut self) -> Result<Option<NodeId>, String> {
|
||||
match self.peek() {
|
||||
Some(b'$') => {
|
||||
self.advance();
|
||||
let name = self.parse_ident();
|
||||
self.skip_whitespace();
|
||||
self.consume_if(b'=');
|
||||
self.skip_whitespace();
|
||||
let val = self.parse_value_line();
|
||||
self.variables.insert(name, val);
|
||||
Ok(None)
|
||||
}
|
||||
Some(b'@') => {
|
||||
self.advance();
|
||||
let name = self.parse_ident();
|
||||
match name.as_str() {
|
||||
"mixin" => {
|
||||
self.skip_whitespace();
|
||||
let mixin_name = self.parse_ident();
|
||||
self.skip_whitespace();
|
||||
let mut props = Vec::new();
|
||||
self.parse_properties(&mut props)?;
|
||||
self.mixins.insert(mixin_name, props);
|
||||
Ok(None)
|
||||
}
|
||||
"anim" => {
|
||||
self.skip_whitespace();
|
||||
let anim_name = self.parse_ident();
|
||||
self.skip_whitespace();
|
||||
self.consume_if(b'{');
|
||||
|
||||
let mut frames = Vec::new();
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.consume_if(b'}') { break; }
|
||||
|
||||
let step = self.parse_selector();
|
||||
let mut props = Vec::new();
|
||||
self.parse_properties(&mut props)?;
|
||||
|
||||
let start_idx = self.module.prop_keys.len() as u32;
|
||||
for (k, v) in props {
|
||||
self.module.prop_keys.push(k);
|
||||
self.module.prop_values.push(v);
|
||||
}
|
||||
let len = self.module.prop_keys.len() as u32 - start_idx;
|
||||
frames.push((step, (start_idx, len)));
|
||||
}
|
||||
|
||||
let id = self.module.push_directive(Directive::StyleAnim { name: anim_name, frames });
|
||||
Ok(Some(NodeId::Directive(id)))
|
||||
}
|
||||
"if" => {
|
||||
self.skip_whitespace();
|
||||
let start = self.pos;
|
||||
while self.pos < self.src.len() && self.src[self.pos] != b'{' { self.pos += 1; }
|
||||
let cond_str = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.trim();
|
||||
|
||||
self.consume_if(b'{');
|
||||
let start_idx = self.module.hierarchy.len() as u32;
|
||||
loop {
|
||||
self.skip_whitespace();
|
||||
if self.consume_if(b'}') { break; }
|
||||
if let Some(node) = self.parse_top_level()? {
|
||||
self.module.hierarchy.push(node);
|
||||
}
|
||||
}
|
||||
let len = self.module.hierarchy.len() as u32 - start_idx;
|
||||
|
||||
// Condition evaluates at runtime via Rhei
|
||||
let condition = Value::Rhei(cond_str.to_string());
|
||||
let id = self.module.push_directive(Directive::If {
|
||||
condition,
|
||||
child_span: (start_idx, len),
|
||||
else_span: None
|
||||
});
|
||||
Ok(Some(NodeId::Directive(id)))
|
||||
}
|
||||
_ => Err(format!("Unknown directive: @{}", name)),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let selector = self.parse_selector();
|
||||
if selector.is_empty() { return Err("Expected selector".into()); }
|
||||
|
||||
let mut props = Vec::new();
|
||||
self.parse_properties(&mut props)?;
|
||||
|
||||
let start_idx = self.module.prop_keys.len() as u32;
|
||||
for (k, v) in props {
|
||||
self.module.prop_keys.push(k);
|
||||
self.module.prop_values.push(v);
|
||||
}
|
||||
let len = self.module.prop_keys.len() as u32 - start_idx;
|
||||
|
||||
let id = self.module.push_directive(Directive::StyleRule { selector, prop_span: (start_idx, len) });
|
||||
Ok(Some(NodeId::Directive(id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_all(&mut self) -> Result<(), String> {
|
||||
self.skip_whitespace();
|
||||
while self.pos < self.src.len() {
|
||||
if let Some(node) = self.parse_top_level()? {
|
||||
self.module.hierarchy.push(node);
|
||||
}
|
||||
self.skip_whitespace();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user