How to Create a JSX-Like Rust Macro: Step-by-Step Guide - Part 2

This is part of a Series - read more
Attribute Slice
There are over 100 attributes in the html spec like:
id
: Provides a unique identifier for an element.class
: Assigns one or more class names to an element, used for styling or scripting multiple elements.src
: Specifies the source URL for elements like <img> or <script>.href
: Indicates the destination URL for an anchor tag <a>.alt
: Provides alternative text for an image if it cannot be displayed.style
: Applies inline CSS styles directly to an element.lang
: Specifies the language of the element's content.disabled
: Disables an input element.placeholder
: Provides a hint in an input field before the user enters a value.title
: Offers extra information about an element, often shown as a tooltip.etc…
Attribute Macro
Attributes must be supported in our JSX like Rust macro… So let’s do just that.
pub fn h_element_with_attrs(tag_name: &str, attrs: &[(&str, &str)], children: &str) -> String {
let mut attr_string = String::new();
for (key, value) in attrs {
attr_string.push_str(&format!(" {}=\"{}\"", key, value));
}
format!("<{}{}>{}</{}>", tag_name, attr_string, children, tag_name)
}
Borrowing a Tuple Slice
Here you’ll see we added a new argument to our fn called
attrs: &[(&str, &str)]
We then create a new mutable string
let mut attr_string = String::new()
Loop over the attrs, destructures them to (key, val) and push them to the newly created string formatted like you would see in html
let mut attr_string = String::new();
for (key, value) in attrs {
attr_string.push_str(&format!(" {}=\"{}\"", key, value));
}
Notice that the first char in the format is a space… that is intended so that the string doesn’t touch the tag name:
format!("<{}{}>{}</{}>", tag_name, attr_string, children, tag_name)
macro_rules!
#[macro_export]
macro_rules! htm_expr_attrs {
(<$tag: ident $($attr_name:ident = $attr_val:literal)*> {{ $content: expr }} </$end_tag: ident>) => {
$crate::htm_element_with_attrs(
stringify!($tag),
&[ $( (stringify!($attr_name), $attr_val) ),* ],
&$content.to_string()
)
};
}
The macro’s call to $crate::htm_element_with_attrs is doing three neat things at once:
Tag name → string
stringify!($tag)
converts the tag identifier (e.g.p
) into its string form"p"
.Attribute list → borrowed slice The repetition
$( … ),*
collects everyattr_name = "value"
pair the caller supplies, turns each into a tuple(stringify!($attr_name), $attr_val)
, and then wraps the whole lot in&[ … ]
to create a borrowed slice.Content → owned string
&$
content.to
_string()
moves the expression between{{ … }}
into an owned String, then passes a reference to it.
Testing
#[test]
fn htm_macro_expr_attrs() {
let num = 42;
let x = htm_expr_attrs!(<p id="foo" class="hello buz">{{ num }}</p>);
assert_eq!(x, "<p id=\"foo\" class=\"hello buz\">42</p>")
}
Note we must escape the quotes in the assert macro
Closing
Few more steps to go…
Arbitrary Rust ExpressionsHTML AttributesAllow Nested HTML
Tag Mismatch Safety
Create Component-Like Functions
Build an HTML Tree Struct
Traits for Render-to-String
Migrate to Procedural Macros
Signals / Reactivity
Subscribe to my newsletter
Read articles from Matthew Harwood directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
