Merge branch 'master' into add_option_remove_indentation

# Conflicts:
#	src/utils/string.rs
This commit is contained in:
Emir Salkic 2022-09-26 11:26:59 +02:00
commit 10e9cbb52b
34 changed files with 316 additions and 190 deletions

View File

@ -32,7 +32,7 @@ jobs:
- build: msrv - build: msrv
os: ubuntu-latest os: ubuntu-latest
# sync MSRV with docs: guide/src/guide/installation.md # sync MSRV with docs: guide/src/guide/installation.md
rust: 1.54.0 rust: 1.56.0
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Install Rust - name: Install Rust

View File

@ -1,5 +1,12 @@
# Changelog # Changelog
## mdBook 0.4.21
[92afe9b...8f01d02](https://github.com/rust-lang/mdBook/compare/92afe9b...8f01d02)
### Fixed
- Fixed an issue where mdBook would fail to compile with Rust nightly-2022-07-22.
[#1861](https://github.com/rust-lang/mdBook/pull/1861)
## mdBook 0.4.20 ## mdBook 0.4.20
[53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0) [53055e0...da166e0](https://github.com/rust-lang/mdBook/compare/53055e0...da166e0)

10
Cargo.lock generated
View File

@ -796,7 +796,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mdbook" name = "mdbook"
version = "0.4.20" version = "0.4.21"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -809,10 +809,10 @@ dependencies = [
"futures-util", "futures-util",
"gitignore", "gitignore",
"handlebars", "handlebars",
"lazy_static",
"log", "log",
"memchr", "memchr",
"notify", "notify",
"once_cell",
"opener", "opener",
"predicates", "predicates",
"pretty_assertions", "pretty_assertions",
@ -997,6 +997,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "once_cell"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.2.3" version = "0.2.3"

View File

@ -1,13 +1,13 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.4.20" version = "0.4.21"
authors = [ authors = [
"Mathieu David <mathieudavid@mathieudavid.org>", "Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>", "Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>" "Matt Ickstadt <mattico8@gmail.com>"
] ]
documentation = "http://rust-lang.github.io/mdBook/index.html" documentation = "http://rust-lang.github.io/mdBook/index.html"
edition = "2018" edition = "2021"
exclude = ["/guide/*"] exclude = ["/guide/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"] keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0" license = "MPL-2.0"
@ -20,9 +20,9 @@ anyhow = "1.0.28"
chrono = "0.4" chrono = "0.4"
clap = { version = "3.0", features = ["cargo"] } clap = { version = "3.0", features = ["cargo"] }
clap_complete = "3.0" clap_complete = "3.0"
once_cell = "1"
env_logger = "0.9.0" env_logger = "0.9.0"
handlebars = "4.0" handlebars = "4.0"
lazy_static = "1.0"
log = "0.4" log = "0.4"
memchr = "2.0" memchr = "2.0"
opener = "0.5" opener = "0.5"
@ -65,3 +65,7 @@ search = ["elasticlunr-rs", "ammonia"]
[[bin]] [[bin]]
doc = false doc = false
name = "mdbook" name = "mdbook"
[[example]]
name = "nop-preprocessor"
test = true

View File

@ -101,4 +101,58 @@ mod nop_lib {
renderer != "not-supported" renderer != "not-supported"
} }
} }
#[cfg(test)]
mod test {
use super::*;
#[test]
fn nop_preprocessor_run() {
let input_json = r##"[
{
"root": "/path/to/book",
"config": {
"book": {
"authors": ["AUTHOR"],
"language": "en",
"multilingual": false,
"src": "src",
"title": "TITLE"
},
"preprocessor": {
"nop": {}
}
},
"renderer": "html",
"mdbook_version": "0.4.21"
},
{
"sections": [
{
"Chapter": {
"name": "Chapter 1",
"content": "# Chapter 1\n",
"number": [1],
"sub_items": [],
"path": "chapter_1.md",
"source_path": "chapter_1.md",
"parent_names": []
}
}
],
"__non_exhaustive": null
}
]"##;
let input_json = input_json.as_bytes();
let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
let expected_book = book.clone();
let result = Nop::new().run(&ctx, book);
assert!(result.is_ok());
// The nop-preprocessor should not have made any changes to the book content.
let actual_book = result.unwrap();
assert_eq!(actual_book, expected_book);
}
}
} }

