utkarshkukreti / markup.rs

A blazing fast, type-safe template engine for Rust.
Apache License 2.0
350 stars 14 forks source link

Add Raw attributes with () syntax #27

Closed DrSloth closed 1 year ago

DrSloth commented 1 year ago

This is a pull request that would resolve my issue #26 this still requires some amount of markup::raw but this helps my use case definitely. Optimally the html escaping would be turned off by default in the attributes but it is not that important.

Hope this helps, have a nice day :)

Kijewski commented 1 year ago

Can you write a little example what this would look like?

DrSloth commented 1 year ago

Should that example go in the example directory? One example would be:

markup::define! {
    Elem<'a>(classes: &'a [String]) {
        div(
            "class="
            '"'
            @for class in classes.iter() {
                @class " "
            }
            '"'
        ) {
            "Element has the classes: "
            @for class in classes.iter() {
                @class " "
            }
        }
    }
}

fn main() {
    println!(
        "{}",
        Elem {
            classes: &["Hello".to_owned(), "World".to_owned()]
        }
    )
}

this would render as: <div class="Hello World ">Element has the classes: Hello World </div> the classes are taken from the slice and printed in the classes attribute.

Kijewski commented 1 year ago

I really don't like the semantics of this feature. Why not make printing raw strings opt-in?

@for class in classes.iter() {
    @markup::raw(class) " "
}
DrSloth commented 1 year ago

Oh that is still opt in entirely. I haven't changed anything about raw string printing. The " are printed raw because they are chars which are apparently not escaped. The problem was that you can't put arbitrary content in the attribute position of an element.

DrSloth commented 1 year ago

The class names are intentionally escaped.

Kijewski commented 1 year ago

Ahh, okay! I misunderstood what you meant with "raw attributes". Yeah, then this sounds like a good addition.

utkarshkukreti commented 1 year ago

I'm not really liking this syntax. There's also a way to achieve this right now using the fact that attribute values use markup::Render to render:

markup::define! {
    Classes<'a>(classes: &'a [String]) {
        @for class in classes.iter() {
            @class " "
        }
    }
    Elem<'a>(classes: &'a [String]) {
        div[class = Classes { classes }] {
        // or this also works:
        // div.{Attr { classes }} {
            "Element has the classes: "
            @for class in classes.iter() {
                @class " "
            }
        }
    }
}

fn main() {
    println!(
        "{}",
        Elem {
            classes: &["Hello".to_owned(), "World".to_owned()]
        }
    )
}

What do you think?

DrSloth commented 1 year ago

Ah i didn't even know you could markup::Render for the Attribute value, but that doesn't fully help my use case. In my use case i also need to change the actually applied attributes which this doesn't achieve. Even if there is a way to programmatically change the attributes value there is no way to take full control of the attributes which is required in my case and i think that freedom should exist in general.

Maybe this shouldn't use () but rather something else such as [[]] or anything along these lines?

utkarshkukreti commented 1 year ago

@DrSloth could you show me some code where you're needing this feature? I'll see if I can find a different solution.

DrSloth commented 1 year ago

I have two cases in one i want to emit a hashmap as data attributes. An example of that where a div holds these attributes looks somewhat like this:

markup::define! {
    DivDataAttrs<'a>(data_attrs: HashMap<String, String>, text: &'a str) {
        div(
            @for (k, v) in data_attrs {
                "data-"@k '=' '"' @v '"'
            }
        ) {
            @text
        }
    }
}

fn main() {
    let mut map = HashMap::new();
    map.insert("hello".to_owned(), "world".to_owned());
    map.insert("name".to_owned(), "Bob".to_owned());
    map.insert("age".to_owned(), "21".to_owned());

    println!(
        "{}",
        DivDataAttrs {
            data_attrs: map,
            text: "Hello, World",
        }
    )
}

in my use case these data attributes are extracted for reuse in multiple different components which also requires this.

My other case is setting some attributes dynamically with an Option<Iterator<Item = String>> if the Option is none the attribute name shouldn't show up.

markup::define! {
    HtmlAttr<'name, Content: markup::Render>(name: &'name str, content: Option<Content>) {
        @if let Some(content) = content {
            '"' @name '"' '=' '"' @content '"'
        }
    }
}

