From 3c9a65f0a576397024c76bcc33950796ec3297ec Mon Sep 17 00:00:00 2001 From: Mike Buland Date: Fri, 12 Jun 2026 14:33:34 -0700 Subject: if/elif/else and loop work fully. Complex identifiers also work now. --- src/lib.rs | 43 ++++-- src/parser.rs | 432 +++++++++++++++++++++++++--------------------------------- 2 files changed, 217 insertions(+), 258 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2b53828..84a5a43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,6 +171,7 @@ impl Crimtag { pub fn render(&self, view: &str, input: &impl MappedStructure ) -> 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 ) @@ -203,12 +204,14 @@ fn exec(input: &impl MappedStructure, tokens: &Vec, buf: &mut String) -> } } } - Token::If(ident,code) => { + Token::If(ident,code,other) => { if let Some(value) = input.get_value( &ident ) && let Value::Bool(b) = value && b { exec(input, code, buf)? + } else { + exec(input, other, buf)? } } Token::Show(ident,p) => { @@ -322,16 +325,13 @@ Now with explicit outputs: } #[test] - fn loops() { + fn loops() -> Result<(),CrimError> { let mut ct = Crimtag::new(); - if let Err(e) = ct.load_static(r#"Looping code: + 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|] <|if|] [|loop tags|> [|show tag|]<|loop|] <|loop|] + - [|show last_name|], [|show first_name|][|if show_title |>: [|show title|] <|else|> **title redacted** <|if|] [|loop tags|> [|show tag|]<|loop|] <|loop|] <|view|] -"#) { - println!("Error: {:?}", e ); - return; - } +"#)?; let ctx = Context::from([ ("people".to_string(), vec![ @@ -361,9 +361,28 @@ Now with explicit outputs: ].into(), ].into()), ]); - match ct.render("index", &ctx) { - Ok(s) => println!("View result: {}", s ), - Err(e) => println!("Error: {:?}", e ), - } + + 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(()) } } diff --git a/src/parser.rs b/src/parser.rs index b681fce..fff00b1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -199,7 +199,7 @@ impl Parser { } fn p_input(&self, ctx: &mut Context ) -> CrimResult> { - let mut children = Vec::new(); + let mut tags = Vec::new(); loop { if ctx.cur().is_none() { break; @@ -207,21 +207,41 @@ impl Parser { match ctx.cur().unwrap().symbol() { SymbolType::Text(_) => { /* Skip top level text */ } SymbolType::StartFlat => { - children.push( self.parse_tag_view( ctx )? ); + let tb = self.parse_tag( ctx )?; + let tb = self.parse_tag_set( ctx, tb )?; + tags.push( tb ); } SymbolType::Error{..} => { self.lex_error( &ctx.cur().unwrap() )?; } _ => { - + return Err(CrimError::parse( + *ctx.cur().unwrap().start(), + format!("Unexpected symbol {:?} found looking for tags.", ctx.cur().unwrap().symbol() ) + )); } } ctx.next(); } - Ok(children) + + let mut views = Vec::new(); + for tag in tags { + let start_pos = tag.start_pos(); + let name = tag.name().clone(); + if tag.is_name(&SymbolType::View) && + let BuildResult::View(view) = tag.build( ctx )? { + views.push( view ); + } else { + return Err(CrimError::parse( + start_pos, + format!("Expected view at root, found {:?}", name ) + )); + } + } + Ok(views) } - fn parse_tag(&self, ctx: &mut Context) -> CrimResult { + fn parse_tag<'a>(&self, ctx: &mut Context<'a>) -> CrimResult> { let start_sym = ctx.cur().unwrap(); let mut tb = TagBuilder::new(&start_sym); @@ -255,7 +275,7 @@ impl Parser { if let Some(end_sym) = ctx.cur() { match end_sym.symbol() { SymbolType::EndPoint | SymbolType::EndFlat => { - tb.set_end( end_sym )?; + tb.set_end( &end_sym )?; } _ => { return Err(CrimError::parse( @@ -271,12 +291,9 @@ impl Parser { Ok(tb) } - fn parse_tag_set<'a>(&self, ctx: &mut Context; base: &mut TagBuilder) -> CrimResult { + fn parse_tag_set<'a>(&self, ctx: &mut Context<'a>, mut base: TagBuilder<'a>) -> CrimResult> { if base.is_unary() { - return Ok(base.build()); - } - if !base.is_multinary_open() { - return Err(CrimError::broken("Found close/mid tag instead of opening")); + return Ok(base); } loop { @@ -285,226 +302,39 @@ impl Parser { } match ctx.cur().unwrap().symbol() { SymbolType::Text(s) => { - base.add_child(Token::Text(s.to_string())); + base.add_child(Entry::Token(Token::Text(s.to_string()))); ctx.next(); } SymbolType::StartFlat | SymbolType::StartPoint => { - let mut tb = self.parse_tag( ctx )?; + let tb = self.parse_tag( ctx )?; match tb.tag_type() { TagType::MultinaryOpen => { - base.add_child( parse_tag_set( ctx, tb )? ); + base.add_child(Entry::TagBuilder(self.parse_tag_set( ctx, tb )?)); } TagType::MultinaryMid => { - base.add_chain( parse_tag_set( ctx, tb )? ); + base.add_chain(Entry::TagBuilder(self.parse_tag_set( ctx, tb )?)); + return Ok(base); } TagType::MultinaryClose => { - if (tb.is_name(SymbolToken::If) && - (base.is_name(SymbolToken::Elif) || - base.is_name(SymbolToken::Else))) || - tb.is_name(base.name().symbol()) { - + if (tb.is_name(&SymbolType::If) && + (base.is_name(&SymbolType::ElIf) || + base.is_name(&SymbolType::Else))) || + tb.is_name(base.name().unwrap().symbol()) { + return Ok(base); } else { return Err(CrimError::parse( - *tb.name().start(), - format!("Found {:?} looking for close of {:?}", tb.name().symbol(), base.name().symbol()) + *tb.name().unwrap().start(), + format!("Found {:?} looking for close of {:?}", tb.name().unwrap().symbol(), base.name().unwrap().symbol()) )); } } - } - } - SymbolType::Error{..} => { - return self.lex_error( &ctx.cur().unwrap() ); - } - _ => { - return Err(CrimError::parse( - *ctx.cur().unwrap().start(), - format!("Unexpected token: {:?}", ctx.cur().unwrap().symbol()), - )); - } - } - } - } - - fn parse_tag_view(&self, ctx: &mut Context) -> CrimResult { - let tb = self.parse_open_tag( ctx )?; - if !tb.is_name( &SymbolType::View ) { - return Err(CrimError::parse(tb.start_pos(), - "Unexpected tag type, only view allowed at root".into() - )); - } - - if tb.is_unary() { - return tb.build_view(ctx, Vec::new()); - } - let mut children = Vec::new(); - let mut outputs = Vec::new(); - - //println!("--parse-tag-view-- tag parsed, next token: {:?}", ctx.cur() ); - loop { - if ctx.cur().is_none() { - return Err(CrimError::eos(format!("close tag for {:?}", tb.name()).as_str())) - } - match ctx.cur().unwrap().symbol() { - SymbolType::Text(s) => { - children.push( Token::Text(s.to_string()) ); - ctx.next(); - } - SymbolType::StartFlat => { - let subtb = self.parse_tag( ctx )?; - if subtb.is_name(&SymbolType::Output) { - outputs.push(subtb.build_output()?); - } else { - children.push(subtb.build()?); - } - } - SymbolType::StartPoint => { - let end = self.parse_end_tag( ctx )?; - //println!("End tag: {:?}, open tag: {:?}", end, tb.name() ); - if tb.is_name(end.symbol()) { - // They match, time to decide what we're doing. - let any_outputs = outputs.len() > 0; - let all_text = children.iter().all( - |t| matches!(t, Token::Text(..)) - ); - if !any_outputs { - // Create an implicit output called "content" - outputs.push(Output{ - name: "content".to_string(), - code: children, - }); - } else { - if !all_text { - return Err(CrimError::parse( - *end.start(), - "You cannot mix non-output and output tags in a view.".to_string() - )); - } + TagType::Unary => { + base.add_child(Entry::TagBuilder(tb)); + } + TagType::Unknown => { + return Err(CrimError::broken(base.start_pos(), "Found unknown tag when looking for something else.")); } - return tb.build_view(ctx,outputs); - } else { - // They don't match, complain. - return Err(CrimError::parse( - *end.start(), - "Mismatched open and closing tags.".to_string() - )); - } - } - SymbolType::Error{..} => { - return self.lex_error( &ctx.cur().unwrap() ); - } - _ => { - return Err(CrimError::parse( - *ctx.cur().unwrap().start(), - format!("Unexpected tag in view root: {:?}", ctx.cur().unwrap().symbol()) - )); - } - } - } - } - - fn parse_open_tag<'a>(&self, ctx: &mut Context<'a>) -> CrimResult> { - let start_sym = ctx.cur().unwrap(); - let mut tb = TagBuilder::new(&start_sym); - - if ctx.next().is_some() { - match ctx.cur().unwrap().symbol() { - SymbolType::View | SymbolType::Show | SymbolType::Loop | - SymbolType::If | SymbolType::ElIf | SymbolType::Else | - SymbolType::Output => { - let name_sym = ctx.cur().unwrap(); - ctx.next(); - tb.set_name( name_sym ); - } - _ => { - return Err(CrimError::parse( - *ctx.cur().unwrap().start(), - "Unexpected symbol".to_string() - )); - } - } - } else { - return Err(CrimError::eos("tag type")); - } - - self.parse_tag_params( ctx, &mut tb )?; - self.parse_tag_props( ctx, &mut tb )?; - - if let Some(end_sym) = ctx.cur() { - match end_sym.symbol() { - SymbolType::EndPoint => { - tb.set_type( TagType::MultinaryOpen ); - } - SymbolType::EndFlat => { - tb.set_type( TagType::Unary ); - } - _ => { - return Err(CrimError::parse( - *end_sym.start(), - format!("Unexpected symbol {:?} looking for end of tag.", end_sym.symbol()) - )); - } - } - } - - ctx.next(); - - Ok(tb) - } - - fn parse_end_tag<'a>(&self, ctx: &mut Context<'a> ) -> CrimResult> { - if !ctx.cur().is("end tag",|s| *s == SymbolType::StartPoint )? { - return Err(CrimError::parse( - *ctx.cur().unwrap().start(), - "Invalid end tag?".to_string() - )); - } - if !ctx.next().is_valid_tag_name()? { - return Err(CrimError::parse( - *ctx.cur().unwrap().start(), - "Invalid tag name".to_string() - )); - } - let name = ctx.cur().unwrap(); - if !ctx.next().is("end of end tag", |s| *s == SymbolType::EndFlat )? { - return Err(CrimError::parse( - *ctx.cur().unwrap().start(), - "Tag should be <| |] style end tag.".to_string(), - )); - } - ctx.next(); - Ok(name) - } - - fn parse_tag_body<'a>(&self, ctx: &mut Context<'a> ) -> CrimResult> { - let mut tb = self.parse_open_tag( ctx )?; - if tb.is_unary() { - return Ok(tb); - } - - loop { - if ctx.cur().is_none() { - return Err(CrimError::eos(format!("close tag for {:?}", tb.name()).as_str())); - } - match ctx.cur().unwrap().symbol() { - SymbolType::Text(s) => { - tb.add_child(Token:: Text(s.to_string())); - ctx.next(); - } - SymbolType::StartFlat => { - tb.add_child( self.parse_tag_body( ctx )?.build()? ); - } - SymbolType::StartPoint => { - let end = self.parse_end_tag( ctx )?; - if tb.is_name(end.symbol()) { - // They match, end. - return Ok(tb); - } else { - // They don't match, complain. - return Err(CrimError::parse( - *end.start(), - "Mismatched open and closing tags.".to_string() - )); } } SymbolType::Error{..} => { @@ -600,14 +430,87 @@ enum TagType { MultinaryClose, } +#[derive(Debug)] +enum Entry<'a>{ + Token(Token), + TagBuilder(TagBuilder<'a>), +} + +type EntryList<'a> = Vec>; + +trait EntryListConverter { + fn has_outputs(&self) -> bool; + fn to_outputs(&mut self, ctx: &mut Context ) -> CrimResult>; + fn to_tokens(&mut self, ctx: &mut Context) -> CrimResult>; +} + +impl<'a> EntryListConverter for EntryList<'a> { + fn has_outputs(&self) -> bool { + self.iter().any(|e| + if let Entry::TagBuilder(tb) = e + && tb.is_name(&SymbolType::Output) { + true + } else { + false + } + ) + } + + fn to_outputs(&mut self, ctx: &mut Context) -> CrimResult> { + let mut outputs = Vec::new(); + + if self.has_outputs() { + // We have an explicit output, we cant have anything else + for e in self.drain(..) { + let tb = if let Entry::TagBuilder(tb) = e { + tb + } else { + continue; + }; + if let BuildResult::Output(out) = tb.build(ctx)? { + outputs.push( out ); + } + } + } else { + // No outputs, so we create one implicit output + outputs.push(Output { + name: "content".into(), + code: self.to_tokens(ctx)? + }); + } + + Ok(outputs) + } + + fn to_tokens(&mut self, ctx: &mut Context) -> CrimResult> { + let mut tokens : Vec = Vec::new(); + for e in self.drain(..) { + match e { + Entry::Token(token) => { + tokens.push( token ); + } + Entry::TagBuilder(tb) => { + if let BuildResult::Token(token) = tb.build(ctx)? { + tokens.push( token ); + } else { + return Err(CrimError::broken( Position::none(), "Non-token result built.")); + } + } + } + } + Ok(tokens) + } +} + +#[derive(Debug)] struct TagBuilder<'a> { name: Option>, params: Vec, props: Properties, - children: Vec, + children: Vec>, tag_type: TagType, start: Symbol<'a>, - chain: Vec, + chain: Vec>, } #[derive(Debug)] @@ -636,29 +539,30 @@ impl<'a> TagBuilder<'a> { } pub fn set_end(&mut self, end: &Symbol) -> CrimResult<()> { - if start.symbol() == SymbolType::StartFlat { - if end.symbol() == SymbolType::EndFlat { + if *self.start.symbol() == SymbolType::StartFlat { + if *end.symbol() == SymbolType::EndFlat { self.tag_type = TagType::Unary; - } else if end.symbol() == SymbolType::EndPoint { + } else if *end.symbol() == SymbolType::EndPoint { self.tag_type = TagType::MultinaryOpen; } else { - return Err(CrimError::broken( *self.end.start(), "Invalid bracket token type.")); + return Err(CrimError::broken( *end.start(), "Invalid bracket token type.")); } - } else if start.symbol() == SymbolType::StartPoint { - if end.symbol() == SymbolType::EndFlat { + } else if *self.start.symbol() == SymbolType::StartPoint { + if *end.symbol() == SymbolType::EndFlat { self.tag_type = TagType::MultinaryClose; - } else if end.symbol() == SymbolType::EndPoint { + } else if *end.symbol() == SymbolType::EndPoint { self.tag_type = TagType::MultinaryMid; } else { - return Err(CrimError::broken( *self.end.start(), "Invalid bracket token type.")); + return Err(CrimError::broken( *end.start(), "Invalid bracket token type.")); } } else { return Err(CrimError::broken( *self.start.start(), "Invalid bracket token type.")); } + Ok(()) } pub fn start_pos(&self) -> Position { - self.start.start() + *self.start.start() } pub fn is_unary(&self) -> bool { @@ -670,18 +574,20 @@ impl<'a> TagBuilder<'a> { } pub fn can_have_params(&self) -> bool { - match self.name.symbol() { + match self.name.unwrap().symbol() { SymbolType::View | SymbolType::Output | SymbolType::Loop => true, SymbolType::Show | SymbolType::If | SymbolType::ElIf | SymbolType::Else => false, + _ => false, } } pub fn can_have_expr(&self) -> bool { - match self.name.symbol() { + match self.name.unwrap().symbol() { SymbolType::View | SymbolType::Output | SymbolType::Loop => false, SymbolType::Show | SymbolType::If | SymbolType::ElIf | SymbolType::Else => true, + _ => false, } } @@ -726,14 +632,18 @@ impl<'a> TagBuilder<'a> { self.props.insert( key, value ); } - pub fn add_child(&mut self, token: Token) { - self.children.push( token ); + pub fn add_child(&mut self, tb: Entry<'a>) { + self.children.push( tb ); } - pub fn append_children(&mut self, children: &mut Vec::) { + pub fn append_children(&mut self, children: &mut Vec::>) { self.children.append( children ); } + pub fn add_chain(&mut self, tb: Entry<'a>) { + self.chain.push( tb ); + } + pub fn set_type(&mut self, tag_type: TagType) { self.tag_type = tag_type; } @@ -742,7 +652,7 @@ impl<'a> TagBuilder<'a> { self.tag_type } - pub fn build(mut self) -> CrimResult { + pub fn build(mut self, ctx: &mut Context ) -> CrimResult { if let Some(sym) = self.name { match sym.symbol() { SymbolType::View => { @@ -754,7 +664,7 @@ impl<'a> TagBuilder<'a> { Ok(BuildResult::View(View { name: name, source: ctx.source(), - outputs: outputs, + outputs: self.children.to_outputs(ctx)?, //theme: Option layout: self.props.get("layout").cloned(), })) @@ -767,7 +677,7 @@ impl<'a> TagBuilder<'a> { }; Ok(BuildResult::Output(Output { name: name, - code: self.children, + code: self.children.to_tokens(ctx)?, })) } SymbolType::Loop => { @@ -780,7 +690,9 @@ impl<'a> TagBuilder<'a> { "Identifier for loop variable name.".into()) ); }; - Ok(BuildResult::Token(Token::Loop(id, self.children))) + Ok(BuildResult::Token( + Token::Loop(id, self.children.to_tokens(ctx)?) + )) } SymbolType::Show => { let id = if let ParamValue::Identifier(id) @@ -804,17 +716,25 @@ impl<'a> TagBuilder<'a> { "Identifier for if variable name.".into()) ); }; - if self.chain.len() == 1 && - let Token::If(id, mut children, chain) = self.chain[0] && - id.len() == 0 { + + let is_else = if self.chain.len() == 1 && + let Entry::TagBuilder(tb) = &self.chain[0] && + tb.is_name(&SymbolType::Else) && + tb.params.len() == 0 { + true + } else { + false + }; + + if is_else && + let Entry::TagBuilder(mut tb) = self.chain.swap_remove(0) { self.chain.clear(); - self.chain.append(children); + self.chain.append(&mut tb.children); } - Ok(BuildResult::Token(Token::If(id, self.children, self.chain))) + Ok(BuildResult::Token(Token::If(id, self.children.to_tokens(ctx)?, self.chain.to_tokens(ctx)?))) } SymbolType::ElIf => { - let id = if let ParamValue::Identifier(id) - = self.params.swap_remove(0) { + let id = if let ParamValue::Identifier(id) = self.params.swap_remove(0) { id } else { return Err(CrimError::parse( @@ -822,17 +742,37 @@ impl<'a> TagBuilder<'a> { "Identifier for elif variable name.".into()) ); }; - Ok(BuildResult::Token(Token::If(Identifier::new(), self.children, self.chain))) + + // this is copied from if, since they're the same this + // should be encapsulated and moved into a function... + // ...but I can't do that right now. + let is_else = if self.chain.len() == 1 && + let Entry::TagBuilder(tb) = &self.chain[0] && + tb.is_name(&SymbolType::Else) && + tb.params.len() == 0 { + true + } else { + false + }; + + if is_else && + let Entry::TagBuilder(mut tb) = self.chain.swap_remove(0) { + self.chain.clear(); + self.chain.append(&mut tb.children); + } + + + Ok(BuildResult::Token(Token::If(id, self.children.to_tokens(ctx)?, self.chain.to_tokens(ctx)?))) } SymbolType::Else => { - Ok(BuildResult::Token(Token::If(Identifier::new(), self.children, self.chain))) + Ok(BuildResult::Token(Token::If(Identifier::new(), self.children.to_tokens(ctx)?, self.chain.to_tokens(ctx)?))) } _ => { - Err(CrimError::parse( self.start_pos, "Bad tag type".into())) + Err(CrimError::parse( self.start_pos(), "Bad tag type".into())) } } } else { - Err(CrimError::parse(self.start_pos, "Bad tag type".into())) + Err(CrimError::parse(self.start_pos(), "Bad tag type".into())) } } } -- cgit v1.2.3