From b03ce49bab2641c260268c454b336bcb630a81a5 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Thu, 3 Oct 2019 09:42:07 +0300 Subject: [PATCH] Use `Write` instead of owned strings (#8) Writing to a mutable buffer is more performant than returning owned strings, and should be the way to go --- Cargo.lock | 7 -- README.md | 114 ++++++++++++++++++++-- render/Cargo.toml | 1 - render/src/fragment.rs | 12 +-- render/src/html.rs | 9 +- render/src/html_escaping.rs | 26 +++++ render/src/lib.rs | 124 ++++++++++++++++++++++-- render/src/render.rs | 61 ++++++++++++ render/src/renderable.rs | 96 ------------------ render/src/simple_element.rs | 55 ++++++----- render/src/text_element.rs | 27 +++--- render_macros/src/children.rs | 5 +- render_macros/src/element_attributes.rs | 19 ++-- render_macros/src/function_component.rs | 13 ++- render_macros/src/lib.rs | 50 +++++----- render_tests/src/lib.rs | 119 ++++++++++------------- 16 files changed, 461 insertions(+), 277 deletions(-) create mode 100644 render/src/html_escaping.rs create mode 100644 render/src/render.rs delete mode 100644 render/src/renderable.rs diff --git a/Cargo.lock b/Cargo.lock index d3d27bf..d32fe1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,11 +22,6 @@ name = "difference" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "htmlescape" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "output_vt100" version = "0.1.2" @@ -66,7 +61,6 @@ dependencies = [ name = "render" version = "0.2.0" dependencies = [ - "htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "render_macros 0.2.0", ] @@ -128,7 +122,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" "checksum ctor 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3e061727ebef83bbccac7c27b9a5ff9fd83094d34cb20f4005440a9562a27de7" "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" -"checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" "checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" "checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" "checksum proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e98a83a9f9b331f54b924e68a66acb1bb35cb01fb0a23645139967abefb697e8" diff --git a/README.md b/README.md index 6e0930d..8069ce7 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,25 @@ XML rendering, but can work with other usages as well, like ReasonML's [`Pastel` ## How? -A renderable component is a struct that implements the `Renderable` trait. There +A renderable component is a struct that implements the `Render` trait. There are multiple macros that provide a better experience implementing Renderable: -* `html!` for the JSX ergonomics -* `#[component]` for the syntactic-sugar of function components +* `#[component]` for defining components using a function +* `rsx!` for composing elements with JSX ergonomics +* `html!` for composing elements and render them to a string -## Why this is different from `typed-html`? +## Why is this different from... + +### `handlebars`? + +Handlebars is an awesome spec that lets us devs define templates and work +seemlessly between languages and frameworks. Unfortunately, it does not guarantee any of Rust's +type-safety, due to its spec. This forces you to write tests for validating types for your views, like you would in a dynamically typed language. These tests weren't necessary in a type-safe language like Rust — but Handlebars is JSON-oriented, which doesn't comply Rust's type system. + +`render` provides the same level of type-safety Rust provides, with no compromises of +ergonomics or speed. + +### `typed-html`? `typed-html` is a wonderful library. Unfortunately, it focused its power in strictness of the HTML spec itself, and doesn't allow arbitrary compositions of custom elements. @@ -22,6 +34,92 @@ are multiple macros that provide a better experience implementing Renderable: ## Usage +> Note: `render` needs the `nightly` Rust compiler, for now, so it will have hygienic macros. + +This means you will need to add the following feature flag in the root of your `lib.rs`/`main.rs`: + +```rust +#![feature(proc_macro_hygiene)] +``` + +### Simple HTML rendering + +In order to render a simple HTML fragment into a `String`, use the `rsx!` macro to generate a +component tree, and call `render` on it: + +```rust +#![feature(proc_macro_hygiene)] + +use render::{rsx, Render}; + +let tree = rsx! { +
+

{"Hello!"}

+

{"Hello world!"}

+
+}; + +assert_eq!(tree.render(), "

Hello!

Hello world!

"); +``` + +Because this is so common, there's another macro called `html!` that calls `rsx!` to generate +a component tree, and then calls `render` on it. Most of the time, you'll find yourself using +the `rsx!` macro to compose arbitrary components, and only calling `html!` when you need a +String output, when sending a response or generating a Markdown file. + +In Render, attributes and plain strings are escaped using the `render::html_escaping` module. In order to +use un-escaped values so you can dangerously insert raw HTML, use the `raw!` macro around your +string: + +```rust +#![feature(proc_macro_hygiene)] + +use render::{html, raw}; + +let tree = html! { +
+

{""}

+

{raw!("")}

+
+}; + +assert_eq!(tree, "

<Hello />

"); +``` + +### Custom components + +Render's greatest ability is to provide type-safety along with custom renderable components. +Introducing new components is as easy as defining a function that returns a `Render` value. + +In order to build up components from other components or HTML nodes, you can use the `rsx!` +macro, which generates a `Render` component tree: + +```rust +#![feature(proc_macro_hygiene)] + +use render::{component, rsx, html}; + +#[component] +fn Heading<'title>(title: &'title str) { + rsx! {

{title}

} +} + +let rendered_html = html! { + +}; + +assert_eq!(rendered_html, r#"

Hello world!

"#); +``` + +If you pay close attention, you see that the function `Heading` is: + +* declared with an uppercase. Underneath, it generates a struct with the same name, and +implements the `Render` trait on it. +* does not have a return type. This is because everything is written to a writer, for +performance reasons. + +#### Full example + ```rust #![feature(proc_macro_hygiene)] @@ -31,15 +129,17 @@ use render::{ // A macro to create components component, // A macro to compose components in JSX fashion + rsx, + // A macro to render components in JSX fashion html, // A trait for custom components - Renderable, + Render, }; // This can be any layout we want #[component] -fn Page<'a, Children: Renderable>(title: &'a str, children: Children) -> String { - html! { +fn Page<'a, Children: Render>(title: &'a str, children: Children) { + rsx! { <> diff --git a/render/Cargo.toml b/render/Cargo.toml index 7efc0f8..7ab26a3 100644 --- a/render/Cargo.toml +++ b/render/Cargo.toml @@ -14,7 +14,6 @@ license = "MIT" [dependencies] render_macros = { path = "../render_macros", version = "0.2" } -htmlescape = "0.3" [dev-dependencies] pretty_assertions = "0.6" diff --git a/render/src/fragment.rs b/render/src/fragment.rs index 97e22ac..5f97f20 100644 --- a/render/src/fragment.rs +++ b/render/src/fragment.rs @@ -1,6 +1,7 @@ //! The fragment component -use crate::Renderable; +use crate::Render; +use std::fmt::{Result, Write}; /// A top-level root component to combine a same-level components /// in a RSX fashion @@ -8,7 +9,6 @@ use crate::Renderable; /// ```rust /// # #![feature(proc_macro_hygiene)] /// # use pretty_assertions::assert_eq; -/// # use render::html::HTML5Doctype; /// # use render_macros::html; /// let result = html! { /// <> @@ -19,12 +19,12 @@ use crate::Renderable; /// assert_eq!(result, ""); /// ``` #[derive(Debug)] -pub struct Fragment { +pub struct Fragment { pub children: T, } -impl Renderable for Fragment { - fn render(self) -> String { - self.children.render() +impl Render for Fragment { + fn render_into(self, writer: &mut W) -> Result { + self.children.render_into(writer) } } diff --git a/render/src/html.rs b/render/src/html.rs index 401b009..8dfea8c 100644 --- a/render/src/html.rs +++ b/render/src/html.rs @@ -1,6 +1,7 @@ //! HTML utilities -use crate::Renderable; +use crate::Render; +use std::fmt::{Result, Write}; /// HTML 5 doctype declaration /// @@ -23,8 +24,8 @@ use crate::Renderable; #[derive(Debug)] pub struct HTML5Doctype; -impl Renderable for HTML5Doctype { - fn render(self) -> String { - "".to_string() +impl Render for HTML5Doctype { + fn render_into(self, writer: &mut W) -> Result { + write!(writer, "") } } diff --git a/render/src/html_escaping.rs b/render/src/html_escaping.rs new file mode 100644 index 0000000..e9a101a --- /dev/null +++ b/render/src/html_escaping.rs @@ -0,0 +1,26 @@ +use std::fmt::{Result, Write}; + +/// Simple HTML escaping, so strings can be safely rendered. +/// +/// ```rust +/// # use pretty_assertions::assert_eq; +/// # use render::html_escaping; +/// +/// let mut buf = String::new(); +/// html_escaping::escape_html(r#""#, &mut buf).unwrap(); +/// assert_eq!(buf, "<hello world="attribute" />"); +/// ``` +pub fn escape_html(html: &str, writer: &mut W) -> Result { + for c in html.chars() { + match c { + '>' => write!(writer, ">")?, + '<' => write!(writer, "<")?, + '"' => write!(writer, """)?, + '&' => write!(writer, "&")?, + '\'' => write!(writer, "'")?, + c => writer.write_char(c)?, + }; + } + + Ok(()) +} diff --git a/render/src/lib.rs b/render/src/lib.rs index 6ee1090..4ae7572 100644 --- a/render/src/lib.rs +++ b/render/src/lib.rs @@ -6,13 +6,25 @@ //! //! # How? //! -//! A renderable component is a struct that implements the `Renderable` trait. There +//! A renderable component is a struct that implements the `Render` trait. There //! are multiple macros that provide a better experience implementing Renderable: //! -//! * `html!` for the JSX ergonomics -//! * `#[component]` for the syntactic-sugar of function components +//! * `#[component]` for defining components using a function +//! * `rsx!` for composing elements with JSX ergonomics +//! * `html!` for composing elements and render them to a string //! -//! # Why this is different from `typed-html`? +//! # Why is this different from... +//! +//! ## `handlebars`? +//! +//! Handlebars is an awesome spec that lets us devs define templates and work +//! seemlessly between languages and frameworks. Unfortunately, it does not guarantee any of Rust's +//! type-safety, due to its spec. This forces you to write tests for validating types for your views, like you would in a dynamically typed language. These tests weren't necessary in a type-safe language like Rust — but Handlebars is JSON-oriented, which doesn't comply Rust's type system. +//! +//! `render` provides the same level of type-safety Rust provides, with no compromises of +//! ergonomics or speed. +//! +//! ## `typed-html`? //! //! `typed-html` is a wonderful library. Unfortunately, it focused its power in strictness of the HTML spec itself, and doesn't allow arbitrary compositions of custom elements. //! @@ -20,6 +32,95 @@ //! //! # Usage //! +//! > Note: `render` needs the `nightly` Rust compiler, for now, so it will have hygienic macros. +//! +//! This means you will need to add the following feature flag in the root of your `lib.rs`/`main.rs`: +//! +//! ```rust +//! #![feature(proc_macro_hygiene)] +//! ``` +//! +//! ## Simple HTML rendering +//! +//! In order to render a simple HTML fragment into a `String`, use the `rsx!` macro to generate a +//! component tree, and call `render` on it: +//! +//! ```rust +//! #![feature(proc_macro_hygiene)] +//! # use pretty_assertions::assert_eq; +//! +//! use render::{rsx, Render}; +//! +//! let tree = rsx! { +//!
+//!

