lambda-fairy / maud

:pencil: Compile-time HTML templates for Rust
https://maud.lambda.xyz
Apache License 2.0
1.98k stars 132 forks source link

Access maud's AST and change (e.g. add attribute) programatically? #365

Open yatesco opened 1 year ago

yatesco commented 1 year ago

Hi there,

I am writing the same fragment of HTML over and over and want to extract a helper function to do it. In this case the helper function will do something like:

form {
    ....
(passed_in_element)
}

and is called like:

let contents = html! {
  div {....}
};
let form = my_form_helper::render(contents);

I really want the helper function to add an attribute to the passed_in_element - is this possible?

Thanks!

lambda-fairy commented 1 year ago

Hi @yatesco!

Unfortunately I don't know of a clean way to do that. The html! macro compiles down to string concatenation. None of the AST exists at runtime.

There are also security implications to passing arbitrary attributes around. In the future, I'd like to implement context-aware escaping (#181). But for that to work, each attribute must understand the context in which its used. An example is the src attribute – a user-supplied src on an <img> is (mostly) harmless, but that same src on a <script> can lead to XSS.

What's the attribute that you want to add to the element? Can you capture all the possibilities in an enum?

yatesco commented 1 year ago

hi @lambda-fairy - so the idea is that the app is primarily HTML served from the back, which is of course why I'm using this excellent tool :-), but I want to enhance the front end with snippets of dynamic behaviour, similar to htmx.org.

I have a series of attributes which are processed at runtime to introduce dynamic behaviour, so, for example, I might have a (badly designed) accordion that looks like:

<div my-attribute-type="accordion">
  <div my-acccordion-index="1"></div>
  <div my-acccordion-index="2"></div>
  <div my-acccordion-index="3"></div>
  <div my-acccordion-index="4"></div>
</div>

It's simple to have an fn:

fn accordion(pages: Vec<AccordionPage>) -> Markup {

}

But what I really want is:

fn accordion(pages: Vec<Markup>) -> Markup {}

which I call like:

accordion(vec![html! { "first page" }, html! {"second page"}])

and that's where I'm blocked. I can use an HTML parser lib to do this, but I wondered if maud had something OOTB.

rben01 commented 1 year ago

On the one hand, I agree that having the AST available at runtime would be interesting. On the other hand, I feel like this particular situation may be better handled in Rust proper.

struct AccordionPageTemplate {
    data: String,
}
struct AccordionPage {
    template: AccordionPageTemplate,
    index: usize,
}
impl Render for AccordionPage {
    fn render(&self) -> Markup {
        html! {
            div my-acccordion-index=(self.index) {
                {"my data is " (self.template.data)}
            }
        }
    }
}

fn accordion(page_templates: Vec<AccordionPageTemplate>) -> Markup {
    html! {
        div my-attribute-type="accordion" {
            @for (index, template) in page_templates.into_iter().enumerate() {
                (AccordionPage { template, index })
            }
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let accordion = accordion(vec![
        AccordionPageTemplate {
            data: "First".into(),
        },
        AccordionPageTemplate {
            data: "Second".into(),
        },
    ]);
    println!("{}", accordion.render().into_string());

    Ok(())
}
<div my-attribute-type="accordion"><div my-acccordion-index="0">my data is First</div><div my-acccordion-index="1">my data is Second</div></div>

What I wonder is: given the power and flexibility of Rust's type system, is there any situation where being able to use maud’s AST would solve a problem that couldn't be solved by implementing Render on the right types?

yatesco commented 1 year ago

Thanks @rben01 - you make a compelling point. I'm nervous about getting too Java-esque with classes everywhere, but I can't fully articulate why yet. I need to let this simmer in the back of my mind for a bit :-)