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
|
||||
Cargo.lock
|
||||
docs/site
|
||||
|
|
|
@ -7,4 +7,5 @@ members = [
|
|||
]
|
||||
exclude = [
|
||||
"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