Search code examples
rustrust-macros

Expanding a recursive macro in rust, similar to serde_json but for HTML elements


#[macro_export]
macro_rules! reactant {
    // Converts (function) {...} into element
    ( $f:ident $t:tt ) => {
        {
            let mut elem = HtmlElement::new($f);
            reactant!(@expand elem $t);
            elem
        }
    };

    // Expands {...} and parses each internal node individually
    ( @expand $self:ident { $($t:tt),* } ) => {
        $(
            reactant!(@generate $self $t);
        )*
    };

    // Takes a (function) {...} node, feeds it back recursively, and pushes it into parent
    ( @generate $self:ident $t1:tt $t2:tt) => {
        {
            $self.push_inner(reactant!($t1 $t2));
        }
    };

    // Takes a literal and sets the parent internal to a string
    ( @generate $self:ident $l:literal) => {
        {
            $self.set_inner(String::from($l)).unwrap();
        }
    };

}

#[allow(unused_macros)]
#[cfg(test)]
mod tests {
    use crate::html::types::*;
    use crate::html::HtmlElement;
    use crate::reactant;

    #[test]
    fn test() {
        // Doesn't work, not expecting '{' after second div, although the first one works fine
        let x = reactant!(div {
            div {
                "test"
            }
        });

        // Works, outputs <div>thing</div>
        let y = reactant!(div {
            "hello",
            "thing"
        });

    }
}

I am working on making an uncreatively named HTML library in Rust, and am also learning macros at the same time (the macro documentation is confusing). Part of the project is making a macro that generates HTML elements recursively to make a document, in a similar appearance to serde_json. The syntax is shown in the test cases. Basically, every HTML element (div, h1, etc.) is mapped to a function that outputs a struct that I have crafted for HTML elements. I managed to get the macro working in one way, but it only allowed for HTML children when I want it to also take literals to fill in, say, an h1 with a string (test case 1). Test case 2 shows where it doesn't work, and I am not understanding why it can't read the {.


Solution

  • Let's try to trace what happens when the macro is being expanded:

    reactant!(div { div { "test" } });
    

    This triggers the first rule: $f:ident $t:tt with:

    • $f set to div
    • $t set to { div { "test" } }

    Which gets expanded to:

    reactant!(@expand elem { div { "test" } });
    

    I believe you intended the second step to trigger rule number 2: @expand $self:ident { $($t:tt),* } with:

    • $self set to elem
    • $t set to div { "test" }

    Which would get expanded to:

    reactant!(@generate elem div { "test" });
    

    But div { "test" } is actually two tts and so can't be parsed by this rule (or by any other rule).


    If my interpretation of your intentions is correct, you will need to have separate rules to handle each case and process the list iteratively:

        // Expands {...} and parses each internal node individually
        ( @expand $self:ident { $t1:tt $t2:tt, $($tail:tt)* } ) => {
                reactant!(@generate $self $t1 $t2);
                reactant!(@expand $self { $($tail)* })
        };
        ( @expand $self:ident { $t:tt, $($tail:tt)* } ) => {
                reactant!(@generate $self $t);
                reactant!(@expand $self { $($tail)* });
        };
        // Duplicate rules to handle the last item if it's not followed by a ","
        ( @expand $self:ident { $t1:tt $t2:tt } ) => {
                reactant!(@generate $self $t1 $t2);
        };
        ( @expand $self:ident { $t:tt } ) => {
                reactant!(@generate $self $t);
        };
    

    Playground