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; mod context; pub use error::{CrimError,CrimResult}; pub use position::*; pub use context::{Context,Value,MappedStructure,StatefulContext,MappedValue,MappedList}; #[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 MappedStructure<'a>>(&self, input: &T) -> 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 MappedStructure<'a>>(&self, view: &str, input: &T ) -> 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 MappedStructure<'a>>(&self, view: &str, input: &T ) -> 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 MappedStructure<'a>>(input: &StatefulContext, tokens: &Vec, buf: &mut String) -> CrimResult<()> { for token in tokens { match token { Token::Loop(_ident,_code) => { /* if let Some(value) = input.get_value( &ident ) && let Value::List(list) = value { for rec in &list { if let Value::Dictionary(d) = rec { if matches!(d, Context) && matches!(input.root, Context) { println!("Matches."); } else { println!("Doesn't match."); } exec( &StatefulContext::root(d), code, buf )? } } } */ return Ok(()); } 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()), ].into(), [ ("first_name".into(), "Chris".into()), ("last_name".into(), "Perkens".into()), ("show_title".into(), false.into()), ("title".into(), "Baconeer".into()), ].into(), [ ("first_name".into(), "Will".into()), ("last_name".into(), "Power".into()), ("show_title".into(), false.into()), ("title".into(), "CFO".into()), ].into(), [ ("first_name".into(), "Justin".into()), ("last_name".into(), "Time".into()), ("show_title".into(), true.into()), ("title".into(), "CIO".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, } } } impl<'a> MappedList<'a> for Vec { fn get_index(&'a self, id: usize) -> Option> { if let Some(x) = self.get(id) { Some(MappedValue::<'a>::Struct(x)) } else { None } } fn for_each(&'a self, func: &dyn Fn(&MappedValue<'a>)) { for i in self { func( &MappedValue::Struct(i) ); } } } #[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| { if let MappedValue::Struct(s) = x { println!(" - {:?}", s.get_value(&"title")); } }); } Ok(()) } }