Test documentation examples in CI ()

Closes 

Closes 
This commit is contained in:
Chris Wong 2021-01-15 17:40:46 +13:00 committed by GitHub
parent 0a0172e0fe
commit eaf552d417
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 208 additions and 82 deletions

View file

@ -26,33 +26,14 @@ jobs:
override: true
components: clippy
# Do *not* use `--all-features` here, as the optional dependencies take a
# long time to build, and will be tested in the "examples" job anyway
- name: Run tests
run: cargo test --workspace --all-targets
- name: Check Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
# Optional features (i.e. web framework integrations) take a long time to
# build and rarely break. Speed up CI by checking them separately.
all-features:
name: All features
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- name: Check build
run: cargo check --workspace --all-features --all-targets
# Please keep this in sync with `publish-docs.yml`
documentation:
name: Documentation
@ -76,6 +57,25 @@ jobs:
- name: Build documentation
run: cd docs && make -j$(nproc)
examples:
name: Examples
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- name: Doctest
run: cd doctest && cargo test
rustfmt:
name: Rustfmt
runs-on: ubuntu-latest

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
target
/Cargo.lock
docs/site
doctest/Cargo.lock
doctest/lib.rs

View file

@ -6,4 +6,5 @@ members = [
]
exclude = [
"docs",
"doctest",
]

View file