this would look somewhat like this. And it is then used somwhat like this:

    img(
        @HtmlAttr {name: "id", content: id.as_ref()}
            @HtmlAttr {name: "src", content: Some(&image_data.image_path)}
            @HtmlAttr {
                name: "style",
                content: styles.map(|styles| super::seperated(" ", styles))
            }
            @HtmlAttr {name: "width", content: image_data.width}
            @HtmlAttr {name: "aspect-ratio", content: image_data.aspect_ratio.as_ref()}
        );

This would be an example of an image defined with this. The idea behind this is to create iterators that write directly to the buffer instead of doing intermediate allocations.

I am using this to create a dynamic web view to configure an iot device and memory allocation is a bottleneck.

utkarshkukreti commented 1 year ago

For the first one, what do you think if we support "spread" attributes using the struct update syntax which supports passing any iterator? With that, and the fact that tuples allow you to concatenate multiple Render into one without allocation, this would work:

markup::define! {
        C(data: HashMap<&'static str, &'static str>) {
            div[..data.iter().map(|(k, v)| (("data-", k), v))] {}
        }
}

C { data: [("foo", "bar"), ("baz", "quux")].iter().cloned().collect() }

=>

<div data-foo="bar" data-baz="quux"></div>

(I've got an implementation of this working locally.)


For the second one, if the attribute list is not dynamic, this is already supported:

div[foo = None, bar = "baz"] {}

renders

<div bar="baz"></div>

None attribute values are completely skipped from the output.

DrSloth commented 1 year ago

That looks quite nice. If the rest spread syntax would also skip None values completely, that would mostly solve my problems. The main problem with the spread syntax would be it only supporting Iterator of one type. This means i would manually have to create some Either type which renders one of multiple variants and then create functions to chain the iterators returning that Either type. But that would be okay. I actually really like the idea of the rest spread here. Maybe it would be possible to pass it a tuple of multiple iterators which are then all rendered?

utkarshkukreti commented 1 year ago

Yes, it would use the same logic as normal attributes so None will be skipped, and you can spread multiple iterators like div[..foo, ..bar]. Here are the tests that I've written:

t! {
    t16,
    {
        A() {
            div[..Vec::<(String, String)>::new(), ..[("foo", "bar")]] {}
        }
        B<'a>(attrs: &'a [(&'static str, Option<&'static str>)]) {
            div[id = "b", ..*attrs] {}
        }
        C(data: std::collections::BTreeMap<&'static str, &'static str>) {
            div[..data.iter().map(|(k, v)| (("data-", k), v))] {}
        }
    },
    A {} => r#"<div foo="bar"></div>"#,
    B {
        attrs: &[("foo", None), ("bar", Some("baz"))]
    } => r#"<div id="b" bar="baz"></div>"#,
    C {
        data: [("foo", "bar"), ("baz", "quux")].iter().cloned().collect()
    } => r#"<div data-baz="quux" data-foo="bar"></div>"#,
}

I think this would solve both of your use cases.

DrSloth commented 1 year ago

Yes it does, this looks really nice. Thank you very much. Should this be documented somewhere? Would you perhaps be interested in a pull request updating the documentation with syntax description and such?

utkarshkukreti commented 1 year ago

@DrSloth I finally managed to write some syntax reference (https://github.com/utkarshkukreti/markup.rs#syntax-reference-wip). I've included an example of this new spread attributes feature. I welcome any improvements you can think of. The source of the reference is in docs/reference.md. It's evaluated and inserted into README.md using a Ruby script (run make if you have ruby installed, otherwise just send a PR with changes to docs/reference.md and I'll run the script myself).

DrSloth commented 1 year ago

Looks very nice. I don't have anything to add that i can directly see. Will this new version be published to crates.io?

DrSloth commented 1 year ago

I will have another look through the docs today, i will mainly try to integrate the documentation in the rustdocs. I have ruby installed and i will maybe adapt the ruby script to separate the syntax docs in to a separate file. I will close this pull request and the issue now as is has been solved. Thanks :)