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(), "");
+```
+
+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! {
+
+};
+
+assert_eq!(tree, "");
+```
+
+### 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(), "");
+//! ```
+//!
+//! 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, "");
+//! ```
+//!
+//! ## 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}>",
- 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);
+ }
+}