diff --git a/docs/Makefile b/docs/Makefile
index 5c108fbc..7686dbc1 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,4 +1,4 @@
-slugs := index getting-started text-escaping elements-attributes splices-toggles control-structures partials render-trait web-frameworks faq
+slugs := index getting-started text-escaping elements-attributes splices-toggles control-structures partials web-frameworks faq
slug_to_md = content/$(1).md
slug_to_html = site/$(1).html
diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md
index 751ec546..dc979216 100644
--- a/docs/content/getting-started.md
+++ b/docs/content/getting-started.md
@@ -24,19 +24,19 @@ use maud::html;
fn main() {
let name = "Lyra";
- let markup = html! {
+ let hello = html! {
p { "Hi, " (name) "!" }
};
- println!("{}", markup.into_string());
+ println!("{}", hello.into_string());
}
```
`html!` takes a single argument:
a template using Maud's custom syntax.
-This call expands to an expression of type [`Markup`][Markup],
+This call expands to an expression of type [`Html`][Html],
which can then be converted to a `String` using `.into_string()`.
-[Markup]: https://docs.rs/maud/*/maud/type.Markup.html
+[Html]: https://docs.rs/maud/*/maud/struct.Html.html
Run this program with `cargo run`,
and you should get the following:
diff --git a/docs/content/partials.md b/docs/content/partials.md
index 8c402a9a..9ce7afae 100644
--- a/docs/content/partials.md
+++ b/docs/content/partials.md
@@ -2,16 +2,16 @@
Maud does not have a built-in concept of partials or sub-templates.
Instead,
-you can compose your markup with any function that returns `Markup`.
+you can compose your markup with any function that returns `Html`.
The following example defines a `header` and `footer` function.
These functions are combined to form the final `page`.
```rust
-use maud::{DOCTYPE, html, Markup};
+use maud::{DOCTYPE, html, Html};
/// A basic header with a dynamic `page_title`.
-fn header(page_title: &str) -> Markup {
+fn header(page_title: &str) -> Html {
html! {
(DOCTYPE)
meta charset="utf-8";
@@ -20,7 +20,7 @@ fn header(page_title: &str) -> Markup {
}
/// A static footer.
-fn footer() -> Markup {
+fn footer() -> Html {
html! {
footer {
a href="rss.atom" { "RSS Feed" }
@@ -28,12 +28,11 @@ fn footer() -> Markup {
}
}
-/// The final Markup, including `header` and `footer`.
+/// The final page, including `header` and `footer`.
///
-/// Additionally takes a `greeting_box` that's `Markup`, not `&str`.
-pub fn page(title: &str, greeting_box: Markup) -> Markup {
+/// Additionally takes a `greeting_box` that's `Html`, not `&str`.
+pub fn page(title: &str, greeting_box: Html) -> Html {
html! {
- // Add the header markup to the page
(header(title))
h1 { (title) }
(greeting_box)
@@ -42,12 +41,12 @@ pub fn page(title: &str, greeting_box: Markup) -> Markup {
}
```
-Using the `page` function will return the markup for the whole page.
+Using the `page` function will return the HTML for the whole page.
Here's an example:
```rust
-# use maud::{html, Markup};
-# fn page(title: &str, greeting_box: Markup) -> Markup { greeting_box }
+# use maud::{html, Html};
+# fn page(title: &str, greeting_box: Html) -> Html { greeting_box }
page("Hello!", html! {
div { "Greetings, Maud." }
});
diff --git a/docs/content/render-trait.md b/docs/content/render-trait.md
deleted file mode 100644
index e49b73ab..00000000
--- a/docs/content/render-trait.md
+++ /dev/null
@@ -1,91 +0,0 @@
-# The `Render` trait
-
-Maud uses the [`Render`][Render] trait to convert [`(spliced)`](splices-toggles.md) values to HTML.
-This is implemented for many Rust primitive types (`&str`, `i32`) by default, but you can implement it for your own types as well.
-
-Below are some examples of implementing `Render`.
-Feel free to use these snippets in your own project!
-
-## Example: a shorthand for including CSS stylesheets
-
-When writing a web page,
-it can be annoying to write `link rel="stylesheet"` over and over again.
-This example provides a shorthand for linking to CSS stylesheets.
-
-```rust
-use maud::{html, Markup, Render};
-
-/// Links to a CSS stylesheet at the given path.
-struct Css(&'static str);
-
-impl Render for Css {
- fn render(&self) -> Markup {
- html! {
- link rel="stylesheet" type="text/css" href=(self.0);
- }
- }
-}
-```
-
-## Example: a wrapper that calls `std::fmt::Debug`
-
-When debugging an application,
-it can be useful to see its internal state.
-But these internal data types often don't implement `Display`.
-This wrapper lets us use the [`Debug`][Debug] trait instead.
-
-To avoid extra allocation,
-we override the `.render_to()` method instead of `.render()`.
-This doesn't do any escaping by default,
-so we wrap the output in an `Escaper` as well.
-
-```rust
-use maud::{Escaper, html, Render};
-use std::fmt;
-use std::fmt::Write as _;
-
-/// Renders the given value using its `Debug` implementation.
-struct Debug(T);
-
-impl Render for Debug {
- fn render_to(&self, output: &mut String) {
- let mut escaper = Escaper::new(output);
- write!(escaper, "{:?}", self.0).unwrap();
- }
-}
-```
-
-## Example: rendering Markdown using `pulldown-cmark` and `ammonia`
-
-[`pulldown-cmark`][pulldown-cmark] is a popular library
-for converting Markdown to HTML.
-
-We also use the [`ammonia`][ammonia] library,
-which sanitizes the resulting markup.
-
-```rust
-use ammonia;
-use maud::{Markup, PreEscaped, Render};
-use pulldown_cmark::{Parser, html};
-
-/// Renders a block of Markdown using `pulldown-cmark`.
-struct Markdown>(T);
-
-impl> Render for Markdown {
- fn render(&self) -> Markup {
- // Generate raw HTML
- let mut unsafe_html = String::new();
- let parser = Parser::new(self.0.as_ref());
- html::push_html(&mut unsafe_html, parser);
- // Sanitize it with ammonia
- let safe_html = ammonia::clean(&unsafe_html);
- PreEscaped(safe_html)
- }
-}
-```
-
-[Debug]: https://doc.rust-lang.org/std/fmt/trait.Debug.html
-[Display]: https://doc.rust-lang.org/std/fmt/trait.Display.html
-[Render]: https://docs.rs/maud/*/maud/trait.Render.html
-[pulldown-cmark]: https://docs.rs/pulldown-cmark/0.0.8/pulldown_cmark/index.html
-[ammonia]: https://github.com/notriddle/ammonia
diff --git a/docs/content/splices-toggles.md b/docs/content/splices-toggles.md
index a5fbabd1..3d4e5523 100644
--- a/docs/content/splices-toggles.md
+++ b/docs/content/splices-toggles.md
@@ -94,30 +94,44 @@ html! {
### What can be spliced?
-You can splice any value that implements [`Render`][Render].
+You can splice any value that implements [`ToHtml`][ToHtml].
Most primitive types (such as `str` and `i32`) implement this trait,
so they should work out of the box.
To get this behavior for a custom type,
-you can implement the [`Render`][Render] trait by hand.
-The [`PreEscaped`][PreEscaped] wrapper type,
-which outputs its argument without escaping,
-works this way.
-See the [traits](render-trait.md) section for details.
+you can implement the [`ToHtml`][ToHtml] trait by hand.
```rust
-use maud::PreEscaped;
-let post = "Pre-escaped
";
-# let _ = maud::
-html! {
- h1 { "My super duper blog post" }
- (PreEscaped(post))
+use maud::{Html, ToHtml, html};
+
+struct Pony {
+ name: String,
+ cuteness: i32,
}
-# ;
+
+impl ToHtml for Pony {
+ fn to_html(&self) -> Html {
+ html! {
+ p {
+ "Pony " (self.name) " is " (self.cuteness) " cute!"
+ }
+ }
+ }
+}
+
+let sweetie_belle = Pony {
+ name: "Sweetie Belle".into(),
+ cuteness: 99,
+};
+
+let example = html! {
+ (sweetie_belle)
+};
+
+assert_eq!(example.into_string(), "Pony Sweetie Belle is 99 cute!
");
```
-[Render]: https://docs.rs/maud/*/maud/trait.Render.html
-[PreEscaped]: https://docs.rs/maud/*/maud/struct.PreEscaped.html
+[ToHtml]: https://docs.rs/maud/*/maud/trait.ToHtml.html
## Toggles: `[foo]`
diff --git a/docs/content/text-escaping.md b/docs/content/text-escaping.md
index da673137..b5af1d96 100644
--- a/docs/content/text-escaping.md
+++ b/docs/content/text-escaping.md
@@ -39,24 +39,33 @@ html! {
[raw strings]: https://doc.rust-lang.org/reference/tokens.html#raw-string-literals
-## Escaping and `PreEscaped`
+## Escaping
By default,
HTML special characters are escaped automatically.
-Wrap the string in `(PreEscaped())` to disable this escaping.
-(See the section on [splices](splices-toggles.md) to
-learn more about how this works.)
```rust
-use maud::PreEscaped;
-# let _ = maud::
-html! {
- "" // <script>...
- (PreEscaped("")) // ";
+let markup = html! {
+ (unsafe_input)
+};
+assert_eq!(markup.into_string(), "<script>alert('Bwahahaha!')</script>");
```
+[xss]: https://www.cloudflare.com/en-au/learning/security/threats/cross-site-scripting/
+
## The `DOCTYPE` constant
If you want to add a `` declaration to your page,
@@ -71,3 +80,53 @@ html! {
}
# ;
```
+
+## Inline `"))
+ }
+ # ;
+ ```
+
+[from_const_unchecked]: https://docs.rs/maud/*/maud/struct.Html.html#method.from_const_unchecked
+
+When Maud implements [context-aware escaping],
+these workarounds will no longer be needed.
+
+[context-aware escaping]: https://github.com/lambda-fairy/maud/issues/181
+
+## Custom escaping
+
+If your use case isn't covered by these examples,
+check out the [advanced API].
+
+[advanced API]: https://docs.rs/maud/*/maud/struct.Html.html
diff --git a/docs/content/web-frameworks.md b/docs/content/web-frameworks.md
index 8c52fb09..a9992cd8 100644
--- a/docs/content/web-frameworks.md
+++ b/docs/content/web-frameworks.md
@@ -24,11 +24,11 @@ that implements the `actix_web::Responder` trait.
```rust,no_run
use actix_web::{get, App, HttpServer, Result as AwResult};
-use maud::{html, Markup};
+use maud::{html, Html};
use std::io;
#[get("/")]
-async fn index() -> AwResult {
+async fn index() -> AwResult {
Ok(html! {
html {
body {
@@ -59,18 +59,18 @@ maud = { version = "*", features = ["rocket"] }
# ...
```
-This adds a `Responder` implementation for the `Markup` type,
+This adds a `Responder` implementation for the `Html` type,
so you can return the result directly:
```rust,no_run
#![feature(decl_macro)]
-use maud::{html, Markup};
+use maud::{html, Html};
use rocket::{get, routes};
use std::borrow::Cow;
#[get("/")]
-fn hello<'a>(name: Cow<'a, str>) -> Markup {
+fn hello<'a>(name: Cow<'a, str>) -> Html {
html! {
h1 { "Hello, " (name) "!" }
p { "Nice to meet you!" }
@@ -86,7 +86,7 @@ fn main() {
Unlike with the other frameworks,
Rouille doesn't need any extra features at all!
-Calling `Response::html` on the rendered `Markup` will Just Work®.
+Calling `Response::html` on the rendered `Html` will Just Work®.
```rust,no_run
use maud::html;
@@ -118,7 +118,7 @@ maud = { version = "*", features = ["tide"] }
# ...
```
-This adds an implementation of `From>`
+This adds an implementation of `From`
for the `Response` struct.
Once provided,
callers may return results of `html!` directly as responses:
@@ -154,14 +154,14 @@ maud = { version = "*", features = ["axum"] }
# ...
```
-This adds an implementation of `IntoResponse` for `Markup`/`PreEscaped`.
+This adds an implementation of `IntoResponse` for `Html`.
This then allows you to use it directly as a response!
```rust,no_run
-use maud::{html, Markup};
+use maud::{html, Html};
use axum::{Router, routing::get};
-async fn hello_world() -> Markup {
+async fn hello_world() -> Html {
html! {
h1 { "Hello, World!" }
}
diff --git a/docs/src/bin/build_nav.rs b/docs/src/bin/build_nav.rs
index 13a93d5f..9bd11fd3 100644
--- a/docs/src/bin/build_nav.rs
+++ b/docs/src/bin/build_nav.rs
@@ -1,7 +1,7 @@
use comrak::{self, nodes::AstNode, Arena};
use docs::{
page::{Page, COMRAK_OPTIONS},
- string_writer::StringWriter,
+ text_writer::TextWriter,
};
use std::{env, error::Error, fs, io, path::Path, str};
@@ -51,7 +51,7 @@ fn load_page_title<'a>(
let page = Page::load(arena, path)?;
let title = page.title.map(|title| {
let mut buffer = String::new();
- comrak::format_commonmark(title, &COMRAK_OPTIONS, &mut StringWriter(&mut buffer)).unwrap();
+ comrak::format_commonmark(title, &COMRAK_OPTIONS, &mut TextWriter(&mut buffer)).unwrap();
buffer
});
Ok(title)
diff --git a/docs/src/lib.rs b/docs/src/lib.rs
index ac61d5b3..02155c06 100644
--- a/docs/src/lib.rs
+++ b/docs/src/lib.rs
@@ -1,5 +1,5 @@
#![feature(once_cell)]
pub mod page;
-pub mod string_writer;
+pub mod text_writer;
pub mod views;
diff --git a/docs/src/string_writer.rs b/docs/src/string_writer.rs
deleted file mode 100644
index ea7291ab..00000000
--- a/docs/src/string_writer.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use std::{io, str};
-
-pub struct StringWriter<'a>(pub &'a mut String);
-
-impl<'a> io::Write for StringWriter<'a> {
- fn write(&mut self, buf: &[u8]) -> io::Result {
- str::from_utf8(buf)
- .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
- .map(|s| {
- self.0.push_str(s);
- buf.len()
- })
- }
-
- fn flush(&mut self) -> io::Result<()> {
- Ok(())
- }
-}
diff --git a/docs/src/text_writer.rs b/docs/src/text_writer.rs
new file mode 100644
index 00000000..8edeb62c
--- /dev/null
+++ b/docs/src/text_writer.rs
@@ -0,0 +1,18 @@
+use std::{fmt, io, str};
+
+pub struct TextWriter(pub T);
+
+impl io::Write for TextWriter {
+ fn write(&mut self, buf: &[u8]) -> io::Result {
+ let s =
+ str::from_utf8(buf).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
+ self.0
+ .write_str(s)
+ .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ Ok(())
+ }
+}
diff --git a/docs/src/views.rs b/docs/src/views.rs
index 47bdc59a..a46ef115 100644
--- a/docs/src/views.rs
+++ b/docs/src/views.rs
@@ -1,17 +1,23 @@
use comrak::nodes::AstNode;
-use maud::{html, Markup, PreEscaped, Render, DOCTYPE};
+use maud::{html, Html, HtmlBuilder, ToHtml, DOCTYPE};
use std::str;
use crate::{
page::{Page, COMRAK_OPTIONS},
- string_writer::StringWriter,
+ text_writer::TextWriter,
};
struct Comrak<'a>(&'a AstNode<'a>);
-impl<'a> Render for Comrak<'a> {
- fn render_to(&self, buffer: &mut String) {
- comrak::format_html(self.0, &COMRAK_OPTIONS, &mut StringWriter(buffer)).unwrap();
+impl<'a> ToHtml for Comrak<'a> {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ // XSS-Safety: The input Markdown comes from docs, which are trusted.
+ comrak::format_html(
+ self.0,
+ &COMRAK_OPTIONS,
+ &mut TextWriter(builder.as_mut_string_unchecked()),
+ )
+ .unwrap();
}
}
@@ -19,12 +25,13 @@ impl<'a> Render for Comrak<'a> {
/// general but not suitable for links in the navigation bar.
struct ComrakRemovePTags<'a>(&'a AstNode<'a>);
-impl<'a> Render for ComrakRemovePTags<'a> {
- fn render(&self) -> Markup {
+impl<'a> ToHtml for ComrakRemovePTags<'a> {
+ fn to_html(&self) -> Html {
let mut buffer = String::new();
- comrak::format_html(self.0, &COMRAK_OPTIONS, &mut StringWriter(&mut buffer)).unwrap();
+ comrak::format_html(self.0, &COMRAK_OPTIONS, &mut TextWriter(&mut buffer)).unwrap();
assert!(buffer.starts_with("") && buffer.ends_with("
\n"));
- PreEscaped(
+ // XSS-Safety: The input Markdown comes from docs, which are trusted.
+ Html::from_unchecked(
buffer
.trim_start_matches("")
.trim_end_matches("
\n")
@@ -35,9 +42,9 @@ impl<'a> Render for ComrakRemovePTags<'a> {
struct ComrakText<'a>(&'a AstNode<'a>);
-impl<'a> Render for ComrakText<'a> {
- fn render_to(&self, buffer: &mut String) {
- comrak::format_commonmark(self.0, &COMRAK_OPTIONS, &mut StringWriter(buffer)).unwrap();
+impl<'a> ToHtml for ComrakText<'a> {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ comrak::format_commonmark(self.0, &COMRAK_OPTIONS, &mut TextWriter(builder)).unwrap();
}
}
@@ -47,7 +54,7 @@ pub fn main<'a>(
nav: &[(&str, &'a AstNode<'a>)],
version: &str,
hash: &str,
-) -> Markup {
+) -> Html {
html! {
(DOCTYPE)
meta charset="utf-8";
diff --git a/maud/Cargo.toml b/maud/Cargo.toml
index 45c9ef94..94cedb49 100644
--- a/maud/Cargo.toml
+++ b/maud/Cargo.toml
@@ -14,9 +14,8 @@ edition = "2021"
[features]
default = []
axum = ["axum-core", "http"]
-
-# Web framework integrations
actix-web = ["actix-web-dep", "futures-util"]
+sanitize = ["ammonia"]
[dependencies]
maud_macros = { version = "0.23.0", path = "../maud_macros" }
@@ -27,6 +26,7 @@ actix-web-dep = { package = "actix-web", version = ">= 2, < 4", optional = true,
tide = { version = "0.16.0", optional = true, default-features = false }
axum-core = { version = "0.1", optional = true }
http = { version = "0.2", optional = true }
+ammonia = { version = "3.1.2", optional = true }
[dev-dependencies]
trybuild = { version = "1.0.33", features = ["diff"] }
diff --git a/maud/benches/complicated_maud.rs b/maud/benches/complicated_maud.rs
index d0cf0958..1732bdf9 100644
--- a/maud/benches/complicated_maud.rs
+++ b/maud/benches/complicated_maud.rs
@@ -2,7 +2,7 @@
extern crate test;
-use maud::{html, Markup};
+use maud::{html, Html};
#[derive(Debug)]
struct Entry {
@@ -11,7 +11,7 @@ struct Entry {
}
mod btn {
- use maud::{html, Markup, Render};
+ use maud::{html, Html, ToHtml};
#[derive(Copy, Clone)]
pub enum RequestMethod {
@@ -41,8 +41,8 @@ mod btn {
}
}
- impl<'a> Render for Button<'a> {
- fn render(&self) -> Markup {
+ impl<'a> ToHtml for Button<'a> {
+ fn to_html(&self) -> Html {
match self.req_meth {
RequestMethod::Get => {
html! { a.btn href=(self.path) { (self.label) } }
@@ -59,7 +59,7 @@ mod btn {
}
}
-fn layout>(title: S, inner: Markup) -> Markup {
+fn layout>(title: S, inner: Html) -> Html {
html! {
html {
head {
diff --git a/maud/src/lib.rs b/maud/src/lib.rs
index c2c0beaa..08a8aed6 100644
--- a/maud/src/lib.rs
+++ b/maud/src/lib.rs
@@ -18,196 +18,283 @@ pub use maud_macros::{html, html_debug};
mod escape;
-/// An adapter that escapes HTML special characters.
-///
-/// The following characters are escaped:
-///
-/// * `&` is escaped as `&`
-/// * `<` is escaped as `<`
-/// * `>` is escaped as `>`
-/// * `"` is escaped as `"`
-///
-/// All other characters are passed through unchanged.
-///
-/// **Note:** In versions prior to 0.13, the single quote (`'`) was
-/// escaped as well.
-///
-/// # Example
-///
-/// ```rust
-/// use maud::Escaper;
-/// use std::fmt::Write;
-/// let mut s = String::new();
-/// write!(Escaper::new(&mut s), "").unwrap();
-/// assert_eq!(s, "<script>launchMissiles()</script>");
-/// ```
-pub struct Escaper<'a>(&'a mut String);
-
-impl<'a> Escaper<'a> {
- /// Creates an `Escaper` from a `String`.
- pub fn new(buffer: &'a mut String) -> Escaper<'a> {
- Escaper(buffer)
- }
-}
-
-impl<'a> fmt::Write for Escaper<'a> {
- fn write_str(&mut self, s: &str) -> fmt::Result {
- escape::escape_to_string(s, self.0);
- Ok(())
- }
-}
-
/// Represents a type that can be rendered as HTML.
///
-/// To implement this for your own type, override either the `.render()`
-/// or `.render_to()` methods; since each is defined in terms of the
-/// other, you only need to implement one of them. See the example below.
-///
/// # Minimal implementation
///
/// An implementation of this trait must override at least one of
-/// `.render()` or `.render_to()`. Since the default definitions of
-/// these methods call each other, not doing this will result in
-/// infinite recursion.
+/// `.to_html()` or `.push_html_to()`. Since the default definitions of
+/// these methods call each other, not doing this will result in infinite
+/// recursion.
///
/// # Example
///
/// ```rust
-/// use maud::{html, Markup, Render};
+/// use maud::{html, Html, ToHtml};
///
/// /// Provides a shorthand for linking to a CSS stylesheet.
/// pub struct Stylesheet(&'static str);
///
-/// impl Render for Stylesheet {
-/// fn render(&self) -> Markup {
+/// impl ToHtml for Stylesheet {
+/// fn to_html(&self) -> Html {
/// html! {
/// link rel="stylesheet" type="text/css" href=(self.0);
/// }
/// }
/// }
/// ```
-pub trait Render {
- /// Renders `self` as a block of `Markup`.
- fn render(&self) -> Markup {
- let mut buffer = String::new();
- self.render_to(&mut buffer);
- PreEscaped(buffer)
+pub trait ToHtml {
+ /// Creates an HTML representation of `self`.
+ fn to_html(&self) -> Html {
+ let mut builder = HtmlBuilder::new();
+ self.push_html_to(&mut builder);
+ builder.finalize()
}
- /// Appends a representation of `self` to the given buffer.
+ /// Appends an HTML representation of `self` to the given buffer.
///
- /// Its default implementation just calls `.render()`, but you may
+ /// Its default implementation just calls `.to_html()`, but you may
/// override it with something more efficient.
- ///
- /// Note that no further escaping is performed on data written to
- /// the buffer. If you override this method, you must make sure that
- /// any data written is properly escaped, whether by hand or using
- /// the [`Escaper`](struct.Escaper.html) wrapper struct.
- fn render_to(&self, buffer: &mut String) {
- buffer.push_str(&self.render().into_string());
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ self.to_html().push_html_to(builder)
}
}
-impl Render for str {
- fn render_to(&self, w: &mut String) {
- escape::escape_to_string(self, w);
+impl ToHtml for str {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ // XSS-Safety: Special characters will be escaped by `escape_to_string`.
+ escape::escape_to_string(self, builder.as_mut_string_unchecked());
}
}
-impl Render for String {
- fn render_to(&self, w: &mut String) {
- str::render_to(self, w);
+impl ToHtml for String {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ str::push_html_to(self, builder);
}
}
-impl<'a> Render for Cow<'a, str> {
- fn render_to(&self, w: &mut String) {
- str::render_to(self, w);
+impl<'a> ToHtml for Cow<'a, str> {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ str::push_html_to(self, builder);
}
}
-impl<'a> Render for Arguments<'a> {
- fn render_to(&self, w: &mut String) {
- let _ = Escaper::new(w).write_fmt(*self);
+impl<'a> ToHtml for Arguments<'a> {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ let _ = builder.write_fmt(*self);
}
}
-impl<'a, T: Render + ?Sized> Render for &'a T {
- fn render_to(&self, w: &mut String) {
- T::render_to(self, w);
+impl<'a, T: ToHtml + ?Sized> ToHtml for &'a T {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ T::push_html_to(self, builder);
}
}
-impl<'a, T: Render + ?Sized> Render for &'a mut T {
- fn render_to(&self, w: &mut String) {
- T::render_to(self, w);
+impl<'a, T: ToHtml + ?Sized> ToHtml for &'a mut T {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ T::push_html_to(self, builder);
}
}
-impl Render for Box {
- fn render_to(&self, w: &mut String) {
- T::render_to(self, w);
+impl ToHtml for Box {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ T::push_html_to(self, builder);
}
}
-macro_rules! impl_render_with_display {
+macro_rules! impl_to_html_with_display {
($($ty:ty)*) => {
$(
- impl Render for $ty {
- fn render_to(&self, w: &mut String) {
- format_args!("{self}").render_to(w);
+ impl ToHtml for $ty {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ let _ = write!(builder, "{self}");
}
}
)*
};
}
-impl_render_with_display! {
+impl_to_html_with_display! {
char f32 f64
}
-macro_rules! impl_render_with_itoa {
+macro_rules! impl_to_html_with_itoa {
($($ty:ty)*) => {
$(
- impl Render for $ty {
- fn render_to(&self, w: &mut String) {
- let _ = itoa::fmt(w, *self);
+ impl ToHtml for $ty {
+ fn push_html_to(&self, builder: &mut HtmlBuilder) {
+ // XSS-Safety: The characters '0' through '9', and '-', are HTML safe.
+ let _ = itoa::fmt(builder.as_mut_string_unchecked(), *self);
}
}
)*
};
}
-impl_render_with_itoa! {
+impl_to_html_with_itoa! {
i8 i16 i32 i64 i128 isize
u8 u16 u32 u64 u128 usize
}
-/// A wrapper that renders the inner value without escaping.
-#[derive(Debug, Clone, Copy)]
-pub struct PreEscaped>(pub T);
+/// A fragment of HTML.
+///
+/// This is the type that's returned by the [`html!`] macro.
+///
+/// # Security
+///
+/// All instances of `Html` must be:
+///
+/// 1. **Trusted.** Any embedded scripts (in a `
";
+ ///
+ /// let clean_html = Html::sanitize(untrusted_html);
+ ///
+ /// assert_eq!(clean_html.into_string(), "");
+ /// ```
+ pub fn sanitize(untrusted_html_string: &str) -> Self {
+ // XSS-Safety: Ammonia sanitizes the input.
+ Self::from_unchecked(ammonia::clean(untrusted_html_string))
}
-}
-/// A block of markup is a string that does not need to be escaped.
-///
-/// The `html!` macro expands to an expression of this type.
-pub type Markup = PreEscaped;
+ /// Creates an HTML fragment from a string, without escaping it.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// # fn load_header_from_config() -> String { String::new() }
+ /// use maud::Html;
+ ///
+ /// // XSS-Safety: The config can only be edited by an admin.
+ /// let header = Html::from_unchecked(load_header_from_config());
+ /// ```
+ ///
+ /// # Security
+ ///
+ /// It is strongly recommended to include a `// XSS-Safety:` comment
+ /// that explains why this call is safe.
+ ///
+ /// If your organization has a security team, consider asking them
+ /// for review.
+ pub fn from_unchecked(html_string: impl Into>) -> Self {
+ Self {
+ inner: html_string.into(),
+ }
+ }
-impl + Into> PreEscaped {
- /// Converts the inner value to a string.
+ /// Creates an HTML fragment from a constant string.
+ ///
+ /// This is similar to [`Html::from_unchecked`], but can be called
+ /// in a `const` context.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// use maud::Html;
+ ///
+ /// const ANALYTICS_SCRIPT: Html = Html::from_const_unchecked(
+ /// "",
+ /// );
+ /// ```
+ ///
+ /// # Security
+ ///
+ /// As long as the string is a compile-time constant, it is
+ /// guaranteed to be as *trusted* as its surrounding code.
+ ///
+ /// However, this doesn't guarantee that it's *composable*:
+ ///
+ /// ```rust
+ /// use maud::Html;
+ ///
+ /// // BROKEN - DO NOT USE!
+ /// const UNCLOSED_SCRIPT: Html = Html::from_const_unchecked("