parent
0254fe1f81
commit
2c60c64181
5 changed files with 126 additions and 52 deletions
|
@ -90,6 +90,21 @@ 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].
|
||||
|
@ -145,7 +160,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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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 },
|
||||
|
|
|
@ -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("\"");
|
||||
|
@ -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("\"");
|
||||
|
@ -150,7 +157,7 @@ impl Generator {
|
|||
}
|
||||
AttrType::Empty { toggler: None } => {
|
||||
build.push_str(" ");
|
||||
self.name(name, build);
|
||||
self.attr_name(name, build);
|
||||
}
|
||||
AttrType::Empty {
|
||||
toggler: Some(Toggler { cond, .. }),
|
||||
|
@ -158,7 +165,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.finish()
|
||||
};
|
||||
build.push_tokens(quote!(if (#cond) { #body }));
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
attr_type: ast::AttrType::Empty { toggler },
|
||||
},
|
||||
}
|
||||
}
|
||||
// Empty attribute (new syntax)
|
||||
_ => {
|
||||
let toggler = self.attr_toggler();
|
||||
ast::Attr::Named {
|
||||
named_attr: ast::NamedAttr {
|
||||
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() {
|
||||
|
|
Loading…
Add table
Reference in a new issue