Skip to content

Commit 794e60e

Browse files
committed
Allow splices in attribute names
Closes #444
1 parent 0254fe1 commit 794e60e

File tree

5 files changed

+125
-52
lines changed

5 files changed

+125
-52
lines changed

docs/content/splices-toggles.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ html! {
9090
# ;
9191
```
9292

93+
### Splices in attribute name
94+
95+
You can also use splices in the attribute name:
96+
97+
```rust
98+
let tuple = ("hx-get", "/pony");
99+
# let _ = maud::
100+
html! {
101+
button (tuple.0)=(tuple.1) {
102+
"Get a pony!"
103+
}
104+
}
105+
```
106+
93107
### What can be spliced?
94108

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

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

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

151165
```rust

maud/tests/splices.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ fn locals() {
7373
assert_eq!(result.into_string(), "Pinkie Pie");
7474
}
7575

76+
#[test]
77+
fn attribute_name() {
78+
let tuple = ("hx-get", "/pony");
79+
let result = html! { button (tuple.0)=(tuple.1) { "Get a pony!" } };
80+
assert_eq!(
81+
result.into_string(),
82+
r#"<button hx-get="/pony">Get a pony!</button>"#
83+
);
84+
}
85+
7686
/// An example struct, for testing purposes only
7787
struct Creature {
7888
name: &'static str,

maud_macros/src/ast.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ impl Special {
153153

154154
#[derive(Debug)]
155155
pub struct NamedAttr {
156-
pub name: TokenStream,
156+
pub name: AttrName,
157157
pub attr_type: AttrType,
158158
}
159159

160160
impl NamedAttr {
161161
fn span(&self) -> SpanRange {
162-
let name_span = span_tokens(self.name.clone());
162+
let name_span = span_tokens(self.name.tokens());
163163
if let Some(attr_type_span) = self.attr_type.span() {
164164
name_span.join_range(attr_type_span)
165165
} else {
@@ -168,6 +168,31 @@ impl NamedAttr {
168168
}
169169
}
170170

171+
#[derive(Debug, Clone)]
172+
pub enum AttrName {
173+
Fixed { value: TokenStream },
174+
Splice { expr: TokenStream },
175+
}
176+
177+
impl AttrName {
178+
pub fn tokens(&self) -> TokenStream {
179+
match self {
180+
AttrName::Fixed { value } => value.clone(),
181+
AttrName::Splice { expr, .. } => expr.clone(),
182+
}
183+
}
184+
}
185+
186+
impl std::fmt::Display for AttrName {
187+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188+
match self {
189+
AttrName::Fixed { value } => f.write_str(&value.to_string())?,
190+
AttrName::Splice { expr, .. } => f.write_str(&expr.to_string())?,
191+
};
192+
Ok(())
193+
}
194+
}
195+
171196
#[derive(Debug)]
172197
pub enum AttrType {
173198
Normal { value: Markup },

maud_macros/src/generate.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,19 @@ impl Generator {
123123
build.push_escaped(&name_to_string(name));
124124
}
125125

126+
fn attr_name(&self, name: AttrName, build: &mut Builder) {
127+
match name {
128+
AttrName::Fixed { value } => self.name(value, build),
129+
AttrName::Splice { expr, .. } => self.splice(expr, build),
130+
}
131+
}
132+
126133
fn attrs(&self, attrs: Vec<Attr>, build: &mut Builder) {
127134
for NamedAttr { name, attr_type } in desugar_attrs(attrs) {
128135
match attr_type {
129136
AttrType::Normal { value } => {
130137
build.push_str(" ");
131-
self.name(name, build);
138+
self.attr_name(name, build);
132139
build.push_str("=\"");
133140
self.markup(value, build);
134141
build.push_str("\"");
@@ -140,7 +147,7 @@ impl Generator {
140147
let body = {
141148
let mut build = self.builder();
142149
build.push_str(" ");
143-
self.name(name, &mut build);
150+
self.attr_name(name, &mut build);
144151
build.push_str("=\"");
145152
self.splice(inner_value.clone(), &mut build);
146153
build.push_str("\"");
@@ -150,15 +157,15 @@ impl Generator {
150157
}
151158
AttrType::Empty { toggler: None } => {
152159
build.push_str(" ");
153-
self.name(name, build);
160+
self.attr_name(name, build);
154161
}
155162
AttrType::Empty {
156163
toggler: Some(Toggler { cond, .. }),
157164
} => {
158165
let body = {
159166
let mut build = self.builder();
160167
build.push_str(" ");
161-
self.name(name, &mut build);
168+
self.attr_name(name, &mut build);
162169
build.finish()
163170
};
164171
build.push_tokens(quote!(if (#cond) { #body }));
@@ -224,7 +231,9 @@ fn desugar_classes_or_ids(
224231
});
225232
}
226233
Some(NamedAttr {
227-
name: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))),
234+
name: AttrName::Fixed {
235+
value: TokenStream::from(TokenTree::Ident(Ident::new(attr_name, Span::call_site()))),
236+
},
228237
attr_type: AttrType::Normal {
229238
value: Markup::Block(Block {
230239
markups,

maud_macros/src/parse.rs

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub fn parse(input: TokenStream) -> Vec<ast::Markup> {
1313
#[derive(Clone)]
1414
struct Parser {
1515
/// If we're inside an attribute, then this contains the attribute name.
16-
current_attr: Option<String>,
16+
current_attr: Option<ast::AttrName>,
1717
input: <TokenStream as IntoIterator>::IntoIter,
1818
}
1919

@@ -580,48 +580,7 @@ impl Parser {
580580
let mut attrs = Vec::new();
581581
loop {
582582
if let Some(name) = self.try_namespaced_name() {
583-
// Attribute
584-
match self.peek() {
585-
// Non-empty attribute
586-
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
587-
self.advance();
588-
// Parse a value under an attribute context
589-
assert!(self.current_attr.is_none());
590-
self.current_attr = Some(ast::name_to_string(name.clone()));
591-
let attr_type = match self.attr_toggler() {
592-
Some(toggler) => ast::AttrType::Optional { toggler },
593-
None => {
594-
let value = self.markup();
595-
ast::AttrType::Normal { value }
596-
}
597-
};
598-
self.current_attr = None;
599-
attrs.push(ast::Attr::Named {
600-
named_attr: ast::NamedAttr { name, attr_type },
601-
});
602-
}
603-
// Empty attribute (legacy syntax)
604-
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => {
605-
self.advance();
606-
let toggler = self.attr_toggler();
607-
attrs.push(ast::Attr::Named {
608-
named_attr: ast::NamedAttr {
609-
name: name.clone(),
610-
attr_type: ast::AttrType::Empty { toggler },
611-
},
612-
});
613-
}
614-
// Empty attribute (new syntax)
615-
_ => {
616-
let toggler = self.attr_toggler();
617-
attrs.push(ast::Attr::Named {
618-
named_attr: ast::NamedAttr {
619-
name: name.clone(),
620-
attr_type: ast::AttrType::Empty { toggler },
621-
},
622-
});
623-
}
624-
}
583+
attrs.push(self.attr(ast::AttrName::Fixed { value: name }));
625584
} else {
626585
match self.peek() {
627586
// Class shorthand
@@ -644,6 +603,18 @@ impl Parser {
644603
name,
645604
});
646605
}
606+
// Spliced attribute name
607+
Some(TokenTree::Group(ref group))
608+
if group.delimiter() == Delimiter::Parenthesis =>
609+
{
610+
match self.markup() {
611+
ast::Markup::Splice { expr, .. } => {
612+
attrs.push(self.attr(ast::AttrName::Splice { expr }));
613+
}
614+
// If it's not a splice, backtrack and bail out
615+
_ => break,
616+
}
617+
}
647618
// If it's not a valid attribute, backtrack and bail out
648619
_ => break,
649620
}
@@ -665,7 +636,7 @@ impl Parser {
665636
ast::Attr::Id { .. } => "id".to_string(),
666637
ast::Attr::Named { named_attr } => named_attr
667638
.name
668-
.clone()
639+
.tokens()
669640
.into_iter()
670641
.map(|token| token.to_string())
671642
.collect(),
@@ -685,6 +656,50 @@ impl Parser {
685656
attrs
686657
}
687658

659+
fn attr(&mut self, name: ast::AttrName) -> ast::Attr {
660+
match self.peek() {
661+
// Non-empty attribute
662+
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '=' => {
663+
self.advance();
664+
// Parse a value under an attribute context
665+
assert!(self.current_attr.is_none());
666+
self.current_attr = Some(name.clone());
667+
let attr_type = match self.attr_toggler() {
668+
Some(toggler) => ast::AttrType::Optional { toggler },
669+
None => {
670+
let value = self.markup();
671+
ast::AttrType::Normal { value }
672+
}
673+
};
674+
self.current_attr = None;
675+
ast::Attr::Named {
676+
named_attr: ast::NamedAttr { name, attr_type },
677+
}
678+
}
679+
// Empty attribute (legacy syntax)
680+
Some(TokenTree::Punct(ref punct)) if punct.as_char() == '?' => {
681+
self.advance();
682+
let toggler = self.attr_toggler();
683+
ast::Attr::Named {
684+
named_attr: ast::NamedAttr {
685+
name: name,
686+
attr_type: ast::AttrType::Empty { toggler },
687+
},
688+
}
689+
}
690+
// Empty attribute (new syntax)
691+
_ => {
692+
let toggler = self.attr_toggler();
693+
ast::Attr::Named {
694+
named_attr: ast::NamedAttr {
695+
name: name,
696+
attr_type: ast::AttrType::Empty { toggler },
697+
},
698+
}
699+
}
700+
}
701+
}
702+
688703
/// Parses the name of a class or ID.
689704
fn class_or_id_name(&mut self) -> ast::Markup {
690705
if let Some(symbol) = self.try_name() {

0 commit comments

Comments
 (0)