feat: add styles
This commit is contained in:
12
src/ast.rs
12
src/ast.rs
@@ -18,6 +18,10 @@ pub enum Value {
|
|||||||
Rhei(String),
|
Rhei(String),
|
||||||
Null,
|
Null,
|
||||||
Array(Vec<Value>),
|
Array(Vec<Value>),
|
||||||
|
// glts
|
||||||
|
Call(String, Vec<Value>), // rgba(30, 30, 46, 0.88)
|
||||||
|
Unit(f64, String), // 8px, 120ms
|
||||||
|
Ident(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -57,6 +61,14 @@ pub enum Directive {
|
|||||||
child_span: (u32, u32),
|
child_span: (u32, u32),
|
||||||
},
|
},
|
||||||
RheiBlock(String),
|
RheiBlock(String),
|
||||||
|
StyleRule {
|
||||||
|
selector: String,
|
||||||
|
prop_span: (u32, u32),
|
||||||
|
},
|
||||||
|
StyleAnim {
|
||||||
|
name: String,
|
||||||
|
frames: Vec<(String, (u32, u32))>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|||||||
248
src/compiler.rs
248
src/compiler.rs
@@ -10,23 +10,22 @@ impl<'a> Compiler<'a> {
|
|||||||
pub fn new(module: &'a ModuleSoA) -> Self {
|
pub fn new(module: &'a ModuleSoA) -> Self {
|
||||||
Self {
|
Self {
|
||||||
module,
|
module,
|
||||||
buf: Vec::new(),
|
buf: Vec::with_capacity(module.hierarchy.len() * 32 + std::mem::size_of_val(&MAGIC_HEADER)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile(mut self, root_nodes: &[NodeId]) -> Vec<u8> {
|
pub fn compile(mut self, root_nodes: &[NodeId]) -> Vec<u8> {
|
||||||
self.buf.extend_from_slice(&MAGIC_HEADER);
|
self.buf.extend_from_slice(&MAGIC_HEADER);
|
||||||
|
|
||||||
self.compile_span(root_nodes);
|
self.compile_span(root_nodes);
|
||||||
|
|
||||||
self.buf
|
self.buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn compile_span(&mut self, nodes: &[NodeId]) {
|
fn compile_span(&mut self, nodes: &[NodeId]) {
|
||||||
for node in nodes {
|
for &node in nodes {
|
||||||
match node {
|
match node {
|
||||||
NodeId::Element(id) => self.compile_element(*id),
|
NodeId::Element(id) => self.compile_element(id),
|
||||||
NodeId::Directive(id) => self.compile_directive(*id),
|
NodeId::Directive(id) => self.compile_directive(id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,18 +38,17 @@ impl<'a> Compiler<'a> {
|
|||||||
self.buf.push(OP_END_BLOCK);
|
self.buf.push(OP_END_BLOCK);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_element(&mut self, id: u32) {
|
fn compile_element(&mut self, id: u32) {
|
||||||
let idx = id as usize;
|
let idx = id as usize;
|
||||||
let typ = &self.module.elem_types[idx];
|
|
||||||
|
|
||||||
self.buf.push(OP_ELEM_PUSH);
|
self.buf.push(OP_ELEM_PUSH);
|
||||||
self.write_string(typ);
|
self.write_string(&self.module.elem_types[idx]);
|
||||||
|
|
||||||
let (p_start, p_len) = self.module.elem_prop_spans[idx];
|
let (p_start, p_len) = self.module.elem_prop_spans[idx];
|
||||||
for i in p_start..(p_start + p_len) {
|
let p_start = p_start as usize;
|
||||||
let key = &self.module.prop_keys[i as usize];
|
let p_end = p_start + p_len as usize;
|
||||||
let val = &self.module.prop_values[i as usize];
|
|
||||||
self.compile_property(key, val);
|
for i in p_start..p_end {
|
||||||
|
self.compile_property(&self.module.prop_keys[i], &self.module.prop_values[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(content) = &self.module.elem_content[idx] {
|
if let Some(content) = &self.module.elem_content[idx] {
|
||||||
@@ -60,15 +58,17 @@ impl<'a> Compiler<'a> {
|
|||||||
|
|
||||||
let child_span = self.module.elem_child_spans[idx];
|
let child_span = self.module.elem_child_spans[idx];
|
||||||
if child_span.1 > 0 {
|
if child_span.1 > 0 {
|
||||||
self.compile_block(child_span);
|
let start = child_span.0 as usize;
|
||||||
|
let len = child_span.1 as usize;
|
||||||
|
let nodes = &self.module.hierarchy[start..start + len];
|
||||||
|
self.compile_span(nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.buf.push(OP_ELEM_POP);
|
self.buf.push(OP_ELEM_POP);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_directive(&mut self, id: u32) {
|
fn compile_directive(&mut self, id: u32) {
|
||||||
let dir = &self.module.directives[id as usize];
|
match &self.module.directives[id as usize] {
|
||||||
match dir {
|
|
||||||
Directive::Version(v) => {
|
Directive::Version(v) => {
|
||||||
self.buf.push(OP_VERSION);
|
self.buf.push(OP_VERSION);
|
||||||
self.write_i64(*v);
|
self.write_i64(*v);
|
||||||
@@ -87,19 +87,14 @@ impl<'a> Compiler<'a> {
|
|||||||
self.write_string(name);
|
self.write_string(name);
|
||||||
self.write_u32(prop_span.1);
|
self.write_u32(prop_span.1);
|
||||||
|
|
||||||
let (start, len) = *prop_span;
|
let start = prop_span.0 as usize;
|
||||||
for i in start..(start + len) {
|
let end = start + prop_span.1 as usize;
|
||||||
let key = &self.module.prop_keys[i as usize];
|
for i in start..end {
|
||||||
let val = &self.module.prop_values[i as usize];
|
self.write_string(&self.module.prop_keys[i]);
|
||||||
self.write_string(key);
|
self.compile_value(&self.module.prop_values[i]);
|
||||||
self.compile_value(val);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Directive::Component {
|
Directive::Component { name, params, child_span } => {
|
||||||
name,
|
|
||||||
params,
|
|
||||||
child_span,
|
|
||||||
} => {
|
|
||||||
self.buf.push(OP_COMPONENT);
|
self.buf.push(OP_COMPONENT);
|
||||||
self.write_string(name);
|
self.write_string(name);
|
||||||
self.write_u32(params.len() as u32);
|
self.write_u32(params.len() as u32);
|
||||||
@@ -114,11 +109,7 @@ impl<'a> Compiler<'a> {
|
|||||||
self.write_string(name);
|
self.write_string(name);
|
||||||
self.compile_value(value);
|
self.compile_value(value);
|
||||||
}
|
}
|
||||||
Directive::If {
|
Directive::If { condition, child_span, else_span } => {
|
||||||
condition,
|
|
||||||
child_span,
|
|
||||||
else_span,
|
|
||||||
} => {
|
|
||||||
self.buf.push(OP_IF);
|
self.buf.push(OP_IF);
|
||||||
self.compile_value(condition);
|
self.compile_value(condition);
|
||||||
self.compile_block(*child_span);
|
self.compile_block(*child_span);
|
||||||
@@ -129,21 +120,13 @@ impl<'a> Compiler<'a> {
|
|||||||
self.buf.push(0);
|
self.buf.push(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Directive::Each {
|
Directive::Each { item, collection, child_span } => {
|
||||||
item,
|
|
||||||
collection,
|
|
||||||
child_span,
|
|
||||||
} => {
|
|
||||||
self.buf.push(OP_EACH);
|
self.buf.push(OP_EACH);
|
||||||
self.write_string(item);
|
self.write_string(item);
|
||||||
self.compile_value(collection);
|
self.compile_value(collection);
|
||||||
self.compile_block(*child_span);
|
self.compile_block(*child_span);
|
||||||
}
|
}
|
||||||
Directive::On {
|
Directive::On { event, args, child_span } => {
|
||||||
event,
|
|
||||||
args,
|
|
||||||
child_span,
|
|
||||||
} => {
|
|
||||||
self.buf.push(OP_ON);
|
self.buf.push(OP_ON);
|
||||||
self.write_string(event);
|
self.write_string(event);
|
||||||
self.write_u32(args.len() as u32);
|
self.write_u32(args.len() as u32);
|
||||||
@@ -157,127 +140,134 @@ impl<'a> Compiler<'a> {
|
|||||||
self.buf.push(OP_RHEI_BLK);
|
self.buf.push(OP_RHEI_BLK);
|
||||||
self.write_string(code);
|
self.write_string(code);
|
||||||
}
|
}
|
||||||
|
Directive::StyleRule { selector, prop_span } => {
|
||||||
|
self.buf.push(OP_STYLE_RULE);
|
||||||
|
self.write_string(selector);
|
||||||
|
self.write_u32(prop_span.1);
|
||||||
|
|
||||||
|
let start = prop_span.0 as usize;
|
||||||
|
let end = start + prop_span.1 as usize;
|
||||||
|
for i in start..end {
|
||||||
|
self.write_string(&self.module.prop_keys[i]);
|
||||||
|
self.compile_value(&self.module.prop_values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Directive::StyleAnim { name, frames } => {
|
||||||
|
self.buf.push(OP_STYLE_ANIM);
|
||||||
|
self.write_string(name);
|
||||||
|
self.write_u32(frames.len() as u32);
|
||||||
|
for (step, span) in frames {
|
||||||
|
self.write_string(step);
|
||||||
|
self.write_u32(span.1);
|
||||||
|
let start = span.0 as usize;
|
||||||
|
let end = start + span.1 as usize;
|
||||||
|
for i in start..end {
|
||||||
|
self.write_string(&self.module.prop_keys[i]);
|
||||||
|
self.compile_value(&self.module.prop_values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn compile_property(&mut self, key: &str, val: &Value) {
|
fn compile_property(&mut self, key: &str, val: &Value) {
|
||||||
|
let opcode = match val {
|
||||||
|
Value::String(_) => OP_PROP_STR,
|
||||||
|
Value::Int(_) => OP_PROP_INT,
|
||||||
|
Value::Float(_) => OP_PROP_FLOAT,
|
||||||
|
Value::Bool(_) => OP_PROP_BOOL,
|
||||||
|
Value::Color(_) => OP_PROP_COLOR,
|
||||||
|
Value::FsPath(_) => OP_PROP_FSPATH,
|
||||||
|
Value::Variable(_) => OP_PROP_VAR,
|
||||||
|
Value::Rhei(_) => OP_PROP_RHEI,
|
||||||
|
Value::Null => OP_PROP_NULL,
|
||||||
|
Value::Array(_) => OP_PROP_ARRAY,
|
||||||
|
// glts
|
||||||
|
Value::Call(_, _) => OP_PROP_CALL,
|
||||||
|
Value::Unit(_, _) => OP_PROP_UNIT,
|
||||||
|
Value::Ident(_) => OP_PROP_IDENT,
|
||||||
|
};
|
||||||
|
self.buf.push(opcode);
|
||||||
|
self.write_string(key);
|
||||||
|
|
||||||
match val {
|
match val {
|
||||||
Value::String(s) => {
|
Value::Null => {}
|
||||||
self.buf.push(OP_PROP_STR);
|
_ => self.compile_value_data(val),
|
||||||
self.write_string(key);
|
|
||||||
self.write_string(s);
|
|
||||||
}
|
|
||||||
Value::Int(i) => {
|
|
||||||
self.buf.push(OP_PROP_INT);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_i64(*i);
|
|
||||||
}
|
|
||||||
Value::Float(f) => {
|
|
||||||
self.buf.push(OP_PROP_FLOAT);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_f64(*f);
|
|
||||||
}
|
|
||||||
Value::Bool(b) => {
|
|
||||||
self.buf.push(OP_PROP_BOOL);
|
|
||||||
self.write_string(key);
|
|
||||||
self.buf.push(if *b { 1 } else { 0 });
|
|
||||||
}
|
|
||||||
Value::Color(c) => {
|
|
||||||
self.buf.push(OP_PROP_COLOR);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_string(c);
|
|
||||||
}
|
|
||||||
Value::FsPath(p) => {
|
|
||||||
self.buf.push(OP_PROP_FSPATH);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_string(p);
|
|
||||||
}
|
|
||||||
Value::Variable(v) => {
|
|
||||||
self.buf.push(OP_PROP_VAR);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_string(v);
|
|
||||||
}
|
|
||||||
Value::Rhei(r) => {
|
|
||||||
self.buf.push(OP_PROP_RHEI);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_string(r);
|
|
||||||
}
|
|
||||||
Value::Null => {
|
|
||||||
self.buf.push(OP_PROP_NULL);
|
|
||||||
self.write_string(key);
|
|
||||||
}
|
|
||||||
Value::Array(arr) => {
|
|
||||||
self.buf.push(OP_PROP_ARRAY);
|
|
||||||
self.write_string(key);
|
|
||||||
self.write_u32(arr.len() as u32);
|
|
||||||
for v in arr {
|
|
||||||
self.compile_value(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn compile_value(&mut self, val: &Value) {
|
fn compile_value(&mut self, val: &Value) {
|
||||||
|
let opcode = match val {
|
||||||
|
Value::String(_) => OP_PROP_STR,
|
||||||
|
Value::Int(_) => OP_PROP_INT,
|
||||||
|
Value::Float(_) => OP_PROP_FLOAT,
|
||||||
|
Value::Bool(_) => OP_PROP_BOOL,
|
||||||
|
Value::Color(_) => OP_PROP_COLOR,
|
||||||
|
Value::FsPath(_) => OP_PROP_FSPATH,
|
||||||
|
Value::Variable(_) => OP_PROP_VAR,
|
||||||
|
Value::Rhei(_) => OP_PROP_RHEI,
|
||||||
|
Value::Null => OP_PROP_NULL,
|
||||||
|
Value::Array(_) => OP_PROP_ARRAY,
|
||||||
|
// glts
|
||||||
|
Value::Call(_, _) => OP_PROP_CALL,
|
||||||
|
Value::Unit(_, _) => OP_PROP_UNIT,
|
||||||
|
Value::Ident(_) => OP_PROP_IDENT,
|
||||||
|
};
|
||||||
|
self.buf.push(opcode);
|
||||||
|
self.compile_value_data(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn compile_value_data(&mut self, val: &Value) {
|
||||||
match val {
|
match val {
|
||||||
Value::String(s) => {
|
Value::String(s) | Value::Color(s) | Value::FsPath(s) | Value::Variable(s) | Value::Rhei(s) => {
|
||||||
self.buf.push(OP_PROP_STR);
|
|
||||||
self.write_string(s);
|
self.write_string(s);
|
||||||
}
|
}
|
||||||
Value::Int(i) => {
|
Value::Int(i) => self.write_i64(*i),
|
||||||
self.buf.push(OP_PROP_INT);
|
Value::Float(f) => self.write_f64(*f),
|
||||||
self.write_i64(*i);
|
Value::Bool(b) => self.buf.push(*b as u8),
|
||||||
}
|
Value::Null => {}
|
||||||
Value::Float(f) => {
|
|
||||||
self.buf.push(OP_PROP_FLOAT);
|
|
||||||
self.write_f64(*f);
|
|
||||||
}
|
|
||||||
Value::Bool(b) => {
|
|
||||||
self.buf.push(OP_PROP_BOOL);
|
|
||||||
self.buf.push(if *b { 1 } else { 0 });
|
|
||||||
}
|
|
||||||
Value::Color(c) => {
|
|
||||||
self.buf.push(OP_PROP_COLOR);
|
|
||||||
self.write_string(c);
|
|
||||||
}
|
|
||||||
Value::FsPath(p) => {
|
|
||||||
self.buf.push(OP_PROP_FSPATH);
|
|
||||||
self.write_string(p);
|
|
||||||
}
|
|
||||||
Value::Variable(v) => {
|
|
||||||
self.buf.push(OP_PROP_VAR);
|
|
||||||
self.write_string(v);
|
|
||||||
}
|
|
||||||
Value::Rhei(r) => {
|
|
||||||
self.buf.push(OP_PROP_RHEI);
|
|
||||||
self.write_string(r);
|
|
||||||
}
|
|
||||||
Value::Null => {
|
|
||||||
self.buf.push(OP_PROP_NULL);
|
|
||||||
}
|
|
||||||
Value::Array(arr) => {
|
Value::Array(arr) => {
|
||||||
self.buf.push(OP_PROP_ARRAY);
|
|
||||||
self.write_u32(arr.len() as u32);
|
self.write_u32(arr.len() as u32);
|
||||||
for v in arr {
|
for v in arr {
|
||||||
self.compile_value(v);
|
self.compile_value(v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Value::Ident(s) => self.write_string(s),
|
||||||
|
Value::Unit(num, unit) => {
|
||||||
|
self.write_f64(*num);
|
||||||
|
self.write_string(unit);
|
||||||
|
}
|
||||||
|
Value::Call(name, args) => {
|
||||||
|
self.write_string(name);
|
||||||
|
self.write_u32(args.len() as u32);
|
||||||
|
for arg in args {
|
||||||
|
self.compile_value(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn write_string(&mut self, s: &str) {
|
fn write_string(&mut self, s: &str) {
|
||||||
let bytes = s.as_bytes();
|
let bytes = s.as_bytes();
|
||||||
self.write_u32(bytes.len() as u32);
|
self.write_u32(bytes.len() as u32);
|
||||||
self.buf.extend_from_slice(bytes);
|
self.buf.extend_from_slice(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn write_u32(&mut self, v: u32) {
|
fn write_u32(&mut self, v: u32) {
|
||||||
self.buf.extend_from_slice(&v.to_le_bytes());
|
self.buf.extend_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn write_i64(&mut self, v: i64) {
|
fn write_i64(&mut self, v: i64) {
|
||||||
self.buf.extend_from_slice(&v.to_le_bytes());
|
self.buf.extend_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
fn write_f64(&mut self, v: f64) {
|
fn write_f64(&mut self, v: f64) {
|
||||||
self.buf.extend_from_slice(&v.to_le_bytes());
|
self.buf.extend_from_slice(&v.to_le_bytes());
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/lib.rs
17
src/lib.rs
@@ -2,7 +2,24 @@ pub mod ast;
|
|||||||
pub mod compiler;
|
pub mod compiler;
|
||||||
pub mod opcodes;
|
pub mod opcodes;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
pub mod style_parser;
|
||||||
|
|
||||||
pub use ast::{Directive, ModuleSoA, NodeId, Value};
|
pub use ast::{Directive, ModuleSoA, NodeId, Value};
|
||||||
pub use compiler::Compiler;
|
pub use compiler::Compiler;
|
||||||
pub use parser::Parser;
|
pub use parser::Parser;
|
||||||
|
pub use style_parser::StyleParser;
|
||||||
|
|
||||||
|
pub fn compile_project(gltm_src: &str, glts_src: &str) -> Result<Vec<u8>, String> {
|
||||||
|
let mut module = ModuleSoA::new();
|
||||||
|
|
||||||
|
let mut style_parser = StyleParser::new(glts_src, &mut module);
|
||||||
|
style_parser.parse_all()?;
|
||||||
|
|
||||||
|
let mut gltm_parser = Parser::with_module(gltm_src, module);
|
||||||
|
gltm_parser.parse_all()?;
|
||||||
|
|
||||||
|
let module = gltm_parser.module;
|
||||||
|
let compiler = Compiler::new(&module);
|
||||||
|
|
||||||
|
Ok(compiler.compile(&module.hierarchy))
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,4 +29,14 @@ pub const OP_PROP_FSPATH: u8 = 0x25;
|
|||||||
pub const OP_PROP_VAR: u8 = 0x26;
|
pub const OP_PROP_VAR: u8 = 0x26;
|
||||||
pub const OP_PROP_RHEI: u8 = 0x27;
|
pub const OP_PROP_RHEI: u8 = 0x27;
|
||||||
|
|
||||||
|
pub const OP_STYLE_RULE: u8 = 0x0B;
|
||||||
|
pub const OP_STYLE_ANIM: u8 = 0x0C;
|
||||||
|
|
||||||
|
pub const OP_PROP_CALL: u8 = 0x2A;
|
||||||
|
pub const OP_PROP_UNIT: u8 = 0x2B;
|
||||||
|
pub const OP_PROP_IDENT: u8 = 0x2C;
|
||||||
|
|
||||||
|
|
||||||
pub const OP_END_BLOCK: u8 = 0xFF;
|
pub const OP_END_BLOCK: u8 = 0xFF;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
355
src/parser.rs
355
src/parser.rs
@@ -7,20 +7,25 @@ pub struct Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
impl<'a> Parser<'a> {
|
||||||
|
|
||||||
pub fn new(src: &'a str) -> Self {
|
pub fn new(src: &'a str) -> Self {
|
||||||
|
Self::with_module(src, ModuleSoA::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_module(src: &'a str, module: ModuleSoA) -> Self {
|
||||||
Self {
|
Self {
|
||||||
src: src.as_bytes(),
|
src: src.as_bytes(),
|
||||||
pos: 0,
|
pos: 0,
|
||||||
module: ModuleSoA::new(),
|
module,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline(always)]
|
||||||
fn peek(&self) -> Option<u8> {
|
fn peek(&self) -> Option<u8> {
|
||||||
self.src.get(self.pos).copied()
|
self.src.get(self.pos).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline(always)]
|
||||||
fn advance(&mut self) {
|
fn advance(&mut self) {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
}
|
}
|
||||||
@@ -36,15 +41,15 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn skip_whitespace(&mut self) {
|
fn skip_whitespace(&mut self) {
|
||||||
while let Some(c) = self.peek() {
|
let len = self.src.len();
|
||||||
|
while self.pos < len {
|
||||||
|
let c = self.src[self.pos];
|
||||||
if c.is_ascii_whitespace() {
|
if c.is_ascii_whitespace() {
|
||||||
self.advance();
|
self.pos += 1;
|
||||||
} else if c == b'/' && self.src.get(self.pos + 1) == Some(&b'/') {
|
} else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' {
|
||||||
while let Some(cc) = self.peek() {
|
self.pos += 2;
|
||||||
if cc == b'\n' {
|
while self.pos < len && self.src[self.pos] != b'\n' {
|
||||||
break;
|
self.pos += 1;
|
||||||
}
|
|
||||||
self.advance();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
@@ -54,86 +59,89 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
fn parse_ident(&mut self) -> String {
|
fn parse_ident(&mut self) -> String {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
while let Some(c) = self.peek() {
|
let len = self.src.len();
|
||||||
if c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c == b'.' {
|
while self.pos < len {
|
||||||
self.advance();
|
let c = self.src[self.pos];
|
||||||
|
if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.') {
|
||||||
|
self.pos += 1;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::str::from_utf8(&self.src[start..self.pos])
|
unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() }
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b'{') {
|
if self.consume_if(b'{') {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
let mut depth = 1;
|
let mut depth = 1;
|
||||||
while depth > 0 {
|
let len = self.src.len();
|
||||||
let c = self.peek().ok_or("Unexpected EOF in Rhei block")?;
|
|
||||||
self.advance();
|
while depth > 0 && self.pos < len {
|
||||||
if c == b'{' { depth += 1; }
|
match self.src[self.pos] {
|
||||||
if c == b'}' { depth -= 1; }
|
b'{' => depth += 1,
|
||||||
|
b'}' => depth -= 1,
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
let expr = std::str::from_utf8(&self.src[start..self.pos - 1])
|
self.pos += 1;
|
||||||
.unwrap().trim().to_string();
|
}
|
||||||
Ok(expr)
|
if depth > 0 {
|
||||||
|
return Err("Unexpected EOF in Rhei block".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos - 1]) };
|
||||||
|
Ok(expr.trim().to_string())
|
||||||
} else {
|
} else {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
let mut parens = 0;
|
let mut parens = 0;
|
||||||
let mut brackets = 0;
|
let mut brackets = 0;
|
||||||
let mut in_str = false;
|
let mut in_str = false;
|
||||||
let mut escape = false;
|
let mut escape = false;
|
||||||
while let Some(c) = self.peek() {
|
let len = self.src.len();
|
||||||
|
|
||||||
|
while self.pos < len {
|
||||||
|
let c = self.src[self.pos];
|
||||||
if escape {
|
if escape {
|
||||||
escape = false;
|
escape = false;
|
||||||
self.advance();
|
self.pos += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if c == b'\\' {
|
|
||||||
escape = true;
|
match c {
|
||||||
self.advance();
|
b'\\' => escape = true,
|
||||||
continue;
|
b'"' => in_str = !in_str,
|
||||||
}
|
b'(' if !in_str => parens += 1,
|
||||||
if c == b'"' { in_str = !in_str; }
|
b')' if !in_str => {
|
||||||
if !in_str {
|
|
||||||
if c == b'(' { parens += 1; }
|
|
||||||
if c == b')' {
|
|
||||||
if parens == 0 { break; }
|
if parens == 0 { break; }
|
||||||
parens -= 1;
|
parens -= 1;
|
||||||
}
|
}
|
||||||
if c == b'[' { brackets += 1; }
|
b'[' if !in_str => brackets += 1,
|
||||||
if c == b']' {
|
b']' if !in_str && brackets > 0 => brackets -= 1,
|
||||||
if brackets > 0 { brackets -= 1; }
|
b',' | b'\n' | b'}' | b'{' if !in_str && parens == 0 && brackets == 0 => break,
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
if parens == 0 && brackets == 0 {
|
self.pos += 1;
|
||||||
if c == b',' || c == b'\n' || c == b'}' || c == b'{' {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) };
|
||||||
}
|
Ok(expr.trim().to_string())
|
||||||
self.advance();
|
|
||||||
}
|
|
||||||
let expr = std::str::from_utf8(&self.src[start..self.pos])
|
|
||||||
.unwrap().trim().to_string();
|
|
||||||
Ok(expr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_value(&mut self) -> Result<Value, String> {
|
fn parse_value(&mut self) -> Result<Value, String> {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let c = self.peek().ok_or("Expected value, found EOF")?;
|
let c = self.peek().ok_or("Expected value, found EOF")?;
|
||||||
|
|
||||||
match c {
|
match c {
|
||||||
b'"' => {
|
b'"' => {
|
||||||
self.advance();
|
self.advance();
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
while self.peek() != Some(b'"') {
|
let len = self.src.len();
|
||||||
if self.peek().is_none() {
|
while self.pos < len && self.src[self.pos] != b'"' {
|
||||||
return Err("Unexpected EOF inside string literal".into());
|
self.pos += 1;
|
||||||
}
|
}
|
||||||
self.advance();
|
if self.pos >= len {
|
||||||
|
return Err("Unexpected EOF inside string literal".into());
|
||||||
}
|
}
|
||||||
let val = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string();
|
let val = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string();
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -145,8 +153,7 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
loop {
|
loop {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b']') { break; }
|
if self.consume_if(b']') { break; }
|
||||||
let val = self.parse_value()?;
|
items.push(self.parse_value()?);
|
||||||
items.push(val);
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b',');
|
self.consume_if(b',');
|
||||||
}
|
}
|
||||||
@@ -154,9 +161,7 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
b'#' => {
|
b'#' => {
|
||||||
self.advance();
|
self.advance();
|
||||||
let mut color = String::from("#");
|
Ok(Value::Color(format!("#{}", self.parse_ident())))
|
||||||
color.push_str(&self.parse_ident());
|
|
||||||
Ok(Value::Color(color))
|
|
||||||
}
|
}
|
||||||
b'$' => {
|
b'$' => {
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -164,17 +169,16 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
b'!' => {
|
b'!' => {
|
||||||
self.advance();
|
self.advance();
|
||||||
let ident = self.parse_ident();
|
if self.parse_ident() != "rhei" { return Err("Expected !rhei".into()); }
|
||||||
if ident != "rhei" { return Err("Expected !rhei".into()); }
|
|
||||||
self.consume_if(b':');
|
self.consume_if(b':');
|
||||||
Ok(Value::Rhei(self.parse_rhei_expr()?))
|
Ok(Value::Rhei(self.parse_rhei_expr()?))
|
||||||
}
|
}
|
||||||
b'0'..=b'9' | b'-' => {
|
b'0'..=b'9' | b'-' => {
|
||||||
let s = self.parse_ident();
|
let s = self.parse_ident();
|
||||||
if s.contains('.') {
|
if s.contains('.') {
|
||||||
Ok(Value::Float(s.parse().unwrap()))
|
s.parse().map(Value::Float).map_err(|_| format!("Invalid float: {}", s))
|
||||||
} else {
|
} else {
|
||||||
Ok(Value::Int(s.parse().unwrap()))
|
s.parse().map(Value::Int).map_err(|_| format!("Invalid int: {}", s))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -186,13 +190,15 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
"fs" => {
|
"fs" => {
|
||||||
self.consume_if(b':');
|
self.consume_if(b':');
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
while let Some(cc) = self.peek() {
|
let len = self.src.len();
|
||||||
if cc.is_ascii_whitespace() || cc == b',' || cc == b')' || cc == b'}' {
|
while self.pos < len {
|
||||||
|
let cc = self.src[self.pos];
|
||||||
|
if cc.is_ascii_whitespace() || matches!(cc, b',' | b')' | b'}') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
self.advance();
|
self.pos += 1;
|
||||||
}
|
}
|
||||||
let path = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string();
|
let path = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.to_string();
|
||||||
Ok(Value::FsPath(path))
|
Ok(Value::FsPath(path))
|
||||||
}
|
}
|
||||||
_ => Err(format!("Unknown value token: {}", s)),
|
_ => Err(format!("Unknown value token: {}", s)),
|
||||||
@@ -206,22 +212,22 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
loop {
|
loop {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b')') {
|
if self.consume_if(b')') { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
let key = self.parse_ident();
|
let key = self.parse_ident();
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
return Err(format!(
|
return Err(format!("Expected property identifier, found {:?}", self.peek().map(|b| b as char)));
|
||||||
"Expected property identifier, found {:?}",
|
|
||||||
self.peek().map(|b| b as char)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b'=') {
|
if !self.consume_if(b'=') {
|
||||||
|
return Err(format!("Expected '=' after property key '{}', found {:?}", key, self.peek().map(|b| b as char)));
|
||||||
|
}
|
||||||
|
|
||||||
let val = self.parse_value()?;
|
let val = self.parse_value()?;
|
||||||
self.module.prop_keys.push(key);
|
self.module.prop_keys.push(key);
|
||||||
self.module.prop_values.push(val);
|
self.module.prop_values.push(val);
|
||||||
}
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b',');
|
self.consume_if(b',');
|
||||||
}
|
}
|
||||||
@@ -238,11 +244,8 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
let mut direct_children = Vec::new();
|
let mut direct_children = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b'}') {
|
if self.consume_if(b'}') { break; }
|
||||||
break;
|
direct_children.push(self.parse_node()?);
|
||||||
}
|
|
||||||
let node = self.parse_node()?;
|
|
||||||
direct_children.push(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_idx = self.module.hierarchy.len() as u32;
|
let start_idx = self.module.hierarchy.len() as u32;
|
||||||
@@ -257,148 +260,102 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
let dir = match name.as_str() {
|
let dir = match name.as_str() {
|
||||||
"version" => {
|
"version" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let v = self.parse_ident().parse().unwrap();
|
Directive::Version(self.parse_ident().parse().map_err(|_| "Invalid version")?)
|
||||||
Directive::Version(v)
|
|
||||||
}
|
}
|
||||||
"style" => Directive::Style(match self.parse_value()? {
|
"style" => match self.parse_value()? {
|
||||||
Value::String(s) => s,
|
Value::String(s) => Directive::Style(s),
|
||||||
_ => return Err("Style must be a string".into()),
|
_ => return Err("Style must be a string".into()),
|
||||||
}),
|
},
|
||||||
"global" => {
|
"global" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if !self.consume_if(b'$') {
|
if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @global".into()); }
|
||||||
return Err("Expected $ var".into());
|
let name = self.parse_ident();
|
||||||
}
|
|
||||||
let var_name = self.parse_ident();
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b'=');
|
if !self.consume_if(b'=') { return Err(format!("Expected '=' after global variable name '${}'", name)); }
|
||||||
let val = self.parse_value()?;
|
Directive::Global { name, value: self.parse_value()? }
|
||||||
Directive::Global {
|
|
||||||
name: var_name,
|
|
||||||
value: val,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"singleton" => {
|
"singleton" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let sname = self.parse_ident();
|
let name = self.parse_ident();
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let start_idx = self.module.prop_keys.len() as u32;
|
let start_idx = self.module.prop_keys.len() as u32;
|
||||||
if self.consume_if(b'{') {
|
if !self.consume_if(b'{') { return Err(format!("Expected '{{' for singleton '{}'", name)); }
|
||||||
loop {
|
loop {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b'}') {
|
if self.consume_if(b'}') { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = self.parse_ident();
|
let key = self.parse_ident();
|
||||||
if key.is_empty() {
|
if key.is_empty() { return Err("Expected singleton property identifier".into()); }
|
||||||
return Err(format!(
|
|
||||||
"Expected singleton property identifier, found {:?}",
|
|
||||||
self.peek().map(|b| b as char)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b'=');
|
if !self.consume_if(b'=') { return Err(format!("Expected '=' after singleton property key '{}'", key)); }
|
||||||
let val = self.parse_value()?;
|
let val = self.parse_value()?;
|
||||||
self.module.prop_keys.push(key);
|
self.module.prop_keys.push(key);
|
||||||
self.module.prop_values.push(val);
|
self.module.prop_values.push(val);
|
||||||
}
|
|
||||||
|
self.skip_whitespace();
|
||||||
|
self.consume_if(b',');
|
||||||
}
|
}
|
||||||
let len = (self.module.prop_keys.len() as u32) - start_idx;
|
let len = (self.module.prop_keys.len() as u32) - start_idx;
|
||||||
Directive::Singleton {
|
Directive::Singleton { name, prop_span: (start_idx, len) }
|
||||||
name: sname,
|
|
||||||
prop_span: (start_idx, len),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"component" => {
|
"component" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let cname = self.parse_ident();
|
let name = self.parse_ident();
|
||||||
let mut params = Vec::new();
|
let mut params = Vec::new();
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b'(') {
|
if self.consume_if(b'(') {
|
||||||
loop {
|
loop {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b')') {
|
if self.consume_if(b')') { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pname = self.parse_ident();
|
let pname = self.parse_ident();
|
||||||
|
if pname.is_empty() { return Err("Expected parameter name in @component".into()); }
|
||||||
if pname.is_empty() {
|
|
||||||
return Err(format!(
|
|
||||||
"Expected parameter name in @component, found {:?}",
|
|
||||||
self.peek().map(|b| b as char)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b':');
|
if !self.consume_if(b':') { return Err(format!("Expected ':' after parameter '{}'", pname)); }
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let ptype = self.parse_ident();
|
let ptype = self.parse_ident();
|
||||||
|
if ptype.is_empty() { return Err(format!("Expected type after ':' for parameter '{}'", pname)); }
|
||||||
params.push((pname, ptype));
|
params.push((pname, ptype));
|
||||||
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b',');
|
self.consume_if(b',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let child_span = self.parse_block()?;
|
Directive::Component { name, params, child_span: self.parse_block()? }
|
||||||
Directive::Component {
|
|
||||||
name: cname,
|
|
||||||
params,
|
|
||||||
child_span,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"let" => {
|
"let" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b'$');
|
if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @let".into()); }
|
||||||
let var_name = self.parse_ident();
|
let name = self.parse_ident();
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b'=');
|
if !self.consume_if(b'=') { return Err(format!("Expected '=' after variable name '${}'", name)); }
|
||||||
let val = self.parse_value()?;
|
Directive::Let { name, value: self.parse_value()? }
|
||||||
Directive::Let {
|
|
||||||
name: var_name,
|
|
||||||
value: val,
|
|
||||||
}
|
}
|
||||||
}
|
"if" => {
|
||||||
"if" => {
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
|
let condition = self.parse_value()?;
|
||||||
let expr_str = self.parse_rhei_expr()?;
|
|
||||||
let condition = Value::Rhei(expr_str);
|
|
||||||
|
|
||||||
let child_span = self.parse_block()?;
|
let child_span = self.parse_block()?;
|
||||||
let mut else_span = None;
|
let mut else_span = None;
|
||||||
self.skip_whitespace();
|
|
||||||
|
|
||||||
|
self.skip_whitespace();
|
||||||
let backup = self.pos;
|
let backup = self.pos;
|
||||||
if self.consume_if(b'@') {
|
if self.consume_if(b'@') {
|
||||||
if self.parse_ident() == "else" {
|
if self.parse_ident() == "else" {
|
||||||
else_span = Some(self.parse_block()?);
|
else_span = Some(self.parse_block()?);
|
||||||
} else {
|
} else {
|
||||||
self.pos = backup;
|
self.pos = backup; // rollback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Directive::If {
|
Directive::If { condition, child_span, else_span }
|
||||||
condition,
|
|
||||||
child_span,
|
|
||||||
else_span,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"each" => {
|
"each" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b'$');
|
if !self.consume_if(b'$') { return Err("Expected '$' before item identifier in @each".into()); }
|
||||||
let item = self.parse_ident();
|
let item = self.parse_ident();
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let in_kw = self.parse_ident();
|
if self.parse_ident() != "in" { return Err("Expected 'in' keyword in @each".into()); }
|
||||||
if in_kw != "in" {
|
Directive::Each { item, collection: self.parse_value()?, child_span: self.parse_block()? }
|
||||||
return Err("Expected 'in' in @each".into());
|
|
||||||
}
|
|
||||||
let collection = self.parse_value()?;
|
|
||||||
let child_span = self.parse_block()?;
|
|
||||||
Directive::Each {
|
|
||||||
item,
|
|
||||||
collection,
|
|
||||||
child_span,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"on" => {
|
"on" => {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
@@ -408,54 +365,54 @@ fn parse_rhei_expr(&mut self) -> Result<String, String> {
|
|||||||
if self.consume_if(b'(') {
|
if self.consume_if(b'(') {
|
||||||
loop {
|
loop {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.consume_if(b')') {
|
if self.consume_if(b')') { break; }
|
||||||
break;
|
|
||||||
}
|
|
||||||
let k = self.parse_ident();
|
let k = self.parse_ident();
|
||||||
|
if k.is_empty() { return Err("Expected argument name in @on".into()); }
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b'=');
|
if !self.consume_if(b'=') { return Err(format!("Expected '=' after event argument key '{}'", k)); }
|
||||||
let v = self.parse_value()?;
|
args.push((k, self.parse_value()?));
|
||||||
args.push((k, v));
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
self.consume_if(b',');
|
self.consume_if(b',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let child_span = self.parse_block()?;
|
Directive::On { event, args, child_span: self.parse_block()? }
|
||||||
Directive::On {
|
|
||||||
event,
|
|
||||||
args,
|
|
||||||
child_span,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => return Err(format!("Unknown directive: @{}", name)),
|
_ => return Err(format!("Unknown directive: @{}", name)),
|
||||||
};
|
};
|
||||||
Ok(NodeId::Directive(self.module.push_directive(dir)))
|
Ok(NodeId::Directive(self.module.push_directive(dir)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_element(&mut self) -> Result<NodeId, String> {
|
fn parse_element(&mut self) -> Result<NodeId, String> {
|
||||||
let name = self.parse_ident();
|
let name = self.parse_ident();
|
||||||
let el_id = self.module.push_element(name);
|
let el_id = self.module.push_element(name);
|
||||||
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
if self.peek() == Some(b'(') {
|
if self.peek() == Some(b'(') {
|
||||||
let span = self.parse_properties()?;
|
self.module.elem_prop_spans[el_id as usize] = self.parse_properties()?;
|
||||||
self.module.elem_prop_spans[el_id as usize] = span;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
let p = self.peek();
|
match self.peek() {
|
||||||
if p == Some(b'{') {
|
Some(b'{') => {
|
||||||
let span = self.parse_block()?;
|
self.module.elem_child_spans[el_id as usize] = self.parse_block()?;
|
||||||
self.module.elem_child_spans[el_id as usize] = span;
|
|
||||||
} else if let Some(ch) = p {
|
|
||||||
if ch == b'"' || ch == b'#' || ch == b'$' || ch == b'!' || ch == b'-' || ch.is_ascii_digit() || ch == b'[' || ch.is_ascii_lowercase() {
|
|
||||||
let val = self.parse_value()?;
|
|
||||||
self.module.elem_content[el_id as usize] = Some(val);
|
|
||||||
}
|
}
|
||||||
|
Some(ch) if matches!(ch, b'"' | b'#' | b'$' | b'!' | b'-' | b'[' | b'0'..=b'9') => {
|
||||||
|
self.module.elem_content[el_id as usize] = Some(self.parse_value()?);
|
||||||
|
}
|
||||||
|
Some(b'a'..=b'z') => {
|
||||||
|
let backup = self.pos;
|
||||||
|
let ident = self.parse_ident();
|
||||||
|
self.pos = backup;
|
||||||
|
|
||||||
|
if matches!(ident.as_str(), "true" | "false" | "null" | "fs") {
|
||||||
|
self.module.elem_content[el_id as usize] = Some(self.parse_value()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(NodeId::Element(el_id))
|
Ok(NodeId::Element(el_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_node(&mut self) -> Result<NodeId, String> {
|
pub fn parse_node(&mut self) -> Result<NodeId, String> {
|
||||||
self.skip_whitespace();
|
self.skip_whitespace();
|
||||||
@@ -465,22 +422,13 @@ fn parse_element(&mut self) -> Result<NodeId, String> {
|
|||||||
b'@' => self.parse_directive(),
|
b'@' => self.parse_directive(),
|
||||||
b'!' => {
|
b'!' => {
|
||||||
self.advance();
|
self.advance();
|
||||||
let i = self.parse_ident();
|
if self.parse_ident() != "rhei" { return Err("Expected rhei".into()); }
|
||||||
if i != "rhei" { return Err("Expected rhei".into()); }
|
|
||||||
self.consume_if(b':');
|
self.consume_if(b':');
|
||||||
let expr = self.parse_rhei_expr()?;
|
let expr = self.parse_rhei_expr()?;
|
||||||
Ok(NodeId::Directive(
|
Ok(NodeId::Directive(self.module.push_directive(Directive::RheiBlock(expr))))
|
||||||
self.module.push_directive(Directive::RheiBlock(expr)),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
b'A'..=b'Z' => self.parse_element(),
|
b'A'..=b'Z' => self.parse_element(),
|
||||||
b'"' | b'#' | b'$' | b'-' | b'0'..=b'9' | b'[' => {
|
ch if matches!(ch, b'"' | b'#' | b'$' | b'-' | b'[' | b'0'..=b'9') => {
|
||||||
let val = self.parse_value()?;
|
|
||||||
let el_id = self.module.push_element("#text".to_string());
|
|
||||||
self.module.elem_content[el_id as usize] = Some(val);
|
|
||||||
Ok(NodeId::Element(el_id))
|
|
||||||
}
|
|
||||||
ch if ch.is_ascii_lowercase() => {
|
|
||||||
let val = self.parse_value()?;
|
let val = self.parse_value()?;
|
||||||
let el_id = self.module.push_element("#text".to_string());
|
let el_id = self.module.push_element("#text".to_string());
|
||||||
self.module.elem_content[el_id as usize] = Some(val);
|
self.module.elem_content[el_id as usize] = Some(val);
|
||||||
@@ -489,13 +437,8 @@ fn parse_element(&mut self) -> Result<NodeId, String> {
|
|||||||
_ => {
|
_ => {
|
||||||
let extract_start = self.pos.saturating_sub(10);
|
let extract_start = self.pos.saturating_sub(10);
|
||||||
let extract_end = std::cmp::min(self.src.len(), self.pos + 20);
|
let extract_end = std::cmp::min(self.src.len(), self.pos + 20);
|
||||||
let context = std::str::from_utf8(&self.src[extract_start..extract_end])
|
let context = std::str::from_utf8(&self.src[extract_start..extract_end]).unwrap_or("");
|
||||||
.unwrap_or("");
|
Err(format!("Unexpected token: '{}' at position {}. Context: \"...{}...\"", c as char, self.pos, context.trim()))
|
||||||
|
|
||||||
Err(format!(
|
|
||||||
"Unexpected token: '{}' at position {}. Context around: \"...{}...\"",
|
|
||||||
c as char, self.pos, context.trim()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/tests.rs
269
src/tests.rs
@@ -257,3 +257,272 @@ fn test_full_desktop_compilation() {
|
|||||||
);
|
);
|
||||||
assert!(bytecode.len() > 50, "Байткод подозрительно мал");
|
assert!(bytecode.len() > 50, "Байткод подозрительно мал");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod style_parser_tests {
|
||||||
|
use crate::ast::{Directive, ModuleSoA, NodeId, Value};
|
||||||
|
use crate::style_parser::StyleParser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_parser_basic_rule() {
|
||||||
|
let input = "Screen { background: transparent; color: #fff }";
|
||||||
|
let mut m = ModuleSoA::new();
|
||||||
|
let mut parser = StyleParser::new(input, &mut m);
|
||||||
|
parser.parse_all().expect("Парсинг базового правила должен пройти успешно");
|
||||||
|
|
||||||
|
assert_eq!(m.hierarchy.len(), 1, "Должна быть одна директива");
|
||||||
|
assert_eq!(m.directives.len(), 1);
|
||||||
|
|
||||||
|
if let Directive::StyleRule { selector, prop_span } = &m.directives[0] {
|
||||||
|
assert_eq!(selector, "Screen");
|
||||||
|
assert_eq!(prop_span.1, 2, "Должно быть 2 свойства");
|
||||||
|
|
||||||
|
let start = prop_span.0 as usize;
|
||||||
|
assert_eq!(m.prop_keys[start], "background");
|
||||||
|
assert_eq!(m.prop_values[start], Value::Ident("transparent".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(m.prop_keys[start + 1], "color");
|
||||||
|
assert_eq!(m.prop_values[start + 1], Value::Color("#fff".to_string()));
|
||||||
|
} else {
|
||||||
|
panic!("Ожидался узел StyleRule");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_parser_compile_time_variables() {
|
||||||
|
let input = r#"
|
||||||
|
$bg = #1e1e2e
|
||||||
|
$pad = 16px
|
||||||
|
Panel {
|
||||||
|
background: $bg
|
||||||
|
padding: $pad
|
||||||
|
color: $Config.textColor
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut m = ModuleSoA::new();
|
||||||
|
let mut parser = StyleParser::new(input, &mut m);
|
||||||
|
parser.parse_all().expect("Парсинг с переменными");
|
||||||
|
|
||||||
|
if let Directive::StyleRule { prop_span, .. } = &m.directives[0] {
|
||||||
|
let start = prop_span.0 as usize;
|
||||||
|
assert_eq!(m.prop_values[start], Value::Color("#1e1e2e".to_string()));
|
||||||
|
assert_eq!(m.prop_values[start + 1], Value::Unit(16.0, "px".to_string()));
|
||||||
|
assert_eq!(m.prop_values[start + 2], Value::Variable("Config.textColor".to_string()));
|
||||||
|
} else {
|
||||||
|
panic!("Ожидался StyleRule");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_parser_mixins() {
|
||||||
|
let input = r#"
|
||||||
|
@mixin flex-center {
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
}
|
||||||
|
Box {
|
||||||
|
@use flex-center
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut m = ModuleSoA::new();
|
||||||
|
let mut parser = StyleParser::new(input, &mut m);
|
||||||
|
parser.parse_all().unwrap();
|
||||||
|
|
||||||
|
if let Directive::StyleRule { prop_span, .. } = &m.directives[0] {
|
||||||
|
assert_eq!(prop_span.1, 3, "Свойства из миксина должны добавиться к правилу");
|
||||||
|
|
||||||
|
let start = prop_span.0 as usize;
|
||||||
|
assert_eq!(m.prop_keys[start], "display");
|
||||||
|
assert_eq!(m.prop_values[start], Value::Ident("flex".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(m.prop_keys[start + 1], "align-items");
|
||||||
|
assert_eq!(m.prop_values[start + 1], Value::Ident("center".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(m.prop_keys[start + 2], "width");
|
||||||
|
assert_eq!(m.prop_values[start + 2], Value::Unit(100.0, "%".to_string()));
|
||||||
|
} else {
|
||||||
|
panic!("Ожидался StyleRule");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_parser_values_and_calls() {
|
||||||
|
let input = r#"
|
||||||
|
Overlay {
|
||||||
|
background: rgba(30, 30, 46, 0.88)
|
||||||
|
inset: 0 0 36px 0
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.45)
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut m = ModuleSoA::new();
|
||||||
|
let mut parser = StyleParser::new(input, &mut m);
|
||||||
|
parser.parse_all().unwrap();
|
||||||
|
|
||||||
|
let start = match &m.directives[0] {
|
||||||
|
Directive::StyleRule { prop_span, .. } => prop_span.0 as usize,
|
||||||
|
_ => panic!("Ожидался StyleRule"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// background: rgba(...)
|
||||||
|
assert_eq!(
|
||||||
|
m.prop_values[start],
|
||||||
|
Value::Call(
|
||||||
|
"rgba".to_string(),
|
||||||
|
vec![Value::Int(30), Value::Int(30), Value::Int(46), Value::Float(0.88)]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// inset: 0 0 36px 0 (парсится как Array)
|
||||||
|
assert_eq!(
|
||||||
|
m.prop_values[start + 1],
|
||||||
|
Value::Array(vec![
|
||||||
|
Value::Int(0),
|
||||||
|
Value::Int(0),
|
||||||
|
Value::Unit(36.0, "px".to_string()),
|
||||||
|
Value::Int(0)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Value::Array(arr) = &m.prop_values[start + 2] {
|
||||||
|
assert_eq!(arr[0], Value::Int(0));
|
||||||
|
assert_eq!(arr[1], Value::Unit(2.0, "px".to_string()));
|
||||||
|
assert_eq!(arr[2], Value::Unit(8.0, "px".to_string()));
|
||||||
|
assert!(matches!(arr[3], Value::Call(..)));
|
||||||
|
} else {
|
||||||
|
panic!("box-shadow должен быть спарсен как массив (набор значений)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_parser_animation() {
|
||||||
|
let input = r#"
|
||||||
|
@anim fade-in {
|
||||||
|
from { opacity: 0 }
|
||||||
|
to { opacity: 1 }
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut m = ModuleSoA::new();
|
||||||
|
let mut parser = StyleParser::new(input, &mut m);
|
||||||
|
parser.parse_all().unwrap();
|
||||||
|
|
||||||
|
if let Directive::StyleAnim { name, frames } = &m.directives[0] {
|
||||||
|
assert_eq!(name, "fade-in");
|
||||||
|
assert_eq!(frames.len(), 2);
|
||||||
|
assert_eq!(frames[0].0, "from");
|
||||||
|
assert_eq!(frames[1].0, "to");
|
||||||
|
} else {
|
||||||
|
panic!("Ожидалась директива StyleAnim");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod style_compiler_tests {
|
||||||
|
use crate::ast::ModuleSoA;
|
||||||
|
use crate::compiler::Compiler;
|
||||||
|
use crate::opcodes::*;
|
||||||
|
use crate::style_parser::StyleParser;
|
||||||
|
|
||||||
|
struct BytecodeBuilder {
|
||||||
|
buf: Vec<u8>,
|
||||||
|
}
|
||||||
|
impl BytecodeBuilder {
|
||||||
|
fn new() -> Self {
|
||||||
|
let mut b = Self { buf: Vec::new() };
|
||||||
|
b.buf.extend_from_slice(&MAGIC_HEADER);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
fn push_u8(mut self, val: u8) -> Self {
|
||||||
|
self.buf.push(val);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn push_u32(mut self, val: u32) -> Self {
|
||||||
|
self.buf.extend_from_slice(&val.to_le_bytes());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn push_str(mut self, s: &str) -> Self {
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
self.buf.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||||
|
self.buf.extend_from_slice(bytes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn push_f64(mut self, val: f64) -> Self {
|
||||||
|
self.buf.extend_from_slice(&val.to_le_bytes());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn build(self) -> Vec<u8> {
|
||||||
|
self.buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compiler_style_exact_bytes() {
|
||||||
|
let input = "Box { padding: 8px; color: transparent }";
|
||||||
|
let mut m = ModuleSoA::new();
|
||||||
|
let mut parser = StyleParser::new(input, &mut m);
|
||||||
|
parser.parse_all().unwrap();
|
||||||
|
|
||||||
|
let compiler = Compiler::new(&m);
|
||||||
|
let bytecode = compiler.compile(&m.hierarchy);
|
||||||
|
|
||||||
|
let expected = BytecodeBuilder::new()
|
||||||
|
.push_u8(OP_STYLE_RULE)
|
||||||
|
.push_str("Box")
|
||||||
|
.push_u32(2)
|
||||||
|
// 1: padding: 8px
|
||||||
|
.push_str("padding")
|
||||||
|
.push_u8(OP_PROP_UNIT)
|
||||||
|
.push_f64(8.0)
|
||||||
|
.push_str("px")
|
||||||
|
// 2: color: transparent
|
||||||
|
.push_str("color")
|
||||||
|
.push_u8(OP_PROP_IDENT)
|
||||||
|
.push_str("transparent")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(bytecode, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod integration_tests {
|
||||||
|
use crate::compile_project;
|
||||||
|
use crate::opcodes::MAGIC_HEADER;
|
||||||
|
|
||||||
|
const TEST_GLTS: &str = r#"
|
||||||
|
$accent = #cba6f7
|
||||||
|
@mixin flex-col {
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
}
|
||||||
|
Panel.desktop {
|
||||||
|
@use flex-col
|
||||||
|
background: rgba(30, 30, 46, 0.88)
|
||||||
|
border: 1px solid $accent
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const TEST_GLTM: &str = r#"
|
||||||
|
@version 1
|
||||||
|
Panel(id="main", class="desktop") {
|
||||||
|
Button(label="Start")
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_full_compile_project() {
|
||||||
|
let result = compile_project(TEST_GLTM, TEST_GLTS);
|
||||||
|
assert!(result.is_ok(), "Проект должен успешно скомпилироваться: {:?}", result.err());
|
||||||
|
|
||||||
|
let bytecode = result.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
&bytecode[0..4],
|
||||||
|
&MAGIC_HEADER,
|
||||||
|
"Байткод должен начинаться с магического заголовка GLBC"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(bytecode.len() > 100, "Байткод слишком мал, возможно скомпилировалось не все");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user