Add support for Option<> field values

Signed-off-by: Olivier <olivier@librepush.net>
This commit is contained in:
Olivier 'reivilibre' 2025-05-09 20:10:25 +01:00
parent d08c04de5b
commit d9f008d5ed
4 changed files with 211 additions and 71 deletions

View File

@ -26,10 +26,15 @@ pub struct HtmlElement {
pub children: Vec<Block>,
pub classes: Vec<IStr>,
pub dom_id: Option<IStr>,
pub attributes: BTreeMap<IStr, Expression>,
pub attributes: BTreeMap<IStr, (Expression, ElementAttributeFlags)>,
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,

View File

@ -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.

View File

@ -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<BTreeMap<IStr, Expression>> = 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<BTreeMap<IStr, (Expression, ElementAttributeFlags)>> {
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<IStr> {
let esc = input.as_str();
Ok(match esc {

View File

@ -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<String, Vec<Block>>,
) -> Result<BTreeMap<String, Vec<Step>>, 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<Step>,
) -> 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<Step>,
) {
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<IStr>,
instructions: &mut Vec<Step>,
) {
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::*;