Merge branch 'draft-no-index' of https://github.com/joshrotenberg/mdBook into draft-no-index

This commit is contained in:
josh rotenberg 2022-04-04 18:59:47 -07:00
commit 8357811d96
32 changed files with 917 additions and 317 deletions

View File

@ -31,7 +31,8 @@ jobs:
rust: stable rust: stable
- build: msrv - build: msrv
os: ubuntu-latest os: ubuntu-latest
rust: 1.46.0 # sync MSRV with docs: guide/src/guide/installation.md
rust: 1.54.0
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Install Rust - name: Install Rust
@ -48,4 +49,4 @@ jobs:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Install Rust - name: Install Rust
run: rustup update stable && rustup default stable && rustup component add rustfmt run: rustup update stable && rustup default stable && rustup component add rustfmt
- run: cargo fmt -- --check - run: cargo fmt --check

View File

@ -1,5 +1,40 @@
# Changelog # Changelog
## mdBook 0.4.17
[a5fddfa...981b79b](https://github.com/rust-lang/mdBook/compare/a5fddfa...981b79b)
### Fixed
- Fixed parsing of `output.html.print` configuration table.
[#1775](https://github.com/rust-lang/mdBook/pull/1775)
## mdBook 0.4.16
[68a5c09...a5fddfa](https://github.com/rust-lang/mdBook/compare/68a5c09...a5fddfa)
### Added
- Added `output.html.print.page-break` config option to control whether or not
there is a page break between chapters in the print output.
[#1728](https://github.com/rust-lang/mdBook/pull/1728)
- Added `output.html.playground.runnable` config option to globally disable
the run button in code blocks.
[#1546](https://github.com/rust-lang/mdBook/pull/1546)
### Changed
- The `cargo serve` live reload websocket now uses the protocol, host, and
port of the current page, allowing access through a proxy.
[#1771](https://github.com/rust-lang/mdBook/pull/1771)
- The 404 not-found page now includes the books title in the HTML title tag.
[#1693](https://github.com/rust-lang/mdBook/pull/1693)
- Migrated to clap 3.0 which which handles CLI option parsing.
[#1731](https://github.com/rust-lang/mdBook/pull/1731)
### Fixed
- Minor fixes to the markdown parser.
[#1729](https://github.com/rust-lang/mdBook/pull/1729)
- Fixed incorrect parsing in `SUMMARY.md` when it didn't start with a title.
[#1744](https://github.com/rust-lang/mdBook/pull/1744)
- Fixed duplicate anchor IDs for links in search results.
[#1749](https://github.com/rust-lang/mdBook/pull/1749)
## mdBook 0.4.15 ## mdBook 0.4.15
[5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09) [5eb7d46...68a5c09](https://github.com/rust-lang/mdBook/compare/5eb7d46...68a5c09)

79
Cargo.lock generated
View File

@ -185,17 +185,27 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.3" version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375"
dependencies = [ dependencies = [
"ansi_term",
"atty", "atty",
"bitflags", "bitflags",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim", "strsim",
"termcolor",
"textwrap", "textwrap",
"unicode-width", ]
"vec_map",
[[package]]
name = "clap_complete"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d044e9db8cd0f68191becdeb5246b7462e4cf0c069b19ae00d1bf3fa9889498d"
dependencies = [
"clap",
] ]
[[package]] [[package]]
@ -830,13 +840,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mdbook" name = "mdbook"
version = "0.4.15" version = "0.4.17"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
"chrono", "chrono",
"clap", "clap",
"clap_complete",
"elasticlunr-rs", "elasticlunr-rs",
"env_logger", "env_logger",
"futures-util", "futures-util",
@ -1053,6 +1064,15 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "output_vt100" name = "output_vt100"
version = "0.1.2" version = "0.1.2"
@ -1258,9 +1278,9 @@ dependencies = [
[[package]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.9.0" version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acd16514d1af5f7a71f909a44ef253cdb712a376d7ebc8ae4a471a9be9743548" checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"memchr", "memchr",
@ -1390,9 +1410,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.4" version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1454,21 +1474,9 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "0.11.0" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]] [[package]]
name = "serde" name = "serde"
@ -1590,9 +1598,9 @@ dependencies = [
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.8.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "strum" name = "strum"
@ -1659,12 +1667,9 @@ dependencies = [
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "time" name = "time"
@ -1872,12 +1877,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"
@ -1902,12 +1901,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.3"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.4.15" version = "0.4.17"
authors = [ authors = [
"Mathieu David <mathieudavid@mathieudavid.org>", "Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>", "Michael-F-Bryan <michaelfbryan@gmail.com>",
@ -18,15 +18,16 @@ description = "Creates a book from markdown files"
[dependencies] [dependencies]
anyhow = "1.0.28" anyhow = "1.0.28"
chrono = "0.4" chrono = "0.4"
clap = "2.24" clap = { version = "3.0", features = ["cargo"] }
clap_complete = "3.0"
env_logger = "0.7.1" env_logger = "0.7.1"
handlebars = "4.0" handlebars = "4.0"
lazy_static = "1.0" lazy_static = "1.0"
log = "0.4" log = "0.4"
memchr = "2.0" memchr = "2.0"
opener = "0.5" opener = "0.5"
pulldown-cmark = { version = "0.9", default-features = false } pulldown-cmark = { version = "0.9.1", default-features = false }
regex = "1.0.0" regex = "1.5.5"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
@ -52,7 +53,7 @@ ammonia = { version = "3", optional = true }
assert_cmd = "1" assert_cmd = "1"
predicates = "2" predicates = "2"
select = "0.5" select = "0.5"
semver = "0.11.0" semver = "1.0"
pretty_assertions = "0.6" pretty_assertions = "0.6"
walkdir = "2.0" walkdir = "2.0"

View File

@ -1,5 +1,5 @@
use crate::nop_lib::Nop; use crate::nop_lib::Nop;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches};
use mdbook::book::Book; use mdbook::book::Book;
use mdbook::errors::Error; use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext}; use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
@ -7,12 +7,12 @@ use semver::{Version, VersionReq};
use std::io; use std::io;
use std::process; use std::process;
pub fn make_app() -> App<'static, 'static> { pub fn make_app() -> App<'static> {
App::new("nop-preprocessor") App::new("nop-preprocessor")
.about("A mdbook preprocessor which does precisely nothing") .about("A mdbook preprocessor which does precisely nothing")
.subcommand( .subcommand(
SubCommand::with_name("supports") App::new("supports")
.arg(Arg::with_name("renderer").required(true)) .arg(Arg::new("renderer").required(true))
.about("Check whether a renderer is supported by this preprocessor"), .about("Check whether a renderer is supported by this preprocessor"),
) )
} }

View File

@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
```sh ```sh
mkdir bin mkdir bin
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.15/mdbook-v0.4.15-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.17/mdbook-v0.4.17-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
bin/mdbook build bin/mdbook build
``` ```

View File

@ -61,7 +61,7 @@ The `chapter.content` is just a string which happens to be markdown. While it's
entirely possible to use regular expressions or do a manual find & replace, entirely possible to use regular expressions or do a manual find & replace,
you'll probably want to process the input into something more computer-friendly. you'll probably want to process the input into something more computer-friendly.
The [`pulldown-cmark`][pc] crate implements a production-quality event-based The [`pulldown-cmark`][pc] crate implements a production-quality event-based
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to
translate events back into markdown text. translate events back into markdown text.
The following code block shows how to remove all emphasis from markdown, The following code block shows how to remove all emphasis from markdown,

View File

@ -173,10 +173,12 @@ By default, mdBook will include an icon on the top right of the book (which look
```toml ```toml
[output.html.print] [output.html.print]
enable = true # include support for printable output enable = true # include support for printable output
page-break = true # insert page-break after each chapter
``` ```
- **enable:** Enable print support. When `false`, all print support will not be - **enable:** Enable print support. When `false`, all print support will not be
rendered. Defaults to `true`. rendered. Defaults to `true`.
- **page-break** Insert page breaks between chapters. Defaults to `true`.
### `[output.html.fold]` ### `[output.html.fold]`
@ -205,6 +207,7 @@ editable = false # allows editing the source code
copyable = true # include the copy button for copying code snippets copyable = true # include the copy button for copying code snippets
copy-js = true # includes the JavaScript for the code editor copy-js = true # includes the JavaScript for the code editor
line-numbers = false # displays line numbers for editable code line-numbers = false # displays line numbers for editable code
runnable = true # displays a run button for rust code
``` ```
- **editable:** Allow editing the source code. Defaults to `false`. - **editable:** Allow editing the source code. Defaults to `false`.
@ -212,6 +215,7 @@ line-numbers = false # displays line numbers for editable code
- **copy-js:** Copy JavaScript files for the editor to the output directory. - **copy-js:** Copy JavaScript files for the editor to the output directory.
Defaults to `true`. Defaults to `true`.
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`. - **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
[Ace]: https://ace.c9.io/ [Ace]: https://ace.c9.io/

View File

@ -41,7 +41,7 @@ println!("Hello, World!");
If there is no `main` function, then the code is automatically wrapped inside one. If there is no `main` function, then the code is automatically wrapped inside one.
If you wish to disable the play button, you can include the `noplayground` option on the code block like this: If you wish to disable the play button for a code block, you can include the `noplayground` option on the code block like this:
~~~markdown ~~~markdown
```rust,noplayground ```rust,noplayground
@ -51,6 +51,13 @@ println!("Hello {}!", name);
``` ```
~~~ ~~~
Or, if you wish to disable the play button for all code blocks in your book, you can write the config to the `book.toml` like this.
```toml
[output.html.playground]
runnable = false
```
## Rust code block attributes ## Rust code block attributes
Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example: Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example:

View File

@ -97,7 +97,7 @@ So if you have images or other static files, just include them somewhere in the
Once you've written your book, you may want to host it somewhere for others to view. Once you've written your book, you may want to host it somewhere for others to view.
The first step is to build the output of the book. The first step is to build the output of the book.
This can be done with the `mbdook build` command in the same directory where the `book.toml` file is located: This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located:
```sh ```sh
mdbook build mdbook build

View File

@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`.
To build the `mdbook` executable from source, you will first need to install Rust and Cargo. To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
Follow the instructions on the [Rust installation page]. Follow the instructions on the [Rust installation page].
mdBook currently requires at least Rust version 1.46. mdBook currently requires at least Rust version 1.54.
Once you have installed Rust, the following command can be used to build and install mdBook: Once you have installed Rust, the following command can be used to build and install mdBook:

View File

@ -536,6 +536,10 @@ impl<'a> SummaryParser<'a> {
// Skip a HTML element such as a comment line. // Skip a HTML element such as a comment line.
Some(Event::Html(_)) => {} Some(Event::Html(_)) => {}
// Otherwise, no title. // Otherwise, no title.
Some(ev) => {
self.back(ev);
return None;
}
_ => return None, _ => return None,
} }
} }
@ -647,6 +651,18 @@ mod tests {
assert_eq!(got, should_be); assert_eq!(got, should_be);
} }
#[test]
fn no_initial_title() {
let src = "[Link]()";
let mut parser = SummaryParser::new(src);
assert!(parser.parse_title().is_none());
assert!(matches!(
parser.next_event(),
Some(Event::Start(Tag::Paragraph))
));
}
#[test] #[test]
fn parse_title_with_styling() { fn parse_title_with_styling() {
let src = "# My **Awesome** Summary"; let src = "# My **Awesome** Summary";

View File

@ -1,23 +1,29 @@
use crate::{get_book_dir, open}; use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
use std::path::Path; use std::path::Path;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("build") App::new("build")
.about("Builds a book from its markdown files") .about("Builds a book from its markdown files")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .long("dest-dir")
.value_name("dest-dir")
.help(
"Output directory for the book{n}\
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
),
) )
.arg_from_usage( .arg(arg!([dir]
"[dir] 'Root directory for the book{n}\ "Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'") .arg(arg!(-o --open "Opens the compiled book in a web browser"))
} }
// Build command implementation // Build command implementation

View File

@ -1,23 +1,28 @@
use crate::get_book_dir; use crate::get_book_dir;
use anyhow::Context; use anyhow::Context;
use clap::{App, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::MDBook; use mdbook::MDBook;
use std::fs; use std::fs;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("clean") App::new("clean")
.about("Deletes a built book") .about("Deletes a built book")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
Running this command deletes this directory.{n}\ .long("dest-dir")
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .value_name("dest-dir")
) .help(
.arg_from_usage( "Output directory for the book{n}\
"[dir] 'Root directory for the book{n}\ Relative paths are interpreted relative to the book's root directory.{n}\
(Defaults to the Current Directory when omitted)'", If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
),
) )
.arg(arg!([dir]
"Root directory for the book{n}\
(Defaults to the Current Directory when omitted)"
))
} }
// Clean command implementation // Clean command implementation

View File

@ -1,5 +1,5 @@
use crate::get_book_dir; use crate::get_book_dir;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::config; use mdbook::config;
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
@ -8,25 +8,25 @@ use std::io::Write;
use std::process::Command; use std::process::Command;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("init") App::new("init")
.about("Creates the boilerplate structure and files for a new book") .about("Creates the boilerplate structure and files for a new book")
// the {n} denotes a newline which will properly aligned in all help messages // the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage( .arg(arg!([dir]
"[dir] 'Directory to create the book in{n}\ "Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg_from_usage("--theme 'Copies the default theme into your source folder'") .arg(arg!(--theme "Copies the default theme into your source folder"))
.arg_from_usage("--force 'Skips confirmation prompts'") .arg(arg!(--force "Skips confirmation prompts"))
.arg( .arg(
Arg::with_name("title") Arg::new("title")
.long("title") .long("title")
.takes_value(true) .takes_value(true)
.help("Sets the book title") .help("Sets the book title")
.required(false), .required(false),
) )
.arg( .arg(
Arg::with_name("ignore") Arg::new("ignore")
.long("ignore") .long("ignore")
.takes_value(true) .takes_value(true)
.possible_values(&["none", "git"]) .possible_values(&["none", "git"])

View File

@ -1,7 +1,7 @@
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
use super::watch; use super::watch;
use crate::{get_book_dir, open}; use crate::{get_book_dir, open};
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use futures_util::sink::SinkExt; use futures_util::sink::SinkExt;
use futures_util::StreamExt; use futures_util::StreamExt;
use mdbook::errors::*; use mdbook::errors::*;
@ -18,37 +18,43 @@ use warp::Filter;
const LIVE_RELOAD_ENDPOINT: &str = "__livereload"; const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("serve") App::new("serve")
.about("Serves a book at http://localhost:3000, and rebuilds it on changes") .about("Serves a book at http://localhost:3000, and rebuilds it on changes")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg( .arg(
Arg::with_name("hostname") Arg::new("dest-dir")
.short("n") .short('d')
.long("dest-dir")
.value_name("dest-dir")
.help(
"Output directory for the book{n}\
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
),
)
.arg(arg!([dir]
"Root directory for the book{n}\
(Defaults to the Current Directory when omitted)"
))
.arg(
Arg::new("hostname")
.short('n')
.long("hostname") .long("hostname")
.takes_value(true) .takes_value(true)
.default_value("localhost") .default_value("localhost")
.empty_values(false) .forbid_empty_values(true)
.help("Hostname to listen on for HTTP connections"), .help("Hostname to listen on for HTTP connections"),
) )
.arg( .arg(
Arg::with_name("port") Arg::new("port")
.short("p") .short('p')
.long("port") .long("port")
.takes_value(true) .takes_value(true)
.default_value("3000") .default_value("3000")
.empty_values(false) .forbid_empty_values(true)
.help("Port to use for HTTP connections"), .help("Port to use for HTTP connections"),
) )
.arg_from_usage("-o, --open 'Opens the book server in a web browser'") .arg(arg!(-o --open "Opens the compiled book in a web browser"))
} }
// Serve command implementation // Serve command implementation
@ -62,11 +68,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let address = format!("{}:{}", hostname, port); let address = format!("{}:{}", hostname, port);
let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT);
let update_config = |book: &mut MDBook| { let update_config = |book: &mut MDBook| {
book.config book.config
.set("output.html.livereload-url", &livereload_url) .set("output.html.live-reload-endpoint", &LIVE_RELOAD_ENDPOINT)
.expect("livereload-url update failed"); .expect("live-reload-endpoint update failed");
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();
} }

View File

@ -1,29 +1,37 @@
use crate::get_book_dir; use crate::get_book_dir;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("test") App::new("test")
.about("Tests that a book's Rust code samples compile") .about("Tests that a book's Rust code samples compile")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .long("dest-dir")
.value_name("dest-dir")
.help(
"Output directory for the book{n}\
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
),
) )
.arg_from_usage( .arg(arg!([dir]
"[dir] 'Root directory for the book{n}\ "Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg(Arg::with_name("library-path") .arg(Arg::new("library-path")
.short("L") .short('L')
.long("library-path") .long("library-path")
.value_name("dir") .value_name("dir")
.takes_value(true) .takes_value(true)
.use_delimiter(true)
.require_delimiter(true) .require_delimiter(true)
.multiple(true) .multiple_values(true)
.empty_values(false) .multiple_occurrences(true)
.forbid_empty_values(true)
.help("A comma-separated list of directories to add to {n}the crate search path when building tests")) .help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
} }

View File

@ -1,5 +1,5 @@
use crate::{get_book_dir, open}; use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand}; use clap::{arg, App, Arg, ArgMatches};
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::utils; use mdbook::utils;
use mdbook::MDBook; use mdbook::MDBook;
@ -10,19 +10,25 @@ use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
// Create clap subcommand arguments // Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> { pub fn make_subcommand<'help>() -> App<'help> {
SubCommand::with_name("watch") App::new("watch")
.about("Watches a book's files and rebuilds it on changes") .about("Watches a book's files and rebuilds it on changes")
.arg_from_usage( .arg(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\ Arg::new("dest-dir")
Relative paths are interpreted relative to the book's root directory.{n}\ .short('d')
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'", .long("dest-dir")
.value_name("dest-dir")
.help(
"Output directory for the book{n}\
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
),
) )
.arg_from_usage( .arg(arg!([dir]
"[dir] 'Root directory for the book{n}\ "Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'", (Defaults to the Current Directory when omitted)"
) ))
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg(arg!(-o --open "Opens the compiled book in a web browser"))
} }
// Watch command implementation // Watch command implementation

View File

@ -533,14 +533,14 @@ pub struct HtmlConfig {
/// directly jumping to editing the currently viewed page. /// directly jumping to editing the currently viewed page.
/// Contains {path} that is replaced with chapter source file path /// Contains {path} that is replaced with chapter source file path
pub edit_url_template: Option<String>, pub edit_url_template: Option<String>,
/// This is used as a bit of a workaround for the `mdbook serve` command. /// Endpoint of websocket, for livereload usage. Value loaded from .toml file
/// Basically, because you set the websocket port from the command line, the /// is ignored, because our code overrides this field with the value [`LIVE_RELOAD_ENDPOINT`]
/// `mdbook serve` command needs a way to let the HTML renderer know where ///
/// to point livereloading at, if it has been enabled. /// [`LIVE_RELOAD_ENDPOINT`]: cmd::serve::LIVE_RELOAD_ENDPOINT
/// ///
/// This config item *should not be edited* by the end user. /// This config item *should not be edited* by the end user.
#[doc(hidden)] #[doc(hidden)]
pub livereload_url: Option<String>, pub live_reload_endpoint: Option<String>,
/// The mapping from old pages to new pages/URLs to use when generating /// The mapping from old pages to new pages/URLs to use when generating
/// redirects. /// redirects.
pub redirect: HashMap<String, String>, pub redirect: HashMap<String, String>,
@ -569,7 +569,7 @@ impl Default for HtmlConfig {
input_404: None, input_404: None,
site_url: None, site_url: None,
cname: None, cname: None,
livereload_url: None, live_reload_endpoint: None,
redirect: HashMap::new(), redirect: HashMap::new(),
} }
} }
@ -588,15 +588,20 @@ impl HtmlConfig {
/// Configuration for how to render the print icon, print.html, and print.css. /// Configuration for how to render the print icon, print.html, and print.css.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(default, rename_all = "kebab-case")]
pub struct Print { pub struct Print {
/// Whether print support is enabled. /// Whether print support is enabled.
pub enable: bool, pub enable: bool,
/// Insert page breaks between chapters. Default: `true`.
pub page_break: bool,
} }
impl Default for Print { impl Default for Print {
fn default() -> Self { fn default() -> Self {
Self { enable: true } Self {
enable: true,
page_break: true,
}
} }
} }
@ -625,6 +630,8 @@ pub struct Playground {
pub copy_js: bool, pub copy_js: bool,
/// Display line numbers on playground snippets. Default: `false`. /// Display line numbers on playground snippets. Default: `false`.
pub line_numbers: bool, pub line_numbers: bool,
/// Display the run button. Default: `true`
pub runnable: bool,
} }
impl Default for Playground { impl Default for Playground {
@ -634,6 +641,7 @@ impl Default for Playground {
copyable: true, copyable: true,
copy_js: true, copy_js: true,
line_numbers: false, line_numbers: false,
runnable: true,
} }
} }
} }
@ -776,6 +784,7 @@ mod tests {
copyable: true, copyable: true,
copy_js: true, copy_js: true,
line_numbers: false, line_numbers: false,
runnable: true,
}; };
let html_should_be = HtmlConfig { let html_should_be = HtmlConfig {
curly_quotes: true, curly_quotes: true,
@ -806,6 +815,22 @@ mod tests {
assert_eq!(got.html_config().unwrap(), html_should_be); assert_eq!(got.html_config().unwrap(), html_should_be);
} }
#[test]
fn disable_runnable() {
let src = r#"
[book]
title = "Some Book"
description = "book book book"
authors = ["Shogo Takata"]
[output.html.playground]
runnable = false
"#;
let got = Config::from_str(src).unwrap();
assert_eq!(got.html_config().unwrap().playground.runnable, false);
}
#[test] #[test]
fn edition_2015() { fn edition_2015() {
let src = r#" let src = r#"
@ -1150,4 +1175,24 @@ mod tests {
Config::from_str(src).unwrap(); Config::from_str(src).unwrap();
} }
#[test]
fn print_config() {
let src = r#"
[output.html.print]
enable = false
"#;
let got = Config::from_str(src).unwrap();
let html_config = got.html_config().unwrap();
assert_eq!(html_config.print.enable, false);
assert_eq!(html_config.print.page_break, true);
let src = r#"
[output.html.print]
page-break = false
"#;
let got = Config::from_str(src).unwrap();
let html_config = got.html_config().unwrap();
assert_eq!(html_config.print.enable, true);
assert_eq!(html_config.print.page_break, false);
}
} }

