From 65849b051e601c1b786f1ee33cb5eec5e5f3b3e0 Mon Sep 17 00:00:00 2001
From: Chris Wong <lambda.fairy@gmail.com>
Date: Sat, 24 Nov 2018 16:13:36 +1300
Subject: [PATCH] Rewrite the book!

---
 .gitignore                      |   1 +
 Cargo.toml                      |   1 +
 docs/Cargo.toml                 |  17 +++++
 docs/content/getting-started.md |  56 +++++++++++++++
 docs/content/index.md           |  33 +++++++++
 docs/src/main.rs                | 123 ++++++++++++++++++++++++++++++++
 docs/src/views.rs               | 104 +++++++++++++++++++++++++++
 docs/styles.css                 |  75 +++++++++++++++++++
 docs/watch.sh                   |  12 ++++
 9 files changed, 422 insertions(+)
 create mode 100644 docs/Cargo.toml
 create mode 100644 docs/content/getting-started.md
 create mode 100644 docs/content/index.md
 create mode 100644 docs/src/main.rs
 create mode 100644 docs/src/views.rs
 create mode 100644 docs/styles.css
 create mode 100755 docs/watch.sh

diff --git a/.gitignore b/.gitignore
index a9d37c5..9adeb4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 target
 Cargo.lock
+docs/site
diff --git a/Cargo.toml b/Cargo.toml
index 13092b4..0b4badd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,4 +7,5 @@ members = [
 ]
 exclude = [
     "benchmarks",
+    "docs",
 ]
