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

Matthew HarwoodMatthew Harwood
3 min read

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:

  1. Tag name → stringstringify!($tag) converts the tag identifier (e.g. p) into its string form "p".

  2. Attribute list → borrowed slice The repetition $( … ),* collects every attr_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.

  3. 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…

  1. Arbitrary Rust Expressions

  2. HTML Attributes

  3. Allow Nested HTML

  4. Tag Mismatch Safety

  5. Create Component-Like Functions

  6. Build an HTML Tree Struct

  7. Traits for Render-to-String

  8. Migrate to Procedural Macros

  9. Signals / Reactivity

0
Subscribe to my newsletter

Read articles from Matthew Harwood directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Matthew Harwood
Matthew Harwood