{"Hello!"}

+//!

{"Hello world!"}

+//!
+//! }; +//! +//! assert_eq!(tree.render(), "

Hello!

Hello world!

"); +//! ``` +//! +//! Because this is so common, there's another macro called `html!` that calls `rsx!` to generate +//! a component tree, and then calls `render` on it. Most of the time, you'll find yourself using +//! the `rsx!` macro to compose arbitrary components, and only calling `html!` when you need a +//! String output, when sending a response or generating a Markdown file. +//! +//! In Render, attributes and plain strings are escaped using the `render::html_escaping` module. In order to +//! use un-escaped values so you can dangerously insert raw HTML, use the `raw!` macro around your +//! string: +//! +//! ```rust +//! #![feature(proc_macro_hygiene)] +//! # use pretty_assertions::assert_eq; +//! +//! use render::{html, raw}; +//! +//! let tree = html! { +//!
+//!

{""}

+//!

{raw!("")}

+//!
+//! }; +//! +//! assert_eq!(tree, "

<Hello />

"); +//! ``` +//! +//! ## Custom components +//! +//! Render's greatest ability is to provide type-safety along with custom renderable components. +//! Introducing new components is as easy as defining a function that returns a `Render` value. +//! +//! In order to build up components from other components or HTML nodes, you can use the `rsx!` +//! macro, which generates a `Render` component tree: +//! +//! ```rust +//! #![feature(proc_macro_hygiene)] +//! # use pretty_assertions::assert_eq; +//! +//! use render::{component, rsx, html}; +//! +//! #[component] +//! fn Heading<'title>(title: &'title str) { +//! rsx! {

