stebalien / horrorshow-rs Goto Github PK
View Code? Open in Web Editor NEWA macro-based html builder for rust
Home Page: https://docs.rs/horrorshow/
License: Apache License 2.0
A macro-based html builder for rust
Home Page: https://docs.rs/horrorshow/
License: Apache License 2.0
It's common in Rust web server frameworks to manage instances of bytes::Bytes
, which is essentially a reference counted Vec
. It's sufficiently ubiquitous that I think there'd be value in adding write_to_bytes
or into_bytes
to Template
(perhaps behind a feature flag).
I had a need for something like https://github.com/JedWatson/classnames
I came up with the following macro:
macro_rules! classnames {
// base cases
($name: expr) => (
format!("{}", $name)
);
($name:expr => $should_include:expr) => (
if $should_include {
format!("{}", $name)
} else {
format!("")
}
);
// expansions
($name:expr, $($tail:tt)+) => {
format!("{} {}", $name, classnames!($($tail)*))
};
($name:expr => $should_include:expr, $($tail:tt)+) => {
if $should_include {
format!("{} {}", $name, classnames!($($tail)*))
} else {
classnames!($($tail)*)
};
};
}
// usage:
html!{
div(class = classnames!("active" => true, "button-style")) {
: "this is a button"
}
div(class = classnames!("active" => false, "button-style")) {
: "this is a button"
}
}
rust playground with examples: https://is.gd/e6Jahb
I just learned rust macros to make this; it can probably be refactored into something more nicer.
I'm wondering if this utility macro would be something worth adding to horrorshow. I can try to PR.
Since rust 1.20.0, we have a compile_error!
macro. We should use this to make error messages not suck.
Can't do this until we can destructure boxes.
Rust 2018 lets us import macros in a nice way (use horrorshow::html;
), but the macro author has to opt into this behavior themselves.
I think it should be enough to add #[macro_export(local_inner_macros)]
to every macro definition in the crate, but I'd test first to be sure.
See: https://doc.rust-lang.org/edition-guide/rust-2018/macros/macro-changes.html#local-helper-macros
This is necessary to be able to safely e.g., have colors for elements that are determined at runtime. It is also necessary to support proper URL handling.
The code that is causing this error is below:
fn make_div() -> Box<dyn RenderBox> {
box_html! {
div(id="renders-incorrectly");
}
}
The final div
with id renders-incorrectly
doesn't render <div id="renders-incorrectly"></div>
. Instead, it renders <div id="renders-incorrectly">
and then appends a closing </div>
at the end of the entire document. Replacing that div with:
:Raw("<div id=\"renders-correctly\"></div>");
fixes the problem, although I think they should be the same thing.
I'd like to look into this problem and try to fix it myself when I get the time.
Hi there,
And thanks for Horrorshow!
Just a dumb question: how to insert the document type (<!doctype html>
)?
Loving this library so far!
It's fairly common to use custom HTML tags, eg. <alert type="success">..</alert>
instead of <span class="alert alert-success">..</span>
and obviously this example works with horrorshow.
Custom tags also sometimes include hyphens, though, particularly if the tags are being namespaced (eg. angular and <ng-*>
tags). Horrorshow supports tag names with underscores, but is there any way to allow hyphens as well?
My Rust macro game is bad - I assume this is a no-go because hyphen isn't valid in an identifier.
As a prenote I only have a general idea of how rust works and are making a web site/server to learn it.
I have multiple functions that return Box<RenderBox>
that are later used in a function that returns the rendered string.
Version: 0.6.2
Log
error: no rules expected the token `tmpl`
--> src\resources\partials\element.rs:131:5
|
131 | / box_html! {
132 | | nav(class="nav nav_top full_width") {
133 | | ul(class="nav__menu flex flex--nowrap flex__center wrapper") {
134 | | li(class="nav__items--container") {
... |
151 | | }
152 | | }
| |_____^
|
= note: this error originates in a macro outside of the current crate
Code
box_html! {
nav(class="nav nav_top full_width") {
ul(class="nav__menu flex flex--nowrap flex__center wrapper") {
li(class="nav__items--container") {
ul(class="nav__items--list flex flex--nowrap") {
li(class="flex__item") {
a(href="/", class="nav__link") {
: "Story";
b : "Archive";
}
}
: navbar_item(item_grow)
@ if navbar_drawer {
: navbar_drawer(drawer);
} else {
: navbar_item(item_login);
}
}
}
}
}
}
When pasting the README example, I get an assertion panic. You can see what that looks like below:
~/D/test_horrorshow (master|✔) $ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/test_horrorshow`
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"<!DOCTYPE html><html><head><title>Hello world!</title></head><body><h1 id=\"heading\">Hello! This is <html /></h1><p>Let's <i>count</i> to 10!</p><ol id=\"count\"><li first>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ol><br /><br /><p>Easy!</p></body></html>"`,
right: `"<!DOCTYPE html><html><head><title>Hello world!</title></head><body><h1 id=\"heading\">Hello! This is <html /></h1><p>Let's <i>count</i> to 10!</p><ol id=\"count\"><li first>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li></ol><br><br><p>Easy!</p></body></html>"`', src/main.rs:69:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
~/D/test_horrorshow (master|✔) [101]$ cargo check
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
~/D/test_horrorshow (master|✔) $
As you can see, the difference comes in the <br>
tags, where the assertion assumes that the break will read <br />
instead, like in XHTML. I think it would look nice to fix this to avoid confusion.
Anyways, here's the full example for easy pasting:
README example:
#[macro_use]
extern crate horrorshow;
use horrorshow::prelude::*;
use horrorshow::helper::doctype;
fn main() {
let actual = format!("{}", html! {
: doctype::HTML;
html {
head {
title : "Hello world!";
}
body {
// attributes
h1(id="heading") {
// Insert escaped text
: "Hello! This is <html />"
}
p {
// Insert raw text (unescaped)
: Raw("Let's <i>count</i> to 10!")
}
ol(id="count") {
// You can embed for loops, while loops, and if statements.
@ for i in 0..10 {
li(first? = (i == 0)) {
// Format some text.
: format_args!("{}", i+1)
}
}
}
// You need semi-colons for tags without children.
br; br;
p {
// You can also embed closures.
|tmpl| {
tmpl << "Easy!";
}
}
}
}
});
let expected = "\
<!DOCTYPE html>\
<html>\
<head>\
<title>Hello world!</title>\
</head>\
<body>\
<h1 id=\"heading\">Hello! This is <html /></h1>\
<p>Let's <i>count</i> to 10!</p>\
<ol id=\"count\">\
<li first>1</li>\
<li>2</li>\
<li>3</li>\
<li>4</li>\
<li>5</li>\
<li>6</li>\
<li>7</li>\
<li>8</li>\
<li>9</li>\
<li>10</li>\
</ol>\
<br /><br />\
<p>Easy!</p>\
</body>\
</html>";
assert_eq!(expected, actual);
}
BoxedRenderMut
, BoxedRenderOnce
, BoxRender
. We need to be able to return owned templates. Having three types is annoying but flexible.
Got the following warning when compiling horrorshow 0.5.7
with rust 1.17.0
:
warning: missing fragment specifier
--> <append_html macros>:127:7
|
127 | } $ ( $ next ) * ) => {
| ^^^^^^
|
= note: #[warn(missing_fragment_specifier)] on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #40107 <https://github.com/rust-lang/rust/issues/40107>
Hi, I think I have a (partial) solution to the nesting problem: a function to format content as-if it is a nested and indented HTML element.
For instance, if I generate some HTML like so:
let content = html! {
ul {
li { : "toot" }
}
}
I get the following HTML:
<ul><li>toot</li></ul>
As an aside, this puzzles me, is there anything I can do to get indented output? It'd be a great feature to me, I would expect output like this
<ul>
<li>
toot
</li>
</ul>
Anyway, now I want to insert it into an HTML page:
let page = html! {
html {
body {
: Raw(content)
}
}
}
And this gets me:
<html><body><ul><li>toot</li></ul></body></html>
Now, what I'm suggesting is a function or character to insert raw text with newlines and an indent, like so:
let page = html! {
html {
body {
> content
}
}
}
Or perhaps: > Raw(content)
, or : Block(content)
, or some other variant.
For the result:
<html><body>
<ul><li>toot</li></ul>
</body></html>
It would take the indent level from the parent's indent level and add one to it. If the raw content is multiline, it would indent all lines separately, like so:
<html><body>
<ul><li>toot</li></ul>
<ul><li>toot</li></ul>
</body></html>
What do you think? Would this be hard to implement? It'd really be a boon to getting nice formatted HTML output out of my Markdown server project (https://gitlab.com/willemmali-rust/markbrowse).
use write_char when writing to a fmt::Write
. This is only stable in 1.2.
I'm unclear on whether or not horrorshow supports boolean attributes, like required
:
<input type="text" value="value" required />
I tried assigning it a boolean value but bool
doesn't implement RenderOnce
so that didn't work.
Thanks!
For deeply nested horrorshow templates, it may be useful to have a pretty html output (i.e. space/tabs). Would this be something that may be added to horrorshow-rs?
For now I'm relying on chrome devtools.
The data structure created by horrorshow might easily be suitable to create a DOM from it as well, in parallel to serialization to text-encoded HTML. Some features of horrorshow might be unsupported (I'm unsure as to how much raw text can be used with modern DOM APIs), but that might be acceptable for use cases.
The typed-html crate provides something some steps in that direction (I haven't seen an example of actual shared HTML, but it can be used to render either to HTML or to dodrio), but is not actively maintained any more, and horrorshow's syntax seems nicer to me.
The application I see for it would be to use the same code to render things server side, but (with a vastly different backend) to also perform client-side updates from WASM.
There was one previous issue in that direction (#29) where DOM was described as out-of-scope, so this is more about exploring DOM creation based on horrorshow, for which I'd like to gather input here:
thanks for your library! Unfortunately I am struggling to find out how to pass a simple variable to the template e.g.
let actual = html! {
: doctype::HTML;
html {
head {
title : "Hello world!";
}
}
}
how would I pass the variable title using your example ? I also think web templates are mostly used to display data so perhaps you can add that to the example..not sure why the calculations are important in this scenario! sorry if I misunderstood something...
am I also right to assume that normal HTML has to be translated into the rust tags inside html! to work right ?
If I reformat the document in Intellij, it doesn't fix the indentation within the macro, so if I start with:
pub(crate) mod table {
use horrorshow::prelude::*;
use horrorshow::helper::doctype;
pub(crate) fn render() -> Box<horrorshow::RenderBox> {
box_html! {
table {
thead {
tr {
th {: "Column 1"}
th {: "Column 1"}
th {: "Column 1"}
}
}
tbody {
tr {
td {: "Data 1"}
td {: "Data 1"}
td {: "Data 1"}
}
}
}
label(typ="text", name="name");
: "First criteria"
}
}
}
and then ask IntelliJ for reformat, it doesn't change it at all. I am also not getting any errors inline until I manually ask it to compile/build.
Is this expected and simply a price of using Rust's macros? or have I missed something?
(it's a price well paid as the compile time safety is great!)
I want to fold some Horrorshow templates, I'm using Horrorshow to make directory listings. However, I'm having a hard time understanding how to make this work, because I can't match
on the output of html!
and I can't fold
over them either.
I'm sorry for the glorified support request, I'll try to rework your advice into a pull-request which adds examples to the documentation if you like.
Here's the code I want to write, but the only way to make it work I have found is passing around the rendered HTML by calling .into_string().unwrap()
on the html!
output every time.
(Sorry for the large blurb of code, the first map
is largely irrelevant but I thought it would be useful for context.)
fn list_directories_recursive(&self, path: &Path) -> Option<String> {
let listings = path
.read_dir().unwrap()
.filter_map(|de| de.ok()) // remove invalid paths
.map(|de| de.path()) // DirectoryEntry -> Path
.filter(|p| self.visible_path(p)) // remove invisible paths
.map(|path| {
if let &Some(href) = &path.as_os_str().to_str() {
if let &Some(file_name) = &path.file_name() {
if let Some(name) = file_name.to_str() {
let href = href.replacen(".", "", 1); // ./thing -> /thing
let name = name.replacen("/", "", 1); // ./dir/thing -> thing
return Some((path, href, name));
}
}
}
None
})
.filter_map(|s| s) // remove empty/non-UTF directories
.map(|(path, href, name)| { // Path -> String
html! {
@if self.list_recursively && path.is_dir() {
@if let Some(sub) = self.list_directories_recursive(&path) {
: sub
}
}
tr {
td {
a(href=&href) {
: name
}
}
}
}
})
.fold(html! {}, |acc, html| html! {
: acc;
: html;
})
.into_string().unwrap();
if listings.len() != 0 {
Some(listings)
} else {
None
}
Problem One: I need to know if this crate can be used in a project that is run with #![no_std]
.
Problem Two: You should probably put how to add the crate to your Cargo.toml in the README. I had to dig through the horrorshow.rs Cargo.toml file to find the version.
Just some suggestions (and a question). 😺
I'm trying to implement a "callback" where as a user types, the content they have typed thus far is sent back to Rust for some processing (namely, autocomplete). I'm unclear on the syntax or in general how this would be accomplished given the tools exposed in horrorshow. So, my question is two parts:
a) Is this possible at all?
b) If yes to (a), what would that look like?
After I figure this out, I would like to write up examples/documentation and contribute them to the repository to help others out.
Thanks!
Say I wanted to make a layout that I can inject different content into based on a route. How could I go about doing so?
fn layout(page_title: String, content: ???) -> String {
format!("{}",html!{
head {
title : page_title;
}
body {
:content
}
})
}
fn home_content() -> Box<RenderOnce> {
box_html! {
h1 { :"Home Page" }
}
}
fn about_content() -> Box<RenderOnce> {
box_html! {
h1 { :"About Us" }
}
}
fn contact_content() -> Box<RenderOnce> {
box_html! {
h1 { :"Contact Us" }
}
}
//and i could call the layout like this
layout(String::from("Home"),home_content());
layout(String::from("About"),about_content());
layout(String::from("Contact"),contact_content());
html body article p : "Text";
Unfortunately, this isn't possible in 1.0/1.1 but it is possible in 1.2.
fn main() {
use horrorshow::{*, helper::doctype};
println!("{}", html!{ : "м, о"; })
}
outputs <, >
.
I think it's because we're truncating char
s to u8
s here.
I love the templating system. I am also fairly new to Rust. I come from a Python/Django background and have decided to switch for various reasons. I can't figure out how to get ActixWeb to render the template, it just gives me the escaped string. Below is what I tried, but feel free to use any of the routes available and I'll figure it out (eventually).
#[macro_use]
extern crate horrorshow;
use horrorshow::prelude::*;
use horrorshow::helper::doctype;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
fn template() -> String {
let my_title = "Hello world!";
let actual = format!("{}", html! {
: doctype::HTML;
html {
head {
title:my_title;
}
body {
h1(id="heading", class="title") : my_title;
p {
: "Hello! This is <html>";
}
}
}
});
return actual;
}
async fn index() -> impl Responder {
template()
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(
web::scope("/app").route("/index.html", web::get().to(index)),
)
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Excerpt from lldb output
[...]
Process 20325 stopped
* thread #7, stop reason = EXC_BAD_ACCESS (code=2, address=0x700009117ff8)
frame #0: 0x000000010007eeb0 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
102
103 #[inline]
104 fn size_hint(&self) -> usize {
-> 105 RenderBox::size_hint_box(self)
106 }
107 }
108
Target 0: (priroda) stopped.
(lldb) bt
[...]
* thread #7, stop reason = EXC_BAD_ACCESS (code=2, address=0x700009117ff8)
* frame #0: 0x000000010007eeb0 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #1: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #2: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #3: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #4: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #5: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #6: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #7: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #8: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #9: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #10: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #11: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #12: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #13: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #14: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #15: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #16: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #17: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
[...]
frame #64895: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #64896: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #64897: 0x0000000100074065 priroda`_$LT$T$u20$as$u20$horrorshow..render..RenderBox$GT$::size_hint_box::h782068eaecdd52ae(self=0x0000700009313078) at render.rs:79
frame #64898: 0x000000010007eeb5 priroda`_$LT$alloc..boxed..Box$LT$horrorshow..render..RenderBox$u20$$u2b$$u20$core..marker..Send$u20$$u2b$$u20$$u27$b$GT$$u20$as$u20$horrorshow..render..RenderOnce$GT$::size_hint::hcc77a206d30b40bb(self=0x0000700009313078) at render.rs:105
frame #64899: 0x0000000100073565 priroda`horrorshow::template::Template::into_string::h347df756764525af(self=Box<RenderBox> @ 0x0000700009313078) at template.rs:13
[...]
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.