Rewrite the book!

This commit is contained in:
Chris Wong 2018-11-24 16:13:36 +13:00
parent 65874efb98
commit 65849b051e
9 changed files with 422 additions and 0 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
target
Cargo.lock
docs/site

View file

@ -7,4 +7,5 @@ members = [
]
exclude = [
"benchmarks",
"docs",
]

17
docs/Cargo.toml Normal file
View file

@ -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 = "*"

View file

@ -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!

33
docs/content/index.md Normal file
View file

@ -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.

123
docs/src/main.rs Normal file
View file

@ -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(())
}

104
docs/src/views.rs Normal file
View file

@ -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))
}
}
}

75
docs/styles.css Normal file
View file

@ -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;
}
}

12
docs/watch.sh Executable file
View file

@ -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