Skip to content

Commit 49dc424

Browse files
committed
Split into separate Html and HtmlBuilder types
1 parent 5e180e1 commit 49dc424

File tree

3 files changed

+112
-79
lines changed

3 files changed

+112
-79
lines changed

maud/src/lib.rs

Lines changed: 108 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -46,69 +46,69 @@ mod escape;
4646
pub trait ToHtml {
4747
/// Creates an HTML representation of `self`.
4848
fn to_html(&self) -> Html {
49-
let mut buffer = Html::default();
50-
self.push_html_to(&mut buffer);
51-
buffer
49+
let mut builder = HtmlBuilder::new();
50+
self.push_html_to(&mut builder);
51+
builder.finalize()
5252
}
5353

5454
/// Appends an HTML representation of `self` to the given buffer.
5555
///
5656
/// Its default implementation just calls `.to_html()`, but you may
5757
/// override it with something more efficient.
58-
fn push_html_to(&self, buffer: &mut Html) {
59-
self.to_html().push_html_to(buffer)
58+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
59+
self.to_html().push_html_to(builder)
6060
}
6161
}
6262

6363
impl ToHtml for str {
64-
fn push_html_to(&self, buffer: &mut Html) {
64+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
6565
// XSS-Safety: Special characters will be escaped by `escape_to_string`.
66-
escape::escape_to_string(self, buffer.as_mut_string_unchecked());
66+
escape::escape_to_string(self, builder.as_mut_string_unchecked());
6767
}
6868
}
6969

7070
impl ToHtml for String {
71-
fn push_html_to(&self, buffer: &mut Html) {
72-
str::push_html_to(self, buffer);
71+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
72+
str::push_html_to(self, builder);
7373
}
7474
}
7575

7676
impl<'a> ToHtml for Cow<'a, str> {
77-
fn push_html_to(&self, buffer: &mut Html) {
78-
str::push_html_to(self, buffer);
77+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
78+
str::push_html_to(self, builder);
7979
}
8080
}
8181

8282
impl<'a> ToHtml for Arguments<'a> {
83-
fn push_html_to(&self, buffer: &mut Html) {
84-
let _ = buffer.write_fmt(*self);
83+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
84+
let _ = builder.write_fmt(*self);
8585
}
8686
}
8787

8888
impl<'a, T: ToHtml + ?Sized> ToHtml for &'a T {
89-
fn push_html_to(&self, buffer: &mut Html) {
90-
T::push_html_to(self, buffer);
89+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
90+
T::push_html_to(self, builder);
9191
}
9292
}
9393

9494
impl<'a, T: ToHtml + ?Sized> ToHtml for &'a mut T {
95-
fn push_html_to(&self, buffer: &mut Html) {
96-
T::push_html_to(self, buffer);
95+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
96+
T::push_html_to(self, builder);
9797
}
9898
}
9999

100100
impl<T: ToHtml + ?Sized> ToHtml for Box<T> {
101-
fn push_html_to(&self, buffer: &mut Html) {
102-
T::push_html_to(self, buffer);
101+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
102+
T::push_html_to(self, builder);
103103
}
104104
}
105105