{title}

} +//! } +//! +//! let rendered_html = html! { +//! +//! }; +//! +//! assert_eq!(rendered_html, r#"

Hello world!

"#); +//! ``` +//! +//! If you pay close attention, you see that the function `Heading` is: +//! +//! * declared with an uppercase. Underneath, it generates a struct with the same name, and +//! implements the `Render` trait on it. +//! * does not have a return type. This is because everything is written to a writer, for +//! performance reasons. +//! +//! ### Full example +//! //! ```rust //! #![feature(proc_macro_hygiene)] //! @@ -29,15 +130,17 @@ //! // A macro to create components //! component, //! // A macro to compose components in JSX fashion +//! rsx, +//! // A macro to render components in JSX fashion //! html, //! // A trait for custom components -//! Renderable, +//! Render, //! }; //! //! // This can be any layout we want //! #[component] -//! fn Page<'a, Children: Renderable>(title: &'a str, children: Children) -> String { -//! html! { +//! fn Page<'a, Children: Render>(title: &'a str, children: Children) { +//! rsx! { //! <> //! //! @@ -74,14 +177,17 @@ //! # assert_eq!(actual, expected); //! ``` +#![feature(proc_macro_hygiene)] + pub mod fragment; pub mod html; -mod renderable; +pub mod html_escaping; +mod render; mod simple_element; mod text_element; +pub use self::render::Render; pub use fragment::Fragment; pub use render_macros::{component, html, rsx}; -pub use renderable::Renderable; pub use simple_element::SimpleElement; pub use text_element::Raw; diff --git a/render/src/render.rs b/render/src/render.rs new file mode 100644 index 0000000..0f96665 --- /dev/null +++ b/render/src/render.rs @@ -0,0 +1,61 @@ +use std::fmt::{Result, Write}; + +/// Render a component +/// +/// This is the underlying mechanism of the `#[component]` macro +pub trait Render: Sized { + /// Render the component to a writer. + /// Make sure you escape html correctly using the `render::html_escaping` module + fn render_into(self, writer: &mut W) -> Result; + + /// Render the component to string + fn render(self) -> String { + let mut buf = String::new(); + self.render_into(&mut buf).unwrap(); + buf + } +} + +/// Does nothing +impl Render for () { + fn render_into(self, _writer: &mut W) -> Result { + Ok(()) + } +} + +/// Renders `A`, then `B` +impl Render for (A, B) { + fn render_into(self, writer: &mut W) -> Result { + self.0.render_into(writer)?; + self.1.render_into(writer) + } +} + +/// Renders `A`, then `B`, then `C` +impl Render for (A, B, C) { + fn render_into(self, writer: &mut W) -> Result { + self.0.render_into(writer)?; + self.1.render_into(writer)?; + self.2.render_into(writer) + } +} + +/// Renders `T` or nothing +impl Render for Option { + fn render_into(self, writer: &mut W) -> Result { + match self { + None => Ok(()), + Some(x) => x.render_into(writer), + } + } +} + +/// Renders `O` or `E` +impl Render for std::result::Result { + fn render_into(self, writer: &mut W) -> Result { + match self { + Ok(o) => o.render_into(writer), + Err(e) => e.render_into(writer), + } + } +} diff --git a/render/src/renderable.rs b/render/src/renderable.rs deleted file mode 100644 index fb74c4e..0000000 --- a/render/src/renderable.rs +++ /dev/null @@ -1,96 +0,0 @@ -/// A renderable component -pub trait Renderable: core::fmt::Debug + Sized { - /// Render the component to the HTML representation. - /// - /// Mostly done using the `html!` macro to generate strings - /// by composing tags. - /// - /// A simple implementation: - /// - /// ```rust - /// # #![feature(proc_macro_hygiene)] - /// # use render_macros::html; - /// # use render::Renderable; - /// # use pretty_assertions::assert_eq; - /// #[derive(Debug)] - /// struct Header<'t> { title: &'t str } - /// - /// impl<'t> Renderable for Header<'t> { - /// fn render(self) -> String { - /// html! { - ///