View File

@ -43,7 +43,7 @@ mdbook test path/to/book
The `--library-path` (`-L`) option allows you to add directories to the library The `--library-path` (`-L`) option allows you to add directories to the library
search path used by `rustdoc` when it builds and tests the examples. Multiple search path used by `rustdoc` when it builds and tests the examples. Multiple
directories can be specified with multiple options (`-L foo -L bar`) or with a directories can be specified with multiple options (`-L foo -L bar`) or with a
comma-delimited list (`-L foo,bar`). The path should point to the Cargo comma-delimited list (`-L foo,bar`). The path should point to the Cargo
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that [build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
contains the build output of your project. For example, if your Rust project's book is in a directory contains the build output of your project. For example, if your Rust project's book is in a directory
named `my-book`, the following command would include the crate's dependencies when running `test`: named `my-book`, the following command would include the crate's dependencies when running `test`:
@ -61,3 +61,8 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`. `book.toml`, or to `./book`.
#### --chapter
The `--chapter` (`-c`) option allows you to test a specific chapter of the
book using the chapter name or the relative path to the chapter.

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.20/mdbook-v0.4.20-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.21/mdbook-v0.4.21-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
bin/mdbook build bin/mdbook build
``` ```

View File

@ -157,7 +157,8 @@ The following configuration options are available:
Defaults to `404.md`. Defaults to `404.md`.
- **site-url:** The url where the book will be hosted. This is required to ensure - **site-url:** The url where the book will be hosted. This is required to ensure
navigation links and script/css imports in the 404 file work correctly, even when accessing navigation links and script/css imports in the 404 file work correctly, even when accessing
urls in subdirectories. Defaults to `/`. urls in subdirectories. Defaults to `/`. If `site-url` is set,
make sure to use document relative links for your assets, meaning they should not start with `/`.
- **cname:** The DNS subdomain or apex domain at which your book will be hosted. - **cname:** The DNS subdomain or apex domain at which your book will be hosted.
This string will be written to a file named CNAME in the root of your site, as This string will be written to a file named CNAME in the root of your site, as
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages

View File

@ -8,7 +8,7 @@ use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use crate::config::BuildConfig; use crate::config::BuildConfig;
use crate::errors::*; use crate::errors::*;
use crate::utils::bracket_escape; use crate::utils::bracket_escape;
use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Load a book into memory from its `src/` directory. /// Load a book into memory from its `src/` directory.

View File

@ -6,6 +6,7 @@ use super::MDBook;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use crate::theme; use crate::theme;
use log::{debug, error, info, trace};
/// A helper for setting up a new book and its directory structure. /// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

View File

@ -14,6 +14,7 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::init::BookBuilder; pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use log::{debug, error, info, log_enabled, trace, warn};
use std::io::Write; use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@ -246,6 +247,13 @@ impl MDBook {
/// Run `rustdoc` tests on the book, linking against the provided libraries. /// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// test_chapter with chapter:None will run all tests.
self.test_chapter(library_paths, None)
}
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
/// If `chapter` is `None`, all tests will be run.
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
let library_args: Vec<&str> = (0..library_paths.len()) let library_args: Vec<&str> = (0..library_paths.len())
.map(|_| "-L") .map(|_| "-L")
.zip(library_paths.into_iter()) .zip(library_paths.into_iter())
@ -254,6 +262,8 @@ impl MDBook {
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let mut chapter_found = false;
// FIXME: Is "test" the proper renderer name to use here? // FIXME: Is "test" the proper renderer name to use here?
let preprocess_context = let preprocess_context =
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string()); PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
@ -270,8 +280,16 @@ impl MDBook {
_ => continue, _ => continue,
}; };
let path = self.source_dir().join(&chapter_path); if let Some(chapter) = chapter {
info!("Testing file: {:?}", path); if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
if chapter == "?" {
info!("Skipping chapter '{}'...", ch.name);
}
continue;
}
}
chapter_found = true;
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
// write preprocessed file to tempdir // write preprocessed file to tempdir
let path = temp_dir.path().join(&chapter_path); let path = temp_dir.path().join(&chapter_path);
@ -311,6 +329,11 @@ impl MDBook {
if failed { if failed {
bail!("One or more tests failed"); bail!("One or more tests failed");
} }
if let Some(chapter) = chapter {
if !chapter_found {
bail!("Chapter not found: {}", chapter);
}
}
Ok(()) Ok(())
} }

View File

@ -1,4 +1,5 @@
use crate::errors::*; use crate::errors::*;
use log::{debug, trace, warn};
use memchr::{self, Memchr}; use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, HeadingLevel, Tag}; use pulldown_cmark::{self, Event, HeadingLevel, Tag};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -89,8 +89,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let input_404 = book let input_404 = book
.config .config
.get("output.html.input-404") .get("output.html.input-404")
.map(toml::Value::as_str) .and_then(toml::Value::as_str)
.and_then(std::convert::identity) // flatten
.map(ToString::to_string); .map(ToString::to_string);
let file_404 = get_404_output_file(&input_404); let file_404 = get_404_output_file(&input_404);

View File

@ -17,6 +17,16 @@ pub fn make_subcommand<'help>() -> App<'help> {
Relative paths are interpreted relative to the book's root directory.{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`.", If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.",
), ),
).arg(
Arg::new("chapter")
.short('c')
.long("chapter")
.value_name("chapter")
.help(
"Only test the specified chapter{n}\
Where the name of the chapter is defined in the SUMMARY.md file.{n}\
Use the special name \"?\" to the list of chapter names."
)
) )
.arg(arg!([dir] .arg(arg!([dir]
"Root directory for the book{n}\ "Root directory for the book{n}\
@ -41,14 +51,18 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
.values_of("library-path") .values_of("library-path")
.map(std::iter::Iterator::collect) .map(std::iter::Iterator::collect)
.unwrap_or_default(); .unwrap_or_default();
let chapter: Option<&str> = args.value_of("chapter");
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(&book_dir)?;
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();
} }
match chapter {
book.test(library_paths)?; Some(_) => book.test_chapter(library_paths, chapter),
None => book.test(library_paths),
}?;
Ok(()) Ok(())
} }