106106
macro_rules! impl_to_html_with_display {
107107
($($ty:ty)*) => {
108108
$(
109109
impl ToHtml for $ty {
110-
fn push_html_to(&self, buffer: &mut Html) {
111-
let _ = write!(buffer, "{self}");
110+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
111+
let _ = write!(builder, "{self}");
112112
}
113113
}
114114
)*
@@ -123,9 +123,9 @@ macro_rules! impl_to_html_with_itoa {
123123
($($ty:ty)*) => {
124124
$(
125125
impl ToHtml for $ty {
126-
fn push_html_to(&self, buffer: &mut Html) {
126+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
127127
// XSS-Safety: The characters '0' through '9', and '-', are HTML safe.
128-
let _ = itoa::fmt(buffer.as_mut_string_unchecked(), *self);
128+
let _ = itoa::fmt(builder.as_mut_string_unchecked(), *self);
129129
}
130130
}
131131
)*
@@ -179,8 +179,8 @@ impl_to_html_with_itoa! {
179179
///
180180
/// - **I have performance-sensitive rendering code that needs direct
181181
/// access to the buffer.**
182-
/// - Use [`Html::as_mut_string_unchecked`], and ask a security
183-
/// expert for review.
182+
/// - Use [`HtmlBuilder::as_mut_string_unchecked`], and ask a
183+
/// security expert for review.
184184
///
185185
/// - **I have special requirements and the other options don't work for
186186
/// me.**
@@ -274,6 +274,75 @@ impl Html {
274274
}
275275
}
276276

277+
/// Exposes the underlying buffer as a `&str`.
278+
pub fn as_str(&self) -> &str {
279+
&self.inner
280+
}
281+
282+
/// Converts the inner value to a `String`.
283+
pub fn into_string(self) -> String {
284+
self.inner.into_owned()
285+
}
286+
}
287+
288+
impl ToHtml for Html {
289+
fn push_html_to(&self, builder: &mut HtmlBuilder) {
290+
// XSS-Safety: `self` is already guaranteed to be trusted HTML.
291+
builder.as_mut_string_unchecked().push_str(self.as_str());
292+
}
293+
}
294+
295+
impl From<Html> for String {
296+
fn from(html: Html) -> String {
297+
html.into_string()
298+
}
299+
}
300+
301+
/// The literal string `<!DOCTYPE html>`.
302+
///
303+
/// # Example
304+
///
305+
/// A minimal web page:
306+
///
307+
/// ```rust
308+
/// use maud::{DOCTYPE, html};
309+
///
310+
/// let page = html! {
311+
/// (DOCTYPE)
312+
/// html {
313+
/// head {
314+
/// meta charset="utf-8";
315+
/// title { "Test page" }
316+
/// }
317+
/// body {
318+
/// p { "Hello, world!" }
319+
/// }
320+
/// }
321+
/// };
322+
/// ```
323+
pub const DOCTYPE: Html = Html::from_const_unchecked("<!DOCTYPE html>");
324+
325+
/// A partially created fragment of HTML.
326+
///
327+
/// Unlike [`Html`], an `HtmlBuilder` might have unclosed elements or
328+
/// attributes.
329+
///
330+
/// This type cannot be constructed by hand. The [`html!`] macro creates
331+
/// one internally, and passes it to [`ToHtml::push_html_to`].
332+
#[derive(Clone, Debug)]
333+
pub struct HtmlBuilder {
334+
inner: Cow<'static, str>,
335+
}
336+
337+
impl HtmlBuilder {
338+
/// For internal use only.
339+
#[doc(hidden)]
340+
pub fn new() -> Self {
341+
Self {
342+
inner: Cow::Owned(String::new()),
343+
}
344+
}
345+
277346
/// For internal use only.
278347
#[doc(hidden)]
279348
pub fn with_capacity(capacity: usize) -> Self {
@@ -287,11 +356,6 @@ impl Html {
287356
value.push_html_to(self);
288357
}
289358

290-
/// Exposes the underlying buffer as a `&str`.
291-
pub fn as_str(&self) -> &str {
292-
&self.inner
293-
}
294-
295359
/// Exposes the underlying buffer as a `&mut String`.
296360
///
297361
/// This is useful for performance-sensitive use cases that need
@@ -300,12 +364,16 @@ impl Html {
300364
/// # Example
301365
///
302366
/// ```rust
303-
/// use maud::Html;
367+
/// use maud::{HtmlBuilder, ToHtml};
304368
/// # mod base64 { pub fn encode(_: &mut String, _: &[u8]) {} }
305369
///
306-
/// fn append_base64_to_html(buffer: &mut Html, bytes: &[u8]) {
307-
/// // XSS-Safety: The characters [A-Za-z0-9+/=] are all HTML-safe.
308-
/// base64::encode(buffer.as_mut_string_unchecked(), bytes);
370+
/// struct Base64<'a>(&'a [u8]);
371+
///
372+
/// impl<'a> ToHtml for Base64<'a> {
373+
/// fn push_html_to(&self, builder: &mut HtmlBuilder) {
374+
/// // XSS-Safety: The characters [A-Za-z0-9+/=] are all HTML-safe.
375+
/// base64::encode(builder.as_mut_string_unchecked(), self.0);
376+
/// }
309377
/// }
310378
/// ```
311379
///
@@ -323,56 +391,21 @@ impl Html {
323391
self.inner.to_mut()
324392
}
325393

326-
/// Converts the inner value to a `String`.
327-
pub fn into_string(self) -> String {
328-
self.inner.into_owned()
394+
/// For internal use only.
395+
#[doc(hidden)]
396+
pub fn finalize(self) -> Html {
397+
// XSS-Safety: This is called from the `html!` macro, which enforces safety itself.
398+
Html::from_unchecked(self.inner)
329399
}
330400
}
331401

332-
impl Write for Html {
402+
impl Write for HtmlBuilder {
333403
fn write_str(&mut self, text: &str) -> fmt::Result {
334404
self.push(text);
335405
Ok(())
336406
}
337407
}
338408

339-
impl ToHtml for Html {
340-
fn push_html_to(&self, buffer: &mut Html) {
341-
// XSS-Safety: `self` is already guaranteed to be trusted HTML.
342-
buffer.as_mut_string_unchecked().push_str(self.as_str());
343-
}
344-
}
345-
346-
impl From<Html> for String {
347-
fn from(html: Html) -> String {
348-
html.into_string()
349-
}
350-
}
351-
352-
/// The literal string `<!DOCTYPE html>`.
353-
///
354-
/// # Example
355-
///
356-
/// A minimal web page:
357-
///
358-
/// ```rust
359-
/// use maud::{DOCTYPE, html};
360-
///
361-
/// let page = html! {
362-
/// (DOCTYPE)
363-
/// html {
364-
/// head {
365-
/// meta charset="utf-8";
366-
/// title { "Test page" }
367-
/// }
368-
/// body {
369-
/// p { "Hello, world!" }
370-
/// }
371-
/// }
372-
/// };
373-
/// ```
374-
pub const DOCTYPE: Html = Html::from_const_unchecked("<!DOCTYPE html>");
375-
376409
#[cfg(feature = "rocket")]
377410
mod rocket_support {
378411
extern crate std;

maud/tests/misc.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use maud::{self, html, Html, ToHtml};
1+
use maud::{self, html, Html, HtmlBuilder, ToHtml};
22

33
#[test]
44
fn issue_13() {
@@ -55,7 +55,7 @@ fn issue_23() {
5555
fn render_impl() {
5656
struct R(&'static str);
5757
impl ToHtml for R {
58-
fn push_html_to(&self, buffer: &mut Html) {
58+
fn push_html_to(&self, buffer: &mut HtmlBuilder) {
5959
buffer.push(self.0);
6060
}
6161
}

maud_macros/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ fn expand(input: TokenStream) -> TokenStream {
3737
let stmts = generate::generate(markups, output_ident.clone());
3838
quote!({
3939
extern crate maud;
40-
let mut #output_ident = maud::Html::with_capacity(#size_hint);
40+
let mut #output_ident = maud::HtmlBuilder::with_capacity(#size_hint);
4141
#stmts
42-
#output_ident
42+
maud::HtmlBuilder::finalize(#output_ident)
4343
})
4444
}

0 commit comments

Comments
 (0)