Closed DrSloth closed 1 year ago
Can you write a little example what this would look like?
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.
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) " "
}
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.
The class names are intentionally escaped.
Ahh, okay! I misunderstood what you meant with "raw attributes". Yeah, then this sounds like a good addition.
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?
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?
@DrSloth could you show me some code where you're needing this feature? I'll see if I can find a different solution.
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.
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.
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?
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.
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?
@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).
Looks very nice. I don't have anything to add that i can directly see. Will this new version be published to crates.io?
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 :)
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 :)