Merge branch 'rust-lang:master' into master

This commit is contained in:
kumavale 2023-07-10 10:05:05 +09:00 committed by GitHub
commit 8c662b750f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1533 additions and 740 deletions

View File

@ -32,9 +32,9 @@ jobs:
- build: msrv - build: msrv
os: ubuntu-20.04 os: ubuntu-20.04
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml # sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
rust: 1.60.0 rust: 1.65.0
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v3
- name: Install Rust - name: Install Rust
run: bash ci/install-rust.sh ${{ matrix.rust }} run: bash ci/install-rust.sh ${{ matrix.rust }}
- name: Build and run tests - name: Build and run tests
@ -46,7 +46,7 @@ jobs:
name: Rustfmt name: Rustfmt
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@v3
- 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,53 @@
# Changelog # Changelog
## mdBook 0.4.31
[v0.4.30...v0.4.31](https://github.com/rust-lang/mdBook/compare/v0.4.30...v0.4.31)
### Fixed
- Fixed menu border render flash during page navigation.
[#2101](https://github.com/rust-lang/mdBook/pull/2101)
- Fixed flicker setting sidebar scroll position.
[#2104](https://github.com/rust-lang/mdBook/pull/2104)
- Fixed compile error with proc-macro2 on latest Rust nightly.
[#2109](https://github.com/rust-lang/mdBook/pull/2109)
## mdBook 0.4.30
[v0.4.29...v0.4.30](https://github.com/rust-lang/mdBook/compare/v0.4.29...v0.4.30)
### Added
- Added support for heading attributes.
Attributes are specified in curly braces just after the heading text.
An HTML ID can be specified with `#` and classes with `.`.
For example: `## My heading {#custom-id .class1 .class2}`
[#2013](https://github.com/rust-lang/mdBook/pull/2013)
- Added support for hidden code lines for languages other than Rust.
The `output.html.code.hidelines` table allows you to define the prefix character that will be used to hide code lines based on the language.
[#2093](https://github.com/rust-lang/mdBook/pull/2093)
### Fixed
- Fixed a few minor markdown rendering issues.
[#2092](https://github.com/rust-lang/mdBook/pull/2092)
## mdBook 0.4.29
[v0.4.28...v0.4.29](https://github.com/rust-lang/mdBook/compare/v0.4.28...v0.4.29)
### Changed
- Built-in fonts are no longer copied when `fonts/fonts.css` is overridden in the theme directory.
Additionally, the warning about `copy-fonts` has been removed if `fonts/fonts.css` is specified.
[#2080](https://github.com/rust-lang/mdBook/pull/2080)
- `mdbook init --force` now skips all interactive prompts as intended.
[#2057](https://github.com/rust-lang/mdBook/pull/2057)
- Updated dependencies
[#2063](https://github.com/rust-lang/mdBook/pull/2063)
[#2086](https://github.com/rust-lang/mdBook/pull/2086)
[#2082](https://github.com/rust-lang/mdBook/pull/2082)
[#2084](https://github.com/rust-lang/mdBook/pull/2084)
[#2085](https://github.com/rust-lang/mdBook/pull/2085)
### Fixed
- Switched from the `gitignore` library to `ignore`. This should bring some improvements with gitignore handling.
[#2076](https://github.com/rust-lang/mdBook/pull/2076)
## mdBook 0.4.28 ## mdBook 0.4.28
[v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28) [v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28)

839
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.4.28" version = "0.4.31"
authors = [ authors = [
"Mathieu David <mathieudavid@mathieudavid.org>", "Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>", "Michael-F-Bryan <michaelfbryan@gmail.com>",
@ -14,55 +14,55 @@ license = "MPL-2.0"
readme = "README.md" readme = "README.md"
repository = "https://github.com/rust-lang/mdBook" repository = "https://github.com/rust-lang/mdBook"
description = "Creates a book from markdown files" description = "Creates a book from markdown files"
rust-version = "1.60" rust-version = "1.65"
[dependencies] [dependencies]
anyhow = "1.0.28" anyhow = "1.0.71"
chrono = { version = "0.4", default-features = false, features = ["clock"] } chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
clap = { version = "4.0.29", features = ["cargo", "wrap_help"] } clap = { version = "4.2.7", features = ["cargo", "wrap_help"] }
clap_complete = "4.0.6" clap_complete = "4.2.3"
once_cell = "1" once_cell = "1.17.1"
env_logger = "0.10.0" env_logger = "0.10.0"
handlebars = "4.0" handlebars = "4.3.7"
log = "0.4" log = "0.4.17"
memchr = "2.0" memchr = "2.5.0"
opener = "0.5" opener = "0.5.2"
pulldown-cmark = { version = "0.9.1", default-features = false } pulldown-cmark = { version = "0.9.3", default-features = false }
regex = "1.5.5" regex = "1.8.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0.96"
shlex = "1" shlex = "1.1.0"
tempfile = "3.0" tempfile = "3.4.0"
toml = "0.5.1" toml = "0.5.11"
topological-sort = "0.2.2" topological-sort = "0.2.2"
# Watch feature # Watch feature
notify = { version = "5.0.0", optional = true } notify = { version = "5.1.0", optional = true }
notify-debouncer-mini = { version = "0.2.1", optional = true } notify-debouncer-mini = { version = "0.2.1", optional = true }
gitignore = { version = "1.0", optional = true } ignore = { version = "0.4.20", optional = true }
# Serve feature # Serve feature
futures-util = { version = "0.3.4", optional = true } futures-util = { version = "0.3.28", optional = true }
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true } tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
warp = { version = "0.3.2", default-features = false, features = ["websocket"], optional = true } warp = { version = "0.3.5", default-features = false, features = ["websocket"], optional = true }
# Search feature # Search feature
elasticlunr-rs = { version = "3.0.0", optional = true } elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "3", optional = true } ammonia = { version = "3.3.0", optional = true }
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.7" assert_cmd = "2.0.11"
predicates = "2" predicates = "2.1.5"
select = "0.6.0" select = "0.6.0"
semver = "1.0" semver = "1.0.17"
pretty_assertions = "1.2.1" pretty_assertions = "1.3.0"
walkdir = "2.0" walkdir = "2.3.3"
[features] [features]
default = ["watch", "serve", "search"] default = ["watch", "serve", "search"]
watch = ["notify", "notify-debouncer-mini", "gitignore"] watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore"]
serve = ["futures-util", "tokio", "warp"] serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
search = ["elasticlunr-rs", "ammonia"] search = ["dep:elasticlunr-rs", "dep:ammonia"]
[[bin]] [[bin]]
doc = false doc = false

View File

@ -17,6 +17,9 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path
editable = true editable = true
line-numbers = true line-numbers = true
[output.html.code.hidelines]
python = "~"
[output.html.search] [output.html.search]
limit-results = 20 limit-results = 20
use-boolean-and = true use-boolean-and = true

View File

@ -67,4 +67,16 @@ mdbook init --title="my amazing book"
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book. Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
If not supplied, an interactive prompt will ask whether it should be created. If not supplied, an interactive prompt will ask whether it should be created.
```bash
mdbook init --ignore=none
```
```bash
mdbook init --ignore=git
```
[building]: build.md [building]: build.md
#### --force
Skip the prompts to create a `.gitignore` and for the title for the book.

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

View File

@ -35,7 +35,7 @@ For example, if you have a preprocessor called `mdbook-example`, then you can in
With this table, mdBook will execute the `mdbook-example` preprocessor. With this table, mdBook will execute the `mdbook-example` preprocessor.
This table can include additional key-value pairs that are specific to the preprocessor. This table can include additional key-value pairs that are specific to the preprocessor.
For example, if our example prepocessor needed some extra configuration options: For example, if our example preprocessor needed some extra configuration options:
```toml ```toml
[preprocessor.example] [preprocessor.example]

View File

@ -150,9 +150,9 @@ The following configuration options are available:
- **edit-url-template:** Edit url template, when provided shows a - **edit-url-template:** Edit url template, when provided shows a
"Suggest an edit" button (which looks like <i class="fa fa-edit"></i>) for directly jumping to editing the currently "Suggest an edit" button (which looks like <i class="fa fa-edit"></i>) for directly jumping to editing the currently
viewed page. For e.g. GitHub projects set this to viewed page. For e.g. GitHub projects set this to
`https://github.com/<owner>/<repo>/edit/master/{path}` or for `https://github.com/<owner>/<repo>/edit/<branch>/{path}` or for
Bitbucket projects set it to Bitbucket projects set it to
`https://bitbucket.org/<owner>/<repo>/src/master/{path}?mode=edit` `https://bitbucket.org/<owner>/<repo>/src/<branch>/{path}?mode=edit`
where {path} will be replaced with the full path of the file in the where {path} will be replaced with the full path of the file in the
repository. repository.
- **input-404:** The name of the markdown file used for missing files. - **input-404:** The name of the markdown file used for missing files.
@ -182,7 +182,7 @@ 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`. - **page-break:** Insert page breaks between chapters. Defaults to `true`.
### `[output.html.fold]` ### `[output.html.fold]`
@ -218,11 +218,25 @@ runnable = true # displays a run button for rust code
- **copyable:** Display the copy button on code snippets. Defaults to `true`. - **copyable:** Display the copy button on code snippets. Defaults to `true`.
- **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`. - **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/
### `[output.html.code]`
The `[output.html.code]` table provides options for controlling code blocks.
```toml
[output.html.code]
# A prefix string per language (one or more chars).
# Any line starting with whitespace+prefix is hidden.
hidelines = { python = "~" }
```
- **hidelines:** A table that defines how [hidden code lines](../mdbook.md#hiding-code-lines) work for each language.
The key is the language and the value is a string that will cause code lines starting with that prefix to be hidden.
### `[output.html.search]` ### `[output.html.search]`
The `[output.html.search]` table provides options for controlling the built-in text [search]. The `[output.html.search]` table provides options for controlling the built-in text [search].

View File

@ -124,7 +124,7 @@ mdBook has several extensions beyond the standard CommonMark specification.
### Strikethrough ### Strikethrough
Text may be rendered with a horizontal line through the center by wrapping the Text may be rendered with a horizontal line through the center by wrapping the
text with two tilde characters on each side: text with one or two tilde characters on each side:
```text ```text
An example of ~~strikethrough text~~. An example of ~~strikethrough text~~.
@ -220,3 +220,16 @@ To enable it, see the [`output.html.curly-quotes`] config option.
[tables]: https://github.github.com/gfm/#tables-extension- [tables]: https://github.github.com/gfm/#tables-extension-
[task list extension]: https://github.github.com/gfm/#task-list-items-extension- [task list extension]: https://github.github.com/gfm/#task-list-items-extension-
[`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options [`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options
### Heading attributes
Headings can have a custom HTML ID and classes. This lets you maintain the same ID even if you change the heading's text, it also lets you add multiple classes in the heading.
Example:
```md
# Example heading { #first .class1 .class2 }
```
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/specs/heading_attrs.txt).

View File

@ -2,11 +2,11 @@
## Hiding code lines ## Hiding code lines
There is a feature in mdBook that lets you hide code lines by prepending them There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
with a `#` [like you would with Rustdoc][rustdoc-hide].
This currently only works with Rust language code blocks.
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
```bash ```bash
# fn main() { # fn main() {
@ -28,7 +28,47 @@ Will render as
# } # }
``` ```
The code block has an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines. When you tap or hover the mouse over the code block, there will be an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
By default, this only works for code examples that are annotated with `rust`.
However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s):
```toml
[output.html.code.hidelines]
python = "~"
```
The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this:
```bash
~hidden()
nothidden():
~ hidden()
~hidden()
nothidden()
```
will render as
```python
~hidden()
nothidden():
~ hidden()
~hidden()
nothidden()
```
This behavior can be overridden locally with a different prefix. This has the same effect as above:
~~~markdown
```python,hidelines=!!!
!!!hidden()
nothidden():
!!! hidden()
!!!hidden()
nothidden()
```
~~~
## Rust Playground ## Rust Playground

View File

@ -1,6 +1,6 @@
# Theme # Theme
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to The default renderer uses a [handlebars](https://handlebarsjs.com) template to
render your markdown files and comes with a default theme included in the mdBook render your markdown files and comes with a default theme included in the mdBook
binary. binary.

View File

@ -77,38 +77,6 @@ the `theme` folder of your book.
Now your theme will be used instead of the default theme. Now your theme will be used instead of the default theme.
## Hiding code lines
There is a feature in mdBook that lets you hide code lines by prepending them
with a `#`.
```bash
# fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
# }
```
Will render as
```rust
# fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
# }
```
**At the moment, this only works for code examples that are annotated with
`rust`. Because it would collide with semantics of some programming languages.
In the future, we want to make this configurable through the `book.toml` so that
everyone can benefit from it.**
## Improve default theme ## Improve default theme
If you think the default theme doesn't look quite right for a specific language, If you think the default theme doesn't look quite right for a specific language,

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.60. mdBook currently requires at least Rust version 1.65.
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

@ -39,9 +39,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
.chain(summary.suffix_chapters.iter()) .chain(summary.suffix_chapters.iter())
.collect(); .collect();
while !items.is_empty() { while let Some(next) = items.pop() {
let next = items.pop().expect("already checked");
if let SummaryItem::Link(ref link) = *next { if let SummaryItem::Link(ref link) = *next {
if let Some(ref location) = link.location { if let Some(ref location) = link.location {
let filename = src_dir.join(location); let filename = src_dir.join(location);
@ -277,7 +275,7 @@ fn load_chapter<P: AsRef<Path>>(
} }
let stripped = location let stripped = location
.strip_prefix(&src_dir) .strip_prefix(src_dir)
.expect("Chapters are always inside a book"); .expect("Chapters are always inside a book");
Chapter::new(&link.name, content, stripped, parent_names.clone()) Chapter::new(&link.name, content, stripped, parent_names.clone())
@ -317,7 +315,7 @@ impl<'a> Iterator for BookItems<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let item = self.items.pop_front(); let item = self.items.pop_front();
if let Some(&BookItem::Chapter(ref ch)) = item { if let Some(BookItem::Chapter(ch)) = item {
// if we wanted a breadth-first iterator we'd `extend()` here // if we wanted a breadth-first iterator we'd `extend()` here
for sub_item in ch.sub_items.iter().rev() { for sub_item in ch.sub_items.iter().rev() {
self.items.push_front(sub_item); self.items.push_front(sub_item);

View File

@ -198,8 +198,7 @@ impl BookBuilder {
writeln!(f, "- [Chapter 1](./chapter_1.md)")?; writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
let chapter_1 = src_dir.join("chapter_1.md"); let chapter_1 = src_dir.join("chapter_1.md");
let mut f = let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
File::create(&chapter_1).with_context(|| "Unable to create chapter_1.md")?;
writeln!(f, "# Chapter 1")?; writeln!(f, "# Chapter 1")?;
} else { } else {
trace!("Existing summary found, no need to create stub files."); trace!("Existing summary found, no need to create stub files.");
@ -212,10 +211,10 @@ impl BookBuilder {
fs::create_dir_all(&self.root)?; fs::create_dir_all(&self.root)?;
let src = self.root.join(&self.config.book.src); let src = self.root.join(&self.config.book.src);
fs::create_dir_all(&src)?; fs::create_dir_all(src)?;
let build = self.root.join(&self.config.build.build_dir); let build = self.root.join(&self.config.build.build_dir);
fs::create_dir_all(&build)?; fs::create_dir_all(build)?;
Ok(()) Ok(())
} }

View File

@ -99,7 +99,7 @@ impl MDBook {
let root = book_root.into(); let root = book_root.into();
let src_dir = root.join(&config.book.src); let src_dir = root.join(&config.book.src);
let book = book::load_book(&src_dir, &config.build)?; let book = book::load_book(src_dir, &config.build)?;
let renderers = determine_renderers(&config); let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?; let preprocessors = determine_preprocessors(&config)?;
@ -122,7 +122,7 @@ impl MDBook {
let root = book_root.into(); let root = book_root.into();
let src_dir = root.join(&config.book.src); let src_dir = root.join(&config.book.src);
let book = book::load_book_from_disk(&summary, &src_dir)?; let book = book::load_book_from_disk(&summary, src_dir)?;
let renderers = determine_renderers(&config); let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?; let preprocessors = determine_preprocessors(&config)?;
@ -309,7 +309,7 @@ impl MDBook {
info!("Testing chapter '{}': {:?}", ch.name, chapter_path); 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);
let mut tmpf = utils::fs::create_file(&path)?; let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(ch.content.as_bytes())?; tmpf.write_all(ch.content.as_bytes())?;
@ -319,13 +319,13 @@ impl MDBook {
if let Some(edition) = self.config.rust.edition { if let Some(edition) = self.config.rust.edition {
match edition { match edition {
RustEdition::E2015 => { RustEdition::E2015 => {
cmd.args(&["--edition", "2015"]); cmd.args(["--edition", "2015"]);
} }
RustEdition::E2018 => { RustEdition::E2018 => {
cmd.args(&["--edition", "2018"]); cmd.args(["--edition", "2018"]);
} }
RustEdition::E2021 => { RustEdition::E2021 => {
cmd.args(&["--edition", "2021"]); cmd.args(["--edition", "2021"]);
} }
} }
} }

View File

@ -16,7 +16,7 @@ pub fn make_subcommand() -> Command {
// Build command implementation // Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
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.get_one::<PathBuf>("dest-dir") { if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.into(); book.config.build.build_dir = dest_dir.into();

View File

@ -16,7 +16,7 @@ pub fn make_subcommand() -> Command {
// Clean command implementation // Clean command implementation
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> { pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let book = MDBook::load(&book_dir)?; let book = MDBook::load(book_dir)?;
let dir_to_remove = match args.get_one::<PathBuf>("dest-dir") { let dir_to_remove = match args.get_one::<PathBuf>("dest-dir") {
Some(dest_dir) => dest_dir.into(), Some(dest_dir) => dest_dir.into(),

View File

@ -56,7 +56,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
"git" => builder.create_gitignore(true), "git" => builder.create_gitignore(true),
_ => builder.create_gitignore(false), _ => builder.create_gitignore(false),
}; };
} else { } else if !args.get_flag("force") {
println!("\nDo you want a .gitignore to be created? (y/n)"); println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() { if confirm() {
builder.create_gitignore(true); builder.create_gitignore(true);
@ -65,6 +65,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
config.book.title = if args.contains_id("title") { config.book.title = if args.contains_id("title") {
args.get_one::<String>("title").map(String::from) args.get_one::<String>("title").map(String::from)
} else if args.get_flag("force") {
None
} else { } else {
request_book_title() request_book_title()
}; };
@ -84,7 +86,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
/// Obtains author name from git config file by running the `git config` command. /// Obtains author name from git config file by running the `git config` command.
fn get_author_name() -> Option<String> { fn get_author_name() -> Option<String> {
let output = Command::new("git") let output = Command::new("git")
.args(&["config", "--get", "user.name"]) .args(["config", "--get", "user.name"])
.output() .output()
.ok()?; .ok()?;
@ -114,5 +116,5 @@ fn confirm() -> bool {
io::stdout().flush().unwrap(); io::stdout().flush().unwrap();
let mut s = String::new(); let mut s = String::new();
io::stdin().read_line(&mut s).ok(); io::stdin().read_line(&mut s).ok();
matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes") matches!(s.trim(), "Y" | "y" | "yes" | "Yes")
} }

View File

@ -48,7 +48,7 @@ pub fn make_subcommand() -> Command {
// Serve command implementation // Serve command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
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)?;
let port = args.get_one::<String>("port").unwrap(); let port = args.get_one::<String>("port").unwrap();
let hostname = args.get_one::<String>("hostname").unwrap(); let hostname = args.get_one::<String>("hostname").unwrap();
@ -102,7 +102,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
info!("Building book..."); info!("Building book...");
// FIXME: This area is really ugly because we need to re-set livereload :( // FIXME: This area is really ugly because we need to re-set livereload :(
let result = MDBook::load(&book_dir).and_then(|mut b| { let result = MDBook::load(book_dir).and_then(|mut b| {
update_config(&mut b); update_config(&mut b);
b.build() b.build()
}); });

View File

@ -44,7 +44,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str()); let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str());
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.get_one::<PathBuf>("dest-dir") { if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.to_path_buf(); book.config.build.build_dir = dest_dir.to_path_buf();

View File

@ -1,5 +1,6 @@
use super::command_prelude::*; use super::command_prelude::*;
use crate::{get_book_dir, open}; use crate::{get_book_dir, open};
use ignore::gitignore::Gitignore;
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::utils; use mdbook::utils;
use mdbook::MDBook; use mdbook::MDBook;
@ -20,7 +21,7 @@ pub fn make_subcommand() -> Command {
// Watch command implementation // Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
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)?;
let update_config = |book: &mut MDBook| { let update_config = |book: &mut MDBook| {
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") { if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
@ -41,7 +42,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
trigger_on_change(&book, |paths, book_dir| { trigger_on_change(&book, |paths, book_dir| {
info!("Files changed: {:?}\nBuilding book...\n", paths); info!("Files changed: {:?}\nBuilding book...\n", paths);
let result = MDBook::load(&book_dir).and_then(|mut b| { let result = MDBook::load(book_dir).and_then(|mut b| {
update_config(&mut b); update_config(&mut b);
b.build() b.build()
}); });
@ -62,14 +63,14 @@ fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
match find_gitignore(book_root) { match find_gitignore(book_root) {
Some(gitignore_path) => { Some(gitignore_path) => {
match gitignore::File::new(gitignore_path.as_path()) { let (ignore, err) = Gitignore::new(&gitignore_path);
Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths), if let Some(err) = err {
Err(_) => { warn!(
// We're unable to read the .gitignore file, so we'll silently allow everything. "error reading gitignore `{}`: {err}",
// Please see discussion: https://github.com/rust-lang/mdBook/pull/1051 gitignore_path.display()
paths.iter().map(|path| path.to_path_buf()).collect() );
}
} }
filter_ignored_files(ignore, paths)
} }
None => { None => {
// There is no .gitignore file. // There is no .gitignore file.
@ -85,18 +86,13 @@ fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
.find(|p| p.exists()) .find(|p| p.exists())
} }
fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> { fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
paths paths
.iter() .iter()
.filter(|path| match exclusion_checker.is_excluded(path) { .filter(|path| {
Ok(exclude) => !exclude, !ignore
Err(error) => { .matched_path_or_any_parents(path, path.is_dir())
warn!( .is_ignore()
"Unable to determine if {:?} is excluded: {:?}. Including it.",
&path, error
);
true
}
}) })
.map(|path| path.to_path_buf()) .map(|path| path.to_path_buf())
.collect() .collect()

View File

@ -308,7 +308,7 @@ impl<'de> serde::Deserialize<'de> for Config {
warn!("`description` under a table called `[book]`, move the `destination` entry"); warn!("`description` under a table called `[book]`, move the `destination` entry");
warn!("from `[output.html]`, renamed to `build-dir`, under a table called"); warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
warn!("`[build]`, and it should all work."); warn!("`[build]`, and it should all work.");
warn!("Documentation: http://rust-lang.github.io/mdBook/format/config.html"); warn!("Documentation: https://rust-lang.github.io/mdBook/format/config.html");
return Ok(Config::from_legacy(raw)); return Ok(Config::from_legacy(raw));
} }
@ -504,6 +504,8 @@ pub struct HtmlConfig {
/// Playground settings. /// Playground settings.
#[serde(alias = "playpen")] #[serde(alias = "playpen")]
pub playground: Playground, pub playground: Playground,
/// Code settings.
pub code: Code,
/// Print settings. /// Print settings.
pub print: Print, pub print: Print,
/// Don't render section labels. /// Don't render section labels.
@ -556,6 +558,7 @@ impl Default for HtmlConfig {
additional_js: Vec::new(), additional_js: Vec::new(),
fold: Fold::default(), fold: Fold::default(),
playground: Playground::default(), playground: Playground::default(),
code: Code::default(),
print: Print::default(), print: Print::default(),
no_section_label: false, no_section_label: false,
search: None, search: None,
@ -642,6 +645,22 @@ impl Default for Playground {
} }
} }
/// Configuration for tweaking how the the HTML renderer handles code blocks.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Code {
/// A prefix string to hide lines per language (one or more chars).
pub hidelines: HashMap<String, String>,
}
impl Default for Code {
fn default() -> Code {
Code {
hidelines: HashMap::new(),
}
}
}
/// Configuration of the search functionality of the HTML renderer. /// Configuration of the search functionality of the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")] #[serde(default, rename_all = "kebab-case")]
@ -703,7 +722,7 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
let mut raw = Value::try_from(&self).expect("unreachable"); let mut raw = Value::try_from(&self).expect("unreachable");
if let Ok(value) = Value::try_from(value) { if let Ok(value) = Value::try_from(value) {
let _ = raw.insert(key, value); raw.insert(key, value);
} else { } else {
return; return;
} }

View File

@ -93,7 +93,7 @@ where
for link in find_links(s) { for link in find_links(s) {
replaced.push_str(&s[previous_end_index..link.start_index]); replaced.push_str(&s[previous_end_index..link.start_index]);
match link.render_with_path(&path, chapter_title) { match link.render_with_path(path, chapter_title) {
Ok(new_content) => { Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH { if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = link.link_type.relative_path(path) { if let Some(rel_path) = link.link_type.relative_path(path) {
@ -327,7 +327,7 @@ impl<'a> Link<'a> {
let base = base.as_ref(); let base = base.as_ref();
match self.link_type { match self.link_type {
// omit the escape char // omit the escape char
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()), LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
LinkType::Include(ref pat, ref range_or_anchor) => { LinkType::Include(ref pat, ref range_or_anchor) => {
let target = base.join(pat); let target = base.join(pat);

View File

@ -1,5 +1,5 @@
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition}; use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
use crate::errors::*; use crate::errors::*;
use crate::renderer::html_handlebars::helpers; use crate::renderer::html_handlebars::helpers;
use crate::renderer::{RenderContext, Renderer}; use crate::renderer::{RenderContext, Renderer};
@ -99,7 +99,7 @@ impl HtmlHandlebars {
ctx.data.insert("title".to_owned(), json!(title)); ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert( ctx.data.insert(
"path_to_root".to_owned(), "path_to_root".to_owned(),
json!(utils::fs::path_to_root(&path)), json!(utils::fs::path_to_root(path)),
); );
if let Some(ref section) = ch.number { if let Some(ref section) = ch.number {
ctx.data ctx.data
@ -110,7 +110,12 @@ impl HtmlHandlebars {
debug!("Render template"); debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?; let rendered = ctx.handlebars.render("index", &ctx.data)?;
let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition); let rendered = self.post_process(
rendered,
&ctx.html_config.playground,
&ctx.html_config.code,
ctx.edition,
);
// Write to file // Write to file
debug!("Creating {}", filepath.display()); debug!("Creating {}", filepath.display());
@ -121,8 +126,12 @@ impl HtmlHandlebars {
ctx.data.insert("path_to_root".to_owned(), json!("")); ctx.data.insert("path_to_root".to_owned(), json!(""));
ctx.data.insert("is_index".to_owned(), json!(true)); ctx.data.insert("is_index".to_owned(), json!(true));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?; let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index = let rendered_index = self.post_process(
self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition); rendered_index,
&ctx.html_config.playground,
&ctx.html_config.code,
ctx.edition,
);
debug!("Creating index.html from {}", ctx_path); debug!("Creating index.html from {}", ctx_path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
} }
@ -182,8 +191,12 @@ impl HtmlHandlebars {
data_404.insert("title".to_owned(), json!(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 = self.post_process(
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); rendered,
&html_config.playground,
&html_config.code,
ctx.config.rust.edition,
);
let output_file = get_404_output_file(&html_config.input_404); let output_file = get_404_output_file(&html_config.input_404);
utils::fs::write_file(destination, output_file, rendered.as_bytes())?; utils::fs::write_file(destination, output_file, rendered.as_bytes())?;
debug!("Creating 404.html ✓"); debug!("Creating 404.html ✓");
@ -195,11 +208,13 @@ impl HtmlHandlebars {
&self, &self,
rendered: String, rendered: String,
playground_config: &Playground, playground_config: &Playground,
code_config: &Code,
edition: Option<RustEdition>, edition: Option<RustEdition>,
) -> String { ) -> String {
let rendered = build_header_links(&rendered); let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered); let rendered = fix_code_blocks(&rendered);
let rendered = add_playground_pre(&rendered, playground_config, edition); let rendered = add_playground_pre(&rendered, playground_config, edition);
let rendered = hide_lines(&rendered, code_config);
rendered rendered
} }
@ -275,7 +290,8 @@ impl HtmlHandlebars {
"FontAwesome/fonts/FontAwesome.ttf", "FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF, theme::FONT_AWESOME_TTF,
)?; )?;
if html_config.copy_fonts { // Don't copy the stock fonts if the user has specified their own fonts to use.
if html_config.copy_fonts && theme.fonts_css.is_none() {
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?; write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
for (file_name, contents) in theme::fonts::LICENSES.iter() { for (file_name, contents) in theme::fonts::LICENSES.iter() {
write_file(destination, file_name, contents)?; write_file(destination, file_name, contents)?;
@ -291,20 +307,13 @@ impl HtmlHandlebars {
} }
if let Some(fonts_css) = &theme.fonts_css { if let Some(fonts_css) = &theme.fonts_css {
if !fonts_css.is_empty() { if !fonts_css.is_empty() {
if html_config.copy_fonts { write_file(destination, "fonts/fonts.css", fonts_css)?;
warn!(
"output.html.copy_fonts is deprecated.\n\
Set copy_fonts=false and ensure the fonts you want are in \
the `theme/fonts/` directory."
);
}
write_file(destination, "fonts/fonts.css", &fonts_css)?;
} }
} }
if !html_config.copy_fonts && theme.fonts_css.is_none() { if !html_config.copy_fonts && theme.fonts_css.is_none() {
warn!( warn!(
"output.html.copy_fonts is deprecated.\n\ "output.html.copy-fonts is deprecated.\n\
This book appears to have copy_fonts=false without a fonts.css file.\n\ This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
Add an empty `theme/fonts/fonts.css` file to squelch this warning." Add an empty `theme/fonts/fonts.css` file to squelch this warning."
); );
} }
@ -553,7 +562,7 @@ impl Renderer for HtmlHandlebars {
// Print version // Print version
let mut print_content = String::new(); let mut print_content = String::new();
fs::create_dir_all(&destination) fs::create_dir_all(destination)
.with_context(|| "Unexpected error when constructing destination path")?; .with_context(|| "Unexpected error when constructing destination path")?;
let mut is_index = true; let mut is_index = true;
@ -589,8 +598,12 @@ impl Renderer for HtmlHandlebars {
debug!("Render template"); debug!("Render template");
let rendered = handlebars.render("index", &data)?; let rendered = handlebars.render("index", &data)?;
let rendered = let rendered = self.post_process(
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); rendered,
&html_config.playground,
&html_config.code,
ctx.config.rust.edition,
);
utils::fs::write_file(destination, "print.html", rendered.as_bytes())?; utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
debug!("Creating print.html ✓"); debug!("Creating print.html ✓");
@ -795,8 +808,10 @@ 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 {
static BUILD_HEADER_LINKS: Lazy<Regex> = static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
Lazy::new(|| Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap()); Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
});
static IGNORE_CLASS: &[&str] = &["menu-title"];
let mut id_counter = HashMap::new(); let mut id_counter = HashMap::new();
@ -806,7 +821,22 @@ fn build_header_links(html: &str) -> String {
.parse() .parse()
.expect("Regex should ensure we only ever get numbers here"); .expect("Regex should ensure we only ever get numbers here");
insert_link_into_header(level, &caps[2], &mut id_counter) // Ignore .menu-title because now it's getting detected by the regex.
if let Some(classes) = caps.get(3) {
for class in classes.as_str().split(" ") {
if IGNORE_CLASS.contains(&class) {
return caps[0].to_string();
}
}
}
insert_link_into_header(
level,
&caps[4],
caps.get(2).map(|x| x.as_str().to_string()),
caps.get(3).map(|x| x.as_str().to_string()),
&mut id_counter,
)
}) })
.into_owned() .into_owned()
} }
@ -816,15 +846,21 @@ fn build_header_links(html: &str) -> String {
fn insert_link_into_header( fn insert_link_into_header(
level: usize, level: usize,
content: &str, content: &str,
id: Option<String>,
classes: Option<String>,
id_counter: &mut HashMap<String, usize>, id_counter: &mut HashMap<String, usize>,
) -> String { ) -> String {
let id = utils::unique_id_from_content(content, id_counter); let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
let classes = classes
.map(|s| format!(" class=\"{s}\""))
.unwrap_or_default();
format!( format!(
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##, r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{text}</a></h{level}>"##,
level = level, level = level,
id = id, id = id,
text = content text = content,
classes = classes
) )
} }
@ -856,67 +892,64 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned() .into_owned()
} }
static CODE_BLOCK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
fn add_playground_pre( fn add_playground_pre(
html: &str, html: &str,
playground_config: &Playground, playground_config: &Playground,
edition: Option<RustEdition>, edition: Option<RustEdition>,
) -> String { ) -> String {
static ADD_PLAYGROUND_PRE: Lazy<Regex> = CODE_BLOCK_RE
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
ADD_PLAYGROUND_PRE
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1]; let text = &caps[1];
let classes = &caps[2]; let classes = &caps[2];
let code = &caps[3]; let code = &caps[3];
if classes.contains("language-rust") { if classes.contains("language-rust")
if (!classes.contains("ignore") && ((!classes.contains("ignore")
&& !classes.contains("noplayground") && !classes.contains("noplayground")
&& !classes.contains("noplaypen") && !classes.contains("noplaypen")
&& playground_config.runnable) && playground_config.runnable)
|| classes.contains("mdbook-runnable") || classes.contains("mdbook-runnable"))
{ {
let contains_e2015 = classes.contains("edition2015"); let contains_e2015 = classes.contains("edition2015");
let contains_e2018 = classes.contains("edition2018"); let contains_e2018 = classes.contains("edition2018");
let contains_e2021 = classes.contains("edition2021"); let contains_e2021 = classes.contains("edition2021");
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 { let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
// the user forced edition, we should not overwrite it // the user forced edition, we should not overwrite it
"" ""
} else {
match edition {
Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018",
Some(RustEdition::E2021) => " edition2021",
None => "",
}
};
// wrap the contents in an external pre block
format!(
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
classes,
edition_class,
{
let content: Cow<'_, str> = if playground_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
.into()
};
hide_lines(&content)
}
)
} else { } else {
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code)) match edition {
} Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018",
Some(RustEdition::E2021) => " edition2021",
None => "",
}
};
// wrap the contents in an external pre block
format!(
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
classes,
edition_class,
{
let content: Cow<'_, str> = if playground_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
.into()
};
content
}
)
} else { } else {
// not language-rust, so no-op // not language-rust, so no-op
text.to_owned() text.to_owned()
@ -925,7 +958,50 @@ fn add_playground_pre(
.into_owned() .into_owned()
} }
fn hide_lines(content: &str) -> String { /// Modifies all `<code>` blocks to convert "hidden" lines and to wrap them in
/// a `<span class="boring">`.
fn hide_lines(html: &str, code_config: &Code) -> String {
let language_regex = Regex::new(r"\blanguage-(\w+)\b").unwrap();
let hidelines_regex = Regex::new(r"\bhidelines=(\S+)").unwrap();
CODE_BLOCK_RE
.replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
if classes.contains("language-rust") {
format!(
"<code class=\"{}\">{}</code>",
classes,
hide_lines_rust(code)
)
} else {
// First try to get the prefix from the code block
let hidelines_capture = hidelines_regex.captures(classes);
let hidelines_prefix = match &hidelines_capture {
Some(capture) => Some(&capture[1]),
None => {
// Then look up the prefix by language
language_regex.captures(classes).and_then(|capture| {
code_config.hidelines.get(&capture[1]).map(|p| p.as_str())
})
}
};
match hidelines_prefix {
Some(prefix) => format!(
"<code class=\"{}\">{}</code>",
classes,
hide_lines_with_prefix(code, prefix)
),
None => text.to_owned(),
}
}
})
.into_owned()
}
fn hide_lines_rust(content: &str) -> String {
static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap()); static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
let mut result = String::with_capacity(content.len()); let mut result = String::with_capacity(content.len());
@ -958,6 +1034,26 @@ fn hide_lines(content: &str) -> String {
result result
} }
fn hide_lines_with_prefix(content: &str, prefix: &str) -> String {
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if line.trim_start().starts_with(prefix) {
let pos = line.find(prefix).unwrap();
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
result += "<span class=\"boring\">";
result += ws;
result += rest;
result += "\n";
result += "</span>";
continue;
}
result += line;
result += "\n";
}
result
}
fn partition_source(s: &str) -> (String, String) { fn partition_source(s: &str) -> (String, String) {
let mut after_header = false; let mut after_header = false;
let mut before = String::new(); let mut before = String::new();
@ -993,6 +1089,7 @@ struct RenderItemContext<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq;
#[test] #[test]
fn original_build_header_links() { fn original_build_header_links() {
@ -1021,6 +1118,21 @@ mod tests {
"<h1>Foo</h1><h3>Foo</h3>", "<h1>Foo</h1><h3>Foo</h3>",
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##, r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
), ),
// id only
(
r##"<h1 id="foobar">Foo</h1>"##,
r##"<h1 id="foobar"><a class="header" href="#foobar">Foo</a></h1>"##,
),
// class only
(
r##"<h1 class="class1 class2">Foo</h1>"##,
r##"<h1 id="foo" class="class1 class2"><a class="header" href="#foo">Foo</a></h1>"##,
),
// both id and class
(
r##"<h1 id="foobar" class="class1 class2">Foo</h1>"##,
r##"<h1 id="foobar" class="class1 class2"><a class="header" href="#foobar">Foo</a></h1>"##,
),
]; ];
for (src, should_be) in inputs { for (src, should_be) in inputs {
@ -1033,17 +1145,17 @@ 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\">}</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n#fn main() {\nx()\n#}</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() {}</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>\";</code></pre>"),
("<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\";</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\";</code>",
"<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>\";</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</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>\";</code>"), "<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</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]</code></pre>"), "<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
]; ];
@ -1063,7 +1175,7 @@ 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\">}</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n#fn main() {\nx()\n#}</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() {}</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>",
@ -1087,7 +1199,7 @@ 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\">}</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n#fn main() {\nx()\n#}</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() {}</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>",
@ -1111,7 +1223,7 @@ 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\">}</span></code></pre>"), "<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n#fn main() {\nx()\n#}</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() {}</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>",
@ -1131,4 +1243,60 @@ mod tests {
assert_eq!(&*got, *should_be); assert_eq!(&*got, *should_be);
} }
} }
#[test]
fn hide_lines_language_rust() {
let inputs = [
(
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",),
(
"<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<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<span class=\"boring\"> bar\n</span>\";</code>",),
(
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</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 {
let got = hide_lines(src, &Code::default());
assert_eq!(&*got, *should_be);
}
}
#[test]
fn hide_lines_language_other() {
let inputs = [
(
"<code class=\"language-python\">~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()</code>",
"<code class=\"language-python\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
(
"<code class=\"language-python hidelines=!!!\">!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()</code>",
"<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
];
for (src, should_be) in &inputs {
let got = hide_lines(
src,
&Code {
hidelines: {
let mut map = HashMap::new();
map.insert("python".to_string(), "~".to_string());
map
},
},
);
assert_eq!(&*got, *should_be);
}
}
} }

View File

@ -127,7 +127,7 @@ fn render(
context.insert( context.insert(
"path_to_root".to_owned(), "path_to_root".to_owned(),
json!(utils::fs::path_to_root(&base_path)), json!(utils::fs::path_to_root(base_path)),
); );
chapter chapter

View File

@ -138,9 +138,11 @@ fn render_item(
in_heading = true; in_heading = true;
} }
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { Event::End(Tag::Heading(i, id, _classes)) if i as u32 <= max_section_depth => {
in_heading = false; in_heading = false;
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter)); section_id = id
.map(|id| id.to_string())
.or_else(|| 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

@ -37,14 +37,14 @@ impl Renderer for MarkdownRenderer {
if !ch.is_draft_chapter() { if !ch.is_draft_chapter() {
utils::fs::write_file( utils::fs::write_file(
&ctx.destination, &ctx.destination,
&ch.path.as_ref().expect("Checked path exists before"), ch.path.as_ref().expect("Checked path exists before"),
ch.content.as_bytes(), ch.content.as_bytes(),
)?; )?;
} }
} }
} }
fs::create_dir_all(&destination) fs::create_dir_all(destination)
.with_context(|| "Unexpected error when constructing destination path")?; .with_context(|| "Unexpected error when constructing destination path")?;
Ok(()) Ok(())

View File

@ -68,7 +68,7 @@ function playground_text(playground, hidden = true) {
} }
// updates the visibility of play button based on `no_run` class and // updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org // used crates vs ones available on https://play.rust-lang.org
function update_play_button(pre_block, playground_crates) { function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.querySelector(".play-button"); var play_button = pre_block.querySelector(".play-button");
@ -179,7 +179,7 @@ function playground_text(playground, hidden = true) {
// even if highlighting doesn't apply // even if highlighting doesn't apply
code_nodes.forEach(function (block) { block.classList.add('hljs'); }); code_nodes.forEach(function (block) { block.classList.add('hljs'); });
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
var lines = Array.from(block.querySelectorAll('.boring')); var lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return // If no lines were hidden, return
@ -551,13 +551,6 @@ function playground_text(playground, hidden = true) {
firstContact = null; firstContact = null;
} }
}, { passive: true }); }, { passive: true });
// Scroll sidebar to current active section
var activeSection = document.getElementById("sidebar").querySelector(".active");
if (activeSection) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
activeSection.scrollIntoView({ block: 'center' });
}
})(); })();
(function chapterNavigation() { (function chapterNavigation() {
@ -676,13 +669,14 @@ function playground_text(playground, hidden = true) {
}, { passive: true }); }, { passive: true });
})(); })();
(function controllBorder() { (function controllBorder() {
menu.classList.remove('bordered'); function updateBorder() {
document.addEventListener('scroll', function () {
if (menu.offsetTop === 0) { if (menu.offsetTop === 0) {
menu.classList.remove('bordered'); menu.classList.remove('bordered');
} else { } else {
menu.classList.add('bordered'); menu.classList.add('bordered');
} }
}, { passive: true }); }
updateBorder();
document.addEventListener('scroll', updateBorder, { passive: true });
})(); })();
})(); })();

View File

@ -110,12 +110,34 @@
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div> <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav> </nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper"> <div id="page-wrapper" class="page-wrapper">
<div class="page"> <div class="page">
{{> header}} {{> header}}
<div id="menu-bar-hover-placeholder"></div> <div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered"> <div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons"> <div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar"> <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>

View File

@ -1,7 +1,7 @@
/* Tomorrow Night Theme */ /* Tomorrow Night Theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* https://github.com/jmblog/color-themes-for-highlightjs */
/* Original theme - https://github.com/chriskempson/tomorrow-theme */ /* Original theme - https://github.com/chriskempson/tomorrow-theme */
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ /* https://github.com/jmblog/color-themes-for-highlightjs */
/* Tomorrow Comment */ /* Tomorrow Comment */
.hljs-comment { .hljs-comment {

View File

@ -210,39 +210,36 @@ mod tests {
}; };
// Create a couple of files // Create a couple of files
if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) { if let Err(err) = fs::File::create(tmp.path().join("file.txt")) {
panic!("Could not create file.txt: {}", err); panic!("Could not create file.txt: {}", err);
} }
if let Err(err) = fs::File::create(&tmp.path().join("file.md")) { if let Err(err) = fs::File::create(tmp.path().join("file.md")) {
panic!("Could not create file.md: {}", err); panic!("Could not create file.md: {}", err);
} }
if let Err(err) = fs::File::create(&tmp.path().join("file.png")) { if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
panic!("Could not create file.png: {}", err); panic!("Could not create file.png: {}", err);
} }
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) { if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
panic!("Could not create sub_dir: {}", err); panic!("Could not create sub_dir: {}", err);
} }
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) { if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
panic!("Could not create sub_dir/file.png: {}", err); panic!("Could not create sub_dir/file.png: {}", err);
} }
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) { if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
panic!("Could not create sub_dir_exists: {}", err); panic!("Could not create sub_dir_exists: {}", err);
} }
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) { if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
panic!("Could not create sub_dir_exists/file.txt: {}", err); panic!("Could not create sub_dir_exists/file.txt: {}", err);
} }
if let Err(err) = symlink( if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
&tmp.path().join("file.png"),
&tmp.path().join("symlink.png"),
) {
panic!("Could not symlink file.png: {}", err); panic!("Could not symlink file.png: {}", err);
} }
// Create output dir // Create output dir
if let Err(err) = fs::create_dir(&tmp.path().join("output")) { if let Err(err) = fs::create_dir(tmp.path().join("output")) {
panic!("Could not create output: {}", err); panic!("Could not create output: {}", err);
} }
if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) { if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
panic!("Could not create output/sub_dir_exists: {}", err); panic!("Could not create output/sub_dir_exists: {}", err);
} }
@ -253,22 +250,22 @@ mod tests {
} }
// Check if the correct files where created // Check if the correct files where created
if !(&tmp.path().join("output/file.txt")).exists() { if !tmp.path().join("output/file.txt").exists() {
panic!("output/file.txt should exist") panic!("output/file.txt should exist")
} }
if (&tmp.path().join("output/file.md")).exists() { if tmp.path().join("output/file.md").exists() {
panic!("output/file.md should not exist") panic!("output/file.md should not exist")
} }
if !(&tmp.path().join("output/file.png")).exists() { if !tmp.path().join("output/file.png").exists() {
panic!("output/file.png should exist") panic!("output/file.png should exist")
} }
if !(&tmp.path().join("output/sub_dir/file.png")).exists() { if !tmp.path().join("output/sub_dir/file.png").exists() {
panic!("output/sub_dir/file.png should exist") panic!("output/sub_dir/file.png should exist")
} }
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() { if !tmp.path().join("output/sub_dir_exists/file.txt").exists() {
panic!("output/sub_dir/file.png should exist") panic!("output/sub_dir/file.png should exist")
} }
if !(&tmp.path().join("output/symlink.png")).exists() { if !tmp.path().join("output/symlink.png").exists() {
panic!("output/symlink.png should exist") panic!("output/symlink.png should exist")
} }
} }

View File

@ -183,6 +183,7 @@ pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
opts.insert(Options::ENABLE_FOOTNOTES); opts.insert(Options::ENABLE_FOOTNOTES);
opts.insert(Options::ENABLE_STRIKETHROUGH); opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS); opts.insert(Options::ENABLE_TASKLISTS);
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
if curly_quotes { if curly_quotes {
opts.insert(Options::ENABLE_SMART_PUNCTUATION); opts.insert(Options::ENABLE_SMART_PUNCTUATION);
} }

View File

@ -13,3 +13,9 @@
##### Really Small Heading ##### Really Small Heading
###### Is it even a heading anymore - heading ###### Is it even a heading anymore - heading
## Custom id {#example-id}
## Custom class {.class1 .class2}
## Both id and class {#example-id2 .class1 .class2}

View File

@ -4,19 +4,19 @@ For copyright and trademark information on these images, please check [rust-artw
## A 16x16 image ## A 16x16 image
![16x16 rust-lang logo](http://rust-lang.org/logos/rust-logo-16x16.png) ![16x16 rust-lang logo](https://rust-lang.org/logos/rust-logo-16x16.png)
## A 32x32 image ## A 32x32 image
![32x32 rust-lang logo](http://rust-lang.org/logos/rust-logo-32x32-blk.png) ![32x32 rust-lang logo](https://rust-lang.org/logos/rust-logo-32x32-blk.png)
## A 256x256 image ## A 256x256 image
![256x256 rust-lang logo](http://rust-lang.org/logos/rust-logo-256x256.png) ![256x256 rust-lang logo](https://rust-lang.org/logos/rust-logo-256x256.png)
## A 512x512 image ## A 512x512 image
![512x512 rust-lang logo](http://rust-lang.org/logos/rust-logo-512x512-blk.png) ![512x512 rust-lang logo](https://rust-lang.org/logos/rust-logo-512x512-blk.png)
## A large image ## A large image

View File

@ -31,7 +31,7 @@ fn main(){
A random image sprinkled in between A random image sprinkled in between
![16x16 rust-lang logo](http://rust-lang.org/logos/rust-logo-16x16.png) ![16x16 rust-lang logo](https://rust-lang.org/logos/rust-logo-16x16.png)
--- ---

View File

@ -1,5 +1,7 @@
# Strikethrough # Strikethrough
~Single strike~
~~This is Striked~~ ~~This is Striked~~
~~This is **strong**, _italic_ , **_both_** and striked~~ ~~This is **strong**, _italic_ , **_both_** and striked~~

View File

@ -57,7 +57,7 @@ _start:
## bash ## bash
``` ```bash
#!/bin/bash #!/bin/bash
###### CONFIG ###### CONFIG

View File

@ -90,7 +90,7 @@ fn relative_command_path() {
.set("output.html", toml::value::Table::new()) .set("output.html", toml::value::Table::new())
.unwrap(); .unwrap();
config.set("output.myrenderer.command", cmd_path).unwrap(); config.set("output.myrenderer.command", cmd_path).unwrap();
let md = MDBook::init(&temp.path()) let md = MDBook::init(temp.path())
.with_config(config) .with_config(config)
.build() .build()
.unwrap(); .unwrap();

24
tests/cli/init.rs Normal file
View File

@ -0,0 +1,24 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
use mdbook::config::Config;
/// Run `mdbook init` with `--force` to skip the confirmation prompts
#[test]
fn base_mdbook_init_can_skip_confirmation_prompts() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.args(["init", "--force"]).current_dir(temp.path());
cmd.assert()
.success()
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
assert_eq!(config.book.title, None);
assert!(!temp.path().join(".gitignore").exists());
}

View File

@ -1,3 +1,4 @@
mod build; mod build;
mod cmd; mod cmd;
mod init;
mod test; mod test;

View File

@ -112,12 +112,12 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
let from = from.as_ref(); let from = from.as_ref();
let to = to.as_ref(); let to = to.as_ref();
for entry in WalkDir::new(&from) { for entry in WalkDir::new(from) {
let entry = entry.with_context(|| "Unable to inspect directory entry")?; let entry = entry.with_context(|| "Unable to inspect directory entry")?;
let original_location = entry.path(); let original_location = entry.path();
let relative = original_location let relative = original_location
.strip_prefix(&from) .strip_prefix(from)
.expect("`original_location` is inside the `from` directory"); .expect("`original_location` is inside the `from` directory");
let new_location = to.join(relative); let new_location = to.join(relative);
@ -126,7 +126,7 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?; fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
} }
fs::copy(&original_location, &new_location) fs::copy(original_location, &new_location)
.with_context(|| "Unable to copy file contents")?; .with_context(|| "Unable to copy file contents")?;
} }
} }

View File

@ -14,6 +14,7 @@
- [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) - [Duplicate Headers](first/duplicate-headers.md)
- [Heading Attributes](first/heading-attributes.md)
- [Second Chapter](second.md) - [Second Chapter](second.md)
- [Nested Chapter](second/nested.md) - [Nested Chapter](second/nested.md)

View File

@ -0,0 +1,5 @@
# Heading Attributes {#attrs}
## Heading with classes {.class1 .class2}
## Heading with id and classes {#both .class1 .class2}

View File

@ -35,6 +35,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
"1.5. Unicode", "1.5. Unicode",
"1.6. No Headers", "1.6. No Headers",
"1.7. Duplicate Headers", "1.7. Duplicate Headers",
"1.8. Heading Attributes",
"2.1. Nested Chapter", "2.1. Nested Chapter",
]; ];
@ -275,7 +276,7 @@ fn root_index_html() -> Result<Document> {
.with_context(|| "Book building failed")?; .with_context(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html"); let index_page = temp.path().join("book").join("index.html");
let html = fs::read_to_string(&index_page).with_context(|| "Unable to read index.html")?; let html = fs::read_to_string(index_page).with_context(|| "Unable to read index.html")?;
Ok(Document::from(html.as_str())) Ok(Document::from(html.as_str()))
} }
@ -412,7 +413,7 @@ fn recursive_includes_are_capped() {
let content = &["Around the world, around the world let content = &["Around the world, around the world
Around the world, around the world Around the world, around the world
Around the world, around the world"]; Around the world, around the world"];
assert_contains_strings(&recursive, content); assert_contains_strings(recursive, content);
} }
#[test] #[test]
@ -462,7 +463,7 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
let second_index = temp.path().join("book").join("second").join("index.html"); let second_index = temp.path().join("book").join("second").join("index.html");
let unexpected_strings = vec!["Second README"]; let unexpected_strings = vec!["Second README"];
assert_doesnt_contain_strings(&second_index, &unexpected_strings); assert_doesnt_contain_strings(second_index, &unexpected_strings);
} }
#[test] #[test]
@ -628,10 +629,8 @@ fn edit_url_has_configured_src_dir_edit_url() {
} }
fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_ { fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_ {
path.components().skip_while(|c| match c { path.components()
Component::Prefix(_) | Component::RootDir => true, .skip_while(|c| matches!(c, Component::Prefix(_) | Component::RootDir))
_ => false,
})
} }
/// Checks formatting of summary names with inline elements. /// Checks formatting of summary names with inline elements.
@ -756,6 +755,7 @@ mod search {
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 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 heading_attrs = get_doc_ref("first/heading-attributes.html#both");
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"];
@ -768,7 +768,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 Duplicate Headers Second Chapter Nested Chapter Conclusion" "Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Heading Attributes Second Chapter Nested Chapter Conclusion"
); );
assert_eq!( assert_eq!(
docs[&summary]["breadcrumbs"], docs[&summary]["breadcrumbs"],
@ -787,6 +787,10 @@ mod search {
docs[&no_headers]["body"], docs[&no_headers]["body"],
"Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex." "Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex."
); );
assert_eq!(
docs[&heading_attrs]["breadcrumbs"],
"First Chapter » Heading Attributes » Heading with id and classes"
);
} }
// Setting this to `true` may cause issues with `cargo watch`, // Setting this to `true` may cause issues with `cargo watch`,
@ -803,7 +807,7 @@ mod search {
let src = read_book_index(temp.path()); let src = read_book_index(temp.path());
let dest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/searchindex_fixture.json"); let dest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/searchindex_fixture.json");
let dest = File::create(&dest).unwrap(); let dest = File::create(dest).unwrap();
serde_json::to_writer_pretty(dest, &src).unwrap(); serde_json::to_writer_pretty(dest, &src).unwrap();
src src
@ -891,8 +895,8 @@ fn custom_fonts() {
assert_eq!(actual_files(&p.join("book/fonts")), &builtin_fonts); assert_eq!(actual_files(&p.join("book/fonts")), &builtin_fonts);
assert!(has_fonts_css(p)); assert!(has_fonts_css(p));
// Mixed with copy_fonts=true // Mixed with copy-fonts=true
// This should generate a deprecation warning. // Should ignore the copy-fonts setting since the user has provided their own fonts.css.
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap(); let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let p = temp.path(); let p = temp.path();
MDBook::init(p).build().unwrap(); MDBook::init(p).build().unwrap();
@ -900,10 +904,10 @@ fn custom_fonts() {
write_file(&p.join("theme/fonts"), "myfont.woff", b"").unwrap(); write_file(&p.join("theme/fonts"), "myfont.woff", b"").unwrap();
MDBook::load(p).unwrap().build().unwrap(); MDBook::load(p).unwrap().build().unwrap();
assert!(has_fonts_css(p)); assert!(has_fonts_css(p));
let mut expected = Vec::from(builtin_fonts); assert_eq!(
expected.push("myfont.woff"); actual_files(&p.join("book/fonts")),
expected.sort(); ["fonts.css", "myfont.woff"]
assert_eq!(actual_files(&p.join("book/fonts")), expected.as_slice()); );
// copy-fonts=false, no theme // copy-fonts=false, no theme
// This should generate a deprecation warning. // This should generate a deprecation warning.
@ -948,3 +952,19 @@ fn custom_fonts() {
&["fonts.css", "myfont.woff"] &["fonts.css", "myfont.woff"]
); );
} }
#[test]
fn custom_header_attributes() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let contents = temp.path().join("book/first/heading-attributes.html");
let summary_strings = &[
r##"<h1 id="attrs"><a class="header" href="#attrs">Heading Attributes</a></h1>"##,
r##"<h2 id="heading-with-classes" class="class1 class2"><a class="header" href="#heading-with-classes">Heading with classes</a></h2>"##,
r##"<h2 id="both" class="class1 class2"><a class="header" href="#both">Heading with id and classes</a></h2>"##,
];
assert_contains_strings(&contents, summary_strings);
}

File diff suppressed because it is too large Load Diff