Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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