Rewrite the book!
This commit is contained in:
parent
65874efb98
commit
65849b051e
9 changed files with 422 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
docs/site
|
||||||
|
|
|
@ -7,4 +7,5 @@ members = [
|
||||||
]
|
]
|
||||||
exclude = [
|
exclude = [
|
||||||
"benchmarks",
|
"benchmarks",
|
||||||
|
"docs",
|
||||||
]
|
]
|
||||||
|
|
17
docs/Cargo.toml
Normal file
17
docs/Cargo.toml
Normal 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 = "*"
|
56
docs/content/getting-started.md
Normal file
56
docs/content/getting-started.md
Normal 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
33
docs/content/index.md
Normal 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
123
docs/src/main.rs
Normal 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
104
docs/src/views.rs
Normal 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
75
docs/styles.css
Normal 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
12
docs/watch.sh
Executable 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
|
Loading…
Add table
Reference in a new issue