{self.title}

- /// } - /// } - /// } - /// - /// // Then you can use it with - /// - /// let rendered_html = html! { - ///
- /// }; - /// - /// # assert_eq!(rendered_html, "

Hello world!

"); - /// ``` - /// - /// ## Children - /// - /// `children` is a special field that will be populated with other `Renderable` if any children was provided, by both the [`html!`] and the [`rsx!`] macros. - /// - /// [`html!`]: ../render_macros/macro.html.html - /// [`rsx!`]: ../render_macros/macro.rsx.html - fn render(self) -> String; -} - -/// Renders an empty string -impl Renderable for () { - fn render(self) -> String { - "".to_string() - } -} - -/// Renders `A` and then `B` -impl Renderable for (A, B) { - fn render(self) -> String { - format!("{}{}", self.0.render(), self.1.render()) - } -} - -/// Renders `A`, `B`, and then `C` -impl Renderable for (A, B, C) { - fn render(self) -> String { - ((self.0, self.1), self.2).render() - } -} - -/// Renders `A`, `B`, `C` and then `D` -impl Renderable for (A, B, C, D) { - fn render(self) -> String { - ((self.0, self.1), (self.2, self.3)).render() - } -} - -/// Renders the `T` or an empty string -impl Renderable for Option { - fn render(self) -> String { - match self { - None => "".to_string(), - Some(x) => x.render(), - } - } -} - -/// Renders `O` or `E` -impl Renderable for Result { - fn render(self) -> String { - match self { - Err(e) => e.render(), - Ok(o) => o.render(), - } - } -} - -impl Renderable for usize { - fn render(self) -> String { - self.to_string() - } -} diff --git a/render/src/simple_element.rs b/render/src/simple_element.rs index b4d0391..c271bae 100644 --- a/render/src/simple_element.rs +++ b/render/src/simple_element.rs @@ -1,41 +1,48 @@ -use crate::Renderable; +use crate::Render; +use crate::html_escaping::escape_html; use std::collections::HashMap; +use std::fmt::{Result, Write}; + +type Attributes<'a> = Option>; /// Simple HTML element tag #[derive(Debug)] -pub struct SimpleElement<'a, T: Renderable> { +pub struct SimpleElement<'a, T: Render> { /// the HTML tag name, like `html`, `head`, `body`, `link`... pub tag_name: &'a str, - pub attributes: Option>, + pub attributes: Attributes<'a>, pub contents: Option, } -fn attributes_to_string( - opt: &Option>, -) -> String { - match opt { - None => "".to_string(), - Some(map) => { - let s: String = map - .iter() - .map(|(key, value)| format!(" {}={:?}", key, value)) - .collect(); - s +fn write_attributes<'a, W: Write>(maybe_attributes: Attributes<'a>, writer: &mut W) -> Result { + match maybe_attributes { + None => Ok(()), + Some(mut attributes) => { + for (key, value) in attributes.drain() { + write!(writer, " {}=\"", key)?; + escape_html(value, writer)?; + write!(writer, "\"")?; + } + Ok(()) } } } -impl<'a, T: Renderable> Renderable for SimpleElement<'a, T> { - fn render(self) -> String { - let attrs = attributes_to_string(&self.attributes); +impl Render for SimpleElement<'_, T> { + fn render_into(self, writer: &mut W) -> Result { match self.contents { - None => format!("<{}{} />", self.tag_name, attrs), - Some(renderable) => format!( - "<{tag_name}{attrs}>{contents}", - tag_name = self.tag_name, - attrs = attrs, - contents = renderable.render() - ), + None => { + write!(writer, "<{}", self.tag_name)?; + write_attributes(self.attributes, writer)?; + write!(writer, " />") + } + Some(renderable) => { + write!(writer, "<{}", self.tag_name)?; + write_attributes(self.attributes, writer)?; + write!(writer, ">")?; + renderable.render_into(writer)?; + write!(writer, "", self.tag_name) + } } } } diff --git a/render/src/text_element.rs b/render/src/text_element.rs index 2f383bc..ea46e14 100644 --- a/render/src/text_element.rs +++ b/render/src/text_element.rs @@ -1,17 +1,16 @@ -use crate::Renderable; -use htmlescape::encode_minimal; +use crate::Render; +use std::fmt::{Result, Write}; +use crate::html_escaping::escape_html; -/// Renders an escaped-html string -impl Renderable for String { - fn render(self) -> String { - encode_minimal(&self) +impl Render for String { + fn render_into(self, writer: &mut W) -> Result { + escape_html(&self, writer) } } -/// Renders an escaped-html string -impl Renderable for &str { - fn render(self) -> String { - encode_minimal(self) +impl Render for &str { + fn render_into(self, writer: &mut W) -> Result { + escape_html(self, writer) } } @@ -26,9 +25,9 @@ impl<'s> From<&'s str> for Raw<'s> { } /// A raw (unencoded) html string -impl<'s> Renderable for Raw<'s> { - fn render(self) -> String { - self.0.to_string() +impl<'s> Render for Raw<'s> { + fn render_into(self, writer: &mut W) -> Result { + write!(writer, "{}", self.0) } } @@ -38,12 +37,14 @@ mod tests { #[test] fn decodes_html() { + use pretty_assertions::assert_eq; let rendered = "".render(); assert_eq!(rendered, "<Hello />"); } #[test] fn allows_raw_text() { + use pretty_assertions::assert_eq; let rendered = Raw::from("").render(); assert_eq!(rendered, ""); } diff --git a/render_macros/src/children.rs b/render_macros/src/children.rs index e05d779..8d9feeb 100644 --- a/render_macros/src/children.rs +++ b/render_macros/src/children.rs @@ -47,9 +47,8 @@ impl Parse for Children { let mut nodes = vec![]; while !input.peek(syn::Token![<]) || !input.peek2(syn::Token![/]) { - if let Ok(child) = input.parse::() { - nodes.push(child); - } + let child = input.parse::()?; + nodes.push(child); } Ok(Self::new(nodes)) diff --git a/render_macros/src/element_attributes.rs b/render_macros/src/element_attributes.rs index 07e34a2..dec180c 100644 --- a/render_macros/src/element_attributes.rs +++ b/render_macros/src/element_attributes.rs @@ -56,17 +56,16 @@ impl Parse for ElementAttributes { fn parse(input: ParseStream) -> Result { let mut attributes: HashSet = HashSet::new(); while input.peek(syn::Ident) { - if let Ok(attribute) = input.parse::() { - let ident = attribute.ident(); - if attributes.contains(&attribute) { - let error_message = format!( - "There is a previous definition of the {} attribute", - quote!(#ident) - ); - ident.span().unwrap().warning(error_message).emit(); - } - attributes.insert(attribute); + let attribute = input.parse::()?; + let ident = attribute.ident(); + if attributes.contains(&attribute) { + let error_message = format!( + "There is a previous definition of the {} attribute", + quote!(#ident) + ); + ident.span().unwrap().warning(error_message).emit(); } + attributes.insert(attribute); } Ok(ElementAttributes::new(attributes)) } diff --git a/render_macros/src/function_component.rs b/render_macros/src/function_component.rs index 5268749..261528b 100644 --- a/render_macros/src/function_component.rs +++ b/render_macros/src/function_component.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::spanned::Spanned; -pub fn to_component(f: syn::ItemFn) -> TokenStream { +pub fn create_function_component(f: syn::ItemFn) -> TokenStream { let struct_name = f.sig.ident; let (impl_generics, ty_generics, where_clause) = f.sig.generics.split_for_impl(); let inputs = f.sig.inputs; @@ -41,10 +41,13 @@ pub fn to_component(f: syn::ItemFn) -> TokenStream { #[derive(Debug)] #vis struct #struct_name#impl_generics #inputs_block - impl#impl_generics ::render::Renderable for #struct_name #ty_generics #where_clause { - fn render(self) -> String { - #inputs_reading - #block + impl#impl_generics ::render::Render for #struct_name #ty_generics #where_clause { + fn render_into(self, w: &mut W) -> std::fmt::Result { + let result = { + #inputs_reading + #block + }; + ::render::Render::render_into(result, w) } } }) diff --git a/render_macros/src/lib.rs b/render_macros/src/lib.rs index b597b46..cac4bf9 100644 --- a/render_macros/src/lib.rs +++ b/render_macros/src/lib.rs @@ -34,21 +34,21 @@ use syn::parse_macro_input; /// ```rust /// # #![feature(proc_macro_hygiene)] /// # use pretty_assertions::assert_eq; -/// # use render_macros::html; -/// use render::Renderable; +/// # use render_macros::{html, rsx}; +/// use render::Render; /// /// #[derive(Debug)] /// struct Heading<'t> { title: &'t str } /// -/// impl<'t> Renderable for Heading<'t> { -/// fn render(self) -> String { -/// html! {

{self.title}

} +/// impl<'t> Render for Heading<'t> { +/// fn render_into(self, writer: &mut W) -> std::fmt::Result { +/// Render::render_into(rsx! {

{self.title}

}, writer) /// } /// } /// -/// let rendered = html! { }; +/// let rendered = html! { }; /// -/// assert_eq!(rendered, r#"

