Skip to content

Commit

Permalink
Allow splices in attribute names
Browse files Browse the repository at this point in the history
  • Loading branch information
BadMannersXYZ committed Sep 28, 2024
1 parent 0254fe1 commit 794e60e
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 52 deletions.
16 changes: 15 additions & 1 deletion docs/content/splices-toggles.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ html! {
# ;
```

### Splices in attribute name

You can also use splices in the attribute name:

```rust
let tuple = ("hx-get", "/pony");
# let _ = maud::
html! {
button (tuple.0)=(tuple.1) {
"Get a pony!"
}
}
```

### What can be spliced?

You can splice any value that implements [`Render`][Render].
Expand Down Expand Up @@ -145,7 +159,7 @@ html! {

### Optional attributes with values: `title=[Some("value")]`

Add optional attributes to an element using `attr=[value]` syntax, with *square* brackets.
Add optional attributes to an element using `attr=[value]` syntax, with _square_ brackets.
These are only rendered if the value is `Some<T>`, and entirely omitted if the value is `None`.

```rust
Expand Down
10 changes: 10 additions & 0 deletions maud/tests/splices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ fn locals() {
assert_eq!(result.into_string(), "Pinkie Pie");
}

#[test]
fn attribute_name() {
let tuple = ("hx-get", "/pony");
let result = html! { button (tuple.0)=(tuple.1) { "Get a pony!" } };
assert_eq!(
result.into_string(),
r#"<button hx-get="/pony">Get a pony!</button>"#
);
}

/// An example struct, for testing purposes only
struct Creature {
name: &'static str,
Expand Down
29 changes: 27 additions & 2 deletions maud_macros/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ impl Special {

#[derive(Debug)]
pub struct NamedAttr {
pub name: TokenStream,
pub name: AttrName,
pub attr_type: AttrType,
}

impl NamedAttr {
fn span(&self) -> SpanRange {
let name_span = span_tokens(self.name.clone());
let name_span = span_tokens(self.name.tokens());
if let Some(attr_type_span) = self.attr_type.span() {
name_span.join_range(attr_type_span)
} else {
Expand All @@ -168,6 +168,31 @@ impl NamedAttr {
}
}

#[derive(Debug, Clone)]
pub enum AttrName {
Fixed { value: TokenStream },
Splice { expr: TokenStream },
}

impl AttrName {
pub fn tokens(&self) -> TokenStream {
match self {
AttrName::Fixed { value } => value.clone(),
AttrName::Splice { expr, .. } => expr.clone(),
}
}
}

impl std::fmt::Display for AttrName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AttrName::Fixed { value } => f.write_str(&value.to_string())?,
AttrName::Splice { expr, .. } => f.write_str(&expr.to_string())?,
};
Ok(())
}
}

#[derive(Debug)]
pub enum AttrType {
Normal { value: Markup },
Expand Down
19 changes: 14 additions & 5 deletions maud_macros/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,19 @@ impl Generator {
build.push_escaped(&name_to_string(name));
}

fn attr_name(&self, name: AttrName, build: &mut Builder) {
match name {
AttrName::Fixed { value } => self.name(value, build),
AttrName::Splice { expr, .. } => self.splice(expr, build),
}
}

fn attrs(&self, attrs: Vec<Attr>, build: &mut Builder) {
for NamedAttr { name, attr_type } in desugar_attrs(attrs) {
match attr_type {
AttrType::Normal { value } => {
build.push_str(" ");
self.name(name, build);
self.attr_name(name, build);
build.push_str("=\"");
self.markup(value, build);
build.push_str("\"");
Expand All @@ -140,7 +147,7 @@ impl Generator {
let body = {
let mut build = self.builder();
build.push_str(" ");
self.name(name, &mut build);
self.attr_name(name, &mut build);
build.push_str("=\"");
self.splice(inner_value.clone(), &mut build);
build.push_str("\"");
Expand All @@ -150,15 +157,15 @@ impl Generator {
}
AttrType::Empty { toggler: None } => {
build.push_str(" ");
self.name(name, build);
self.attr_name(name, build);
}
AttrType::Empty {
toggler: Some(Toggler { cond, .. }),
} => {
let body = {
let mut build = self.builder();
build.push_str(" ");
self.name(name, &mut build);
self.attr_name(name, &mut build);
build.finish()
};
build.push_tokens(quote!(if (#cond) { #body }));
Expand Down Expand Up @@ -224,7 +231,9 @@ fn desugar_classes_or_ids(
});
}
Some(NamedAttr {
name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))),
name: AttrName::Fixed {
value: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))),
},
attr_type: AttrType::Normal {
value: Markup::Block(Block {
markups,
Expand Down
103 changes: 59 additions & 44 deletions maud_macros/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub fn parse(input: TokenStream) -> Vec<ast::Markup> {
#[derive(Clone)]
struct Parser {
/// If we're inside an attribute, then this contains the attribute name.
current_attr: Option<String>,
current_attr: Option<ast::AttrName>,
input: <TokenStream as IntoIterator>::IntoIter,
}

Expand Down Expand Up @@ -580,48 +580,7 @@ impl Parser {
let mut attrs = Vec::new();
loop {
if let Some(name) = self.try_namespaced_name() {
// Attribute
match self.peek() {
// Non-empty attribute
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
self.advance();
// Parse a value under an attribute context
assert!(self.current_attr.is_none());
self.current_attr = Some(ast::name_to_string(name.clone()));
let attr_type = match self.attr_toggler() {
Some(toggler) => ast::AttrType::Optional { toggler },
None => {
let value = self.markup();
ast::AttrType::Normal { value }
}
};
self.current_attr = None;
attrs.push(ast::Attr::Named {
named_attr: ast::NamedAttr { name, attr_type },
});
}
// Empty attribute (legacy syntax)
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => {
self.advance();
let toggler = self.attr_toggler();
attrs.push(ast::Attr::Named {
named_attr: ast::NamedAttr {
name: name.clone(),
attr_type: ast::AttrType::Empty { toggler },
},
});
}
// Empty attribute (new syntax)
_ => {
let toggler = self.attr_toggler();
attrs.push(ast::Attr::Named {
named_attr: ast::NamedAttr {
name: name.clone(),
attr_type: ast::AttrType::Empty { toggler },
},
});
}
}
attrs.push(self.attr(ast::AttrName::Fixed { value: name }));
} else {
match self.peek() {
// Class shorthand
Expand All @@ -644,6 +603,18 @@ impl Parser {
name,
});
}
// Spliced attribute name
Some(TokenTree::Group(ref group))
if group.delimiter() == Delimiter::Parenthesis =>
{
match self.markup() {
ast::Markup::Splice { expr, .. } => {
attrs.push(self.attr(ast::AttrName::Splice { expr }));
}
// If it's not a splice, backtrack and bail out
_ => break,
}
}
// If it's not a valid attribute, backtrack and bail out
_ => break,
}
Expand All @@ -665,7 +636,7 @@ impl Parser {
ast::Attr::Id { .. } => "id".to_string(),
ast::Attr::Named { named_attr } => named_attr
.name
.clone()
.tokens()
.into_iter()
.map(|token| token.to_string())
.collect(),
Expand All @@ -685,6 +656,50 @@ impl Parser {
attrs
}

fn attr(&mut self, name: ast::AttrName) -> ast::Attr {
match self.peek() {
// Non-empty attribute
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
self.advance();
// Parse a value under an attribute context
assert!(self.current_attr.is_none());
self.current_attr = Some(name.clone());
let attr_type = match self.attr_toggler() {
Some(toggler) => ast::AttrType::Optional { toggler },
None => {
let value = self.markup();
ast::AttrType::Normal { value }
}
};
self.current_attr = None;
ast::Attr::Named {
named_attr: ast::NamedAttr { name, attr_type },
}
}
// Empty attribute (legacy syntax)
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => {
self.advance();
let toggler = self.attr_toggler();
ast::Attr::Named {
named_attr: ast::NamedAttr {
name: name,
attr_type: ast::AttrType::Empty { toggler },
},
}
}
// Empty attribute (new syntax)
_ => {
let toggler = self.attr_toggler();
ast::Attr::Named {
named_attr: ast::NamedAttr {
name: name,
attr_type: ast::AttrType::Empty { toggler },
},
}
}
}
}

/// Parses the name of a class or ID.
fn class_or_id_name(&mut self) -> ast::Markup {
if let Some(symbol) = self.try_name() {
Expand Down

0 comments on commit 794e60e

Please sign in to comment.