From 062048ef6e3e060bcf841ea2ec92e026f09d8da0 Mon Sep 17 00:00:00 2001 From: eichlan Date: Tue, 23 Jun 2026 15:31:51 -0700 Subject: Roorganized into workspace and packages. This is to accomidate a new proc_macro package, which can't have anything else in it. --- crimtag/src/lib.rs | 544 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 crimtag/src/lib.rs (limited to 'crimtag/src/lib.rs') diff --git a/crimtag/src/lib.rs b/crimtag/src/lib.rs new file mode 100644 index 0000000..e8f9e9f --- /dev/null +++ b/crimtag/src/lib.rs @@ -0,0 +1,544 @@ +use std::path::{PathBuf,Path}; +use std::collections::HashMap; +use core::error::Error; +use std::time::SystemTime; + +mod lexer; +mod parser; +mod position; +mod error; +pub mod context; + +pub use error::{CrimError,CrimResult}; +pub use position::*; +pub use context::{Context,Value,MappedStructure,StatefulContext,MappedValue,MappedList,MappedHash}; + +#[derive(Debug)] +pub struct Crimtag { + views: HashMap, + sources: Vec, +} + +#[derive(PartialEq,Eq,Debug,Clone)] +pub enum ViewSource { + File { + path: PathBuf, + loaded: SystemTime, + }, + Static, + External { + key: String, + }, +} + +#[derive(Debug)] +struct View { + name: String, + source: usize, + outputs: Vec, +// theme: Option + layout: Option, +} + +impl View { + fn process(&self, input: &dyn for<'a> MappedStructure<'a>) -> CrimResult { + let mut out_vars = Context::new(); + for output in &self.outputs { + let mut buf = String::new(); + let sc = StatefulContext::root( input ); + exec( &sc, &output.code, &mut buf )?; + out_vars.insert( output.name.clone(), Value::String(buf) ); + } + Ok(out_vars) + } +} + +#[derive(Debug)] +struct Output { + name: String, + code: Vec, +} + +type Properties = HashMap; + +#[derive(Debug)] +enum Token { + Loop(Identifier, Vec), + Show(Identifier,Properties), + If(Identifier, Vec, Vec), + Text(String), +} + +#[derive(Debug,Clone,PartialEq)] +pub enum IdentifierValue { + Root, + Name(String), + Index(usize), +} + +pub type Identifier = Vec; + +impl Crimtag { + pub fn new() -> Self { + Self { + views: HashMap::new(), + sources: vec![ViewSource::Static], + } + } + + fn id_source(&mut self, src: &ViewSource ) -> usize { + for idx in 0..self.sources.len() { + if self.sources[idx] == *src { + return idx; + } + } + + // Nothing found, add a new one + let idx = self.sources.len(); + self.sources.push( src.clone() ); + idx + } + + pub fn load_file(&mut self, path: &Path) -> Result<(),Box::> { + let time = if let Ok(meta) = path.metadata() { + if let Ok(t) = meta.modified() { + t + } else if let Ok(t) = meta.created() { + t + } else { + SystemTime::now() + } + } else { + SystemTime::now() + }; + let source_id = self.id_source( + &ViewSource::File { + path: path.to_path_buf(), + loaded: time, + } + ); + let p = parser::Parser::new(); + self.register_views( + p.parse( + &String::from_utf8( std::fs::read( path )? )?, + source_id + )?, + )?; + Ok(()) + } + + pub fn load_static(&mut self, data: &str) -> CrimResult<()> { + let source_id = self.id_source( &ViewSource::Static ); + let p = parser::Parser::new(); + self.register_views( p.parse( &data, source_id )? ) + } + + pub fn load_external(&mut self, key: &str, data: &str) -> CrimResult<()> { + let source_id = self.id_source( + &ViewSource::External{ key: key.to_string()} + ); + + let p = parser::Parser::new(); + self.register_views( + p.parse( &data, source_id )?, + ) + } + + fn register_views(&mut self, views: Vec) -> CrimResult<()> { + for view in views { + self.views.insert( view.name.clone(), view ); + } + Ok(()) + } + + pub fn get_view_source(&self, view: &str) -> Option { + if let Some(view) = self.views.get(view) { + if view.source < self.sources.len() { + Some(self.sources[view.source].clone()) + } else { + None + } + } else { + None + } + } + + pub fn render_partial(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> CrimResult { + if let Some(view) = self.views.get(view) { + return view.process( input ) + } else { + return Err(CrimError::other("No such view found".into())); + } + } + + pub fn render(&self, view: &str, input: &dyn for<'a> MappedStructure<'a> ) -> CrimResult { + if let Some(view) = self.views.get( view ) { + //println!("::Token tree::\n{:?}", view ); + let mut output = view.process( input )?; + if let Some(l) = &view.layout { + self.render( l, &output ) + } else { + if let Some(content) = output.remove("content") && + let Value::String(s) = content { + + Ok(s) + } else { + Err(CrimError::other("No content found in root layout.".into())) + } + } + + } else { + Err(CrimError::other("No such view found".into())) + } + } +} + +fn exec<'a>(input: &StatefulContext<'a>, tokens: &Vec, buf: &mut String) -> CrimResult<()> { + for token in tokens { + match token { + Token::Loop(ident,code) => { + //println!("!!! Loop over: {:?} {:?}", ident, input.get_value( &ident )); + if let Some(value) = input.get_value( &ident ) && + let MappedValue::List(list) = value { + let output = + &list.for_each(&|rec: &MappedValue, buf: &mut String| { + if let MappedValue::Struct(d) = rec { + exec( &input.local(*d), code, buf )?; + } + Ok(()) + })?; + //println!("!!! Output from loop: {}", output ); + buf.push_str( &output ); + } + } + Token::If(ident,code,other) => { + if let Some(value) = input.get_value( &ident ) && + let MappedValue::Bool(b) = value && b { + exec(input, code, buf)? + } else { + exec(input, other, buf)? + } + } + Token::Show(ident,p) => { + let format = if let Some(s) = p.get("format") { + s + } else { + &"".to_string() + }; + if let Some(value) = input.get_value( &ident ) { + match value { + MappedValue::Struct(_) => { + buf.push_str("Struct"); + } + MappedValue::List(_) => { + buf.push_str("List"); + } + MappedValue::Bool(b) => { + if b { + buf.push_str("true"); + } else { + buf.push_str("false"); + } + } + MappedValue::Str(s) => { + buf.push_str(s); + } + MappedValue::String(s) => { + buf.push_str(s.as_str()); + } + MappedValue::Int8(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::Int16(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::Int32(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::Int64(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::UInt8(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::UInt16(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::UInt32(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::UInt64(i) => { + buf.push_str(&i.to_string()); + } + MappedValue::Float32(f) => { + if format == "" { + buf.push_str(&f.to_string()); + } else { + buf.push_str(&f.to_string()); + } + } + MappedValue::Float64(f) => { + if format == "" { + buf.push_str(&f.to_string()); + } else { + buf.push_str(&f.to_string()); + } + } + } + } + } + Token::Text(s) => { + buf.push_str( s ); + } + //_ => {} + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::lexer::*; + + #[test] + fn lexing() { + let data = r#"Leading comment: [|view "basic" something="yeup"|>Hello there <|view|] Trailing text"#; + let ll = lexer::Lexer::new( &data ); + for sym in ll { + if let SymbolType::Error{what} = sym.symbol() { + println!("Error {}:{}: {:?}", sym.start().line, sym.start().column, what ); + break; + } else { + println!("Symbol: {:?}", sym ); + } + } + } + + #[test] + fn parsing() { + let mut ct = Crimtag::new(); + if let Err(e) = ct.load_static(r#"Here is a sample +[|view "index" theme="standard"|>Hi<|view|] +That was fun! now a placeholder: [|view "placeholder"|] and now one with an implicit [|view "simple"|>Body -> [|show content|] <- and end<|view|] aoeu + +[|view "person" layout="simple"|>Welcome [|show person.last_name|], [|show person.first_name|]! It's lovely to see you again.<|view|] + +Now with explicit outputs: +[|view "complex"|> + [|output "content"|> + Here's a [|show name|]. + <|output|] + [|output "sidebar"|> + What's up world? + <|output|] + [|output "footer"|> + Same, I guess! + <|output|] +<|view|]"#) { + println!("Error: {:?}", e ); + } else { + println!("It finished"); + if let Ok(v) = ct.render( + "person", + &Context::from([ + ("hi".to_string(),Value::String("hi".to_string())), + ("person".to_string(),Value::Dictionary(HashMap::from([ + ("first_name".to_string(), Value::String("Bob".to_string())), + ("last_name".to_string(), Value::String("Smith".to_string())), + ]))), + ]) + ) { + println!("View result: {:?}", v ); + } else { + println!("Error?"); + } + } + } + + #[test] + fn loops() -> Result<(),CrimError> { + let mut ct = Crimtag::new(); + ct.load_static(r#"Looping code: +[|view "index"|>We will enumerate people here:[|loop people|> + - [|show last_name|], [|show first_name|][|if show_title |>: [|show title|] <|else|> **title redacted** <|if|] ::>[|loop tags|> [|show tag|]<|loop|] <:: <|loop|] +<|view|] +"#)?; + + let ctx = Context::from([ + ("people".to_string(), vec![ + [ + ("first_name".into(), "Joe".into()), + ("last_name".into(), "Smith".into()), + ("show_title".into(), true.into()), + ("title".into(), "CEO".into()), + ("tags".into(), vec![ + Context::from([("tag".into(), "jerk".into()),]).into(), + Context::from([("tag".into(), "ugly".into()),]).into(), + ].into()), + ].into(), + [ + ("first_name".into(), "Chris".into()), + ("last_name".into(), "Perkens".into()), + ("show_title".into(), false.into()), + ("title".into(), "Baconeer".into()), + ("tags".into(), vec![ + Context::from([("tag".into(), "jerk".into()),]).into(), + Context::from([("tag".into(), "ugly".into()),]).into(), + ].into()), + ].into(), + [ + ("first_name".into(), "Will".into()), + ("last_name".into(), "Power".into()), + ("show_title".into(), false.into()), + ("title".into(), "CFO".into()), + ("tags".into(), vec![ + Context::from([("tag".into(), "jerk".into()),]).into(), + Context::from([("tag".into(), "ugly".into()),]).into(), + ].into()), + ].into(), + [ + ("first_name".into(), "Justin".into()), + ("last_name".into(), "Time".into()), + ("show_title".into(), true.into()), + ("title".into(), "CIO".into()), + ("tags".into(), vec![ + Context::from([("tag".into(), "jerk".into()),]).into(), + Context::from([("tag".into(), "ugly".into()),]).into(), + ].into()), + ].into(), + ].into()), + ]); + + println!("View result: {}", ct.render("index", &ctx)? ); + + Ok(()) + } + + #[test] + fn conditional() -> Result<(),CrimError> { + let mut ct = Crimtag::new(); + ct.load_static(r#"Hi there +[|view "index"|> + Color: [|if is_red|> red <|elif is_blue |> blue <|else|> green <|if|] +<|view|]"#)?; + + let ctx = Context::from([ + ("is_red".into(), false.into()), + ("is_blue".into(), false.into()), + ("color".into(), "purple".into()), + ]); + + println!("View result: {}", ct.render("index", &ctx)? ); + + Ok(()) + } + + struct Item { + pub id: i32, + pub title: String, + pub body: String, + } + + struct Person { + pub id: i32, + pub username: String, + pub name: String, + } + + struct Page { + pub user: Person, + pub total_items: i32, + pub total_pages: i32, + pub cur_page: i32, + pub items: Vec, + } + + impl Page { + fn new() -> Page { + Page { + user: Person { + id: 443, + username: "eichlan".into(), + name: "Mike".into(), + }, + total_items: 4000, + total_pages: 400, + cur_page: 35, + items: vec![ + Item{ + id: 123, + title: "hi".into(), + body: "body".into(), + }, + Item{ + id: 124, + title: "bye".into(), + body: "wooo".into(), + }, + Item{ + id: 125, + title: "ciao".into(), + body: "whatever".into(), + } + ], + } + } + } + + impl<'a> MappedStructure<'a> for Page { + fn get_value(&'a self, id: &str) -> Option> { + match id { + "user" => Some(MappedValue::Struct(&self.user)), + "total_items" => Some(MappedValue::Int32(self.total_items)), + "total_pages" => Some(MappedValue::Int32(self.total_pages)), + "cur_page" => Some(MappedValue::Int32(self.cur_page)), + "items" => Some(MappedValue::<'a>::List(&self.items)), + _ => None, + } + } + } + + impl<'a> MappedStructure<'a> for Person { + fn get_value(&'a self, id: &str) -> Option> { + match id { + "id" => Some(MappedValue::Int32(self.id)), + "username" => Some(MappedValue::String(&self.username)), + "name" => Some(MappedValue::String(&self.name)), + _ => None, + } + } + } + + impl<'a> MappedStructure<'a> for Item { + fn get_value(&'a self, id: &str) -> Option> { + match id { + "id" => Some(MappedValue::Int32(self.id)), + "title" => Some(MappedValue::String(&self.title)), + "body" => Some(MappedValue::String(&self.body)), + _ => None, + } + } + } + + #[test] + fn custom() -> Result<(), CrimError> { + let page = Page::new(); + + println!("{:?}", + page.get_value(&"total_pages") + ); + + if let Some(MappedValue::List(l)) = page.get_value(&"items") { + l.for_each(&|x: &MappedValue, buf: &mut String| { + if let MappedValue::Struct(s) = x { + println!(" - {:?}", s.get_value(&"title")); + buf.push_str(&format!("{:?}",s.get_value(&"title"))); + } + Ok(()) + }).expect("loop"); + } + Ok(()) + } +} -- cgit v1.2.3