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?
|
### What can be spliced?
|
||||||
|
|
||||||
You can splice any value that implements [`Render`][Render].
|
You can splice any value that implements [`Render`][Render].
|
||||||
|
@ -145,7 +160,7 @@ html! {
|
||||||
|
|
||||||
### Optional attributes with values: `title=[Some("value")]`
|
### 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`.
|
These are only rendered if the value is `Some<T>`, and entirely omitted if the value is `None`.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
@ -73,6 +73,16 @@ fn locals() {
|
||||||
assert_eq!(result.into_string(), "Pinkie Pie");
|
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
|
/// An example struct, for testing purposes only
|
||||||
struct Creature {
|
struct Creature {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
|
|
|
@ -153,13 +153,13 @@ impl Special {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NamedAttr {
|
pub struct NamedAttr {
|
||||||
pub name: TokenStream,
|
pub name: AttrName,
|
||||||
pub attr_type: AttrType,
|
pub attr_type: AttrType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NamedAttr {
|
impl NamedAttr {
|
||||||
fn span(&self) -> SpanRange {
|
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() {
|
if let Some(attr_type_span) = self.attr_type.span() {
|
||||||
name_span.join_range(attr_type_span)
|
name_span.join_range(attr_type_span)
|
||||||
} else {
|
} 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)]
|
#[derive(Debug)]
|
||||||
pub enum AttrType {
|
pub enum AttrType {
|
||||||
Normal { value: Markup },
|
Normal { value: Markup },
|
||||||
|
|
|
@ -123,12 +123,19 @@ impl Generator {
|
||||||
build.push_escaped(&name_to_string(name));
|
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) {
|
fn attrs(&self, attrs: Vec<Attr>, build: &mut Builder) {
|
||||||
for NamedAttr { name, attr_type } in desugar_attrs(attrs) {
|
for NamedAttr { name, attr_type } in desugar_attrs(attrs) {
|
||||||
match attr_type {
|
match attr_type {
|
||||||
AttrType::Normal { value } => {
|
AttrType::Normal { value } => {
|
||||||
build.push_str(" ");
|
build.push_str(" ");
|
||||||
self.name(name, build);
|
self.attr_name(name, build);
|
||||||
build.push_str("=\"");
|
build.push_str("=\"");
|
||||||
self.markup(value, build);
|
self.markup(value, build);
|
||||||
build.push_str("\"");
|
build.push_str("\"");
|
||||||
|
@ -140,7 +147,7 @@ impl Generator {
|
||||||
let body = {
|
let body = {
|
||||||
let mut build = self.builder();
|
let mut build = self.builder();
|
||||||
build.push_str(" ");
|
build.push_str(" ");
|
||||||
self.name(name, &mut build);
|
self.attr_name(name, &mut build);
|
||||||
build.push_str("=\"");
|
build.push_str("=\"");
|
||||||
self.splice(inner_value.clone(), &mut build);
|
self.splice(inner_value.clone(), &mut build);
|
||||||
build.push_str("\"");
|
build.push_str("\"");
|
||||||
|
@ -150,7 +157,7 @@ impl Generator {
|
||||||
}
|
}
|
||||||
AttrType::Empty { toggler: None } => {
|
AttrType::Empty { toggler: None } => {
|
||||||
build.push_str(" ");
|
build.push_str(" ");
|
||||||
self.name(name, build);
|
self.attr_name(name, build);
|
||||||
}
|
}
|
||||||
AttrType::Empty {
|
AttrType::Empty {
|
||||||
toggler: Some(Toggler { cond, .. }),
|
toggler: Some(Toggler { cond, .. }),
|
||||||
|
@ -158,7 +165,7 @@ impl Generator {
|
||||||
let body = {
|
let body = {
|
||||||
let mut build = self.builder();
|
let mut build = self.builder();
|
||||||
build.push_str(" ");
|
build.push_str(" ");
|
||||||
self.name(name, &mut build);
|
self.attr_name(name, &mut build);
|
||||||
build.finish()
|
build.finish()
|
||||||
};
|
};
|
||||||
build.push_tokens(quote!(if (#cond) { #body }));
|
build.push_tokens(quote!(if (#cond) { #body }));
|
||||||
|
@ -224,7 +231,9 @@ fn desugar_classes_or_ids(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(NamedAttr {
|
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 {
|
attr_type: AttrType::Normal {
|
||||||
value: Markup::Block(Block {
|
value: Markup::Block(Block {
|
||||||
markups,
|
markups,
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub fn parse(input: TokenStream) -> Vec<ast::Markup> {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Parser {
|
struct Parser {
|
||||||
/// If we're inside an attribute, then this contains the attribute name.
|
/// 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,
|
input: <TokenStream as IntoIterator>::IntoIter,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,48 +580,7 @@ impl Parser {
|
||||||
let mut attrs = Vec::new();
|
let mut attrs = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
if let Some(name) = self.try_namespaced_name() {
|
if let Some(name) = self.try_namespaced_name() {
|
||||||
// Attribute
|
attrs.push(self.attr(ast::AttrName::Fixed { value: name }));
|
||||||
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 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
match self.peek() {
|
match self.peek() {
|
||||||
// Class shorthand
|
// Class shorthand
|
||||||
|
@ -644,6 +603,18 @@ impl Parser {
|
||||||
name,
|
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
|
// If it's not a valid attribute, backtrack and bail out
|
||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
|
@ -665,7 +636,7 @@ impl Parser {
|
||||||
ast::Attr::Id { .. } => "id".to_string(),
|
ast::Attr::Id { .. } => "id".to_string(),
|
||||||
ast::Attr::Named { named_attr } => named_attr
|
ast::Attr::Named { named_attr } => named_attr
|
||||||
.name
|
.name
|
||||||
.clone()
|
.tokens()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|token| token.to_string())
|
.map(|token| token.to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -685,6 +656,50 @@ impl Parser {
|
||||||
attrs
|
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.
|
/// Parses the name of a class or ID.
|
||||||
fn class_or_id_name(&mut self) -> ast::Markup {
|
fn class_or_id_name(&mut self) -> ast::Markup {
|
||||||
if let Some(symbol) = self.try_name() {
|
if let Some(symbol) = self.try_name() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue