Merge branch 'rust-lang:master' into master
This commit is contained in:
commit
8c662b750f
|
@ -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
|
||||||
|
|
48
CHANGELOG.md
48
CHANGELOG.md
|
@ -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)
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
66
Cargo.toml
66
Cargo.toml
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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,26 +892,26 @@ 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");
|
||||||
|
@ -911,12 +947,9 @@ fn add_playground_pre(
|
||||||
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
hide_lines(&content)
|
content
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
|
|
||||||
}
|
|
||||||
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)) => {
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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 });
|
||||||
})();
|
})();
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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~~
|
||||||
|
|
|
@ -57,7 +57,7 @@ _start:
|
||||||
|
|
||||||
## bash
|
## bash
|
||||||
|
|
||||||
```
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
###### CONFIG
|
###### CONFIG
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
mod build;
|
mod build;
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
mod init;
|
||||||
mod test;
|
mod test;
|
||||||
|
|
|
@ -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")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Heading Attributes {#attrs}
|
||||||
|
|
||||||
|
## Heading with classes {.class1 .class2}
|
||||||
|
|
||||||
|
## Heading with id and classes {#both .class1 .class2}
|
|
@ -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
Loading…
Reference in New Issue