Skip to content

Commit

Permalink
Use Write instead of owned strings (#8)
Browse files Browse the repository at this point in the history
Writing to a mutable buffer is more performant than returning owned strings, and should be the way to go
  • Loading branch information
Schniz authored Oct 3, 2019
1 parent 005fdc1 commit b03ce49
Show file tree
Hide file tree
Showing 16 changed files with 461 additions and 277 deletions.
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 107 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,118 @@ 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.

`render` takes a different approach. For now, HTML is not typed at all. It can get any key and get any string value. The main focus is custom components, so you can create a composable and declarative template with no runtime errors.

## 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! {
<div>
<h1>{"Hello!"}</h1>
<p>{"Hello world!"}</p>
</div>
};

assert_eq!(tree.render(), "<div><h1>Hello!</h1><p>Hello world!</p></div>");
```

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! {
<div>
<p>{"<Hello />"}</p>
<p>{raw!("<Hello />")}</p>
</div>
};

assert_eq!(tree, "<div><p>&lt;Hello /&gt;</p><p><Hello /></p></div>");
```

### 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! { <h1 class={"title"}>{title}</h1> }
}

let rendered_html = html! {
<Heading title={"Hello world!"} />
};

assert_eq!(rendered_html, r#"<h1 class="title">Hello world!</h1>"#);
```

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)]

Expand All @@ -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! {
<>
<HTML5Doctype />
<html>
Expand Down
1 change: 0 additions & 1 deletion render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ license = "MIT"

[dependencies]
render_macros = { path = "../render_macros", version = "0.2" }
htmlescape = "0.3"

[dev-dependencies]
pretty_assertions = "0.6"
12 changes: 6 additions & 6 deletions render/src/fragment.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
//! 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
///
/// ```rust
/// # #![feature(proc_macro_hygiene)]
/// # use pretty_assertions::assert_eq;
/// # use render::html::HTML5Doctype;
/// # use render_macros::html;
/// let result = html! {
/// <>
Expand All @@ -19,12 +19,12 @@ use crate::Renderable;
/// assert_eq!(result, "<a /><b />");
/// ```
#[derive(Debug)]
pub struct Fragment<T: Renderable> {
pub struct Fragment<T: Render> {
pub children: T,
}

impl<T: Renderable> Renderable for Fragment<T> {
fn render(self) -> String {
self.children.render()
impl<T: Render> Render for Fragment<T> {
fn render_into<W: Write>(self, writer: &mut W) -> Result {
self.children.render_into(writer)
}
}
9 changes: 5 additions & 4 deletions render/src/html.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! HTML utilities

use crate::Renderable;
use crate::Render;
use std::fmt::{Result, Write};

/// HTML 5 doctype declaration
///
Expand All @@ -23,8 +24,8 @@ use crate::Renderable;
#[derive(Debug)]
pub struct HTML5Doctype;

impl Renderable for HTML5Doctype {
fn render(self) -> String {
"<!DOCTYPE html>".to_string()
impl Render for HTML5Doctype {
fn render_into<W: Write>(self, writer: &mut W) -> Result {
write!(writer, "<!DOCTYPE html>")
}
}
26 changes: 26 additions & 0 deletions render/src/html_escaping.rs
Original file line number Diff line number Diff line change
@@ -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#"<hello world="attribute" />"#, &mut buf).unwrap();
/// assert_eq!(buf, "&lt;hello world=&quot;attribute&quot; /&gt;");
/// ```
pub fn escape_html<W: Write>(html: &str, writer: &mut W) -> Result {
for c in html.chars() {
match c {
'>' => write!(writer, "&gt;")?,
'<' => write!(writer, "&lt;")?,
'"' => write!(writer, "&quot;")?,
'&' => write!(writer, "&amp;")?,
'\'' => write!(writer, "&apos;")?,
c => writer.write_char(c)?,
};
}

Ok(())
}
Loading

0 comments on commit b03ce49

Please sign in to comment.