From d9f008d5ed3740b72f1aea3ebae207a9291fecda Mon Sep 17 00:00:00 2001 From: Olivier Date: Fri, 9 May 2025 20:10:25 +0100 Subject: [PATCH] Add support for Option<> field values Signed-off-by: Olivier --- hornbeam_grammar/src/ast.rs | 7 +- hornbeam_grammar/src/hornbeam.pest | 11 +- hornbeam_grammar/src/parser.rs | 69 +++++++++- hornbeam_ir/src/ast_to_ir.rs | 195 +++++++++++++++++++---------- 4 files changed, 211 insertions(+), 71 deletions(-) diff --git a/hornbeam_grammar/src/ast.rs b/hornbeam_grammar/src/ast.rs index 7a3d33f..455f501 100644 --- a/hornbeam_grammar/src/ast.rs +++ b/hornbeam_grammar/src/ast.rs @@ -26,10 +26,15 @@ pub struct HtmlElement { pub children: Vec, pub classes: Vec, pub dom_id: Option, - pub attributes: BTreeMap, + pub attributes: BTreeMap, pub loc: Locator, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] +pub struct ElementAttributeFlags { + pub optional: bool, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct ComponentElement { pub name: IStr, diff --git a/hornbeam_grammar/src/hornbeam.pest b/hornbeam_grammar/src/hornbeam.pest index cb8ac5f..7e69c3c 100644 --- a/hornbeam_grammar/src/hornbeam.pest +++ b/hornbeam_grammar/src/hornbeam.pest @@ -18,7 +18,7 @@ BlockContent = _{ } Element = { - ElementName ~ cssClass* ~ domId? ~ (ws+ ~ MapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)? + ElementName ~ cssClass* ~ domId? ~ (ws+ ~ AttrMapLiteral)? ~ lineEnd ~ (NewBlock | NewSlotBlock)? } cssClass = _{ @@ -207,12 +207,19 @@ Variable = { "$" ~ Identifier } ListLiteral = { "[" ~ commaSeparatedExprs ~ "]" } +// Basic key-value pairs forming a map literal {a = .., b = ..}. KVPair = { Identifier ~ wsnl* ~ "=" ~ wsnl* ~ Expr } KVarShorthand = { Variable } commaSeparatedKVPairs = _{ wsnl* ~ ((KVPair | KVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (KVPair | KVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? } MapLiteral = { "{" ~ commaSeparatedKVPairs ~ "}" } - +// Element attribute key-value pairs +// These can have extra features not found in the basic map literals +AttrKVPairOptionalMarker = { "?" } +AttrKVPair = { Identifier ~ AttrKVPairOptionalMarker? ~ wsnl* ~ "=" ~ wsnl* ~ Expr } +AttrKVarShorthand = { Variable ~ AttrKVPairOptionalMarker? } +commaSeparatedAttrKVPairs = _{ wsnl* ~ ((AttrKVPair | AttrKVarShorthand) ~ wsnl* ~ ("," ~ wsnl* ~ (AttrKVPair | AttrKVarShorthand) ~ wsnl*)* ~ ("," ~ wsnl*)?)? } +AttrMapLiteral = { "{" ~ commaSeparatedAttrKVPairs ~ "}" } // As in a let binding or for binding. diff --git a/hornbeam_grammar/src/parser.rs b/hornbeam_grammar/src/parser.rs index 5210c01..01f69e8 100644 --- a/hornbeam_grammar/src/parser.rs +++ b/hornbeam_grammar/src/parser.rs @@ -1,8 +1,9 @@ #![allow(non_snake_case)] use crate::ast::{ - Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, Expression, ForBlock, - HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece, Template, + Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, ElementAttributeFlags, + Expression, ForBlock, HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece, + Template, }; use crate::{intern, IStr, Locator}; use lazy_static::lazy_static; @@ -88,8 +89,8 @@ impl HornbeamParser { Rule::SupplySlot => { supply_slots.push(HornbeamParser::SupplySlot(next)?); } - Rule::MapLiteral => { - attributes = HornbeamParser::MapLiteral(next)?; + Rule::AttrMapLiteral => { + attributes = HornbeamParser::AttrMapLiteral(next)?; } _ => { if let Some(block) = HornbeamParser::helper_block(next)? { @@ -113,6 +114,20 @@ impl HornbeamParser { loc, }) } else { + let attributes: PCResult> = attributes + .into_iter() + .map(|(k, (v, attrs))| { + if attrs.optional { + Err(error( + "Optional arguments to components are currently unsupported", + span, + )) + } else { + Ok((k, v)) + } + }) + .collect(); + let attributes = attributes?; if !supply_slots.is_empty() { let mut slots = BTreeMap::new(); for (slot_name, slot_content_blocks, _slot_span) in supply_slots { @@ -278,6 +293,52 @@ impl HornbeamParser { .collect() } + fn AttrKVPair(input: Node) -> PCResult<(IStr, (Expression, ElementAttributeFlags))> { + Ok(match_nodes!(input.into_children(); + [Identifier(key), Expr(value)] => (key, (value, ElementAttributeFlags { + optional: false + })), + [Identifier(key), AttrKVPairOptionalMarker(_), Expr(value)] => (key, (value, ElementAttributeFlags { + optional: true + })), + )) + } + + fn AttrKVPairOptionalMarker(input: Node) -> PCResult<()> { + Ok(()) + } + + fn AttrKVarShorthand(input: Node) -> PCResult<(IStr, (Expression, ElementAttributeFlags))> { + let (var_expr, optional) = match_nodes!(input.into_children(); + [Variable(var_expr), AttrKVPairOptionalMarker(_)] => { + (var_expr, true) + }, + [Variable(var_expr)] => { + (var_expr, false) + }, + ); + if let Expression::Variable { name, .. } = &var_expr { + Ok((name.clone(), (var_expr, ElementAttributeFlags { optional }))) + } else { + unreachable!("Variable should also be returned from Variable"); + } + } + + fn AttrMapLiteral( + input: Node, + ) -> PCResult> { + input + .into_children() + .map(|node| match node.as_rule() { + Rule::AttrKVPair => HornbeamParser::AttrKVPair(node), + Rule::AttrKVarShorthand => HornbeamParser::AttrKVarShorthand(node), + other => { + unimplemented!("unexpected {other:?} in AttrMapLiteral"); + } + }) + .collect() + } + fn SEscape(input: Node) -> PCResult { let esc = input.as_str(); Ok(match esc { diff --git a/hornbeam_ir/src/ast_to_ir.rs b/hornbeam_ir/src/ast_to_ir.rs index a48caa7..6180075 100644 --- a/hornbeam_ir/src/ast_to_ir.rs +++ b/hornbeam_ir/src/ast_to_ir.rs @@ -1,6 +1,8 @@ use crate::ir::{Step, StepDef}; -use hornbeam_grammar::ast::{Block, Expression, StringExpr, StringPiece, Template}; -use hornbeam_grammar::{intern, Locator}; +use hornbeam_grammar::ast::{ + Binding, Block, Expression, HtmlElement, MatchBinding, StringExpr, StringPiece, Template, +}; +use hornbeam_grammar::{intern, IStr, Locator}; use itertools::Itertools; use std::borrow::Cow; use std::collections::btree_map::Entry; @@ -137,7 +139,7 @@ fn pull_out_entrypoints_from_block<'a>( } /// Step 2. Compile the AST to IR steps. -pub(crate) fn compile_functions<'a>( +pub(crate) fn compile_functions( functions: &BTreeMap>, ) -> Result>, AstToIrError> { let mut result = BTreeMap::new(); @@ -151,7 +153,7 @@ pub(crate) fn compile_functions<'a>( Ok(result) } -fn compile_ast_block_to_steps<'a>( +fn compile_ast_block_to_steps( block: &Block, instructions: &mut Vec, ) -> Result<(), AstToIrError> { @@ -177,7 +179,7 @@ fn compile_ast_block_to_steps<'a>( instructions.push(Step { def: StepDef::WriteLiteral { escape: false, - text: intern(format!(" id=\"")), + text: intern(" id=\""), }, locator: he.loc.clone(), }); @@ -200,73 +202,63 @@ fn compile_ast_block_to_steps<'a>( // This is only handling the case where we are the exclusive owner of all the class // names: see below for more... - if !he.classes.is_empty() { - if !he.attributes.contains_key(&intern("class")) { - instructions.push(Step { - def: StepDef::WriteLiteral { - escape: false, - text: intern(format!(" class=\"")), - }, - locator: he.loc.clone(), - }); - - instructions.push(Step { - def: StepDef::WriteLiteral { - escape: true, - text: intern(he.classes.iter().map(|istr| istr.as_str()).join(" ")), - }, - locator: he.loc.clone(), - }); - - instructions.push(Step { - def: StepDef::WriteLiteral { - escape: false, - text: intern("\""), - }, - locator: he.loc.clone(), - }); - } + if !he.classes.is_empty() && !he.attributes.contains_key(&intern("class")) { + let classes = he.classes.iter().map(|istr| istr.as_str()).join(" "); + gen_steps_to_write_literal_attribute(he, "class", &classes, instructions); } // Write attributes - for (attr_name, attr_expr) in &he.attributes { - instructions.push(Step { - def: StepDef::WriteLiteral { - escape: false, - text: intern(format!(" {attr_name}=\"")), - }, - locator: he.loc.clone(), - }); + for (attr_name, (attr_expr, attr_flags)) in &he.attributes { + let extra_inject = (attr_name.as_str() == "class" && !he.classes.is_empty()) + .then(|| intern(he.classes.iter().map(|istr| istr.as_str()).join(" ") + " ")); + if attr_flags.optional { + let mut some_stage = Vec::new(); + gen_steps_to_write_attribute( + he, + attr_name, + attr_expr, + extra_inject.clone(), + &mut some_stage, + ); + let binding = MatchBinding::TupleVariant { + name: intern("Some"), + pieces: vec![Binding::Variable(intern("___attrval"))], + }; + + let mut arms = vec![(binding, some_stage)]; + + if let Some(extra_inject) = extra_inject { + let mut none_stage = Vec::new(); + gen_steps_to_write_literal_attribute( + he, + attr_name, + extra_inject.trim_end_matches(' '), + &mut none_stage, + ); + arms.push(( + MatchBinding::UnitVariant { + name: intern("None"), + }, + none_stage, + )); + } - if attr_name.as_str() == "class" && !he.classes.is_empty() { - // This handles the case where we need to merge class lists between an - // attribute and the shorthand form. instructions.push(Step { - def: StepDef::WriteLiteral { - escape: true, - text: intern( - he.classes.iter().map(|istr| istr.as_str()).join(" ") + " ", - ), + def: StepDef::Match { + matchable: attr_expr.clone(), + arms, }, locator: he.loc.clone(), - }); + }) + } else { + gen_steps_to_write_attribute( + he, + attr_name, + attr_expr, + extra_inject, + instructions, + ); } - - instructions.push(Step { - def: StepDef::WriteEval { - escape: true, - expr: attr_expr.clone(), - }, - locator: he.loc.clone(), - }); - - instructions.push(Step { - def: StepDef::WriteLiteral { - escape: false, - text: intern("\""), - }, - locator: he.loc.clone(), - }); } // Close the tag @@ -469,6 +461,81 @@ fn compile_ast_block_to_steps<'a>( Ok(()) } +fn gen_steps_to_write_literal_attribute( + he: &HtmlElement, + attr_name: &str, + attr_value: &str, + instructions: &mut Vec, +) { + instructions.push(Step { + def: StepDef::WriteLiteral { + escape: false, + text: intern(format!(" {attr_name}=\"")), + }, + locator: he.loc.clone(), + }); + + instructions.push(Step { + def: StepDef::WriteLiteral { + escape: true, + text: intern(attr_value), + }, + locator: he.loc.clone(), + }); + + instructions.push(Step { + def: StepDef::WriteLiteral { + escape: false, + text: intern("\""), + }, + locator: he.loc.clone(), + }); +} + +fn gen_steps_to_write_attribute( + he: &HtmlElement, + attr_name: &str, + attr_expr: &Expression, + extra_inject: Option, + instructions: &mut Vec, +) { + instructions.push(Step { + def: StepDef::WriteLiteral { + escape: false, + text: intern(format!(" {attr_name}=\"")), + }, + locator: he.loc.clone(), + }); + + if let Some(extra_inject) = extra_inject { + // This handles the case where we need to merge class lists between an + // attribute and the shorthand form. + instructions.push(Step { + def: StepDef::WriteLiteral { + escape: true, + text: extra_inject, + }, + locator: he.loc.clone(), + }); + } + + instructions.push(Step { + def: StepDef::WriteEval { + escape: true, + expr: attr_expr.clone(), + }, + locator: he.loc.clone(), + }); + + instructions.push(Step { + def: StepDef::WriteLiteral { + escape: false, + text: intern("\""), + }, + locator: he.loc.clone(), + }); +} + #[cfg(test)] mod tests { use super::*;