@ -12,6 +12,7 @@ enum Princess { Celestia, Luna, Cadance, TwilightSparkle }
let user = Princess::Celestia;
# let _ = maud::
html! {
@if user == Princess::Luna {
h1 { "Super secret woona to-do list" }
@ -26,12 +27,14 @@ html! {
p { "Nothing to see here; move along." }
}
}
# ;
```
`@if let` is supported as well:
```rust
let user = Some("Pinkie Pie");
# let _ = maud::
html! {
p {
"Hello, "
@ -43,6 +46,7 @@ html! {
"!"
}
}
# ;
```
## Looping with `@for`
@ -51,6 +55,7 @@ Use `@for .. in ..` to loop over the elements of an iterator.
```rust
let names = ["Applejack", "Rarity", "Fluttershy"];
# let _ = maud::
html! {
p { "My favorite ponies are:" }
ol {
@ -59,6 +64,7 @@ html! {
}
}
}
# ;
```
## Declaring variables with `@let`
@ -67,6 +73,7 @@ Declare a new variable within a template using `@let`. This can be useful when w
```rust
let names = ["Applejack", "Rarity", "Fluttershy"];
# let _ = maud::
html! {
@for name in &names {
@let first_letter = name.chars().next().unwrap();
@ -79,6 +86,7 @@ html! {
}
}
}
# ;
```
## Matching with `@match`
@ -90,6 +98,7 @@ enum Princess { Celestia, Luna, Cadance, TwilightSparkle }
let user = Princess::Celestia;
# let _ = maud::
html! {
@match user {
Princess::Luna => {
@ -106,4 +115,5 @@ html! {
_ => p { "Nothing to see here; move along." }
}
}
# ;
```

View file

@ -5,6 +5,7 @@
Write an element using curly braces:
```rust
# let _ = maud::
html! {
h1 { "Poem" }
p {
@ -12,6 +13,7 @@ html! {
" you are a rock."
}
}
# ;
```
Before version 0.18, Maud allowed the curly braces to be omitted. This syntax was [removed][#137] and now causes an error instead.
@ -23,6 +25,7 @@ Before version 0.18, Maud allowed the curly braces to be omitted. This syntax wa
Terminate a void element using a semicolon:
```rust
# let _ = maud::
html! {
link rel="stylesheet" href="poetry.css";
p {
@ -35,6 +38,7 @@ html! {
"Rock."
}
}
# ;
```
The result will be rendered with HTML syntax `<br>` not `<br />`.
@ -48,12 +52,14 @@ Maud also supports ending a void element with a slash: `br /`. This syntax is [d
Maud also supports elements and attributes with hyphens in them. This includes [custom elements], [data attributes], and [ARIA annotations].
```rust
# let _ = maud::
html! {
article data-index="12345" {
h1 { "My blog" }
tag-cloud { "pinkie pie pony cute" }
}
}
# ;
```
[custom elements]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
@ -65,6 +71,7 @@ html! {
Add attributes using the syntax: `attr="value"`. You can attach any number of attributes to an element. The values must be quoted: they are parsed as string literals.
```rust
# let _ = maud::
html! {
ul {
li {
@ -79,6 +86,7 @@ html! {
}
}
}
# ;
```
## Empty attributes: `checked`
@ -86,6 +94,7 @@ html! {
Declare an empty attribute by omitting the value.
```rust
# let _ = maud::
html! {
form {
input type="checkbox" name="cupcakes" checked;
@ -93,6 +102,7 @@ html! {
label for="cupcakes" { "Do you like cupcakes?" }
}
}
# ;
```
Before version 0.22.2, Maud required a `?` suffix on empty attributes: `checked?`. This is no longer necessary ([#238]), but still supported for backward compatibility.
@ -104,17 +114,21 @@ Before version 0.22.2, Maud required a `?` suffix on empty attributes: `checked?
Add classes and IDs to an element using `.foo` and `#bar` syntax. You can chain multiple classes and IDs together, and mix and match them with other attributes:
```rust
# let _ = maud::
html! {
input#cannon.big.scary.bright-red type="button" value="Launch Party Cannon";
}
# ;
```
The classes and IDs can be quoted. This is useful for names with numbers or symbols which otherwise wouldn't parse:
```rust
# let _ = maud::
html! {
div."col-sm-2" { "Bootstrap column!" }
}
# ;
```
## Implicit `div` elements
@ -122,10 +136,12 @@ html! {
If the element name is omitted, but there is a class or ID, then it is assumed to be a `div`.
```rust
# let _ = maud::
html! {
#main {
"Main content!"
.tip { "Storing food in a refrigerator can make it 20% cooler." }
}
}
# ;
```

View file

@ -37,7 +37,7 @@ fn main() {
Run this program with `cargo run`, and you should get the following:
```
```html
<p>Hi, Lyra!</p>
```

View file

@ -3,6 +3,7 @@
# A macro for writing HTML
```rust
# let _ = maud::
html! {
h1 { "Hello, world!" }
p.intro {
@ -11,6 +12,7 @@ html! {
" 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.

View file

@ -44,6 +44,8 @@ Using the `page` function will return the markup for the whole page.
Here's an example:
```rust
# use maud::{html, Markup};
# fn page(title: &str, greeting_box: Markup) -> Markup { greeting_box }
page("Hello!", html! {
div { "Greetings, Maud." }
});

View file

@ -11,7 +11,7 @@ Below are some examples of using `Render`. Feel free to use these snippets in yo
When writing a web page, it can be annoying to write `link rel="stylesheet"` over and over again. This example provides a shorthand for linking to CSS stylesheets.
```rust
use maud::{Markup, Render};
use maud::{html, Markup, Render};
/// Links to a CSS stylesheet at the given path.
struct Css(&'static str);
@ -32,8 +32,9 @@ When debugging an application, it can be useful to see its internal state. But t
To avoid extra allocation, we override the `.render_to()` method instead of `.render()`. This doesn't do any escaping by default, so we wrap the output in an `Escaper` as well.
```rust
use maud::{Render, Escaper};
use maud::{Escaper, html, Render};
use std::fmt;
use std::fmt::Write as _;
/// Renders the given value using its `Debug` implementation.
struct Debug<T: fmt::Debug>(T);
@ -53,9 +54,7 @@ impl<T: fmt::Debug> Render for Debug<T> {
We also use the [`ammonia`][ammonia] library, which sanitizes the resulting markup.
```rust
extern crate ammonia;
extern crate pulldown_cmark;
use ammonia;
use maud::{Markup, PreEscaped, Render};
use pulldown_cmark::{Parser, html};

View file

@ -7,6 +7,7 @@ Use `(foo)` syntax to insert the value of `foo` at runtime. Any HTML special cha
```rust
let best_pony = "Pinkie Pie";
let numbers = [1, 2, 3, 4];
# let _ = maud::
html! {
p { "Hi, " (best_pony) "!" }
p {
@ -14,11 +15,19 @@ html! {
"and the first one is " (numbers[0])
}
}
# ;
```
Arbitrary Rust code can be included in a splice by using a [block](https://doc.rust-lang.org/reference.html#block-expressions). This can be helpful for complex expressions that would be difficult to read otherwise.
```rust
# struct Foo;
# impl Foo { fn time(self) -> Bar { Bar } }
# struct Bar;
# impl Bar { fn format(self, _: &str) -> &str { "" } }
# fn something_convertible_to_foo() -> Option<Foo> { Some(Foo) }
# fn test() -> Option<()> {
# let _ = maud::
html! {
p {
({
@ -27,6 +36,9 @@ html! {
})
}
}
# ;
# Some(())
# }
```
### Splices in attributes
@ -35,22 +47,26 @@ Splices work in attributes as well.
```rust
let secret_message = "Surprise!";
# let _ = maud::
html! {
p title=(secret_message) {
"Nothing to see here, move along."
}
}
# ;
```
To concatenate multiple values within an attribute, wrap the whole thing in braces. This syntax is useful for building URLs.
```rust
const GITHUB: &'static str = "https://github.com";
# let _ = maud::
html! {
a href={ (GITHUB) "/lambda-fairy/maud" } {
"Fork me on GitHub"
}
}
# ;
```
### Splices in classes and IDs
@ -60,11 +76,13 @@ Splices can also be used in classes and IDs.
```rust
let name = "rarity";
let severity = "critical";
# let _ = maud::
html! {
aside#(name) {
p.{ "color-" (severity) } { "This is the worst! Possible! Thing!" }
}
}
# ;
```
### What can be spliced?
@ -76,10 +94,12 @@ To change this behavior for some type, you can implement the [`Render`][Render]
```rust
use maud::PreEscaped;
let post = "<p>Pre-escaped</p>";
# let _ = maud::
html! {
h1 { "My super duper blog post" }
(PreEscaped(post))
}
# ;
```
[Display]: http://doc.rust-lang.org/std/fmt/trait.Display.html
@ -94,6 +114,7 @@ This works on empty attributes:
```rust
let allow_editing = true;
# let _ = maud::
html! {
p contenteditable[allow_editing] {
"Edit me, I "
@ -101,13 +122,16 @@ html! {
" you."
}
}
# ;
```
And classes:
```rust
let cuteness = 95;
# let _ = maud::
html! {
p.cute[cuteness > 50] { "Squee!" }
}
# ;
```

View file

@ -5,9 +5,11 @@
Literal strings use the same syntax as Rust. Wrap them in double quotes, and use a backslash for escapes.
```rust
# let _ = maud::
html! {
"Oatmeal, are you crazy?"
}
# ;
```
## Raw strings
@ -15,6 +17,7 @@ html! {
If the string is long, or contains many special characters, then it may be worth using [raw strings] instead:
```rust
# let _ = maud::
html! {
pre {
r#"
@ -27,6 +30,7 @@ html! {
"#
}
}
# ;
```
[raw strings]: https://doc.rust-lang.org/reference/tokens.html#raw-string-literals
@ -37,10 +41,12 @@ By default, HTML special characters are escaped automatically. Wrap the string i
```rust
use maud::PreEscaped;
# let _ = maud::
html! {
"<script>alert(\"XSS\")</script>" // &lt;script&gt;...
(PreEscaped("<script>alert(\"XSS\")</script>")) // <script>...
}
# ;
```
## The `DOCTYPE` constant
@ -49,7 +55,9 @@ If you want to add a `<!DOCTYPE html>` declaration to your page, you may use the
```rust
use maud::DOCTYPE;
# let _ = maud::
html! {
(DOCTYPE) // <!DOCTYPE html>
}
# ;
```

View file

@ -20,23 +20,28 @@ maud = { version = "*", features = ["actix-web"] }
Actix request handlers can use a `Markup` that implements the `actix_web::Responder` trait.
```rust
```rust,no_run
use actix_web::{get, App, HttpServer, Result as AwResult};
use maud::{html, Markup};
use actix_web::{web, App, HttpServer};
use std::io;
fn index(params: web::Path<(String, u32)>) -> Markup {
html! {
h1 { "Hello " (params.0) " with id " (params.1) "!" }
}
#[get("/")]
async fn index() -> AwResult<Markup> {
Ok(html! {
html {
body {
h1 { "Hello World!" }
}
}
})
}
fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/user/{name}/{id}", web::get().to(index))
})
.bind("127.0.0.1:8080")?
.run()
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(|| App::new().service(index))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
```
@ -53,7 +58,7 @@ maud = { version = "*", features = ["iron"] }
With this feature enabled, you can then build a `Response` from a `Markup` object directly. Here's an example application using Iron and Maud:
```rust
```rust,no_run
use iron::prelude::*;
use iron::status;
use maud::html;
@ -86,7 +91,9 @@ maud = { version = "*", features = ["rocket"] }
This adds a `Responder` implementation for the `Markup` type, so you can return the result directly:
```rust
```rust,no_run
#![feature(decl_macro)]
use maud::{html, Markup};
use rocket::{get, routes};
use std::borrow::Cow;
@ -108,7 +115,7 @@ fn main() {
Unlike with the other frameworks, Rouille doesn't need any extra features at all! Calling `Response::html` on the rendered `Markup` will Just Work®.
```rust
```rust,no_run
use maud::html;
use rouille::{Response, router};
@ -116,10 +123,10 @@ fn main() {
rouille::start_server("localhost:8000", move |request| {
router!(request,
(GET) (/{name: String}) => {
html! {
Response::html(html! {
h1 { "Hello, " (name) "!" }
p { "Nice to meet you!" }
}
})
},
_ => Response::empty_404()
)

View file

@ -7,6 +7,7 @@ use std::fs::{self, File};
use std::io::{self, BufReader};
use std::mem;
use std::path::Path;
use std::str::{self, Utf8Error};
use std::string::FromUtf8Error;
use syntect::highlighting::{Color, ThemeSet};
use syntect::html::highlighted_html_for_string;
@ -102,13 +103,13 @@ fn load_page<'a>(
arena: &'a Arena<AstNode<'a>>,
options: &ComrakOptions,
path: impl AsRef<Path>,
) -> io::Result<Page<'a>> {
) -> Result<Page<'a>, Box<dyn Error>> {
let page = load_page_raw(arena, options, path)?;
lower_headings(page.content);
rewrite_md_links(page.content)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
highlight_code(page.content).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
rewrite_md_links(page.content)?;
strip_hidden_code(page.content)?;
highlight_code(page.content)?;
Ok(page)
}
@ -173,7 +174,32 @@ fn rewrite_md_links<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
Ok(())
}
fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
fn strip_hidden_code<'a>(root: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
for node in root.descendants() {
let mut data = node.data.borrow_mut();
if let NodeValue::CodeBlock(NodeCodeBlock { info, literal, .. }) = &mut data.value {
let info = parse_code_block_info(info)?;
if !info.contains(&"rust") {
continue;
}
*literal = strip_hidden_code_inner(str::from_utf8(literal)?).into_bytes();
}
}
Ok(())
}
fn strip_hidden_code_inner(literal: &str) -> String {
let lines = literal
.split("\n")
.filter(|line| {
let line = line.trim();
line != "#" && !line.starts_with("# ")
})
.collect::<Vec<_>>();
lines.join("\n")
}
fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), Box<dyn Error>> {
let ss = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let mut theme = ts.themes["InspiredGitHub"].clone();
@ -186,9 +212,11 @@ fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
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)
let info = parse_code_block_info(info)?;
let syntax = info
.into_iter()
.filter_map(|token| ss.find_syntax_by_token(&token))
.next()
.unwrap_or_else(|| ss.find_syntax_plain_text());
let mut literal = String::from_utf8(mem::replace(literal, Vec::new()))?;
if !literal.ends_with('\n') {
@ -204,6 +232,10 @@ fn highlight_code<'a>(root: &'a AstNode<'a>) -> Result<(), FromUtf8Error> {
Ok(())
}
fn parse_code_block_info(info: &[u8]) -> Result<Vec<&str>, Utf8Error> {
str::from_utf8(info).map(|info| info.split(",").map(str::trim).collect())
}
fn comrak_options() -> ComrakOptions {
let mut options = ComrakOptions::default();
options.extension.header_ids = Some("".to_string());

17
doctest/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "doctest"
version = "0.1.0"
authors = ["Chris Wong <lambda.fairy@gmail.com>"]
edition = "2018"
[dependencies]
actix-web = "3"
ammonia = "3"
iron = "0.6"
maud = { path = "../maud", features = ["actix-web", "iron", "rocket"] }
pulldown-cmark = "0.8"
rocket = "0.4"
rouille = "3"
[lib]
path = "lib.rs"

4
doctest/README.md Normal file
View file

@ -0,0 +1,4 @@
This is a placeholder package that imports the entire [book] as a doc comment.
This allows for testing the book's code snippets via `cargo test`.
[book]: ../docs/content

31
doctest/build.rs Normal file
View file

@ -0,0 +1,31 @@
use std::ffi::OsStr;
use std::fmt::Write as _;
use std::fs;
fn main() {
const DOCS_DIR: &str = "../docs/content";
// Rebuild if a chapter is added or removed
println!("cargo:rerun-if-changed={}", DOCS_DIR);
let mut buffer = r#"// Automatically @generated do not edit
#![feature(extended_key_value_attributes)]
"#.to_string();
for entry in fs::read_dir(DOCS_DIR).unwrap() {
let entry = entry.unwrap();
assert!(entry.file_type().unwrap().is_file());
let path = entry.path();
assert_eq!(path.extension(), Some(OsStr::new("md")));
let path_str = path.to_str().unwrap();
let slug_str = path.file_stem().unwrap().to_str().unwrap().replace("-", "_");
writeln!(buffer, r#"#[doc = include_str!("{}")]"#, path_str).unwrap();
writeln!(buffer, r#"mod {} {{ }}"#, slug_str).unwrap();
}
fs::write("lib.rs", buffer).unwrap();
}

View file

@ -30,7 +30,3 @@ trybuild = { version = "1.0.33", features = ["diff"] }
[package.metadata.docs.rs]
all-features = true
[[example]]
name = "actix"
required-features = ["actix-web"]

View file

@ -1,25 +0,0 @@
// do not use this line in your application
use actix_web_dep as actix_web;
use actix_web::{get, App, HttpServer, Result as AwResult};
use maud::{html, Markup};
use std::io;
#[get("/")]
async fn index() -> AwResult<Markup> {
Ok(html! {
html {
body {
h1 { "Hello World!" }
}
}
})
}
#[actix_web::main]
async fn main() -> io::Result<()> {
HttpServer::new(|| App::new().service(index))
.bind(("127.0.0.1", 8080))?
.run()
.await
}