View File

@ -5,7 +5,8 @@ extern crate log;
use anyhow::anyhow; use anyhow::anyhow;
use chrono::Local; use chrono::Local;
use clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand}; use clap::{App, AppSettings, Arg, ArgMatches};
use clap_complete::Shell;
use env_logger::Builder; use env_logger::Builder;
use log::LevelFilter; use log::LevelFilter;
use mdbook::utils; use mdbook::utils;
@ -25,25 +26,31 @@ fn main() {
// Check which subcomamnd the user ran... // Check which subcomamnd the user ran...
let res = match app.get_matches().subcommand() { let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => cmd::init::execute(sub_matches), Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
("build", Some(sub_matches)) => cmd::build::execute(sub_matches), Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches), Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches), Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches),
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches), Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
("test", Some(sub_matches)) => cmd::test::execute(sub_matches), Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
("completions", Some(sub_matches)) => (|| { Some(("completions", sub_matches)) => (|| {
let shell: Shell = sub_matches let shell: Shell = sub_matches
.value_of("shell") .value_of("shell")
.ok_or_else(|| anyhow!("Shell name missing."))? .ok_or_else(|| anyhow!("Shell name missing."))?
.parse() .parse()
.map_err(|s| anyhow!("Invalid shell: {}", s))?; .map_err(|s| anyhow!("Invalid shell: {}", s))?;
create_clap_app().gen_completions_to("mdbook", shell, &mut std::io::stdout().lock()); let mut complete_app = create_clap_app();
clap_complete::generate(
shell,
&mut complete_app,
"mdbook",
&mut std::io::stdout().lock(),
);
Ok(()) Ok(())
})(), })(),
(_, _) => unreachable!(), _ => unreachable!(),
}; };
if let Err(e) = res { if let Err(e) = res {
@ -54,14 +61,13 @@ fn main() {
} }
/// Create a list of valid arguments and sub-commands /// Create a list of valid arguments and sub-commands
fn create_clap_app<'a, 'b>() -> App<'a, 'b> { fn create_clap_app() -> App<'static> {
let app = App::new(crate_name!()) let app = App::new(crate_name!())
.about(crate_description!()) .about(crate_description!())
.author("Mathieu David <mathieudavid@mathieudavid.org>") .author("Mathieu David <mathieudavid@mathieudavid.org>")
.version(VERSION) .version(VERSION)
.setting(AppSettings::GlobalVersion) .setting(AppSettings::PropagateVersion)
.setting(AppSettings::ArgRequiredElseHelp) .setting(AppSettings::ArgRequiredElseHelp)
.setting(AppSettings::ColoredHelp)
.after_help( .after_help(
"For more information about a specific command, try `mdbook <command> --help`\n\ "For more information about a specific command, try `mdbook <command> --help`\n\
The source code for mdBook is available at: https://github.com/rust-lang/mdBook", The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
@ -71,12 +77,12 @@ fn create_clap_app<'a, 'b>() -> App<'a, 'b> {
.subcommand(cmd::test::make_subcommand()) .subcommand(cmd::test::make_subcommand())
.subcommand(cmd::clean::make_subcommand()) .subcommand(cmd::clean::make_subcommand())
.subcommand( .subcommand(
SubCommand::with_name("completions") App::new("completions")
.about("Generate shell completions for your shell to stdout") .about("Generate shell completions for your shell to stdout")
.arg( .arg(
Arg::with_name("shell") Arg::new("shell")
.takes_value(true) .takes_value(true)
.possible_values(&Shell::variants()) .possible_values(Shell::possible_values())
.help("the shell to generate completions for") .help("the shell to generate completions for")
.value_name("SHELL") .value_name("SHELL")
.required(true), .required(true),
@ -137,3 +143,8 @@ fn open<P: AsRef<OsStr>>(path: P) {
error!("Error opening web browser: {}", e); error!("Error opening web browser: {}", e);
} }
} }
#[test]
fn verify_app() {
create_clap_app().debug_assert();
}

View File

@ -56,7 +56,7 @@ impl HtmlHandlebars {
let fixed_content = let fixed_content =
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path)); utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
if !ctx.is_index { if !ctx.is_index && ctx.html_config.print.page_break {
// Add page break between chapters // Add page break between chapters
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
// Add both two CSS properties because of the compatibility issue // Add both two CSS properties because of the compatibility issue
@ -170,6 +170,13 @@ impl HtmlHandlebars {
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly // Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
data_404.insert("path".to_owned(), json!("404.md")); data_404.insert("path".to_owned(), json!("404.md"));
data_404.insert("content".to_owned(), json!(html_content_404)); data_404.insert("content".to_owned(), json!(html_content_404));
let mut title = String::from("Page not found");
if let Some(book_title) = &ctx.config.book.title {
title.push_str(" - ");
title.push_str(book_title);
}
data_404.insert("title".to_owned(), json!(title));
let rendered = handlebars.render("index", &data_404)?; let rendered = handlebars.render("index", &data_404)?;
let rendered = let rendered =
@ -606,8 +613,11 @@ fn make_data(
if theme.favicon_svg.is_some() { if theme.favicon_svg.is_some() {
data.insert("favicon_svg".to_owned(), json!("favicon.svg")); data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
} }
if let Some(ref livereload) = html_config.livereload_url { if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
data.insert("livereload".to_owned(), json!(livereload)); data.insert(
"live_reload_endpoint".to_owned(),
json!(live_reload_endpoint),
);
} }
let default_theme = match html_config.default_theme { let default_theme = match html_config.default_theme {
@ -768,16 +778,7 @@ fn insert_link_into_header(
content: &str, content: &str,
id_counter: &mut HashMap<String, usize>, id_counter: &mut HashMap<String, usize>,
) -> String { ) -> String {
let raw_id = utils::id_from_content(content); let id = utils::unique_id_from_content(content, id_counter);
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
let id = match *id_count {
0 => raw_id,
other => format!("{}-{}", raw_id, other),
};
*id_count += 1;
format!( format!(
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##, r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
@ -828,7 +829,8 @@ fn add_playground_pre(
if classes.contains("language-rust") { if classes.contains("language-rust") {
if (!classes.contains("ignore") if (!classes.contains("ignore")
&& !classes.contains("noplayground") && !classes.contains("noplayground")
&& !classes.contains("noplaypen")) && !classes.contains("noplaypen")
&& playground_config.runnable)
|| classes.contains("mdbook-runnable") || classes.contains("mdbook-runnable")
{ {
let contains_e2015 = classes.contains("edition2015"); let contains_e2015 = classes.contains("edition2015");

View File

@ -97,6 +97,7 @@ fn render_item(
breadcrumbs.push(chapter.name.clone()); breadcrumbs.push(chapter.name.clone());
let mut id_counter = HashMap::new();
while let Some(event) = p.next() { while let Some(event) = p.next() {
match event { match event {
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
@ -120,7 +121,7 @@ fn render_item(
} }
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
in_heading = false; in_heading = false;
section_id = Some(utils::id_from_content(&heading)); section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
breadcrumbs.push(heading.clone()); breadcrumbs.push(heading.clone());
} }
Event::Start(Tag::FootnoteDefinition(name)) => { Event::Start(Tag::FootnoteDefinition(name)) => {

View File

@ -219,10 +219,12 @@
</div> </div>
{{#if livereload}} {{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) --> <!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript"> <script type="text/javascript">
var socket = new WebSocket("{{{livereload}}}"); const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) { socket.onmessage = function (event) {
if (event.data === "reload") { if (event.data === "reload") {
socket.close(); socket.close();

View File

@ -9,6 +9,7 @@ use regex::Regex;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write; use std::fmt::Write;
use std::path::Path; use std::path::Path;
@ -44,6 +45,8 @@ pub fn normalize_id(content: &str) -> String {
/// Generate an ID for use with anchors which is derived from a "normalised" /// Generate an ID for use with anchors which is derived from a "normalised"
/// string. /// string.
// This function should be made private when the deprecation expires.
#[deprecated(since = "0.4.16", note = "use unique_id_from_content instead")]
pub fn id_from_content(content: &str) -> String { pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string(); let mut content = content.to_string();
@ -59,10 +62,30 @@ pub fn id_from_content(content: &str) -> String {
// Remove spaces and hashes indicating a header // Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_start_matches('#').trim(); let trimmed = content.trim().trim_start_matches('#').trim();
normalize_id(trimmed) normalize_id(trimmed)
} }
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
///
/// Each ID returned will be unique, if the same `id_counter` is provided on
/// each call.
pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, usize>) -> String {
let id = {
#[allow(deprecated)]
id_from_content(content)
};
// If we have headers with the same normalized id, append an incrementing counter
let id_count = id_counter.entry(id.clone()).or_insert(0);
let unique_id = match *id_count {
0 => id,
id_count => format!("{}-{}", id, id_count),
};
*id_count += 1;
unique_id
}
/// Fix links to the correct location. /// Fix links to the correct location.
/// ///
/// This adjusts links, such as turning `.md` extensions to `.html`. /// This adjusts links, such as turning `.md` extensions to `.html`.
@ -332,8 +355,9 @@ more text with spaces
} }
} }
mod html_munging { #[allow(deprecated)]
use super::super::{id_from_content, normalize_id}; mod id_from_content {
use super::super::id_from_content;
#[test] #[test]
fn it_generates_anchors() { fn it_generates_anchors() {
@ -361,6 +385,10 @@ more text with spaces
); );
assert_eq!(id_from_content("## Über"), "Über"); assert_eq!(id_from_content("## Über"), "Über");
} }
}
mod html_munging {
use super::super::{normalize_id, unique_id_from_content};
#[test] #[test]
fn it_normalizes_ids() { fn it_normalizes_ids() {
@ -379,5 +407,28 @@ more text with spaces
assert_eq!(normalize_id("한국어"), "한국어"); assert_eq!(normalize_id("한국어"), "한국어");
assert_eq!(normalize_id(""), ""); assert_eq!(normalize_id(""), "");
} }
#[test]
fn it_generates_unique_ids_from_content() {
// Same id if not given shared state
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
"中文標題-cjk-title"
);
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut Default::default()),
"中文標題-cjk-title"
);
// Different id if given shared state
let mut id_counter = Default::default();
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über");
assert_eq!(
unique_id_from_content("## 中文標題 CJK title", &mut id_counter),
"中文標題-cjk-title"
);
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-1");
assert_eq!(unique_id_from_content("## Über", &mut id_counter), "Über-2");
}
} }
} }

View File

@ -1,7 +1,6 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook; use crate::dummy_book::DummyBook;
use assert_cmd::Command;
#[test] #[test]
fn mdbook_cli_dummy_book_generates_index_html() { fn mdbook_cli_dummy_book_generates_index_html() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
@ -9,7 +8,7 @@ fn mdbook_cli_dummy_book_generates_index_html() {
// doesn't exist before // doesn't exist before
assert!(!temp.path().join("book").exists()); assert!(!temp.path().join("book").exists());
let mut cmd = Command::cargo_bin("mdbook").unwrap(); let mut cmd = mdbook_cmd();
cmd.arg("build").current_dir(temp.path()); cmd.arg("build").current_dir(temp.path());
cmd.assert() cmd.assert()
.success() .success()

7
tests/cli/cmd.rs Normal file
View File

@ -0,0 +1,7 @@
use assert_cmd::Command;
pub(crate) fn mdbook_cmd() -> Command {
let mut cmd = Command::cargo_bin("mdbook").unwrap();
cmd.env_remove("RUST_LOG");
cmd
}

View File

@ -1,2 +1,3 @@
mod build; mod build;
mod cmd;
mod test; mod test;

View File

@ -1,13 +1,13 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook; use crate::dummy_book::DummyBook;
use assert_cmd::Command;
use predicates::boolean::PredicateBooleanExt; use predicates::boolean::PredicateBooleanExt;
#[test] #[test]
fn mdbook_cli_can_correctly_test_a_passing_book() { fn mdbook_cli_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap(); let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut cmd = Command::cargo_bin("mdbook").unwrap(); let mut cmd = mdbook_cmd();
cmd.arg("test").current_dir(temp.path()); cmd.arg("test").current_dir(temp.path());
cmd.assert().success() cmd.assert().success()
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap())
@ -22,7 +22,7 @@ fn mdbook_cli_can_correctly_test_a_passing_book() {
fn mdbook_cli_detects_book_with_failing_tests() { fn mdbook_cli_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap(); let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut cmd = Command::cargo_bin("mdbook").unwrap(); let mut cmd = mdbook_cmd();
cmd.arg("test").current_dir(temp.path()); cmd.arg("test").current_dir(temp.path());
cmd.assert().failure() cmd.assert().failure()
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]README.md""##).unwrap())

View File

@ -13,6 +13,7 @@
- [Markdown](first/markdown.md) - [Markdown](first/markdown.md)
- [Unicode](first/unicode.md) - [Unicode](first/unicode.md)
- [No Headers](first/no-headers.md) - [No Headers](first/no-headers.md)
- [Duplicate Headers](first/duplicate-headers.md)
- [Second Chapter](second.md) - [Second Chapter](second.md)
- [Nested Chapter](second/nested.md) - [Nested Chapter](second/nested.md)

View File

@ -0,0 +1,9 @@
# Duplicate headers
This page validates behaviour of duplicate headers.
# Header Text
# Header Text
# header-text

View File

@ -17,6 +17,7 @@ use std::ffi::OsStr;
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
use walkdir::{DirEntry, WalkDir}; use walkdir::{DirEntry, WalkDir};
@ -35,6 +36,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
"1.4. Markdown", "1.4. Markdown",
"1.5. Unicode", "1.5. Unicode",
"1.6. No Headers", "1.6. No Headers",
"1.7. Duplicate Headers",
"2.1. Nested Chapter", "2.1. Nested Chapter",
]; ];
@ -150,6 +152,25 @@ fn rendered_code_has_playground_stuff() {
assert_contains_strings(book_js, &[".playground"]); assert_contains_strings(book_js, &[".playground"]);
} }
#[test]
fn rendered_code_does_not_have_playground_stuff_in_html_when_disabled_in_config() {
let temp = DummyBook::new().build().unwrap();
let config = Config::from_str(
"
[output.html.playground]
runnable = false
",
)
.unwrap();
let md = MDBook::load_with_config(temp.path(), config).unwrap();
md.build().unwrap();
let nested = temp.path().join("book/first/nested.html");
let playground_class = vec![r#"class="playground""#];
assert_doesnt_contain_strings(nested, &playground_class);
}
#[test] #[test]
fn anchors_include_text_between_but_not_anchor_comments() { fn anchors_include_text_between_but_not_anchor_comments() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
@ -633,11 +654,12 @@ mod search {
let some_section = get_doc_ref("first/index.html#some-section"); let some_section = get_doc_ref("first/index.html#some-section");
let summary = get_doc_ref("first/includes.html#summary"); let summary = get_doc_ref("first/includes.html#summary");
let no_headers = get_doc_ref("first/no-headers.html"); let no_headers = get_doc_ref("first/no-headers.html");
let duplicate_headers_1 = get_doc_ref("first/duplicate-headers.html#header-text-1");
let conclusion = get_doc_ref("conclusion.html#conclusion"); let conclusion = get_doc_ref("conclusion.html#conclusion");
let bodyidx = &index["index"]["index"]["body"]["root"]; let bodyidx = &index["index"]["index"]["body"]["root"];
let textidx = &bodyidx["t"]["e"]["x"]["t"]; let textidx = &bodyidx["t"]["e"]["x"]["t"];
assert_eq!(textidx["df"], 2); assert_eq!(textidx["df"], 5);
assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0); assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0);
assert_eq!(textidx["docs"][&introduction]["tf"], 1.0); assert_eq!(textidx["docs"][&introduction]["tf"], 1.0);
@ -646,7 +668,7 @@ mod search {
assert_eq!(docs[&some_section]["body"], ""); assert_eq!(docs[&some_section]["body"], "");
assert_eq!( assert_eq!(
docs[&summary]["body"], docs[&summary]["body"],
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Second Chapter Nested Chapter Conclusion" "Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Second Chapter Nested Chapter Conclusion"
); );
assert_eq!( assert_eq!(
docs[&summary]["breadcrumbs"], docs[&summary]["breadcrumbs"],
@ -657,6 +679,10 @@ mod search {
docs[&no_headers]["breadcrumbs"], docs[&no_headers]["breadcrumbs"],
"First Chapter » No Headers" "First Chapter » No Headers"
); );
assert_eq!(
docs[&duplicate_headers_1]["breadcrumbs"],
"First Chapter » Duplicate Headers » Header Text"
);
assert_eq!( assert_eq!(
docs[&no_headers]["body"], docs[&no_headers]["body"],
"Capybara capybara capybara. Capybara capybara capybara." "Capybara capybara capybara. Capybara capybara capybara."

File diff suppressed because it is too large Load Diff