Hello world!

"#); +/// assert_eq!(rendered, r#"

Hello world!

"#); /// ``` /// /// ### Values are always surrounded by curly braces @@ -71,10 +71,10 @@ use syn::parse_macro_input; /// # use render_macros::html; /// # use pretty_assertions::assert_eq; /// let rendered = html! { -///
+///
/// }; /// -/// assert_eq!(rendered, r#"
"#); +/// assert_eq!(rendered, r#"
"#); /// ``` /// /// ### Custom components can't accept dashed-separated values @@ -96,13 +96,13 @@ use syn::parse_macro_input; /// # #![feature(proc_macro_hygiene)] /// # use render_macros::html; /// # use pretty_assertions::assert_eq; -/// let class = "some_class"; +/// let class = "someclass"; /// /// let rendered = html! { ///
/// }; /// -/// assert_eq!(rendered, r#"
"#); +/// assert_eq!(rendered, r#"
"#); /// ``` /// /// ### Punning is not supported for dashed-delimited attributes @@ -120,7 +120,7 @@ use syn::parse_macro_input; #[proc_macro] pub fn html(input: TokenStream) -> TokenStream { let el = proc_macro2::TokenStream::from(rsx(input)); - let result = quote! { ::render::Renderable::render(#el) }; + let result = quote! { ::render::Render::render(#el) }; TokenStream::from(result) } @@ -132,7 +132,7 @@ pub fn rsx(input: TokenStream) -> TokenStream { TokenStream::from(result) } -/// A syntactic sugar for implementing [`Renderable`](../render/trait.Renderable.html) conveniently +/// A syntactic sugar for implementing [`Render`](../render/trait.Render.html) conveniently /// using functions. /// /// This attribute should be above a stand-alone function definition that returns a @@ -140,34 +140,34 @@ pub fn rsx(input: TokenStream) -> TokenStream { /// /// ```rust /// # #![feature(proc_macro_hygiene)] -/// # use render_macros::{component, html}; +/// # use render_macros::{component, rsx}; /// # /// #[component] -/// fn UserFn(name: String) -> String { -/// html! {
{format!("Hello, {}", name)}
} +/// fn UserFn(name: String) { +/// rsx! {
{format!("Hello, {}", name)}
} /// } /// ``` /// -/// Practically, this is exactly the same as using the [Renderable](../render/trait.Renderable.html) trait: +/// Practically, this is exactly the same as using the [Render](../render/trait.Render.html) trait: /// /// ```rust /// # #![feature(proc_macro_hygiene)] -/// # use render_macros::{component, html}; -/// # use render::Renderable; +/// # use render_macros::{component, rsx, html}; +/// # use render::Render; /// # use pretty_assertions::assert_eq; /// # /// #[derive(Debug)] /// struct User { name: String } /// -/// impl render::Renderable for User { -/// fn render(self) -> String { -/// html! {
{format!("Hello, {}", self.name)}
} +/// impl render::Render for User { +/// fn render_into(self, writer: &mut W) -> std::fmt::Result { +/// Render::render_into(rsx! {
{format!("Hello, {}", self.name)}
}, writer) /// } /// } /// /// # #[component] -/// # fn UserFn(name: String) -> String { -/// # html! {
{format!("Hello, {}", name)}
} +/// # fn UserFn(name: String) { +/// # rsx! {
{format!("Hello, {}", name)}
} /// # } /// # /// # let from_fn = html! { @@ -183,5 +183,5 @@ pub fn rsx(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream { let f = parse_macro_input!(item as syn::ItemFn); - function_component::to_component(f) + function_component::create_function_component(f) } diff --git a/render_tests/src/lib.rs b/render_tests/src/lib.rs index 1a0caa3..f111048 100644 --- a/render_tests/src/lib.rs +++ b/render_tests/src/lib.rs @@ -1,81 +1,17 @@ #![feature(proc_macro_hygiene)] -use render::html::HTML5Doctype; -use render::{component, html, rsx, Renderable}; - -#[derive(Debug)] -struct Hello<'a, T: Renderable> { - world: &'a str, - yes: i32, - children: T, -} - -impl<'a, T: Renderable> Renderable for Hello<'a, T> { - fn render(self) -> String { - html! { - - {format!("{}", self.world)} -
- {format!("A number: {}", self.yes)} - {self.children} -
- } - } -} - -pub fn it_works() -> String { - let world = "hello"; - let other_value = rsx! { - {format!("hello world?")} - }; - let value = html! { - <> - - -
{format!("HEY!")}
- {other_value} -
- - }; - value -} - -#[component] -pub fn Layout<'a, Children: Renderable>(title: &'a str, children: Children) -> String { - html! { - - {title} - - {children} - - - } -} - -#[component] -pub fn SomeComponent(name: String) -> String { - html! { -
{format!("Hello, {}", name)}
- } -} - -#[test] -pub fn verify_works() { - println!("{}", it_works()); -} - #[test] pub fn works_with_dashes() { use pretty_assertions::assert_eq; - let value = html! {
}; - assert_eq!(value, r#"
"#); + let value = render::html! {
}; + assert_eq!(value, r#"
"#); } #[test] pub fn works_with_raw() { use pretty_assertions::assert_eq; - use render::raw; + use render::{html, raw}; let actual = html! {
{raw!("")}
@@ -83,3 +19,52 @@ pub fn works_with_raw() { assert_eq!(actual, "
"); } + +mod kaki { + // A simple HTML 5 doctype declaration + use render::html::HTML5Doctype; + use render::{ + // A macro to create components + component, + // A macro to compose components in JSX fashion + rsx, + // A trait for custom components + Render, + }; + + // This can be any layout we want + #[component] + fn Page<'a, Children: Render>(title: &'a str, children: Children) { + rsx! { + <> + + + {title} + + {children} + + + + } + } + + #[test] + fn test() { + use pretty_assertions::assert_eq; + let actual = render::html! { + + {format!("Welcome, {}", "Gal")} + + }; + let expected = concat!( + "", + "", + "Home", + "", + "Welcome, Gal", + "", + "" + ); + assert_eq!(actual, expected); + } +}