View File

@ -49,6 +49,7 @@
#![deny(missing_docs)] #![deny(missing_docs)]
use log::{debug, trace, warn};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
@ -295,7 +296,7 @@ impl Default for Config {
} }
} }
impl<'de> Deserialize<'de> for Config { impl<'de> serde::Deserialize<'de> for Config {
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> { fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
let raw = Value::deserialize(de)?; let raw = Value::deserialize(de)?;
@ -717,6 +718,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::fs::get_404_output_file; use crate::utils::fs::get_404_output_file;
use serde_json::json;
const COMPLEX_CONFIG: &str = r#" const COMPLEX_CONFIG: &str = r#"
[book] [book]

View File

@ -83,17 +83,6 @@
#![deny(missing_docs)] #![deny(missing_docs)]
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_json;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
pub mod book; pub mod book;
pub mod config; pub mod config;
pub mod preprocess; pub mod preprocess;

View File

@ -1,6 +1,7 @@
use super::{Preprocessor, PreprocessorContext}; use super::{Preprocessor, PreprocessorContext};
use crate::book::Book; use crate::book::Book;
use crate::errors::*; use crate::errors::*;
use log::{debug, trace, warn};
use shlex::Shlex; use shlex::Shlex;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};

View File

@ -1,10 +1,11 @@
use regex::Regex; use regex::Regex;
use std::path::Path; use std::path::Path;
use crate::errors::*;
use super::{Preprocessor, PreprocessorContext}; use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
use crate::errors::*;
use log::warn;
use once_cell::sync::Lazy;
/// A preprocessor for converting file name `README.md` to `index.md` since /// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in markdown-based documentation. /// `README.md` is the de facto index file in markdown-based documentation.
@ -67,9 +68,8 @@ fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
} }
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool { fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
lazy_static! { static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^readme$").unwrap());
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
}
RE.is_match( RE.is_match(
path.as_ref() path.as_ref()
.file_stem() .file_stem()

View File

@ -10,6 +10,8 @@ use std::path::{Path, PathBuf};
use super::{Preprocessor, PreprocessorContext}; use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
use log::{error, warn};
use once_cell::sync::Lazy;
const ESCAPE_CHAR: char = '\\'; const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10; const MAX_LINK_NESTED_DEPTH: usize = 10;
@ -434,19 +436,20 @@ impl<'a> Iterator for LinkIter<'a> {
fn find_links(contents: &str) -> LinkIter<'_> { fn find_links(contents: &str) -> LinkIter<'_> {
// lazily compute following regex // lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?; // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
lazy_static! { static RE: Lazy<Regex> = Lazy::new(|| {
static ref RE: Regex = Regex::new( Regex::new(
r"(?x) # insignificant whitespace mode r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link \\\{\{\#.*\}\} # match escaped link
| # or | # or
\{\{\s* # link opening parens and whitespace \{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9_]+) # link type \#([a-zA-Z0-9_]+) # link type
\s+ # separating whitespace \s+ # separating whitespace
([^}]+) # link target path and space separated properties ([^}]+) # link target path and space separated properties
\}\} # link closing parens" \}\} # link closing parens",
) )
.unwrap(); .unwrap()
} });
LinkIter(RE.captures_iter(contents)) LinkIter(RE.captures_iter(contents))
} }

View File

@ -12,12 +12,11 @@ use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use serde::{Deserialize, Serialize};
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// Extra information for a `Preprocessor` to give them more context when /// Extra information for a `Preprocessor` to give them more context when
/// processing a book. /// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@ -14,7 +14,10 @@ use std::path::{Path, PathBuf};
use crate::utils::fs::get_404_output_file; use crate::utils::fs::get_404_output_file;
use handlebars::Handlebars; use handlebars::Handlebars;
use log::{debug, trace, warn};
use once_cell::sync::Lazy;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use serde_json::json;
#[derive(Default)] #[derive(Default)]
pub struct HtmlHandlebars; pub struct HtmlHandlebars;
@ -764,9 +767,8 @@ fn make_data(
/// Goes through the rendered HTML, making sure all header tags have /// Goes through the rendered HTML, making sure all header tags have
/// an anchor respectively so people can link to sections directly. /// an anchor respectively so people can link to sections directly.
fn build_header_links(html: &str) -> String { fn build_header_links(html: &str) -> String {
lazy_static! { static BUILD_HEADER_LINKS: Lazy<Regex> =
static ref BUILD_HEADER_LINKS: Regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap(); Lazy::new(|| Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap());
}
let mut id_counter = HashMap::new(); let mut id_counter = HashMap::new();
@ -807,10 +809,8 @@ fn insert_link_into_header(
// ``` // ```
// This function replaces all commas by spaces in the code block classes // This function replaces all commas by spaces in the code block classes
fn fix_code_blocks(html: &str) -> String { fn fix_code_blocks(html: &str) -> String {
lazy_static! { static FIX_CODE_BLOCKS: Lazy<Regex> =
static ref FIX_CODE_BLOCKS: Regex = Lazy::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
}
FIX_CODE_BLOCKS FIX_CODE_BLOCKS
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
@ -833,10 +833,9 @@ fn add_playground_pre(
playground_config: &Playground, playground_config: &Playground,
edition: Option<RustEdition>, edition: Option<RustEdition>,
) -> String { ) -> String {
lazy_static! { static ADD_PLAYGROUND_PRE: Lazy<Regex> =
static ref ADD_PLAYGROUND_PRE: Regex = Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
}
ADD_PLAYGROUND_PRE ADD_PLAYGROUND_PRE
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1]; let text = &caps[1];
@ -899,18 +898,19 @@ fn add_playground_pre(
} }
fn hide_lines(content: &str) -> String { fn hide_lines(content: &str) -> String {
lazy_static! { static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
}
let mut result = String::with_capacity(content.len()); let mut result = String::with_capacity(content.len());
for line in content.lines() { let mut lines = content.lines().peekable();
while let Some(line) = lines.next() {
// Don't include newline on the last line.
let newline = if lines.peek().is_none() { "" } else { "\n" };
if let Some(caps) = BORING_LINES_REGEX.captures(line) { if let Some(caps) = BORING_LINES_REGEX.captures(line) {
if &caps[2] == "#" { if &caps[2] == "#" {
result += &caps[1]; result += &caps[1];
result += &caps[2]; result += &caps[2];
result += &caps[3]; result += &caps[3];
result += "\n"; result += newline;
continue; continue;
} else if &caps[2] != "!" && &caps[2] != "[" { } else if &caps[2] != "!" && &caps[2] != "[" {
result += "<span class=\"boring\">"; result += "<span class=\"boring\">";
@ -919,13 +919,13 @@ fn hide_lines(content: &str) -> String {
result += &caps[2]; result += &caps[2];
} }
result += &caps[3]; result += &caps[3];
result += "\n"; result += newline;
result += "</span>"; result += "</span>";
continue; continue;
} }
} }
result += line; result += line;
result += "\n"; result += newline;
} }
result result
} }
@ -1005,19 +1005,19 @@ mod tests {
fn add_playground() { fn add_playground() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>", ("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>", ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>", ("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>", ("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>"),
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>", ("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"), "<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>"),
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>", ("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
]; ];
for (src, should_be) in &inputs { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(
@ -1035,13 +1035,13 @@ mod tests {
fn add_playground_edition2015() { fn add_playground_edition2015() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>", ("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>", ("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>", ("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
]; ];
for (src, should_be) in &inputs { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(
@ -1059,13 +1059,13 @@ mod tests {
fn add_playground_edition2018() { fn add_playground_edition2018() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>", ("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>", ("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>", ("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
]; ];
for (src, should_be) in &inputs { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(
@ -1083,13 +1083,13 @@ mod tests {
fn add_playground_edition2021() { fn add_playground_edition2021() {
let inputs = [ let inputs = [
("<code class=\"language-rust\">x()</code>", ("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>", ("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>", ("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>", ("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
]; ];
for (src, should_be) in &inputs { for (src, should_be) in &inputs {
let got = add_playground_pre( let got = add_playground_pre(

View File

@ -4,6 +4,8 @@ use std::path::Path;
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable}; use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
use crate::utils; use crate::utils;
use log::{debug, trace};
use serde_json::json;
type StringMap = BTreeMap<String, String>; type StringMap = BTreeMap<String, String>;
@ -146,15 +148,12 @@ fn render(
trace!("Render template"); trace!("Render template");
_h.template() let t = _h
.ok_or_else(|| RenderError::new("Error with the handlebars template")) .template()
.and_then(|t| { .ok_or_else(|| RenderError::new("Error with the handlebars template"))?;
let mut local_rc = rc.clone(); let local_ctx = Context::wraps(&context)?;
let local_ctx = Context::wraps(&context)?; let mut local_rc = rc.clone();
t.render(r, &local_ctx, &mut local_rc, out) t.render(r, &local_ctx, &mut local_rc, out)
})?;
Ok(())
} }
pub fn previous( pub fn previous(

View File

@ -1,4 +1,5 @@
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError}; use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
use log::trace;
pub fn theme_option( pub fn theme_option(
h: &Helper<'_, '_>, h: &Helper<'_, '_>,

View File

@ -117,35 +117,35 @@ impl HelperDef for RenderToc {
} }
// Link // Link
let path_exists = if let Some(path) = let path_exists: bool;
item.get("path") match item.get("path") {
.and_then(|p| if p.is_empty() { None } else { Some(p) }) Some(path) if !path.is_empty() => {
{ out.write("<a href=\"")?;
out.write("<a href=\"")?; let tmp = Path::new(path)
.with_extension("html")
.to_str()
.unwrap()
// Hack for windows who tends to use `\` as separator instead of `/`
.replace('\\', "/");
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)")) // Add link
.with_extension("html") out.write(&utils::fs::path_to_root(&current_path))?;
.to_str() out.write(&tmp)?;
.unwrap() out.write("\"")?;
// Hack for windows who tends to use `\` as separator instead of `/`
.replace('\\', "/");
// Add link if path == &current_path || is_first_chapter {
out.write(&utils::fs::path_to_root(&current_path))?; is_first_chapter = false;
out.write(&tmp)?; out.write(" class=\"active\"")?;
out.write("\"")?; }
if path == &current_path || is_first_chapter { out.write(">")?;
is_first_chapter = false; path_exists = true;
out.write(" class=\"active\"")?;
} }
_ => {
out.write(">")?; out.write("<div>")?;
true path_exists = false;
} else { }
out.write("<div>")?; }
false
};
if !self.no_section_label { if !self.no_section_label {
// Section does not necessarily exist // Section does not necessarily exist

View File

@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use elasticlunr::{Index, IndexBuilder}; use elasticlunr::{Index, IndexBuilder};
use once_cell::sync::Lazy;
use pulldown_cmark::*; use pulldown_cmark::*;
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
@ -10,7 +11,7 @@ use crate::config::Search;
use crate::errors::*; use crate::errors::*;
use crate::theme::searcher; use crate::theme::searcher;
use crate::utils; use crate::utils;
use log::{debug, warn};
use serde::Serialize; use serde::Serialize;
const MAX_WORD_LENGTH_TO_INDEX: usize = 80; const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
@ -266,21 +267,19 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
} }
fn clean_html(html: &str) -> String { fn clean_html(html: &str) -> String {
lazy_static! { static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
static ref AMMONIA: ammonia::Builder<'static> = { let mut clean_content = HashSet::new();
let mut clean_content = HashSet::new(); clean_content.insert("script");
clean_content.insert("script"); clean_content.insert("style");
clean_content.insert("style"); let mut builder = ammonia::Builder::new();
let mut builder = ammonia::Builder::new(); builder
builder .tags(HashSet::new())
.tags(HashSet::new()) .tag_attributes(HashMap::new())
.tag_attributes(HashMap::new()) .generic_attributes(HashSet::new())
.generic_attributes(HashSet::new()) .link_rel(None)
.link_rel(None) .allowed_classes(HashMap::new())
.allowed_classes(HashMap::new()) .clean_content_tags(clean_content);
.clean_content_tags(clean_content); builder
builder });
};
}
AMMONIA.clean(html).to_string() AMMONIA.clean(html).to_string()
} }

View File

@ -2,7 +2,7 @@ use crate::book::BookItem;
use crate::errors::*; use crate::errors::*;
use crate::renderer::{RenderContext, Renderer}; use crate::renderer::{RenderContext, Renderer};
use crate::utils; use crate::utils;
use log::trace;
use std::fs; use std::fs;
#[derive(Default)] #[derive(Default)]

View File

@ -27,6 +27,7 @@ use std::process::{Command, Stdio};
use crate::book::Book; use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use log::{error, info, trace, warn};
use toml::Value; use toml::Value;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -51,18 +51,18 @@
{{#if mathjax_support}} {{#if mathjax_support}}
<!-- MathJax --> <!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}} {{/if}}
</head> </head>
<body> <body>
<!-- Provide site root to javascript --> <!-- Provide site root to javascript -->
<script type="text/javascript"> <script>
var path_to_root = "{{ path_to_root }}"; var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}"; var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script> </script>
<!-- Work around some values being stored in localStorage wrapped in quotes --> <!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript"> <script>
try { try {
var theme = localStorage.getItem('mdbook-theme'); var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar'); var sidebar = localStorage.getItem('mdbook-sidebar');
@ -78,7 +78,7 @@
</script> </script>
<!-- Set the theme before any content is loaded, prevents flash --> <!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript"> <script>
var theme; var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { } try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; } if (theme === null || theme === undefined) { theme = default_theme; }
@ -90,7 +90,7 @@
</script> </script>
<!-- Hide / unhide sidebar before it is displayed --> <!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript"> <script>
var html = document.querySelector('html'); var html = document.querySelector('html');
var sidebar = 'hidden'; var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) { if (document.body.clientWidth >= 1080) {
@ -171,7 +171,7 @@
{{/if}} {{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM --> <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript"> <script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible'); document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible'); document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) { Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@ -221,7 +221,7 @@
{{#if live_reload_endpoint}} {{#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>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}"; const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress); const socket = new WebSocket(wsAddress);
@ -240,7 +240,7 @@
{{#if google_analytics}} {{#if google_analytics}}
<!-- Google Analytics Tag --> <!-- Google Analytics Tag -->
<script type="text/javascript"> <script>
var localAddrs = ["localhost", "127.0.0.1", ""]; var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is // make sure we don't activate google analytics if the developer is
@ -258,43 +258,43 @@
{{/if}} {{/if}}
{{#if playground_line_numbers}} {{#if playground_line_numbers}}
<script type="text/javascript"> <script>
window.playground_line_numbers = true; window.playground_line_numbers = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_copyable}} {{#if playground_copyable}}
<script type="text/javascript"> <script>
window.playground_copyable = true; window.playground_copyable = true;
</script> </script>
{{/if}} {{/if}}
{{#if playground_js}} {{#if playground_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}ace.js" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}editor.js" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mode-rust.js" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}theme-dawn.js" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}theme-tomorrow_night.js" charset="utf-8"></script>
{{/if}} {{/if}}
{{#if search_js}} {{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}elasticlunr.min.js" charset="utf-8"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}mark.min.js" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}searcher.js" charset="utf-8"></script>
{{/if}} {{/if}}
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}clipboard.min.js" charset="utf-8"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}highlight.js" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script> <script src="{{ path_to_root }}book.js" charset="utf-8"></script>
<!-- Custom JS scripts --> <!-- Custom JS scripts -->
{{#each additional_js}} {{#each additional_js}}
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script> <script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}} {{/each}}
{{#if is_print}} {{#if is_print}}
{{#if mathjax_support}} {{#if mathjax_support}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() { MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
@ -302,7 +302,7 @@
}); });
</script> </script>
{{else}} {{else}}
<script type="text/javascript"> <script>
window.addEventListener('load', function() { window.addEventListener('load', function() {
window.setTimeout(window.print, 100); window.setTimeout(window.print, 100);
}); });

View File

@ -12,7 +12,7 @@ use std::io::Read;
use std::path::Path; use std::path::Path;
use crate::errors::*; use crate::errors::*;
use log::warn;
pub static INDEX: &[u8] = include_bytes!("index.hbs"); pub static INDEX: &[u8] = include_bytes!("index.hbs");
pub static HEAD: &[u8] = include_bytes!("head.hbs"); pub static HEAD: &[u8] = include_bytes!("head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs"); pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");

View File

@ -1,4 +1,5 @@
use crate::errors::*; use crate::errors::*;
use log::{debug, trace};
use std::convert::Into; use std::convert::Into;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;

View File

@ -4,9 +4,10 @@ pub mod fs;
mod string; mod string;
pub(crate) mod toml_ext; pub(crate) mod toml_ext;
use crate::errors::Error; use crate::errors::Error;
use regex::Regex; use log::error;
use once_cell::sync::Lazy;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use regex::Regex;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
@ -20,9 +21,7 @@ pub use self::string::{
/// Replaces multiple consecutive whitespace characters with a single space character. /// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> { pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
lazy_static! { static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s\s+").unwrap());
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
}
RE.replace_all(text, " ") RE.replace_all(text, " ")
} }
@ -51,9 +50,7 @@ pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string(); let mut content = content.to_string();
// Skip any tags or html-encoded stuff // Skip any tags or html-encoded stuff
lazy_static! { static HTML: Lazy<Regex> = Lazy::new(|| Regex::new(r"(<.*?>)").unwrap());
static ref HTML: Regex = Regex::new(r"(<.*?>)").unwrap();
}
content = HTML.replace_all(&content, "").into(); content = HTML.replace_all(&content, "").into();
const REPL_SUB: &[&str] = &["&lt;", "&gt;", "&amp;", "&#39;", "&quot;"]; const REPL_SUB: &[&str] = &["&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
for sub in REPL_SUB { for sub in REPL_SUB {
@ -96,10 +93,9 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
/// None. Ideally, print page links would link to anchors on the print page, /// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult. /// but that is very difficult.
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> { fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
lazy_static! { static SCHEME_LINK: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap(); static MD_LINK: Lazy<Regex> =
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap(); Lazy::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
}
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> { fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
if dest.starts_with('#') { if dest.starts_with('#') {
@ -152,10 +148,8 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
// There are dozens of HTML tags/attributes that contain paths, so // There are dozens of HTML tags/attributes that contain paths, so
// feel free to add more tags if desired; these are the only ones I // feel free to add more tags if desired; these are the only ones I
// care about right now. // care about right now.
lazy_static! { static HTML_LINK: Lazy<Regex> =
static ref HTML_LINK: Regex = Lazy::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap();
}
HTML_LINK HTML_LINK
.replace_all(&html, |caps: &regex::Captures<'_>| { .replace_all(&html, |caps: &regex::Captures<'_>| {

View File

@ -1,3 +1,4 @@
use once_cell::sync::Lazy;
use anyhow::Error; use anyhow::Error;
use regex::Regex; use regex::Regex;
use std::ops::Bound::{Excluded, Included, Unbounded}; use std::ops::Bound::{Excluded, Included, Unbounded};
@ -24,10 +25,10 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
} }
} }
lazy_static! { static ANCHOR_START: Lazy<Regex> =
static ref ANCHOR_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap(); Lazy::new(|| Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap());
static ref ANCHOR_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap(); static ANCHOR_END: Lazy<Regex> =
} Lazy::new(|| Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap());
/// Take anchored lines from a string. /// Take anchored lines from a string.
/// Lines containing anchor are ignored. /// Lines containing anchor are ignored.

View File

@ -10,11 +10,11 @@ fn mdbook_cli_can_correctly_test_a_passing_book() {
let mut cmd = mdbook_cmd(); 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 chapter [^:]*: "README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]intro.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]index.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]nested.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"rustdoc returned an error:\n\n"##).unwrap().not()) .stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap().not())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not()); .stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not());
} }
@ -25,10 +25,10 @@ fn mdbook_cli_detects_book_with_failing_tests() {
let mut cmd = mdbook_cmd(); 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 chapter [^:]*: "README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]intro.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]index.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing file: "([^"]+)[\\/]first[\\/]nested.md""##).unwrap()) .stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"rustdoc returned an error:\n\n"##).unwrap()) .stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap()); .stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
} }

View File

@ -24,3 +24,24 @@ fn mdbook_detects_book_with_failing_tests() {
assert!(md.test(vec![]).is_err()); assert!(md.test(vec![]).is_err());
} }
#[test]
fn mdbook_test_chapter() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
let result = md.test_chapter(vec![], Some("Introduction"));
assert!(
result.is_ok(),
"test_chapter failed with {}",
result.err().unwrap()
);
}
#[test]
fn mdbook_test_chapter_not_found() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test_chapter(vec![], Some("Bogus Chapter Name")).is_err());
}