diff --git a/docs/Cargo.toml b/docs/Cargo.toml
new file mode 100644
index 0000000..4771728
--- /dev/null
+++ b/docs/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+
+name = "docs"
+version = "0.0.0"
+authors = ["Chris Wong <lambda.fairy@gmail.com>"]
+
+license = "CC-BY-SA-4.0"
+repository = "https://github.com/lfairy/maud"
+description = "Documentation for Maud."
+
+edition = "2018"
+
+[dependencies]
+comrak = "*"
+indexmap = "*"
+maud = { path = "../maud" }
+syntect = "*"
diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md
new file mode 100644
index 0000000..86b42f7
--- /dev/null
+++ b/docs/content/getting-started.md
@@ -0,0 +1,56 @@
+# Getting started
+
+## Install nightly Rust
+
+Maud requires the nightly version of Rust.
+If you're using `rustup`,
+see the [documentation][rustup]
+for how to install this version.
+
+[rustup]: https://github.com/rust-lang/rustup.rs/blob/master/README.md#working-with-nightly-rust
+
+## Add Maud to your project
+
+Once Rust is set up,
+create a new project with Cargo:
+
+```sh
+cargo new --bin pony-greeter
+cd pony-greeter
+```
+
+Add `maud` to your `Cargo.toml`:
+
+```toml
+[dependencies]
+maud = "*"
+```
+
+Then save the following to `src/main.rs`:
+
+```rust
+#![feature(proc_macro_hygiene)]
+
+extern crate maud;
+use maud::html;
+
+fn main() {
+    let name = "Lyra";
+    let markup = html! {
+        p { "Hi, " (name) "!" }
+    };
+    println!("{}", markup.into_string());
+}
+```
+
+`html!` takes a single argument: a template using Maud's custom syntax. This call expands to an expression of type [`Markup`][Markup], which can then be converted to a `String` using `.into_string()`.
+
+[Markup]: https://docs.rs/maud/*/maud/type.Markup.html
+
+Run this program with `cargo run`, and you should get the following:
+
+```
+<p>Hi, Lyra!</p>
+```
+
+Congrats – you've written your first Maud program!
diff --git a/docs/content/index.md b/docs/content/index.md
new file mode 100644
index 0000000..9f26d61
--- /dev/null
+++ b/docs/content/index.md
@@ -0,0 +1,33 @@
+```rust
+html! {
+    h1 { "Hello, world!" }
+    p.intro {
+        "This is an example of the "
+        a href="https://github.com/lfairy/maud" { "Maud" }
+        " template language."
+    }
+}
+```
+
+Maud is an HTML [template engine] for Rust. It's implemented as a macro, `html!`, which compiles your markup to specialized Rust code. This unique approach makes Maud templates blazing fast, super type-safe, and easy to deploy.
+
+[template engine]: https://www.simple-is-better.org/template/
+
+## Tight integration with Rust
+
+Since Maud is a Rust macro, it can borrow most of its features from the host language. Pattern matching and `for` loops work as they do in Rust. There is no need to derive JSON conversions, as your templates can work with Rust values directly.
+
+## Type safety
+
+Your templates are checked by the compiler, just like the code around them. Any typos will be caught at compile time, not after your app has already started.
+
+## Minimal runtime
+
+Since most of the work happens at compile time, the runtime footprint is small. The Maud runtime library, including integration with the [Rocket] and [Actix] web frameworks, is around 100 SLoC.
+
+[Rocket]: https://rocket.rs/
+[Actix]: https://actix.rs/
+
+## Simple deployment
+
+There is no need to track separate template files, since all relevant code is linked into the final executable.
diff --git a/docs/src/main.rs b/docs/src/main.rs
new file mode 100644
index 0000000..53cd9bc
--- /dev/null
+++ b/docs/src/main.rs
@@ -0,0 +1,123 @@
+#![feature(crate_visibility_modifier)]
+#![feature(proc_macro_hygiene)]
+
+use comrak::{self, Arena, ComrakOptions};
+use comrak::nodes::{AstNode, NodeCodeBlock, NodeHeading, NodeHtmlBlock, NodeValue};
+use indexmap::IndexMap;
+use std::error::Error;
+use std::fs;
+use std::io;
+use std::mem;
+use std::path::Path;
+use std::string::FromUtf8Error;
+use syntect::parsing::SyntaxSet;
+use syntect::highlighting::{Color, ThemeSet};
+use syntect::html::highlighted_html_for_string;
+
+mod views;
+
+const BOOK_FILES: &[&str] = &[
+    "index",
+    "getting-started",
+];
+
+fn main() -> Result<(), Box<dyn Error>> {
+    fs::create_dir_all("site")?;
+
+    let arena = Arena::new();
+    let options = ComrakOptions {
+        ext_header_ids: Some("".to_string()),
+        ..ComrakOptions::default()
+    };
+
+    let mut pages = IndexMap::<&str, _>::new();
+
+    for path in BOOK_FILES {
+        let mut input_path = Path::new("content").join(path);
+        input_path.set_extension("md");
+
+        let page = load_page(&arena, &options, &input_path)?;
+
+        pages.insert(path, page);
+    }
+
+    for path in pages.keys() {
+        let mut output_path = Path::new("site").join(path);
+        output_path.set_extension("html");
+        println!("{}", output_path.display());
+        let markup = views::main(&options, path, &pages);
+        fs::write(output_path, markup.into_string())?;
+    }
+
+    fs::copy("styles.css", "site/styles.css")?;
+
+    Ok(())
+}
+
+struct Page<'a> {
+    title: Option<&'a AstNode<'a>>,
+    content: &'a AstNode<'a>,
+}
+
+fn load_page<'a>(
+    arena: &'a Arena<AstNode<'a>>,
+    options: &ComrakOptions,
+    path: &Path,
+) -> io::Result<Page<'a>> {
+    let buffer = fs::read_to_string(path)?;
+    let content = comrak::parse_document(arena, &buffer, options);
+
+    let title = content
+        .first_child()
+        .filter(|node| {
+            let mut data = node.data.borrow_mut();
+            if let NodeValue::Heading(NodeHeading { level: 1, .. }) = data.value {
+                node.detach();
+                data.value = NodeValue::Document;
+                true
+            } else {
+                false
+            }
+        });
+
+    lower_headings(content);
+    highlight_code(content)
+        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
+
+    Ok(Page { title, content })
+}
+
+fn lower_headings<'a>(root: &'a AstNode<'a>) {
+    for node in root.descendants() {
+        let mut data = node.data.borrow_mut();
+        if let NodeValue::Heading(NodeHeading { level, .. }) = &mut data.value {
+            *level += 1;
+        }
+    }
+}
+
+fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
+    let ss = SyntaxSet::load_defaults_newlines();
+    let ts = ThemeSet::load_defaults();
+    let mut theme = ts.themes["InspiredGitHub"].clone();
+    theme.settings.background = Some(Color { r: 0xee, g: 0xee, b: 0xee, a: 0xff });
+    for node in root.descendants() {
+        let mut data = node.data.borrow_mut();
+        if let NodeValue::CodeBlock(NodeCodeBlock { info, literal, ..  }) = &mut data.value {
+            let info = String::from_utf8(mem::replace(info, Vec::new()))?;
+            let syntax = ss.find_syntax_by_token(&info)
+                .unwrap_or_else(|| ss.find_syntax_plain_text());
+            let mut literal = String::from_utf8(mem::replace(literal, Vec::new()))?;
+            if !literal.ends_with('\n') {
+                // Syntect expects a trailing newline
+                literal.push('\n');
+            }
+            let html = highlighted_html_for_string(&literal, &ss, syntax, &theme);
+            data.value = NodeValue::HtmlBlock(NodeHtmlBlock {
+                block_type: 0,
+                literal: html.into_bytes(),
+            });
+        }
+    }
+    Ok(())
+}
diff --git a/docs/src/views.rs b/docs/src/views.rs
new file mode 100644
index 0000000..949299b
--- /dev/null
+++ b/docs/src/views.rs
@@ -0,0 +1,104 @@
+use comrak::{self, ComrakOptions};
+use comrak::nodes::AstNode;
+use crate::Page;
+use indexmap::IndexMap;
+use maud::{DOCTYPE, Markup, Render, html};
+use std::io;
+use std::str;
+
+struct StringWriter<'a>(&'a mut String);
+
+impl<'a> io::Write for StringWriter<'a> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        str::from_utf8(buf)
+            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
+            .map(|s| {
+                self.0.push_str(s);
+                buf.len()
+            })
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+struct Comrak<'a>(&'a AstNode<'a>, &'a ComrakOptions);
+
+impl<'a> Render for Comrak<'a> {
+    fn render_to(&self, buffer: &mut String) {
+        comrak::format_html(self.0, self.1, &mut StringWriter(buffer)).unwrap();
+    }
+}
+
+struct ComrakText<'a>(&'a AstNode<'a>, &'a ComrakOptions);
+
+impl<'a> Render for ComrakText<'a> {
+    fn render_to(&self, buffer: &mut String) {
+        comrak::format_commonmark(self.0, self.1, &mut StringWriter(buffer)).unwrap();
+    }
+}
+
+crate fn main<'a>(
+    options: &'a ComrakOptions,
+    path: &str,
+    pages: &IndexMap<&str, Page<'a>>,
+) -> Markup {
+    let page = &pages[path];
+    html! {
+        (DOCTYPE)
+        meta charset="utf-8";
+        title {
+            @if let Some(title) = page.title {
+                (ComrakText(title, options))
+                " \u{2013} "
+            }
+            "Maud, a macro for writing HTML"
+        }
+        link rel="stylesheet" href="styles.css";
+
+        header {
+            h1 {
+                a href="." {
+                    "maud"
+                }
+            }
+            p { "a macro for writing html" }
+        }
+
+        nav {
+            ul {
+                @for (other_path, other_page) in pages {
+                    @if let Some(title) = other_page.title {
+                        li {
+                            a href={ (other_path) ".html" } {
+                                (Comrak(title, options))
+                            }
+                        }
+                    }
+                }
+            }
+            ul {
+                li {
+                    a href="https://docs.rs/maud/" {
+                        "API documentation"
+                    }
+                }
+                li {
+                    a href="https://github.com/lfairy/maud" {
+                        "GitHub"
+                    }
+                }
+            }
+        }
+
+        main {
+            @if let Some(title) = page.title {
+                h2 {
+                    (Comrak(title, options))
+                }
+            }
+            (Comrak(page.content, options))
+        }
+    }
+}
diff --git a/docs/styles.css b/docs/styles.css
new file mode 100644
index 0000000..4a2c76f
--- /dev/null
+++ b/docs/styles.css
@@ -0,0 +1,75 @@
+html {
+  box-sizing: border-box;
+  font: 16px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
+}
+
+*, *::before, *::after {
+  box-sizing: inherit;
+}
+
+body {
+  margin: 0 auto;
+  padding: 2rem;
+  max-width: 40rem;
+}
+
+header {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: baseline;
+}
+
+header h1 {
+  margin: 0;
+}
+
+header p {
+  margin: 0 0 0 1rem;
+}
+
+nav ul {
+  padding: 0;
+  list-style: none;
+}
+
+pre {
+  padding: 1rem;
+  font: 15px Consolas, monospace;
+  background: #eee;
+}
+
+a:link {
+  text-decoration: none;
+}
+
+a:focus, a:hover, a:active {
+  background: #fdd;
+}
+
+@media (min-width: 60rem) {
+  body {
+    max-width: 56rem; /* 15 + 1 + 40 */
+    display: grid;
+    grid-template-columns: 15rem auto;
+    grid-gap: 1rem;
+  }
+
+  header {
+    grid-column: 1 / 3;
+    grid-row: 1;
+  }
+
+  nav {
+    grid-column: 1;
+    grid-row: 2;
+  }
+
+  main {
+    grid-column: 2;
+    grid-row: 2;
+  }
+
+  main h2:first-child {
+    margin-top: .5rem;
+  }
+}
diff --git a/docs/watch.sh b/docs/watch.sh
new file mode 100755
index 0000000..0bf2ab6
--- /dev/null
+++ b/docs/watch.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+set -e
+
+python3 -m http.server -d site &
+server_pid=$!
+trap 'kill $server_pid' EXIT
+
+while true
+do
+    find . -name '*.rs' -o -name '*.md' | entr -d cargo run
+done