Compare commits

...

265 Commits

Author SHA1 Message Date
Eric Huss 2420919ca8
Merge pull request #2259 from stevecheckoway/improve-test-output
Color test output and shorten chapter paths
2024-05-10 18:14:32 +00:00
Eric Huss c671c2e904
Merge pull request #2262 from Janik-Haag/master
Add nix to default languages
2024-04-12 15:27:31 +00:00
Janik H. c9df8dd1f3
Add nix to default languages 2024-04-10 21:56:13 +02:00
Eric Huss 8ae86d4310
Merge pull request #2355 from johamster/reduce_allocations_when_copying_files
Reduce allocations in `fs::copy_files_except_ext`
2024-04-08 21:40:54 +00:00
Johannes Gloeckle c144c26dcf Reduce allocations in `fs::copy_files_except_ext`
Above mentioned function copies files (recursively) from a source to a
destination directory. For that, file/directory paths have to be created
repeatedly. This allocates as directory and file names are concatenated
into an owning path structure.

The number of allocations can be reduced by creating file/directory
paths only once and borrowing them instead of cloning/recreating them.

In bigger projects, this reduces execution time noticeably. Please note
that file system operations are dominant from performance POV.
2024-04-07 10:43:23 +02:00
Eric Huss 481f6b1531
Merge pull request #2351 from rust-lang/dependabot/cargo/mio-0.8.11
Bump mio from 0.8.10 to 0.8.11
2024-04-05 19:36:55 +00:00
dependabot[bot] b267d56ba7
Bump mio from 0.8.10 to 0.8.11
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 18:10:01 +00:00
Eric Huss dd139f8228
Merge pull request #2350 from rust-lang/dependabot/cargo/h2-0.3.26
Bump h2 from 0.3.24 to 0.3.26
2024-04-05 18:04:09 +00:00
dependabot[bot] be4756e4bf
Bump h2 from 0.3.24 to 0.3.26
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:16:31 +00:00
Eric Huss bd323fb930
Merge pull request #2339 from goodmost/master
chore: remove repetitive word
2024-03-19 15:46:26 +00:00
goodmost aff1070f43 chore: remove repetitive word
Signed-off-by: goodmost <zhaohaiyang@outlook.com>
2024-03-19 22:22:16 +08:00
Eric Huss b6742e90b1
Merge pull request #2338 from max-heller/patch-2
Fix typo in docs
2024-03-18 22:59:48 +00:00
Max Heller 95b6ed7965
Fix typo in docs 2024-03-18 18:38:55 -04:00
Eric Huss 5a35144d4f
Merge pull request #2328 from ehuss/clarify-src-path
Clarify Chapter path and source_path.
2024-02-25 23:26:20 +00:00
Eric Huss 5f5f9d6fd5 Clarify Chapter path and source_path. 2024-02-25 15:20:19 -08:00
Eric Huss c602a2fcd6
Merge pull request #2070 from expikr/testbook-add-mathajx-tests
Added missing tests for MathJax to the test book
2024-02-25 23:00:13 +00:00
_ 821d3c423c Add MathJax tests. 2024-02-25 14:53:10 -08:00
Eric Huss 6b89f5dad8
Merge pull request #2327 from ehuss/smart-punctuation
Rename curly-quotes to smart-punctuation.
2024-02-25 22:30:25 +00:00
Eric Huss d28cf53009 Rename curly-quotes to smart-punctuation. 2024-02-25 13:42:44 -08:00
Eric Huss 504900d7bd
Merge pull request #2324 from ehuss/redundant-imports
Fix redundant imports.
2024-02-24 20:19:28 +00:00
Eric Huss 0cc439eee3 Fix redundant imports. 2024-02-24 12:04:57 -08:00
Eric Huss e8b8f34f2b
Merge pull request #2322 from wilwade/patch-1
Fix incorrect theme documentation: Next/Previous should use `title`
2024-02-21 21:25:18 +00:00
Wil Wade 58a23e06a1
Fix incorrect theme documentation
The theme documentation for next and previous used name instead of title
2024-02-20 15:29:30 -05:00
Eric Huss 5a4ac03c0d
Merge pull request #2312 from ehuss/bump-version
Update to 0.4.37
2024-02-07 03:48:57 +00:00
Eric Huss c5a506e240 Update to 0.4.37 2024-02-06 19:34:15 -08:00
Eric Huss bc5cd13c16
Merge pull request #2311 from sspaeti/fix-search-with-form
fix input `s` into a form without triggering search
2024-02-07 03:21:41 +00:00
sspaeti d406c7c09b fix input `s` into a form without triggering search 2024-02-06 10:15:56 +01:00
Eric Huss 9cf3117636
Merge pull request #2309 from ehuss/update-deps
Update dependencies
2024-02-05 22:41:50 +00:00
Eric Huss 61786ddcdf Update dependencies 2024-02-05 14:37:06 -08:00
Eric Huss f33281fae2
Merge pull request #2310 from ehuss/update-env-logger
Update env_logger to 0.11
2024-02-05 22:26:31 +00:00
Eric Huss 93bd457a54 Update env_logger to 0.11 2024-02-05 14:22:21 -08:00
Eric Huss 600824bed2
Merge pull request #2308 from ehuss/pulldown_cmark-0.10
Update pulldown_cmark to 0.10
2024-02-05 22:21:55 +00:00
Eric Huss 42e635bb9e Update pulldown_cmark to 0.10 2024-02-05 14:11:27 -08:00
Eric Huss d48810f045
Merge pull request #2307 from ehuss/backends_receive_render_context_via_stdin
Clean up test backends_receive_render_context_via_stdin
2024-02-05 20:17:35 +00:00
Eric Huss 3387cf373d Clean up test backends_receive_render_context_via_stdin 2024-02-05 09:53:50 -08:00
Eric Huss 7825bd6c5a
Merge pull request #2306 from jvstme/master
docs: Fix broken link
2024-02-04 14:09:09 +00:00
Jvst Me ba14f4ad53
docs: Fix broken link 2024-02-04 16:47:52 +03:00
Eric Huss 02bbc3f777
Merge pull request #2305 from gibbz00/patch-1
Fix minor sentencing issue in build.md
2024-02-04 12:37:31 +00:00
gibbz00 45a2d0b40e Fix minor sentencing issue in build.md 2024-02-04 08:57:50 +01:00
Eric Huss 53eccf7047
Merge pull request #2303 from infogulch/patch-1
summary.md: clarify that part titles must be h1 headers
2024-02-01 23:45:51 +00:00
Joe Taber 63000bc122
summary.md: clarify that part titles must be h1 headers 2024-01-31 01:47:05 -06:00
Eric Huss 220cb4f0c8
Merge pull request #2302 from GeckoEidechse/fix/missing-plus
docs: Add missing `+` in diff code block
2024-01-30 19:55:05 +00:00
GeckoEidechse 7ce3a41184
docs: Add missing `+` in diff code block
The closing bracket for the `if` statement is also nearly added but the leading `+` to indicate that was forgotten.
2024-01-30 17:43:37 +01:00
Eric Huss 51efaf2e81
Merge pull request #2297 from rust-lang/dependabot/cargo/shlex-1.3.0
Bump shlex from 1.2.0 to 1.3.0
2024-01-22 22:41:27 +00:00
dependabot[bot] f0d6d428dc
Bump shlex from 1.2.0 to 1.3.0
Bumps [shlex](https://github.com/comex/rust-shlex) from 1.2.0 to 1.3.0.
- [Changelog](https://github.com/comex/rust-shlex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/comex/rust-shlex/commits)

---
updated-dependencies:
- dependency-name: shlex
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-22 21:52:27 +00:00
Eric Huss 01778fc90a
Merge pull request #2293 from rust-lang/dependabot/cargo/h2-0.3.24
Bump h2 from 0.3.22 to 0.3.24
2024-01-19 17:43:11 +00:00
dependabot[bot] d9928ad3f9
Bump h2 from 0.3.22 to 0.3.24
Bumps [h2](https://github.com/hyperium/h2) from 0.3.22 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.22...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 16:23:16 +00:00
Eric Huss 77b7876986
Merge pull request #2291 from klensy/watch-me
pathdiff only used with watch feature, so make it optional
2024-01-15 13:37:12 +00:00
klensy 745f7c7313 pathdiff only used with watch feature, so make it optional 2024-01-15 12:49:29 +03:00
Eric Huss 0a96d0e3fa
Merge pull request #2290 from klensy/less-clones
removes few more allocs
2024-01-14 14:09:37 +00:00
klensy e3ad9d097e reduce useless regex allocs
from 474mb to 215mb

==40876== Total:     474,156,323 bytes in 1,521,025 blocks
==40876== At t-gmax: 13,872,954 bytes in 4,655 blocks
==40876== At t-end:  488,516 bytes in 884 blocks
==40876== Reads:     820,933,434 bytes
==40876== Writes:    514,838,350 bytes

to

==57763== Total:     215,292,393 bytes in 1,161,048 blocks
==57763== At t-gmax: 13,872,954 bytes in 4,655 blocks
==57763== At t-end:  1,210,783 bytes in 1,274 blocks
==57763== Reads:     598,542,892 bytes
==57763== Writes:    229,841,910 bytes
2024-01-14 15:17:31 +03:00
klensy 573b6522f9 remove useless alloc
on rust reference book this reduces total allocs from 490mb to 474mb:

==23272== Total:     490,538,699 bytes in 1,760,117 blocks
==23272== At t-gmax: 13,872,954 bytes in 4,655 blocks
==23272== At t-end:  488,516 bytes in 884 blocks
==23272== Reads:     830,509,060 bytes
==23272== Writes:    522,290,614 bytes

to

==40876== Total:     474,156,323 bytes in 1,521,025 blocks
==40876== At t-gmax: 13,872,954 bytes in 4,655 blocks
==40876== At t-end:  488,516 bytes in 884 blocks
==40876== Reads:     820,933,434 bytes
==40876== Writes:    514,838,350 bytes
2024-01-14 15:17:07 +03:00
Eric Huss 59d3717159
Merge pull request #2283 from sunng87/feature/hbd-5
feat: upgrade handlebars to 5.0
2024-01-04 21:00:18 +00:00
Ning Sun a42eafc316
feat: upgrade handlebars to 5.0 2024-01-04 20:16:34 +08:00
Eric Huss 11f839b9e5
Merge pull request #2282 from max-heller/patch-1
Fix typo in guide
2024-01-04 01:11:14 +00:00
Max Heller 721274239a
fix typo in guide 2024-01-03 19:46:10 -05:00
Eric Huss 090eba0db5
Merge pull request #2273 from klensy/useless-clone
remove useless string clone
2023-12-16 14:41:39 +00:00
klensy 88be4ac417 remove useless string clone 2023-12-16 13:29:24 +03:00
Dylan DPC c1d622e56e
Merge pull request #2263 from jhult/theme-dir-warning-check
Remove warning check for theme directory as ./src/theme
2023-12-10 08:33:10 +00:00
Jonathan Hult 91af1c3b54
Remove warning check for theme directory as ./src/theme
This was causing an unnecessary warning if &src_dir was the same as ctx.root
2023-12-09 16:53:57 -06:00
Stephen Checkoway 32687e64fe Color test output and shorten chapter paths
Currently, the output from `rustdoc --test` is not colored because
`rustdoc`'s stdout is not a tty. The output of a failed `rustdoc` run is
sent to `mdbook`'s stderr via the `error!()` macro. This commit checks
if stderr is a tty using the standard `.is_terminal()` and if so, passes
`--color always` to `rustdoc`.

The test output from `rustdoc` includes the full path that `rustdoc` was
called with. This obscures the path of the file with the error. E.g.,
```
---- /var/folders/9v/90bm7kb10fx3_bprxltb3t1r0000gn/T/mdbook-tnGJxp/lab0/index.md - Lab_0__Getting_Started (line 3) stdout ----
error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `code`
 --> /var/folders/9v/90bm7kb10fx3_bprxltb3t1r0000gn/T/mdbook-tnGJxp/lab0/index.md:4:6
  |
3 | this code has a bug
  |      ^^^^ expected one of 8 possible tokens

error: aborting due to previous error
```

This commit runs `rustdoc` in the temp directory and replaces any
relative library paths with absolute library paths. This leads to
simpler error messages. The one above becomes
```
---- lab0/index.md - Lab_0__Getting_Started (line 3) stdout ----
error: expected one of `!`, `.`, `::`, `;`, `?`, `{`, `}`, or an operator, found `code`
 --> lab0/index.md:4:6
  |
3 | this code has a bug
  |      ^^^^ expected one of 8 possible tokens

error: aborting due to previous error
```
(with colors, of course).
2023-12-06 12:09:07 -05:00
Eric Huss b7f46213c7
Merge pull request #2253 from ehuss/bump-version
Update to 0.4.36
2023-11-29 22:54:44 +00:00
Eric Huss aa8982bdb4 Update to 0.4.36 2023-11-29 14:50:26 -08:00
Eric Huss 14826db606
Merge pull request #2252 from ehuss/update-lockfile
Update dependencies
2023-11-29 22:44:41 +00:00
Eric Huss 847a582022 Update msrv to 1.70 2023-11-29 14:36:39 -08:00
Eric Huss 97cd00faeb Update dependencies 2023-11-29 14:29:31 -08:00
Eric Huss 8d4193fb46
Merge pull request #2229 from leonzchang/fix/normalize-path
Fix `mdbook serve` unexpected panic
2023-11-29 22:16:39 +00:00
leonzchang 8d4ae388fa
update comment 2023-11-29 11:51:23 +08:00
leonzchang 7082689866
add comment 2023-11-29 11:50:07 +08:00
leonzchang 40c034ed3f
apply suggest changes 2023-11-29 11:34:08 +08:00
Dylan DPC 208d5ea7ab
Merge pull request #2250 from DuckDuckWhale/master
Dep: fix Tungstenite DoS (RUSTSEC-2023-0065)
2023-11-26 09:42:25 +00:00
DuckDuckWhale ed51438c8b
Dep: fix Tungstenite DoS (RUSTSEC-2023-0065) 2023-11-26 01:05:20 -08:00
Eric Huss 49fce6673a
Merge pull request #2209 from cyevgeniy/feat/sidebar-resize-indicator
Add resize indicator to the sidebar
2023-11-24 20:31:14 +00:00
Eric Huss a016ac0d2b
Merge pull request #2173 from 0xpr03/master
upgrade to notify 6.1.1 and watcher-mini 4.1
2023-11-24 20:05:37 +00:00
Eric Huss ad55f5367e
Merge pull request #2162 from ISSOtm/padding-hljs
Fix code blocks "indent" without highlight.js
2023-11-24 19:59:55 +00:00
Eric Huss 660cbfa6ce
Merge pull request #2243 from wyfo/patch-1
fix typo
2023-11-19 13:53:15 +00:00
Joseph Perez 982608246e
fix typo 2023-11-19 14:46:50 +01:00
Dylan DPC 6f6de2cf05
Merge pull request #2235 from ardcore/improvement/fix-print-margin
Make sure page wrapper transform is removed in print mode
2023-11-16 13:44:27 +00:00
Szymon Pilkowski ae3e3f8269 Make sure page wrapper transform is removed in print mode 2023-11-10 21:51:00 +01:00
Eric Huss dc21f1497b
Merge pull request #2232 from arnetheduck/add-nim
Add Nim to default languages
2023-11-09 03:32:44 +00:00
Jacek Sieka 5c8941ba16 Add Nim to default languages
Nim is a systems programming language (included in the highlight.js
`system` group), and we're quite happily using `mdBook` in several of
our documentation projects starting with our [style
guide](https://status-im.github.io/nim-style-guide/).

While we can maintain our own highlight.js, including `Nim` in the
default distribution would allow us to promote more mdBook usage in the
Nim community at the cost of a ~2kb increase in the `highlight.js` size.
2023-11-08 15:34:35 +01:00
leonzchang b0a001c6a4
create relative path through ignore root and path 2023-11-08 13:35:35 +08:00
leonzchang 722c55f85f
normalize path to relative 2023-11-01 12:33:13 +08:00
leonzchang 3ab19f3295
Revert "bump version v0.4.36"
This reverts commit 621ffc46c0.
2023-10-31 12:22:08 +08:00
leonzchang 621ffc46c0
bump version v0.4.36 2023-10-31 12:14:28 +08:00
leonzchang fbb629c02e
normalize path in watch cmd 2023-10-31 12:13:25 +08:00
Evgeny Chaban 80d3a86468 fix: hide resize indicator on devices with limited accuracy 2023-10-04 01:55:20 +03:00
Evgeny Chaban 8e8fd2717e fix(style): use calc function 2023-10-04 01:40:16 +03:00
Evgeny Chaban f92d24e89c feat(sidebar): add sidebar indicator 2023-10-02 18:24:55 +03:00
Eric Huss 94e0a44e15
Merge pull request #2206 from ehuss/bump-version
Update to 0.4.35
2023-09-29 22:43:05 +00:00
Eric Huss f25181f68d Update to 0.4.35 2023-09-29 14:33:45 -07:00
Eric Huss cf19eb1386 Fix `text-direction` in documentation. 2023-09-29 14:33:20 -07:00
Eric Huss 0583119698
Merge pull request #2197 from dluschan/patch-1
Update index.hbs
2023-09-22 17:26:27 +00:00
Dmitry Luschan 3389f3db7f
Update index.hbs
Trailing slash on void elements has no effect and interacts badly with unquoted attribute values.
2023-09-19 20:44:39 +06:00
Eric Huss c642f5f8a3
Merge pull request #2187 from notriddle/notriddle/warning-block
Add `.warning` class for doc author use
2023-09-09 20:13:10 +00:00
Michael Howell ceb8b509e2 Add more guides to stock CSS classes 2023-09-08 13:27:02 -07:00
Michael Howell 65dae11e47 Add `.warning` class for doc author use
This is designed to be compatible with rustdoc's version, in
https://github.com/rust-lang/rust/pull/106561
2023-09-08 13:17:21 -07:00
Dylan DPC d5b1676216
Merge pull request #2168 from pickfire/prefetch
Add prefetch to next link
2023-09-04 18:03:38 +00:00
Eric Huss 09f222baf7
Merge pull request #1641 from cN3rd/rtl
Continued work on "Support right-to-left languages"
2023-09-02 23:57:48 +00:00
Eric Huss 802e7bffc3 Make the arrow keys honor RTL.
At least to my understanding, the pages will flip in the opposite direction.
2023-09-02 16:44:47 -07:00
Eric Huss fb272d1afa Fix theme selector for RTL. 2023-09-02 16:43:21 -07:00
Eric Huss b871676def Fix sidebar behavior with RTL. 2023-09-02 16:43:13 -07:00
Eric Huss 869fe2f50d Remove text_drection_from_lang_code
The test above should cover this sufficiently.
2023-09-02 16:42:53 -07:00
Eric Huss db877b1c9b Update language list to include missing rtl languages. 2023-09-02 16:42:14 -07:00
Eric Huss 4749f9d97a Some minor corrections from code review. 2023-09-02 16:41:59 -07:00
cN3rd 8564a7fb51 Add proper test to inline code within the book. 2023-09-02 14:38:02 -07:00
cN3rd 6be98e0bbd Ensure code segments always render in LTR 2023-09-02 14:37:59 -07:00
cN3rd 5e0c68c45e Fix icons when using RTL 2023-09-02 07:50:23 -07:00
cN3rd 7717b9dcf2 Support `text_direction` attribute in HTML output 2023-09-02 07:50:21 -07:00
cN3rd 819a108f07 Add `text_direction` property in general book metadata
Text direction can selected in the config via the `text_direction` attribute in `book.toml`,
or be derived from the book's language.
2023-09-02 07:49:28 -07:00
Tim Crawford 3a99899114 Use CSS selectors to override properties for RTL
Fix behavior of some elements when displaying page in RTL.

Signed-off-by: Tim Crawford <crawfxrd@gmail.com>
2023-09-02 07:49:28 -07:00
Tim Crawford 1088066c69 Move sidebar, js classes from html to body element
This will be necessary for using CSS selectors on root attributes.

Signed-off-by: Tim Crawford <crawfxrd@gmail.com>
2023-09-02 07:49:27 -07:00
Tim Crawford 73d44503fd Use logical CSS properties
Replace phyiscal properties (top/bottom/left/right) with logical
properties (start/end) that can be used in non-LTR contexts (e.g.,
content in Arabic or Hebrew).

Based on the CSS Logical Properties and Values Level 1 specification,
currently an Editor's Draft [1].

Referencing MDN, all major browsers except Internet Explorer support the
margin, padding, and border properties.

[1]: https://drafts.csswg.org/css-logical/

Signed-off-by: Tim Crawford <crawfxrd@gmail.com>
2023-09-02 07:47:12 -07:00
Eric Huss 25aaff0bd6
Merge pull request #2182 from cuishuang/master
remove the repetitive word
2023-09-02 13:41:15 +00:00
cui fliter 29691461c5 remove the repetitive word
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-09-02 14:04:32 +08:00
Dylan DPC a74e4dcec8
Merge pull request #2181 from tshepang/patch-1
docs: future expansion to non-Rust testing already implied
2023-09-01 08:55:38 +00:00
Tshepang Mbambo 0b0b548d7a
docs: future expansion to non-Rust testing already implied 2023-09-01 05:11:18 +02:00
Dylan DPC 02f3823e4c
Merge pull request #2175 from qaqland/sidebar-pure-css
Sidebar but Pure CSS, fix rust-lang/mdBook#859
2023-08-24 13:11:14 +00:00
qaqland 36327efe9d Sidebar but Pure CSS, fix rust-lang/mdBook#859
* change sidebar-toggle button by input:checked and label
* change nav-wrappers' CSS selectors
* remove loading animation when page opens with z-index
2023-08-24 16:44:25 +08:00
Aron Heinecke 079f52a191
upgrade to notify 6.1.1 and watcher-mini 4.1 2023-08-21 20:28:56 +02:00
Ivan Tham c9f1d01346 Add prefetch to next link
Fix #1975
2023-08-17 00:50:47 +08:00
Eldred Habert 9bc68bdd93
Fix code blocks "indent" without highlight.js
The `.hljs` class added by highlight.js adds a `display: block` rule
which makes `padding` apply correctly (left padding on all lines,
not just the first one).
Make sure that rule is applied even if highlight.js isn't run.
2023-08-08 12:31:55 +02:00
Eric Huss 56c225bd34
Merge pull request #2158 from ehuss/bump-version
Update to 0.4.34
2023-08-05 21:54:12 +00:00
Eric Huss 55c017cad1 Update to 0.4.34 2023-08-05 14:34:42 -07:00
Eric Huss 7849d55b99
Merge pull request #2157 from ehuss/macos-notify-copy
Add workaround for macOS notify problem.
2023-08-05 20:35:33 +00:00
Eric Huss c903cc8827 Add workaround for macOS notify problem. 2023-08-05 13:23:17 -07:00
Eric Huss 4a797b9565 Revert "Merge pull request #2152 from ehuss/macos-notify-kqueue"
This reverts commit 347e7886e1, reversing
changes made to a8fd6038f1.
2023-08-05 12:53:23 -07:00
Eric Huss 57b487eaa3
Merge pull request #2154 from ehuss/bump-version
Update to 0.4.33
2023-08-04 00:11:45 +00:00
Eric Huss 891b7c06f2 Update to 0.4.33 2023-08-03 17:03:40 -07:00
Eric Huss f7e212ec9c
Merge pull request #2150 from proski/header-code-background
Don't use distinct background for code in headers when printing
2023-08-03 22:46:50 +00:00
Pavel Roskin 228538ea62 Don't use distinct background for code in headers when printing
Fixes #1933
2023-08-02 23:31:46 -07:00
Eric Huss 347e7886e1
Merge pull request #2152 from ehuss/macos-notify-kqueue
Switch macOS notify to kqueue.
2023-08-02 22:13:09 +00:00
Eric Huss bfa5fb8844 Switch macOS notify to kqueue. 2023-08-02 09:21:25 -07:00
Eric Huss a8fd6038f1
Merge pull request #2151 from ehuss/revert-toml
Revert toml update
2023-08-02 15:49:44 +00:00
Eric Huss fbfe887084 Add note to not update toml. 2023-08-02 08:39:23 -07:00
Eric Huss aed991f75f Revert "Merge pull request #2125 from ehuss/update-toml"
This reverts commit 89797064b8, reversing
changes made to 7824aed878.
2023-08-02 08:32:44 -07:00
Eric Huss ab2cb71c00
Merge pull request #2134 from GiorgioReale/master
Enhancing themes with `color-scheme: light | dark;` implementation in CSS
2023-07-30 22:50:08 +00:00
Giorgio Reale fcfde083e7 Enhancing themes with `color-scheme: light | dark;` implementation in CSS 2023-07-30 15:43:51 -07:00
Eric Huss 4614a3637a
Merge pull request #2146 from riverbl/fix-extra-watch-dirs
Fix issues with extra-watch-dirs
2023-07-30 17:08:46 +00:00
Eric Huss d450544d6b
Merge pull request #2148 from ehuss/fix-merge-queue
Use a better merge-queue success check.
2023-07-29 16:23:04 +00:00
Eric Huss 9340e6a78d Use a better merge-queue success check. 2023-07-29 09:13:55 -07:00
riverbl e00b8835cc Fix issues with extra-watch-dirs
Fix paths specified in extra-watch-dirs being relative to the current working directory rather than the book root

If there is an error canonicalising paths in extra-watch-dirs, log the error and exit rather than panicking
2023-07-28 20:07:20 +01:00
Eric Huss 429ca06289
Merge pull request #2140 from ehuss/merge-queue-workflow
Prepare CI workflows to support merge queues.
2023-07-24 20:35:16 -07:00
Eric Huss 0fbfc90bea Prepare CI workflows to support merge queues. 2023-07-24 20:16:07 -07:00
Eric Huss 581e5025a2
Merge pull request #2139 from tshepang/patch-1
misplaced bracket
2023-07-21 07:32:56 -07:00
Tshepang Mbambo e57fce290b
misplaced bracket 2023-07-21 15:29:56 +02:00
Eric Huss d5a3682de9
Merge pull request #2137 from ehuss/mdbook-case-link
Fix link on case-sensitive filesystems.
2023-07-19 08:06:35 -07:00
Eric Huss 75f5862218 Fix link on case-sensitive filesystems. 2023-07-19 07:54:39 -07:00
Eric Huss aed518f945
Merge pull request #2129 from ehuss/bump-version
Update to 0.4.32
2023-07-16 17:43:49 -07:00
Eric Huss e942d41c1d
Merge pull request #2128 from ehuss/release-token-perms
deploy: Rewrite and update permissions
2023-07-16 17:38:21 -07:00
Eric Huss 38fcfd8732 Update to 0.4.32 2023-07-16 17:35:51 -07:00
Eric Huss 82ec68128d
Merge pull request #2127 from ehuss/auto-publish
Automatically publish to crates.io on new release
2023-07-16 17:28:50 -07:00
Eric Huss 9497354cfd Rewrite asset deploy.
This switches to `gh` which is the more modern CLI, and also
available by default which removes the old installer script.

This also tightens the scope where GITHUB_TOKEN is exposed to just
the step where `gh` is executed.

Finally, it tightens the permissions on the GITHUB_TOKEN (though
`contents: write` is extremely permissive, since that allows writing to
almost anything in the repo).
2023-07-16 17:16:15 -07:00
Eric Huss baa936439d deploy: Set the default shell so it doesn't need to be repeated. 2023-07-16 17:12:55 -07:00
Eric Huss 394061d28d Rename make-release.sh to make-release-asset.sh
This is to better reflect what the script does.
2023-07-16 17:12:29 -07:00
Eric Huss 0f25db67dc Automatically publish to crates.io on new release 2023-07-16 16:29:45 -07:00
Eric Huss 49ba91961f
Merge pull request #2126 from ehuss/update-deps
Update dependencies
2023-07-16 13:32:55 -07:00
Eric Huss 28ce772ae9 Update msrv to 1.66. 2023-07-16 13:21:45 -07:00
Eric Huss 424c2d9f6b Update dependencies 2023-07-16 13:09:52 -07:00
Eric Huss 89797064b8
Merge pull request #2125 from ehuss/update-toml
Update toml to 0.7.6
2023-07-16 13:08:40 -07:00
Eric Huss 7824aed878
Merge pull request #2124 from ehuss/update-predicates
Update predicates to 3.0.3
2023-07-16 12:51:51 -07:00
Eric Huss 8236c43c90
Merge pull request #2123 from ehuss/update-notify
Update notify to 6.0.1
2023-07-16 12:50:17 -07:00
Eric Huss 6df89fbe94
Merge pull request #2122 from ehuss/update-opener
Update opener to 0.6.1
2023-07-16 12:50:10 -07:00
Eric Huss b423bf7ddd Update toml to 0.7.6 2023-07-16 12:48:01 -07:00
Eric Huss cdbdb8248c
Merge pull request #2121 from ehuss/update-clap
Update to clap 4.3.12
2023-07-16 12:43:44 -07:00
Eric Huss db45052d7e Update predicates to 3.0.3 2023-07-16 12:42:44 -07:00
Eric Huss 804bbf6564 Update notify to 6.0.1 2023-07-16 12:40:45 -07:00
Eric Huss bd3b9bacf6 Update opener to 0.6.1 2023-07-16 12:37:23 -07:00
Eric Huss 5505d57066 Update to clap 4.3.12 2023-07-16 12:33:53 -07:00
Eric Huss cf88c4e720
Merge pull request #2116 from Stargateur/patch-1
Add oh-my-zsh quick exemple to shell completions
2023-07-16 10:59:37 -07:00
Eric Huss 9911e86039
Merge pull request #2118 from zica87/zica87-patch-1
Fix theme-color meta tag not syncing with the theme
2023-07-16 10:55:53 -07:00
zica 9eba0f6ab2
Fix theme-color meta tag not syncing with the theme 2023-07-09 08:57:46 +08:00
Antoine 6d265c1cce
Add oh-my-zsh quick exemple to shell completions
I have trouble to find this information, doesn't cost must to add it here.
2023-07-08 13:48:51 +02:00
Eric Huss 904aa530b5
Merge pull request #2111 from ehuss/bump-version
Update to 0.4.31
2023-06-29 13:10:11 -07:00
Eric Huss fa316f3edc Update to 0.4.31 2023-06-29 12:33:55 -07:00
Eric Huss 41d19e7338
Merge pull request #2110 from ehuss/strikethrough-single
Document that strikethrough can also use a single tilde.
2023-06-29 12:31:38 -07:00
Eric Huss 4f15a3f85c Document that strikethrough can also use a single tilde. 2023-06-29 12:27:06 -07:00
Eric Huss 222166ca5a
Merge pull request #2109 from ehuss/update-proc-macro2
Update proc-macro2
2023-06-29 12:22:50 -07:00
Eric Huss ab3eb81e52 Update proc-macro2 2023-06-29 10:57:45 -07:00
Eric Huss f37486a74f
Merge pull request #2106 from ehuss/spelling
Fix some spellings
2023-06-25 11:54:23 -07:00
Eric Huss a38b854338 Fix some spellings 2023-06-25 11:37:53 -07:00
Eric Huss e18113a746
Merge pull request #2104 from zqianem/gh-443/sidebar-scroll
Fix flicker when setting sidebar scroll position
2023-06-25 11:29:27 -07:00
Dylan DPC d4edbd1acf
Merge pull request #2105 from Spartan2909/patch-1
Fix typo
2023-06-24 20:38:35 +05:30
Caleb Robson 056e45a003
Fix typo 2023-06-24 15:35:40 +01:00
Em Zhan 72b3227824 Fix flicker when setting sidebar scroll position
Previously, sidebar scroll was set in an external script which caused a
flicker as the sidebar is initially rendered without any scroll before
being scrolled to the desired location.

Switching to an inline script right after the HTML tags for the sidebar
seems to avoid the flicker in most cases. In addition, logic is added to
avoid scrolling jumps when navigating via links within the sidebar.
2023-06-21 19:25:21 -05:00
Eric Huss a51f8a6b8e
Merge pull request #2101 from zqianem/gh-443/menu-border
Avoid menu border flash during page navigation
2023-06-19 13:21:08 -07:00
Em Zhan 1ef8d70ac4 Avoid menu border flash during page navigation
Partially addresses #443
2023-06-17 21:42:54 -05:00
Eric Huss a204946d39
Merge pull request #2096 from tshepang/patch-1
main branch is not always "master" these days
2023-06-07 10:01:06 -07:00
Tshepang Mbambo 3c7795cf44
main branch is not always "master" these days 2023-06-07 18:47:15 +02:00
Eric Huss 9349204636
Merge pull request #2094 from ehuss/bump-version
Update to 0.4.30
2023-05-28 15:06:19 -07:00
Eric Huss d2bcd04133 Update to 0.4.30 2023-05-28 14:54:05 -07:00
Eric Huss 61708ad0bd
Merge pull request #2093 from ehuss/hiddenlines
Support hidden lines in languages other than Rust
2023-05-28 14:21:54 -07:00
Eric Huss c9cfe22fd6 Apply some code style changes. 2023-05-28 14:04:58 -07:00
Eric Huss 5572d3d4de Expand on hidelines documentation. 2023-05-28 14:04:58 -07:00
Eric Huss 1441fe0b91 Explicitly document the `hidelines` key. 2023-05-28 14:04:58 -07:00
Jannik Obermann 7df1d8c838 Support hidden lines in languages other than Rust
Co-Authored-By: thecodewarrior <5467669+thecodewarrior@users.noreply.github.com>
2023-05-28 14:04:54 -07:00
Eric Huss 3a51abfcad
Merge pull request #2013 from ImUrX/heading-extension
Add heading extension support
2023-05-28 12:16:26 -07:00
Eric Huss 870e9086dc
Merge pull request #2092 from ehuss/update-pulldown-cmark
Update pulldown-cmark to 0.9.3
2023-05-28 12:12:26 -07:00
Eric Huss 1db52ff531 Fix search for custom heading attributes 2023-05-28 12:03:03 -07:00
Eric Huss e3be293420 Add an integration test for heading attributes 2023-05-28 12:02:59 -07:00
Eric Huss bbc32dff82 Update pulldown-cmark to 0.9.3 2023-05-28 12:00:00 -07:00
Eric Huss 861197e61c Add a test to the test_book for custom heading attributes 2023-05-28 11:33:24 -07:00
Eric Huss 34e5ef22a0 Don't include empty class attribute. 2023-05-28 11:33:00 -07:00
Eric Huss b141297651 Update documentation for heading attributes 2023-05-28 11:31:35 -07:00
Uriel 0cb977e603 docs suggestion
Co-authored-by: Eric Huss <eric@huss.org>
2023-05-28 10:47:14 -07:00
ImUrX c8a5adcee9 fix more mistakes 2023-05-28 10:47:14 -07:00
ImUrX ecdb411711 fix mistakes 2023-05-28 10:47:14 -07:00
ImUrX a4e206168d Add working heading extension 2023-05-28 10:47:13 -07:00
Dylan DPC 4f1b5eae54
Merge pull request #2091 from zjj/master
Update test_book highlight.md
2023-05-25 10:37:34 +05:30
zjj 54f14e89cf
Update test_book highlight.md
Add missing bash annotation
2023-05-25 12:04:43 +08:00
Eric Huss 1b3922d466
Fix typo 2023-05-13 12:52:42 -07:00
Eric Huss 00a30a9984
Merge pull request #2087 from ehuss/bump-version
Update to 0.4.29
2023-05-13 12:35:36 -07:00
Eric Huss db6699dae2 Update to 0.4.29 2023-05-13 12:26:29 -07:00
Eric Huss 4d229d7b94
Merge pull request #2086 from ehuss/sync-cargo.toml
Set minimum versions in Cargo.toml
2023-05-13 12:20:46 -07:00
Eric Huss d94c5f8380 Set minimum versions in Cargo.toml 2023-05-13 12:01:03 -07:00
Eric Huss 099217390e
Merge pull request #2085 from ehuss/update-clap
Update clap
2023-05-13 11:05:00 -07:00
Eric Huss 4c4ab8a57d Update clap 2023-05-13 10:53:22 -07:00
Eric Huss d746b23749
Merge pull request #2084 from ehuss/update-indirect
Update some indirect dependencies
2023-05-13 10:49:39 -07:00
Eric Huss f77c597e01 Update some indirect dependencies 2023-05-13 10:16:26 -07:00
Eric Huss 3c54a4d33b
Merge pull request #2083 from ehuss/fix-clippy
Apply some clippy fixes
2023-05-13 10:15:38 -07:00
Eric Huss cf9de82c2a
Merge pull request #2082 from ehuss/update-direct-dependencies
Update some direct dependencies
2023-05-13 09:56:47 -07:00
Eric Huss c3155e2642 Apply clippy::match_like_matches_macro 2023-05-13 09:55:51 -07:00
Eric Huss d8f171a996 Apply clippy::manual_while_let_some 2023-05-13 09:50:32 -07:00
Eric Huss 0ef3bb1cc6 Apply clippy::needless_borrowed_reference 2023-05-13 09:46:30 -07:00
Eric Huss 54df8234ed Apply clippy::let_unit_value 2023-05-13 09:45:46 -07:00
Eric Huss dc08e37320 Apply clippy::borrow_deref_ref 2023-05-13 09:44:50 -07:00
Eric Huss 45a8575b95 Apply clippy::needless_borrow 2023-05-13 09:44:11 -07:00
Eric Huss be966cfe1f Raise MSRV to 1.65 2023-05-13 09:41:10 -07:00
Eric Huss f4507aeb9b
Merge pull request #2080 from t2y/fix-copy-fonts-message
Fix handling of copy-fonts=true when fonts.css is overridden
2023-05-13 09:19:06 -07:00
Eric Huss 0985691fbd Update some direct dependencies 2023-05-13 09:12:23 -07:00
Eric Huss 01047846a9 Don't copy the stock fonts if the user has overridden fonts.css.
This wasn't behaving as I was really intending.
2023-05-13 09:05:25 -07:00
Eric Huss 75a6d65e5a Don't warn on copy-fonts=true (the default) when fonts.css is overridden. 2023-05-13 08:51:04 -07:00
Eric Huss 71ea92bbec
Merge pull request #2081 from cn-liutailin/patch-2
Update renderers.md
2023-05-12 16:01:21 -07:00
liutailin aac6de01de
Update renderers.md
Missing symbol ":"
2023-05-13 00:04:12 +08:00
Eric Huss af036d9f45
Merge pull request #2057 from seanpoulter/init-skip
fix(cli): init --force skips confirmation prompts
2023-04-30 14:18:30 -07:00
Tetsuya Morimoto 04016f3be6 Refactor the warning message related to copy_fonts so that a user simply configures it 2023-04-28 12:46:49 +09:00
Dylan DPC 41567b0456
Merge pull request #2076 from ehuss/gitignore
Switch from gitignore to ignore
2023-04-23 11:12:26 +05:30
Eric Huss 9db3a601ca
Merge pull request #2071 from expikr/patch-3
Added missing documentation for `mdbook init --ignore=<git|none>`
2023-04-22 12:55:46 -07:00
Eric Huss 35fdd00203 Switch from gitignore to ignore 2023-04-22 12:53:54 -07:00
expikr 7a435be018
Update init.md 2023-04-18 01:53:38 +08:00
Eric Huss dec0e24275
Merge pull request #2063 from rust-lang/dependabot/cargo/h2-0.3.17
Bump h2 from 0.3.15 to 0.3.17
2023-04-13 10:40:49 -07:00
dependabot[bot] c624fc078b
Bump h2 from 0.3.15 to 0.3.17
Bumps [h2](https://github.com/hyperium/h2) from 0.3.15 to 0.3.17.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.15...v0.3.17)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 17:04:53 +00:00
Sean Poulter b9c6b326b7 style(tests): Fixed issues reported by clippy 2023-04-03 08:36:57 -04:00
Sean Poulter 0003072623 docs(cli): Add docs for --force 2023-04-03 08:36:47 -04:00
Sean Poulter bffdb0b03d fix(cli): init --force skips confirmation prompts 2023-04-03 03:05:24 -04:00
Eric Huss b5ffc734a2
Merge pull request #2056 from deining/http_to_https
Convert links from http to https protocol
2023-04-02 14:52:32 -07:00
Andreas Deininger a2c88ae0f1 Convert links from http to https protocol 2023-04-02 21:35:08 +02:00
Eric Huss efb671aaf2
Merge pull request #2042 from ehuss/bump-version
Update to 0.4.28
2023-03-04 16:27:55 -08:00
Eric Huss a4b4b8f649 Update to 0.4.28 2023-03-04 15:40:56 -08:00
Eric Huss 4c59405e5c
Merge pull request #1986 from mgeisler/preprocessors-for-test
Run preprocessors in `mdbook test`
2023-03-04 14:55:10 -08:00
Eric Huss 703a215ef8
Merge pull request #2039 from Skwodo/master
Change overflow-x hidden to clip
2023-03-04 14:05:01 -08:00
Skwodo f5f96bc4f4 change overflow-x hidden to clip 2023-03-02 21:01:38 +01:00
Eric Huss 1668ab7877
Merge pull request #2025 from tshepang/patch-1
Update continuous-integration.md
2023-02-14 06:34:20 -08:00
Tshepang Mbambo 26fc0da9a9
Update continuous-integration.md
typo
2023-02-14 08:29:40 +02:00
Eric Huss c15220d1a1
Merge pull request #2017 from thanatos/roy/fix-sidebar
Default the sidebar to visible in large screens
2023-02-13 12:42:15 -08:00
Eric Huss 7c4562a8b3
Merge pull request #2023 from ehuss/bump-version
Update to 0.4.27
2023-02-13 08:07:35 -08:00
Eric Huss 6e3176f726 Update to 0.4.27 2023-02-13 07:54:28 -08:00
Eric Huss 958b456873
Also make sure releases use --locked
If it somehow gets out of sync, then the release process otherwise wouldn't catch it.
2023-02-13 07:53:32 -08:00
Eric Huss a43b5b69ab
Merge pull request #2022 from ehuss/ci-locked
Make sure CI runs with --locked
2023-02-13 07:51:26 -08:00
Eric Huss 1517435441
Merge pull request #2021 from rust-lang/revert-2009-deps/toml
Revert "bump 'toml' dependency"
2023-02-13 07:44:45 -08:00
Eric Huss 7abb28cb2e Make sure CI runs with --locked 2023-02-13 07:43:46 -08:00
Eric Huss 112fd4aac3
Merge pull request #2020 from mgeisler/patch-1
Remove spammy `debug!` log from `path_to_root`
2023-02-13 07:33:59 -08:00
Martin Geisler c150529c7c
Remove spammy `debug!` log from `path_to_root`
The log statement is empty and simply fills up the logs when you run `mdbook` with `RUST_LOG=debug`.
2023-02-13 16:17:31 +01:00
Roy Wellington Ⅳ fa6aa2ced8 Default the sidebar to visible in large screens
The code here leads me to believe that the intention is for the sidebar
to be default visible on large screens (where `clientWidth` > 1080) and
hidden otherwise.

However, as previously written, if the `localStorage.getItem` call fails
(for example, if the user agent is not accepting cookies), then we fall
back to `sidebar = sidebar || 'visible';` — but `sidebar` is already set
to `hidden`, so the `|| 'visible'` never happens.

This results in the sidebar hiding itself on every navigation through an
mdBook, meaning if you're just switching between sections trying to find
something that you keep needing to re-open the sidebar.
2023-02-11 18:04:58 -05:00
Martin Geisler b09aa0e65c Run preprocessors in `mdbook test`
While adding support for translations[1] to Comprehensive Rust 🦀, I
noticed that `mdbook test` doesn’t execute preprocessors the same way
as `mdbook build`.

This PR makes the two commands use the same code to find and execute
preprocessors.

[1]: https://github.com/google/comprehensive-rust/pull/130
2023-01-15 11:44:46 +01:00
73 changed files with 3175 additions and 1357 deletions

View File

@ -3,6 +3,13 @@ on:
release: release:
types: [created] types: [created]
defaults:
run:
shell: bash
permissions:
contents: write
jobs: jobs:
release: release:
name: Deploy Release name: Deploy Release
@ -28,17 +35,14 @@ jobs:
os: windows-latest os: windows-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Install hub
run: ci/install-hub.sh ${{ matrix.os }}
shell: bash
- name: Install Rust - name: Install Rust
run: ci/install-rust.sh stable ${{ matrix.target }} run: ci/install-rust.sh stable ${{ matrix.target }}
shell: bash - name: Build asset
- name: Build and deploy artifacts run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }}
- name: Update release with new asset
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ci/make-release.sh ${{ matrix.os }} ${{ matrix.target }} run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET
shell: bash
pages: pages:
name: GitHub Pages name: GitHub Pages
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -56,3 +60,14 @@ jobs:
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
cd guide/book cd guide/book
/tmp/deploy /tmp/deploy
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --no-verify

View File

@ -1,10 +1,7 @@
name: CI name: CI
on: on:
# Only run when merging to master, or open/synchronize/reopen a PR.
push:
branches:
- master
pull_request: pull_request:
merge_group:
jobs: jobs:
test: test:
@ -32,13 +29,13 @@ 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.71.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
run: cargo test run: cargo test --locked
- name: Test no default - name: Test no default
run: cargo test --no-default-features run: cargo test --no-default-features
@ -46,7 +43,24 @@ 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
# The success job is here to consolidate the total success/failure state of
# all other jobs. This job is then included in the GitHub branch protection
# rule which prevents merges unless all other jobs are passing. This makes
# it easier to manage the list of jobs via this yml file and to prevent
# accidentally adding new jobs without also updating the branch protections.
success:
name: Success gate
if: always()
needs:
- test
- rustfmt
runs-on: ubuntu-latest
steps:
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
- name: Done
run: exit 0

View File

@ -1,8 +1,180 @@
# Changelog # Changelog
## mdBook 0.4.37
[v0.4.36...v0.4.37](https://github.com/rust-lang/mdBook/compare/v0.4.36...v0.4.37)
### Changed
- ❗️ Updated the markdown parser. This brings in many changes to more closely follow the CommonMark spec. This may cause some small rendering changes. It is recommended to compare the output of the old and new version to check for changes. See <https://github.com/raphlinus/pulldown-cmark/releases/tag/v0.10.0> for more information.
[#2308](https://github.com/rust-lang/mdBook/pull/2308)
- The warning about the legacy `src/theme` directory has been removed.
[#2263](https://github.com/rust-lang/mdBook/pull/2263)
- Updated dependencies. MSRV raised to 1.71.0.
[#2283](https://github.com/rust-lang/mdBook/pull/2283)
[#2293](https://github.com/rust-lang/mdBook/pull/2293)
[#2297](https://github.com/rust-lang/mdBook/pull/2297)
[#2310](https://github.com/rust-lang/mdBook/pull/2310)
[#2309](https://github.com/rust-lang/mdBook/pull/2309)
- Some internal performance/memory improvements.
[#2273](https://github.com/rust-lang/mdBook/pull/2273)
[#2290](https://github.com/rust-lang/mdBook/pull/2290)
- Made the `pathdiff` dependency optional based on the `watch` feature.
[#2291](https://github.com/rust-lang/mdBook/pull/2291)
### Fixed
- The `s` shortcut key handler should not trigger when focus is in an HTML form.
[#2311](https://github.com/rust-lang/mdBook/pull/2311)
## mdBook 0.4.36
[v0.4.35...v0.4.36](https://github.com/rust-lang/mdBook/compare/v0.4.35...v0.4.36)
### Added
- Added Nim to the default highlighted languages.
[#2232](https://github.com/rust-lang/mdBook/pull/2232)
- Added a small indicator for the sidebar resize handle.
[#2209](https://github.com/rust-lang/mdBook/pull/2209)
### Changed
- Updated dependencies. MSRV raised to 1.70.0.
[#2173](https://github.com/rust-lang/mdBook/pull/2173)
[#2250](https://github.com/rust-lang/mdBook/pull/2250)
[#2252](https://github.com/rust-lang/mdBook/pull/2252)
### Fixed
- Fixed blank column in print page when the sidebar was visible.
[#2235](https://github.com/rust-lang/mdBook/pull/2235)
- Fixed indentation of code blocks when Javascript is disabled.
[#2162](https://github.com/rust-lang/mdBook/pull/2162)
- Fixed a panic when `mdbook serve` or `mdbook watch` were given certain kinds of paths.
[#2229](https://github.com/rust-lang/mdBook/pull/2229)
## mdBook 0.4.35
[v0.4.34...v0.4.35](https://github.com/rust-lang/mdBook/compare/v0.4.34...v0.4.35)
### Added
- Added the `book.text-direction` setting for explicit support for right-to-left languages.
[#1641](https://github.com/rust-lang/mdBook/pull/1641)
- Added `rel=prefetch` to the "next" links to potentially improve browser performance.
[#2168](https://github.com/rust-lang/mdBook/pull/2168)
- Added a `.warning` CSS class which is styled for displaying warning blocks.
[#2187](https://github.com/rust-lang/mdBook/pull/2187)
### Changed
- Better support of the sidebar when JavaScript is disabled.
[#2175](https://github.com/rust-lang/mdBook/pull/2175)
## mdBook 0.4.34
[v0.4.33...v0.4.34](https://github.com/rust-lang/mdBook/compare/v0.4.33...v0.4.34)
### Fixed
- Fixed file change watcher failing on macOS with a large number of files.
[#2157](https://github.com/rust-lang/mdBook/pull/2157)
## mdBook 0.4.33
[v0.4.32...v0.4.33](https://github.com/rust-lang/mdBook/compare/v0.4.32...v0.4.33)
### Added
- The `color-scheme` CSS property is now set based on the light/dark theme, which applies some slight color differences in browser elements like scroll bars on some browsers.
[#2134](https://github.com/rust-lang/mdBook/pull/2134)
### Fixed
- Fixed watching of extra-watch-dirs when not running in the book root directory.
[#2146](https://github.com/rust-lang/mdBook/pull/2146)
- Reverted the dependency update to the `toml` crate (again!). This was an unintentional breaking change in 0.4.32.
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
- Changed macOS change notifications to use the kqueue implementation which should fix some issues with repeated rebuilds when a file changed.
[#2152](https://github.com/rust-lang/mdBook/pull/2152)
- Don't set a background color in the print page for code blocks in a header.
[#2150](https://github.com/rust-lang/mdBook/pull/2150)
## mdBook 0.4.32
[v0.4.31...v0.4.32](https://github.com/rust-lang/mdBook/compare/v0.4.31...v0.4.32)
### Fixed
- Fixed theme-color meta tag not syncing with the theme.
[#2118](https://github.com/rust-lang/mdBook/pull/2118)
### Changed
- Updated all dependencies.
[#2121](https://github.com/rust-lang/mdBook/pull/2121)
[#2122](https://github.com/rust-lang/mdBook/pull/2122)
[#2123](https://github.com/rust-lang/mdBook/pull/2123)
[#2124](https://github.com/rust-lang/mdBook/pull/2124)
[#2125](https://github.com/rust-lang/mdBook/pull/2125)
[#2126](https://github.com/rust-lang/mdBook/pull/2126)
## 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
[v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28)
### Changed
- The sidebar is now shown on wide screens when localstorage is disabled.
[#2017](https://github.com/rust-lang/mdBook/pull/2017)
- Preprocessors are now run with `mdbook test`.
[#1986](https://github.com/rust-lang/mdBook/pull/1986)
### Fixed
- Fixed regression in 0.4.26 that prevented the title bar from scrolling properly on smaller screens.
[#2039](https://github.com/rust-lang/mdBook/pull/2039)
## mdBook 0.4.27
[v0.4.26...v0.4.27](https://github.com/rust-lang/mdBook/compare/v0.4.26...v0.4.27)
### Changed
- Reverted the dependency update to the `toml` crate. This was an unintentional breaking change in 0.4.26.
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
## mdBook 0.4.26 ## mdBook 0.4.26
[v0.4.25...v0.4.26](https://github.com/rust-lang/mdBook/compare/v0.4.25...v0.4.26) [v0.4.25...v0.4.26](https://github.com/rust-lang/mdBook/compare/v0.4.25...v0.4.26)
**The 0.4.26 release has been yanked due to an unintentional breaking change.**
### Changed ### Changed
- Removed custom scrollbars for webkit browsers - Removed custom scrollbars for webkit browsers
[#1961](https://github.com/rust-lang/mdBook/pull/1961) [#1961](https://github.com/rust-lang/mdBook/pull/1961)
@ -154,7 +326,7 @@
[#1771](https://github.com/rust-lang/mdBook/pull/1771) [#1771](https://github.com/rust-lang/mdBook/pull/1771)
- The 404 not-found page now includes the books title in the HTML title tag. - The 404 not-found page now includes the books title in the HTML title tag.
[#1693](https://github.com/rust-lang/mdBook/pull/1693) [#1693](https://github.com/rust-lang/mdBook/pull/1693)
- Migrated to clap 3.0 which which handles CLI option parsing. - Migrated to clap 3.0 which handles CLI option parsing.
[#1731](https://github.com/rust-lang/mdBook/pull/1731) [#1731](https://github.com/rust-lang/mdBook/pull/1731)
### Fixed ### Fixed

View File

@ -148,8 +148,28 @@ The following are instructions for updating [highlight.js](https://highlightjs.o
1. Clone the repository at <https://github.com/highlightjs/highlight.js> 1. Clone the repository at <https://github.com/highlightjs/highlight.js>
1. Check out a tagged release (like `10.1.1`). 1. Check out a tagged release (like `10.1.1`).
1. Run `npm install` 1. Run `npm install`
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml` 1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim nix properties r scala x86asm yaml`
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`. 1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js). 1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release. 1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine. 1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
## Publishing new releases
Instructions for mdBook maintainers to publish a new release:
1. Create a PR to update the version and update the CHANGELOG:
1. Update the version in `Cargo.toml`
2. Run `cargo test` to verify that everything is passing, and to update `Cargo.lock`.
3. Double-check for any SemVer breaking changes.
Try [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks), though beware that the current version of mdBook isn't properly adhering to SemVer due to the lack of `#[non_exhaustive]` and other issues. See https://github.com/rust-lang/mdBook/issues/1835.
4. Update `CHANGELOG.md` with any changes that users may be interested in.
5. Update `continuous-integration.md` to update the version number for the installation instructions.
6. Commit the changes, and open a PR.
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
```bash
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \
gh release create -R rust-lang/mdbook v$MDBOOK_VERS \
--title v$MDBOOK_VERS \
--notes "See https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#mdbook-${MDBOOK_VERS//.} for a complete list of changes."
```

1242
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mdbook" name = "mdbook"
version = "0.4.26" version = "0.4.37"
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,56 @@ 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.71"
[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.3.12", features = ["cargo", "wrap_help"] }
clap_complete = "4.0.6" clap_complete = "4.3.2"
once_cell = "1" once_cell = "1.17.1"
env_logger = "0.10.0" env_logger = "0.11.1"
handlebars = "4.0" handlebars = "5.0"
log = "0.4" log = "0.4.17"
memchr = "2.0" memchr = "2.5.0"
opener = "0.5" opener = "0.6.1"
pulldown-cmark = { version = "0.9.1", default-features = false } pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] }
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.3.0"
tempfile = "3.0" tempfile = "3.4.0"
toml = "0.5.1" toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
topological-sort = "0.2.2" topological-sort = "0.2.2"
# Watch feature # Watch feature
notify = { version = "5.0.0", optional = true } notify = { version = "6.1.1", optional = true }
notify-debouncer-mini = { version = "0.2.1", optional = true } notify-debouncer-mini = { version = "0.4.1", optional = true }
gitignore = { version = "1.0", optional = true } ignore = { version = "0.4.20", optional = true }
pathdiff = { version = "0.2.1", 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.6", 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 = "3.0.3"
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", "dep:pathdiff"]
serve = ["futures-util", "tokio", "warp"] serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
search = ["elasticlunr-rs", "ammonia"] search = ["dep:elasticlunr-rs", "dep:ammonia"]
[[bin]] [[bin]]
doc = false doc = false

View File

@ -1,24 +0,0 @@
#!/usr/bin/env bash
# Installs the `hub` executable into hub/bin
set -ex
case $1 in
ubuntu*)
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-linux-amd64-2.12.8.tgz -o hub.tgz
mkdir hub
tar -xzvf hub.tgz --strip=1 -C hub
;;
macos*)
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-darwin-amd64-2.12.8.tgz -o hub.tgz
mkdir hub
tar -xzvf hub.tgz --strip=1 -C hub
;;
windows*)
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-windows-amd64-2.12.8.zip -o hub.zip
7z x hub.zip -ohub
;;
*)
echo "OS should be first parameter, was: $1"
;;
esac
echo "$PWD/hub/bin" >> $GITHUB_PATH

View File

@ -17,7 +17,7 @@ then
export "CARGO_TARGET_$(echo $target | tr a-z- A-Z_)_LINKER"=rust-lld export "CARGO_TARGET_$(echo $target | tr a-z- A-Z_)_LINKER"=rust-lld
fi fi
export CARGO_PROFILE_RELEASE_LTO=true export CARGO_PROFILE_RELEASE_LTO=true
cargo build --bin mdbook --release --target $target cargo build --locked --bin mdbook --release --target $target
cd target/$target/release cd target/$target/release
case $1 in case $1 in
ubuntu*) ubuntu*)
@ -44,9 +44,10 @@ case $1 in
esac esac
cd ../.. cd ../..
if [[ -z "$GITHUB_TOKEN" ]] if [[ -z "$GITHUB_ENV" ]]
then then
echo "$GITHUB_TOKEN not set, skipping deploy." echo "GITHUB_ENV not set, run: gh release upload $TAG target/$asset"
else else
hub release edit -m "" --attach $asset $TAG echo "MDBOOK_TAG=$TAG" >> $GITHUB_ENV
echo "MDBOOK_ASSET=target/$asset" >> $GITHUB_ENV
fi fi

View File

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

View File

@ -7,8 +7,8 @@ mdbook build
``` ```
It will try to parse your `SUMMARY.md` file to understand the structure of your It will try to parse your `SUMMARY.md` file to understand the structure of your
book and fetch the corresponding files. Note that files mentioned in `SUMMARY.md` book and fetch the corresponding files. Note that this will also create files
but not present will be created. mentioned in `SUMMARY.md` which are not yet present.
The rendered output will maintain the same directory structure as the source for The rendered output will maintain the same directory structure as the source for
convenience. Large books will therefore remain structured when rendered. convenience. Large books will therefore remain structured when rendered.

View File

@ -6,7 +6,11 @@ This means when you type `mdbook` in your shell, you can then press your shell's
The completions first need to be installed for your shell: The completions first need to be installed for your shell:
```bash ```bash
# bash
mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook
# oh-my-zsh
mdbook completions zsh > ~/.oh-my-zsh/completions/_mdbook
autoload -U compinit && compinit
``` ```
The command prints a completion script for the given shell. The command prints a completion script for the given shell.

View File

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

View File

@ -6,8 +6,7 @@ of code examples that could get outdated. Therefore it is very important for
them to be able to automatically test these code examples. them to be able to automatically test these code examples.
mdBook supports a `test` command that will run all available tests in a book. At mdBook supports a `test` command that will run all available tests in a book. At
the moment, only rustdoc tests are supported, but this may be expanded upon in the moment, only Rust tests are supported.
the future.
#### Disable tests on a code block #### Disable tests on a code block

View File

@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
```sh ```sh
mkdir bin mkdir bin
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.26/mdbook-v0.4.26-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
bin/mdbook build bin/mdbook build
``` ```
@ -83,7 +83,7 @@ Or if you have your own style checks, spell checker, or any other tests it might
## Deploying ## Deploying
You may want to automatically deploy your book. You may want to automatically deploy your book.
Some may want to do this with every time a change is pushed, and others may want to only deploy when a specific release is tagged. Some may want to do this every time a change is pushed, and others may want to only deploy when a specific release is tagged.
You'll also need to understand the specifics on how to push a change to your web service. You'll also need to understand the specifics on how to push a change to your web service.
For example, [GitHub Pages] just requires committing the output onto a specific git branch. For example, [GitHub Pages] just requires committing the output onto a specific git branch.

View File

@ -287,7 +287,7 @@ like this:
+ if cfg.deny_odds && num_words % 2 == 1 { + if cfg.deny_odds && num_words % 2 == 1 {
+ eprintln!("{} has an odd number of words!", ch.name); + eprintln!("{} has an odd number of words!", ch.name);
+ process::exit(1); + process::exit(1);
} + }
} }
} }
} }

View File

@ -46,6 +46,9 @@ This is general information about your book.
`src` directly under the root folder. But this is configurable with the `src` `src` directly under the root folder. But this is configurable with the `src`
key in the configuration file. key in the configuration file.
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example. - **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
This is also used to derive the direction of text (RTL, LTR) within the book.
- **text-direction**: The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). Possible values: `ltr`, `rtl`.
When not specified, the text direction is derived from the book's `language` attribute.
**book.toml** **book.toml**
```toml ```toml
@ -55,6 +58,7 @@ authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples." description = "The example book covers examples."
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src` src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
language = "en" language = "en"
text-direction = "ltr"
``` ```
### Rust options ### Rust options
@ -97,7 +101,7 @@ extra-watch-dirs = [] # directories to watch for triggering builds
will be created when the book is built (i.e. `create-missing = true`). If this will be created when the book is built (i.e. `create-missing = true`). If this
is `false` then the build process will instead exit with an error if any files is `false` then the build process will instead exit with an error if any files
do not exist. do not exist.
- **use-default-preprocessors:** Disable the default preprocessors of (`links` & - **use-default-preprocessors:** Disable the default preprocessors (of `links` &
`index`) by setting this option to `false`. `index`) by setting this option to `false`.
If you have the same, and/or other preprocessors declared via their table If you have the same, and/or other preprocessors declared via their table

View File

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

View File

@ -97,7 +97,7 @@ description = "The example book covers examples."
theme = "my-theme" theme = "my-theme"
default-theme = "light" default-theme = "light"
preferred-dark-theme = "navy" preferred-dark-theme = "navy"
curly-quotes = true smart-punctuation = true
mathjax-support = false mathjax-support = false
copy-fonts = true copy-fonts = true
additional-css = ["custom.css", "custom2.css"] additional-css = ["custom.css", "custom2.css"]
@ -122,8 +122,10 @@ The following configuration options are available:
the browser requests the dark version of the site via the the browser requests the dark version of the site via the
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) ['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
CSS media query. Defaults to `navy`. CSS media query. Defaults to `navy`.
- **curly-quotes:** Convert straight quotes to curly quotes, except for those - **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
that occur in code blocks and code spans. Defaults to `false`. See [Smart Punctuation].
Defaults to `false`.
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to - **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
`false`. `false`.
- **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory. - **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
@ -150,9 +152,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 +184,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 +220,25 @@ runnable = true # displays a run button for rust code
- **copyable:** Display the copy button on code snippets. Defaults to `true`. - **copyable:** Display the copy button on code snippets. Defaults to `true`.
- **copy-js:** Copy JavaScript files for the editor to the output directory. - **copy-js:** Copy JavaScript files for the editor to the output directory.
Defaults to `true`. Defaults to `true`.
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`. - **line-numbers:** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`. - **runnable:** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
[Ace]: https://ace.c9.io/ [Ace]: https://ace.c9.io/
### `[output.html.code]`
The `[output.html.code]` table provides options for controlling code blocks.
```toml
[output.html.code]
# A prefix string per language (one or more chars).
# Any line starting with whitespace+prefix is hidden.
hidelines = { python = "~" }
```
- **hidelines:** A table that defines how [hidden code lines](../mdbook.md#hiding-code-lines) work for each language.
The key is the language and the value is a string that will cause code lines starting with that prefix to be hidden.
### `[output.html.search]` ### `[output.html.search]`
The `[output.html.search]` table provides options for controlling the built-in text [search]. The `[output.html.search]` table provides options for controlling the built-in text [search].

View File

@ -73,14 +73,14 @@ Linking to a URL or local file is easy:
```markdown ```markdown
Use [mdBook](https://github.com/rust-lang/mdBook). Use [mdBook](https://github.com/rust-lang/mdBook).
Read about [mdBook](mdBook.md). Read about [mdBook](mdbook.md).
A bare url: <https://www.rust-lang.org>. A bare url: <https://www.rust-lang.org>.
``` ```
Use [mdBook](https://github.com/rust-lang/mdBook). Use [mdBook](https://github.com/rust-lang/mdBook).
Read about [mdBook](mdBook.md). Read about [mdBook](mdbook.md).
A bare url: <https://www.rust-lang.org>. A bare url: <https://www.rust-lang.org>.
@ -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~~.
@ -214,9 +214,22 @@ characters:
So, no need to manually enter those Unicode characters! So, no need to manually enter those Unicode characters!
This feature is disabled by default. This feature is disabled by default.
To enable it, see the [`output.html.curly-quotes`] config option. To enable it, see the [`output.html.smart-punctuation`] config option.
[strikethrough]: https://github.github.com/gfm/#strikethrough-extension- [strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
[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.smart-punctuation`]: 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/pulldown-cmark/specs/heading_attrs.txt).

View File

@ -2,11 +2,11 @@
## Hiding code lines ## Hiding code lines
There is a feature in mdBook that lets you hide code lines by prepending them There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
with a `#` [like you would with Rustdoc][rustdoc-hide].
This currently only works with Rust language code blocks.
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
```bash ```bash
# fn main() { # fn main() {
@ -28,7 +28,47 @@ Will render as
# } # }
``` ```
The code block has an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines. When you tap or hover the mouse over the code block, there will be an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
By default, this only works for code examples that are annotated with `rust`.
However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s):
```toml
[output.html.code.hidelines]
python = "~"
```
The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this:
```bash
~hidden()
nothidden():
~ hidden()
~hidden()
nothidden()
```
will render as
```python
~hidden()
nothidden():
~ hidden()
~hidden()
nothidden()
```
This behavior can be overridden locally with a different prefix. This has the same effect as above:
~~~markdown
```python,hidelines=!!!
!!!hidden()
nothidden():
!!! hidden()
!!!hidden()
nothidden()
```
~~~
## Rust Playground ## Rust Playground
@ -274,3 +314,51 @@ contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
```hbs ```hbs
\{{#title My Title}} \{{#title My Title}}
``` ```
## HTML classes provided by mdBook
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
### `class="left"` and `"right"`
These classes are provided by default, for inline HTML to float images.
```html
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
```
### `class="hidden"`
HTML tags with class `hidden` will not be shown.
```html
<div class="hidden">This will not be seen.</div>
```
<div class="hidden">This will not be seen.</div>
### `class="warning"`
To make a warning or similar note stand out, wrap it in a warning div.
```html
<div class="warning">
This is a bad thing that you should pay attention to.
Warning blocks should be used sparingly in documentation, to avoid "warning
fatigue," where people are trained to ignore them because they usually don't
matter for what they're doing.
</div>
```
<div class="warning">
This is a bad thing that you should pay attention to.
Warning blocks should be used sparingly in documentation, to avoid "warning
fatigue," where people are trained to ignore them because they usually don't
matter for what they're doing.
</div>

View File

@ -29,11 +29,12 @@ to be ignored at best, or may cause an error when attempting to build the book.
- [First Chapter](relative/path/to/markdown2.md) - [First Chapter](relative/path/to/markdown2.md)
``` ```
1. ***Part Title*** - Headers can be used as a title for the following numbered 1. ***Part Title*** -
chapters. This can be used to logically separate different sections Level 1 headers can be used as a title for the following numbered chapters.
of the book. The title is rendered as unclickable text. This can be used to logically separate different sections of the book.
Titles are optional, and the numbered chapters can be broken into as many The title is rendered as unclickable text.
parts as desired. Titles are optional, and the numbered chapters can be broken into as many parts as desired.
Part titles must be h1 headers (one `#`), other heading levels are ignored.
```markdown ```markdown
# My Part Title # My Part Title

View File

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

View File

@ -79,7 +79,7 @@ var chapters = {{chapters}};
### 2. previous / next ### 2. previous / next
The previous and next helpers expose a `link` and `name` property to the The previous and next helpers expose a `link` and `title` property to the
previous and next chapters. previous and next chapters.
They are used like this They are used like this
@ -87,7 +87,7 @@ They are used like this
```handlebars ```handlebars
{{#previous}} {{#previous}}
<a href="{{link}}" class="nav-chapters previous"> <a href="{{link}}" class="nav-chapters previous">
<i class="fa fa-angle-left"></i> <i class="fa fa-angle-left"></i> {{title}}
</a> </a>
{{/previous}} {{/previous}}
``` ```

View File

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

View File

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

View File

@ -39,9 +39,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
.chain(summary.suffix_chapters.iter()) .chain(summary.suffix_chapters.iter())
.collect(); .collect();
while !items.is_empty() { while let Some(next) = items.pop() {
let next = items.pop().expect("already checked");
if let SummaryItem::Link(ref link) = *next { if let SummaryItem::Link(ref link) = *next {
if let Some(ref location) = link.location { if let Some(ref location) = link.location {
let filename = src_dir.join(location); let filename = src_dir.join(location);
@ -162,8 +160,20 @@ pub struct Chapter {
/// Nested items. /// Nested items.
pub sub_items: Vec<BookItem>, pub sub_items: Vec<BookItem>,
/// The chapter's location, relative to the `SUMMARY.md` file. /// The chapter's location, relative to the `SUMMARY.md` file.
///
/// **Note**: After the index preprocessor runs, any README files will be
/// modified to be `index.md`. If you need access to the actual filename
/// on disk, use [`Chapter::source_path`] instead.
///
/// This is `None` for a draft chapter.
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
/// The chapter's source file, relative to the `SUMMARY.md` file. /// The chapter's source file, relative to the `SUMMARY.md` file.
///
/// **Note**: Beware that README files will internally be treated as
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
/// exists if you need access to the true file path.
///
/// This is `None` for a draft chapter.
pub source_path: Option<PathBuf>, pub source_path: Option<PathBuf>,
/// An ordered list of the names of each chapter above this one in the hierarchy. /// An ordered list of the names of each chapter above this one in the hierarchy.
pub parent_names: Vec<String>, pub parent_names: Vec<String>,
@ -277,7 +287,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 +327,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);
@ -341,7 +351,6 @@ impl Display for Chapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::Write;
use tempfile::{Builder as TempFileBuilder, TempDir}; use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &str = " const DUMMY_SRC: &str = "

View File

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

View File

@ -15,10 +15,10 @@ pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use log::{debug, error, info, log_enabled, trace, warn}; use log::{debug, error, info, log_enabled, trace, warn};
use std::io::Write; use std::ffi::OsString;
use std::path::PathBuf; use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use std::string::ToString;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
use toml::Value; use toml::Value;
use topological_sort::TopologicalSort; use topological_sort::TopologicalSort;
@ -71,18 +71,24 @@ impl MDBook {
config.update_from_env(); config.update_from_env();
if config if let Some(html_config) = config.html_config() {
.html_config() if html_config.google_analytics.is_some() {
.map_or(false, |html| html.google_analytics.is_some()) warn!(
{ "The output.html.google-analytics field has been deprecated; \
warn!( it will be removed in a future release.\n\
"The output.html.google-analytics field has been deprecated; \ Consider placing the appropriate site tag code into the \
it will be removed in a future release.\n\ theme/head.hbs file instead.\n\
Consider placing the appropriate site tag code into the \ The tracking code may be found in the Google Analytics Admin page.\n\
theme/head.hbs file instead.\n\ "
The tracking code may be found in the Google Analytics Admin page.\n\ );
" }
); if html_config.curly_quotes {
warn!(
"The output.html.curly-quotes field has been renamed to \
output.html.smart-punctuation.\n\
Use the new name in book.toml to remove this warning."
);
}
} }
if log_enabled!(log::Level::Trace) { if log_enabled!(log::Level::Trace) {
@ -99,7 +105,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 +128,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)?;
@ -196,21 +202,26 @@ impl MDBook {
Ok(()) Ok(())
} }
/// Run the entire build process for a particular [`Renderer`]. /// Run preprocessors and return the final book.
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> { pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new( let preprocess_ctx = PreprocessorContext::new(
self.root.clone(), self.root.clone(),
self.config.clone(), self.config.clone(),
renderer.name().to_string(), renderer.name().to_string(),
); );
let mut preprocessed_book = self.book.clone();
for preprocessor in &self.preprocessors { for preprocessor in &self.preprocessors {
if preprocessor_should_run(&**preprocessor, renderer, &self.config) { if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
debug!("Running the {} preprocessor.", preprocessor.name()); debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?; preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
} }
} }
Ok((preprocessed_book, preprocess_ctx))
}
/// Run the entire build process for a particular [`Renderer`].
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
let name = renderer.name(); let name = renderer.name();
let build_dir = self.build_dir_for(name); let build_dir = self.build_dir_for(name);
@ -254,24 +265,45 @@ impl MDBook {
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries. /// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
/// If `chapter` is `None`, all tests will be run. /// If `chapter` is `None`, all tests will be run.
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> { pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
let library_args: Vec<&str> = (0..library_paths.len()) let cwd = std::env::current_dir()?;
.map(|_| "-L") let library_args: Vec<OsString> = library_paths
.zip(library_paths.into_iter()) .into_iter()
.flat_map(|x| vec![x.0, x.1]) .flat_map(|path| {
let path = Path::new(path);
let path = if path.is_relative() {
cwd.join(path).into_os_string()
} else {
path.to_path_buf().into_os_string()
};
[OsString::from("-L"), path]
})
.collect(); .collect();
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?; let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let mut chapter_found = false; let mut chapter_found = false;
// FIXME: Is "test" the proper renderer name to use here? struct TestRenderer;
let preprocess_context = impl Renderer for TestRenderer {
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string()); // FIXME: Is "test" the proper renderer name to use here?
fn name(&self) -> &str {
"test"
}
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?; fn render(&self, _: &RenderContext) -> Result<()> {
// Index Preprocessor is disabled so that chapter paths continue to point to the Ok(())
// actual markdown files. }
}
// Index Preprocessor is disabled so that chapter paths
// continue to point to the actual markdown files.
self.preprocessors = determine_preprocessors(&self.config)?
.into_iter()
.filter(|pre| pre.name() != IndexPreprocessor::NAME)
.collect();
let (book, _) = self.preprocess_book(&TestRenderer)?;
let color_output = std::io::stderr().is_terminal();
let mut failed = false; let mut failed = false;
for item in book.iter() { for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item { if let BookItem::Chapter(ref ch) = *item {
@ -292,27 +324,34 @@ 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())?;
let mut cmd = Command::new("rustdoc"); let mut cmd = Command::new("rustdoc");
cmd.arg(&path).arg("--test").args(&library_args); cmd.current_dir(temp_dir.path())
.arg(&chapter_path)
.arg("--test")
.args(&library_args);
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"]);
} }
} }
} }
if color_output {
cmd.args(&["--color", "always"]);
}
debug!("running {:?}", cmd); debug!("running {:?}", cmd);
let output = cmd.output()?; let output = cmd.output()?;
@ -588,7 +627,7 @@ fn preprocessor_should_run(
mod tests { mod tests {
use super::*; use super::*;
use std::str::FromStr; use std::str::FromStr;
use toml::value::{Table, Value}; use toml::value::Table;
#[test] #[test]
fn config_defaults_to_html_renderer_if_empty() { fn config_defaults_to_html_renderer_if_empty() {

View File

@ -1,10 +1,9 @@
use crate::errors::*; use crate::errors::*;
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use memchr::{self, Memchr}; use memchr::Memchr;
use pulldown_cmark::{self, Event, HeadingLevel, Tag}; use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::iter::FromIterator;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -163,7 +162,7 @@ impl From<Link> for SummaryItem {
/// > match the following regex: "[^<>\n[]]+". /// > match the following regex: "[^<>\n[]]+".
struct SummaryParser<'a> { struct SummaryParser<'a> {
src: &'a str, src: &'a str,
stream: pulldown_cmark::OffsetIter<'a, 'a>, stream: pulldown_cmark::OffsetIter<'a, DefaultBrokenLinkCallback>,
offset: usize, offset: usize,
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it /// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
@ -210,7 +209,7 @@ macro_rules! collect_events {
} }
impl<'a> SummaryParser<'a> { impl<'a> SummaryParser<'a> {
fn new(text: &str) -> SummaryParser<'_> { fn new(text: &'a str) -> SummaryParser<'a> {
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter(); let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
SummaryParser { SummaryParser {
@ -265,7 +264,12 @@ impl<'a> SummaryParser<'a> {
loop { loop {
match self.next_event() { match self.next_event() {
Some(ev @ Event::Start(Tag::List(..))) Some(ev @ Event::Start(Tag::List(..)))
| Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { | Some(
ev @ Event::Start(Tag::Heading {
level: HeadingLevel::H1,
..
}),
) => {
if is_prefix { if is_prefix {
// we've finished prefix chapters and are at the start // we've finished prefix chapters and are at the start
// of the numbered section. // of the numbered section.
@ -275,8 +279,8 @@ impl<'a> SummaryParser<'a> {
bail!(self.parse_error("Suffix chapters cannot be followed by a list")); bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
} }
} }
Some(Event::Start(Tag::Link(_type, href, _title))) => { Some(Event::Start(Tag::Link { dest_url, .. })) => {
let link = self.parse_link(href.to_string()); let link = self.parse_link(dest_url.to_string());
items.push(SummaryItem::Link(link)); items.push(SummaryItem::Link(link));
} }
Some(Event::Rule) => items.push(SummaryItem::Separator), Some(Event::Rule) => items.push(SummaryItem::Separator),
@ -304,10 +308,13 @@ impl<'a> SummaryParser<'a> {
break; break;
} }
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { Some(Event::Start(Tag::Heading {
level: HeadingLevel::H1,
..
})) => {
debug!("Found a h1 in the SUMMARY"); debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
Some(stringify_events(tags)) Some(stringify_events(tags))
} }
@ -336,7 +343,7 @@ impl<'a> SummaryParser<'a> {
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened. /// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
fn parse_link(&mut self, href: String) -> Link { fn parse_link(&mut self, href: String) -> Link {
let href = href.replace("%20", " "); let href = href.replace("%20", " ");
let link_content = collect_events!(self.stream, end Tag::Link(..)); let link_content = collect_events!(self.stream, end TagEnd::Link);
let name = stringify_events(link_content); let name = stringify_events(link_content);
let path = if href.is_empty() { let path = if href.is_empty() {
@ -377,7 +384,12 @@ impl<'a> SummaryParser<'a> {
} }
// The expectation is that pulldown cmark will terminate a paragraph before a new // The expectation is that pulldown cmark will terminate a paragraph before a new
// heading, so we can always count on this to return without skipping headings. // heading, so we can always count on this to return without skipping headings.
Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { Some(
ev @ Event::Start(Tag::Heading {
level: HeadingLevel::H1,
..
}),
) => {
// we're starting a new part // we're starting a new part
self.back(ev); self.back(ev);
break; break;
@ -398,7 +410,7 @@ impl<'a> SummaryParser<'a> {
// Skip over the contents of this tag // Skip over the contents of this tag
while let Some(event) = self.next_event() { while let Some(event) = self.next_event() {
if event == Event::End(other_tag.clone()) { if event == Event::End(other_tag.clone().into()) {
break; break;
} }
} }
@ -469,7 +481,7 @@ impl<'a> SummaryParser<'a> {
last_item.nested_items = sub_items; last_item.nested_items = sub_items;
} }
Some(Event::End(Tag::List(..))) => break, Some(Event::End(TagEnd::List(..))) => break,
Some(_) => {} Some(_) => {}
None => break, None => break,
} }
@ -486,8 +498,8 @@ impl<'a> SummaryParser<'a> {
loop { loop {
match self.next_event() { match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => continue, Some(Event::Start(Tag::Paragraph)) => continue,
Some(Event::Start(Tag::Link(_type, href, _title))) => { Some(Event::Start(Tag::Link { dest_url, .. })) => {
let mut link = self.parse_link(href.to_string()); let mut link = self.parse_link(dest_url.to_string());
let mut number = parent.clone(); let mut number = parent.clone();
number.0.push(num_existing_items as u32 + 1); number.0.push(num_existing_items as u32 + 1);
@ -529,14 +541,18 @@ impl<'a> SummaryParser<'a> {
fn parse_title(&mut self) -> Option<String> { fn parse_title(&mut self) -> Option<String> {
loop { loop {
match self.next_event() { match self.next_event() {
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { Some(Event::Start(Tag::Heading {
level: HeadingLevel::H1,
..
})) => {
debug!("Found a h1 in the SUMMARY"); debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
return Some(stringify_events(tags)); return Some(stringify_events(tags));
} }
// Skip a HTML element such as a comment line. // Skip a HTML element such as a comment line.
Some(Event::Html(_)) => {} Some(Event::Html(_) | Event::InlineHtml(_))
| Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
// Otherwise, no title. // Otherwise, no title.
Some(ev) => { Some(ev) => {
self.back(ev); self.back(ev);
@ -744,7 +760,7 @@ mod tests {
let _ = parser.stream.next(); // Discard opening paragraph let _ = parser.stream.next(); // Discard opening paragraph
let href = match parser.stream.next() { let href = match parser.stream.next() {
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(), Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
other => panic!("Unreachable, {:?}", other), other => panic!("Unreachable, {:?}", other),
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use super::command_prelude::*; use super::command_prelude::*;
use crate::get_book_dir; use crate::get_book_dir;
use clap::builder::NonEmptyStringValueParser; use clap::builder::NonEmptyStringValueParser;
use clap::{Arg, ArgAction, ArgMatches, Command}; use clap::ArgAction;
use mdbook::errors::Result; use mdbook::errors::Result;
use mdbook::MDBook; use mdbook::MDBook;
use std::path::PathBuf; use std::path::PathBuf;
@ -44,7 +44,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str()); let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str());
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?; let mut book = MDBook::load(book_dir)?;
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") { if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.to_path_buf(); book.config.build.build_dir = dest_dir.to_path_buf();

View File

@ -1,8 +1,10 @@
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;
use pathdiff::diff_paths;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::thread::sleep; use std::thread::sleep;
@ -20,7 +22,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 +43,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 +64,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 +87,22 @@ 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> { // Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
// For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
let ignore_root = ignore
.path()
.canonicalize()
.expect("ignore root canonicalize error");
paths paths
.iter() .iter()
.filter(|path| match exclusion_checker.is_excluded(path) { .filter(|path| {
Ok(exclude) => !exclude, let relative_path =
Err(error) => { diff_paths(&path, &ignore_root).expect("One of the paths should be an absolute");
warn!( !ignore
"Unable to determine if {:?} is excluded: {:?}. Including it.", .matched_path_or_any_parents(&relative_path, relative_path.is_dir())
&path, error .is_ignore()
);
true
}
}) })
.map(|path| path.to_path_buf()) .map(|path| path.to_path_buf())
.collect() .collect()
@ -112,8 +118,7 @@ where
// Create a channel to receive the events. // Create a channel to receive the events.
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), None, tx) let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) {
{
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
error!("Error while trying to watch the files:\n\n\t{:?}", e); error!("Error while trying to watch the files:\n\n\t{:?}", e);
@ -134,11 +139,16 @@ where
let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive); let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
for dir in &book.config.build.extra_watch_dirs { for dir in &book.config.build.extra_watch_dirs {
let path = dir.canonicalize().unwrap(); let path = book.root.join(dir);
if let Err(e) = watcher.watch(&path, Recursive) { let canonical_path = path.canonicalize().unwrap_or_else(|e| {
error!("Error while watching extra directory {path:?}:\n {e}");
std::process::exit(1);
});
if let Err(e) = watcher.watch(&canonical_path, Recursive) {
error!( error!(
"Error while watching extra directory {:?}:\n {:?}", "Error while watching extra directory {:?}:\n {:?}",
path, e canonical_path, e
); );
std::process::exit(1); std::process::exit(1);
} }
@ -156,10 +166,8 @@ where
let paths: Vec<_> = all_events let paths: Vec<_> = all_events
.filter_map(|event| match event { .filter_map(|event| match event {
Ok(events) => Some(events), Ok(events) => Some(events),
Err(errors) => { Err(error) => {
for error in errors { log::warn!("error while watching for changes: {error}");
log::warn!("error while watching for changes: {error}");
}
None None
} }
}) })
@ -178,3 +186,44 @@ where
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use ignore::gitignore::GitignoreBuilder;
use std::env;
#[test]
fn test_filter_ignored_files() {
let current_dir = env::current_dir().unwrap();
let ignore = GitignoreBuilder::new(&current_dir)
.add_line(None, "*.html")
.unwrap()
.build()
.unwrap();
let should_remain = current_dir.join("record.text");
let should_filter = current_dir.join("index.html");
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
assert_eq!(remain, vec![should_remain])
}
#[test]
fn filter_ignored_files_should_handle_parent_dir() {
let current_dir = env::current_dir().unwrap();
let ignore = GitignoreBuilder::new(&current_dir)
.add_line(None, "*.html")
.unwrap()
.build()
.unwrap();
let parent_dir = current_dir.join("..");
let should_remain = parent_dir.join("record.text");
let should_filter = parent_dir.join("index.html");
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
assert_eq!(remain, vec![should_remain])
}
}

View File

@ -58,7 +58,7 @@ use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use toml::value::Table; use toml::value::Table;
use toml::{self, Value}; use toml::Value;
use crate::errors::*; use crate::errors::*;
use crate::utils::{self, toml_ext::TomlExt}; use crate::utils::{self, toml_ext::TomlExt};
@ -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));
} }
@ -411,6 +411,9 @@ pub struct BookConfig {
pub multilingual: bool, pub multilingual: bool,
/// The main language of the book. /// The main language of the book.
pub language: Option<String>, pub language: Option<String>,
/// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL).
/// When not specified, the text direction is derived from [`BookConfig::language`].
pub text_direction: Option<TextDirection>,
} }
impl Default for BookConfig { impl Default for BookConfig {
@ -422,6 +425,43 @@ impl Default for BookConfig {
src: PathBuf::from("src"), src: PathBuf::from("src"),
multilingual: false, multilingual: false,
language: Some(String::from("en")), language: Some(String::from("en")),
text_direction: None,
}
}
}
impl BookConfig {
/// Gets the realized text direction, either from [`BookConfig::text_direction`]
/// or derived from [`BookConfig::language`], to be used by templating engines.
pub fn realized_text_direction(&self) -> TextDirection {
if let Some(direction) = self.text_direction {
direction
} else {
TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
}
}
}
/// Text direction to use for HTML output
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum TextDirection {
/// Left to right.
#[serde(rename = "ltr")]
LeftToRight,
/// Right to left
#[serde(rename = "rtl")]
RightToLeft,
}
impl TextDirection {
/// Gets the text direction from language code
pub fn from_lang_code(code: &str) -> Self {
match code {
// list sourced from here: https://github.com/abarrak/rtl/blob/master/lib/rtl/core.rb#L16
"ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
| "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
| "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
_ => TextDirection::LeftToRight,
} }
} }
} }
@ -486,7 +526,9 @@ pub struct HtmlConfig {
/// The theme to use if the browser requests the dark version of the site. /// The theme to use if the browser requests the dark version of the site.
/// Defaults to 'navy'. /// Defaults to 'navy'.
pub preferred_dark_theme: Option<String>, pub preferred_dark_theme: Option<String>,
/// Use "smart quotes" instead of the usual `"` character. /// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
pub smart_punctuation: bool,
/// Deprecated alias for `smart_punctuation`.
pub curly_quotes: bool, pub curly_quotes: bool,
/// Should mathjax be enabled? /// Should mathjax be enabled?
pub mathjax_support: bool, pub mathjax_support: bool,
@ -504,6 +546,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.
@ -548,6 +592,7 @@ impl Default for HtmlConfig {
theme: None, theme: None,
default_theme: None, default_theme: None,
preferred_dark_theme: None, preferred_dark_theme: None,
smart_punctuation: false,
curly_quotes: false, curly_quotes: false,
mathjax_support: false, mathjax_support: false,
copy_fonts: true, copy_fonts: true,
@ -556,6 +601,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,
@ -580,6 +626,11 @@ impl HtmlConfig {
None => root.join("theme"), None => root.join("theme"),
} }
} }
/// Returns `true` if smart punctuation is enabled.
pub fn smart_punctuation(&self) -> bool {
self.smart_punctuation || self.curly_quotes
}
} }
/// Configuration for how to render the print icon, print.html, and print.css. /// Configuration for how to render the print icon, print.html, and print.css.
@ -613,7 +664,7 @@ pub struct Fold {
pub level: u8, pub level: u8,
} }
/// Configuration for tweaking how the the HTML renderer handles the playground. /// Configuration for tweaking how the HTML renderer handles the playground.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")] #[serde(default, rename_all = "kebab-case")]
pub struct Playground { pub struct Playground {
@ -642,6 +693,22 @@ impl Default for Playground {
} }
} }
/// Configuration for tweaking how 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 +770,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;
} }
@ -739,7 +806,7 @@ mod tests {
[output.html] [output.html]
theme = "./themedir" theme = "./themedir"
default-theme = "rust" default-theme = "rust"
curly-quotes = true smart-punctuation = true
google-analytics = "123456" google-analytics = "123456"
additional-css = ["./foo/bar/baz.css"] additional-css = ["./foo/bar/baz.css"]
git-repository-url = "https://foo.com/" git-repository-url = "https://foo.com/"
@ -769,6 +836,7 @@ mod tests {
multilingual: true, multilingual: true,
src: PathBuf::from("source"), src: PathBuf::from("source"),
language: Some(String::from("ja")), language: Some(String::from("ja")),
text_direction: None,
}; };
let build_should_be = BuildConfig { let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"), build_dir: PathBuf::from("outputs"),
@ -785,7 +853,7 @@ mod tests {
runnable: true, runnable: true,
}; };
let html_should_be = HtmlConfig { let html_should_be = HtmlConfig {
curly_quotes: true, smart_punctuation: true,
google_analytics: Some(String::from("123456")), google_analytics: Some(String::from("123456")),
additional_css: vec![PathBuf::from("./foo/bar/baz.css")], additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
theme: Some(PathBuf::from("./themedir")), theme: Some(PathBuf::from("./themedir")),
@ -965,7 +1033,7 @@ mod tests {
[output.html] [output.html]
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book` destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
theme = "my-theme" theme = "my-theme"
curly-quotes = true smart-punctuation = true
google-analytics = "123456" google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"] additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"] additional-js = ["custom.js"]
@ -990,7 +1058,7 @@ mod tests {
let html_should_be = HtmlConfig { let html_should_be = HtmlConfig {
theme: Some(PathBuf::from("my-theme")), theme: Some(PathBuf::from("my-theme")),
curly_quotes: true, smart_punctuation: true,
google_analytics: Some(String::from("123456")), google_analytics: Some(String::from("123456")),
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")], additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
additional_js: vec![PathBuf::from("custom.js")], additional_js: vec![PathBuf::from("custom.js")],
@ -1121,6 +1189,73 @@ mod tests {
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html"); assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
} }
#[test]
fn text_direction_ltr() {
let src = r#"
[book]
text-direction = "ltr"
"#;
let got = Config::from_str(src).unwrap();
assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
}
#[test]
fn text_direction_rtl() {
let src = r#"
[book]
text-direction = "rtl"
"#;
let got = Config::from_str(src).unwrap();
assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
}
#[test]
fn text_direction_none() {
let src = r#"
[book]
"#;
let got = Config::from_str(src).unwrap();
assert_eq!(got.book.text_direction, None);
}
#[test]
fn test_text_direction() {
let mut cfg = BookConfig::default();
// test deriving the text direction from language codes
cfg.language = Some("ar".into());
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
cfg.language = Some("he".into());
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
cfg.language = Some("en".into());
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
cfg.language = Some("ja".into());
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
// test forced direction
cfg.language = Some("ar".into());
cfg.text_direction = Some(TextDirection::LeftToRight);
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
cfg.language = Some("ar".into());
cfg.text_direction = Some(TextDirection::RightToLeft);
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
cfg.language = Some("en".into());
cfg.text_direction = Some(TextDirection::LeftToRight);
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
cfg.language = Some("en".into());
cfg.text_direction = Some(TextDirection::RightToLeft);
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
}
#[test] #[test]
#[should_panic(expected = "Invalid configuration file")] #[should_panic(expected = "Invalid configuration file")]
fn invalid_language_type_error() { fn invalid_language_type_error() {
@ -1193,4 +1328,37 @@ mod tests {
assert!(html_config.print.enable); assert!(html_config.print.enable);
assert!(!html_config.print.page_break); assert!(!html_config.print.page_break);
} }
#[test]
fn curly_quotes_or_smart_punctuation() {
let src = r#"
[book]
title = "mdBook Documentation"
[output.html]
smart-punctuation = true
"#;
let config = Config::from_str(src).unwrap();
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
let src = r#"
[book]
title = "mdBook Documentation"
[output.html]
curly-quotes = true
"#;
let config = Config::from_str(src).unwrap();
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
let src = r#"
[book]
title = "mdBook Documentation"
"#;
let config = Config::from_str(src).unwrap();
assert_eq!(
config.html_config().unwrap_or_default().smart_punctuation(),
false
);
}
} }

View File

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

View File

@ -1,5 +1,5 @@
use crate::book::{Book, BookItem}; use crate::book::{Book, BookItem};
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition}; use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
use crate::errors::*; use crate::errors::*;
use crate::renderer::html_handlebars::helpers; use crate::renderer::html_handlebars::helpers;
use crate::renderer::{RenderContext, Renderer}; use crate::renderer::{RenderContext, Renderer};
@ -54,11 +54,13 @@ impl HtmlHandlebars {
.insert("git_repository_edit_url".to_owned(), json!(edit_url)); .insert("git_repository_edit_url".to_owned(), json!(edit_url));
} }
let content = ch.content.clone(); let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation());
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
let fixed_content = let fixed_content = utils::render_markdown_with_path(
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path)); &ch.content,
ctx.html_config.smart_punctuation(),
Some(path),
);
if !ctx.is_index && ctx.html_config.print.page_break { if !ctx.is_index && ctx.html_config.print.page_break {
// Add page break between chapters // Add page break between chapters
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
@ -99,7 +101,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 +112,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 +128,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())?;
} }
@ -156,7 +167,8 @@ impl HtmlHandlebars {
.to_string() .to_string()
} }
}; };
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes); let html_content_404 =
utils::render_markdown(&content_404, html_config.smart_punctuation());
let mut data_404 = data.clone(); let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url { let base_url = if let Some(site_url) = &html_config.site_url {
@ -182,8 +194,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 +211,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 +293,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 +310,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."
); );
} }
@ -469,25 +481,6 @@ impl HtmlHandlebars {
} }
} }
// TODO(mattico): Remove some time after the 0.1.8 release
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
Ok(entry.file_type()?.is_file()
&& entry.path().extension().map_or(false, |ext| ext == "md"))
}
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
return Ok(false);
}
}
Ok(true)
} else {
Ok(false)
}
}
impl Renderer for HtmlHandlebars { impl Renderer for HtmlHandlebars {
fn name(&self) -> &str { fn name(&self) -> &str {
"html" "html"
@ -520,16 +513,6 @@ impl Renderer for HtmlHandlebars {
None => ctx.root.join("theme"), None => ctx.root.join("theme"),
}; };
if html_config.theme.is_none()
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
{
warn!(
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
theme directory"
);
warn!("Please move your theme files to `./theme` for them to continue being used");
}
let theme = theme::Theme::new(theme_dir); let theme = theme::Theme::new(theme_dir);
debug!("Register the index handlebars template"); debug!("Register the index handlebars template");
@ -553,7 +536,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 +572,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 ✓");
@ -635,6 +622,10 @@ fn make_data(
"language".to_owned(), "language".to_owned(),
json!(config.book.language.clone().unwrap_or_default()), json!(config.book.language.clone().unwrap_or_default()),
); );
data.insert(
"text_direction".to_owned(),
json!(config.book.realized_text_direction()),
);
data.insert( data.insert(
"book_title".to_owned(), "book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()), json!(config.book.title.clone().unwrap_or_default()),
@ -795,8 +786,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 +799,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 +824,21 @@ fn build_header_links(html: &str) -> String {
fn insert_link_into_header( fn insert_link_into_header(
level: usize, level: usize,
content: &str, content: &str,
id: Option<String>,
classes: Option<String>,
id_counter: &mut HashMap<String, usize>, id_counter: &mut HashMap<String, usize>,
) -> String { ) -> String {
let id = utils::unique_id_from_content(content, id_counter); let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
let classes = classes
.map(|s| format!(" class=\"{s}\""))
.unwrap_or_default();
format!( format!(
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##, r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{text}</a></h{level}>"##,
level = level, level = level,
id = id, id = id,
text = content text = content,
classes = classes
) )
} }
@ -856,67 +870,64 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned() .into_owned()
} }
static CODE_BLOCK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
fn add_playground_pre( fn add_playground_pre(
html: &str, html: &str,
playground_config: &Playground, playground_config: &Playground,
edition: Option<RustEdition>, edition: Option<RustEdition>,
) -> String { ) -> String {
static ADD_PLAYGROUND_PRE: Lazy<Regex> = CODE_BLOCK_RE
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
ADD_PLAYGROUND_PRE
.replace_all(html, |caps: &Captures<'_>| { .replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1]; let text = &caps[1];
let classes = &caps[2]; let classes = &caps[2];
let code = &caps[3]; let code = &caps[3];
if classes.contains("language-rust") { if classes.contains("language-rust")
if (!classes.contains("ignore") && ((!classes.contains("ignore")
&& !classes.contains("noplayground") && !classes.contains("noplayground")
&& !classes.contains("noplaypen") && !classes.contains("noplaypen")
&& playground_config.runnable) && playground_config.runnable)
|| classes.contains("mdbook-runnable") || classes.contains("mdbook-runnable"))
{ {
let contains_e2015 = classes.contains("edition2015"); let contains_e2015 = classes.contains("edition2015");
let contains_e2018 = classes.contains("edition2018"); let contains_e2018 = classes.contains("edition2018");
let contains_e2021 = classes.contains("edition2021"); let contains_e2021 = classes.contains("edition2021");
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 { let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
// the user forced edition, we should not overwrite it // the user forced edition, we should not overwrite it
"" ""
} else {
match edition {
Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018",
Some(RustEdition::E2021) => " edition2021",
None => "",
}
};
// wrap the contents in an external pre block
format!(
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
classes,
edition_class,
{
let content: Cow<'_, str> = if playground_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
.into()
};
hide_lines(&content)
}
)
} else { } else {
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code)) match edition {
} Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018",
Some(RustEdition::E2021) => " edition2021",
None => "",
}
};
// wrap the contents in an external pre block
format!(
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
classes,
edition_class,
{
let content: Cow<'_, str> = if playground_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
.into()
};
content
}
)
} else { } else {
// not language-rust, so no-op // not language-rust, so no-op
text.to_owned() text.to_owned()
@ -925,7 +936,51 @@ 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 {
static LANGUAGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
static HIDELINES_REGEX: Lazy<Regex> = Lazy::new(|| 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 +1013,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();
@ -992,7 +1067,10 @@ struct RenderItemContext<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::config::TextDirection;
use super::*; use super::*;
use pretty_assertions::assert_eq;
#[test] #[test]
fn original_build_header_links() { fn original_build_header_links() {
@ -1021,6 +1099,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 +1126,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 +1156,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 +1180,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 +1204,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 +1224,66 @@ 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);
}
}
#[test]
fn test_json_direction() {
assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
}
} }

View File

@ -1,7 +1,9 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable}; use handlebars::{
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, Renderable,
};
use crate::utils; use crate::utils;
use log::{debug, trace}; use log::{debug, trace};
@ -26,9 +28,9 @@ impl Target {
) -> Result<Option<StringMap>, RenderError> { ) -> Result<Option<StringMap>, RenderError> {
match *self { match *self {
Target::Next => { Target::Next => {
let previous_path = previous_item let previous_path = previous_item.get("path").ok_or_else(|| {
.get("path") RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?; })?;
if previous_path == base_path { if previous_path == base_path {
return Ok(Some(current_item.clone())); return Ok(Some(current_item.clone()));
@ -54,15 +56,18 @@ fn find_chapter(
debug!("Get data from context"); debug!("Get data from context");
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone()) serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone()).map_err(|_| {
.map_err(|_| RenderError::new("Could not decode the JSON data")) RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
})
})?; })?;
let base_path = rc let base_path = rc
.evaluate(ctx, "@root/path")? .evaluate(ctx, "@root/path")?
.as_json() .as_json()
.as_str() .as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .ok_or_else(|| {
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
})?
.replace('\"', ""); .replace('\"', "");
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() { if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
@ -98,7 +103,7 @@ fn find_chapter(
} }
} }
previous = Some(item.clone()); previous = Some(item);
} }
_ => continue, _ => continue,
} }
@ -108,7 +113,7 @@ fn find_chapter(
} }
fn render( fn render(
_h: &Helper<'_, '_>, _h: &Helper<'_>,
r: &Handlebars<'_>, r: &Handlebars<'_>,
ctx: &Context, ctx: &Context,
rc: &mut RenderContext<'_, '_>, rc: &mut RenderContext<'_, '_>,
@ -122,27 +127,35 @@ fn render(
.evaluate(ctx, "@root/path")? .evaluate(ctx, "@root/path")?
.as_json() .as_json()
.as_str() .as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .ok_or_else(|| {
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
})?
.replace('\"', ""); .replace('\"', "");
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
.get("name") .get("name")
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data")) .ok_or_else(|| {
RenderErrorReason::Other("No title found for chapter in JSON data".to_owned())
})
.map(|name| context.insert("title".to_owned(), json!(name)))?; .map(|name| context.insert("title".to_owned(), json!(name)))?;
chapter chapter
.get("path") .get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data")) .ok_or_else(|| {
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
})
.and_then(|p| { .and_then(|p| {
Path::new(p) Path::new(p)
.with_extension("html") .with_extension("html")
.to_str() .to_str()
.ok_or_else(|| RenderError::new("Link could not be converted to str")) .ok_or_else(|| {
RenderErrorReason::Other("Link could not be converted to str".to_owned())
})
.map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/")))) .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
})?; })?;
@ -150,14 +163,14 @@ fn render(
let t = _h let t = _h
.template() .template()
.ok_or_else(|| RenderError::new("Error with the handlebars template"))?; .ok_or_else(|| RenderErrorReason::Other("Error with the handlebars template".to_owned()))?;
let local_ctx = Context::wraps(&context)?; let local_ctx = Context::wraps(&context)?;
let mut local_rc = rc.clone(); let mut local_rc = rc.clone();
t.render(r, &local_ctx, &mut local_rc, out) t.render(r, &local_ctx, &mut local_rc, out)
} }
pub fn previous( pub fn previous(
_h: &Helper<'_, '_>, _h: &Helper<'_>,
r: &Handlebars<'_>, r: &Handlebars<'_>,
ctx: &Context, ctx: &Context,
rc: &mut RenderContext<'_, '_>, rc: &mut RenderContext<'_, '_>,
@ -173,7 +186,7 @@ pub fn previous(
} }
pub fn next( pub fn next(
_h: &Helper<'_, '_>, _h: &Helper<'_>,
r: &Handlebars<'_>, r: &Handlebars<'_>,
ctx: &Context, ctx: &Context,
rc: &mut RenderContext<'_, '_>, rc: &mut RenderContext<'_, '_>,

View File

@ -1,8 +1,10 @@
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError}; use handlebars::{
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
};
use log::trace; use log::trace;
pub fn theme_option( pub fn theme_option(
h: &Helper<'_, '_>, h: &Helper<'_>,
_r: &Handlebars<'_>, _r: &Handlebars<'_>,
ctx: &Context, ctx: &Context,
rc: &mut RenderContext<'_, '_>, rc: &mut RenderContext<'_, '_>,
@ -11,14 +13,21 @@ pub fn theme_option(
trace!("theme_option (handlebars helper)"); trace!("theme_option (handlebars helper)");
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| { let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
RenderError::new("Param 0 with String type is required for theme_option helper.") RenderErrorReason::ParamTypeMismatchForName(
"theme_option",
"0".to_owned(),
"string".to_owned(),
)
})?; })?;
let default_theme = rc.evaluate(ctx, "@root/default_theme")?; let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
let default_theme_name = default_theme let default_theme_name = default_theme.as_json().as_str().ok_or_else(|| {
.as_json() RenderErrorReason::ParamTypeMismatchForName(
.as_str() "theme_option",
.ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?; "default_theme".to_owned(),
"string".to_owned(),
)
})?;
out.write(param)?; out.write(param)?;
if param.to_lowercase() == default_theme_name.to_lowercase() { if param.to_lowercase() == default_theme_name.to_lowercase() {

View File

@ -4,7 +4,9 @@ use std::{cmp::Ordering, collections::BTreeMap};
use crate::utils; use crate::utils;
use crate::utils::bracket_escape; use crate::utils::bracket_escape;
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError}; use handlebars::{
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
};
// Handlebars helper to construct TOC // Handlebars helper to construct TOC
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -15,7 +17,7 @@ pub struct RenderToc {
impl HelperDef for RenderToc { impl HelperDef for RenderToc {
fn call<'reg: 'rc, 'rc>( fn call<'reg: 'rc, 'rc>(
&self, &self,
_h: &Helper<'reg, 'rc>, _h: &Helper<'rc>,
_r: &'reg Handlebars<'_>, _r: &'reg Handlebars<'_>,
ctx: &'rc Context, ctx: &'rc Context,
rc: &mut RenderContext<'reg, 'rc>, rc: &mut RenderContext<'reg, 'rc>,
@ -26,13 +28,17 @@ impl HelperDef for RenderToc {
// param is the key of value you want to display // param is the key of value you want to display
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone()) serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
.map_err(|_| RenderError::new("Could not decode the JSON data")) .map_err(|_| {
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
})
})?; })?;
let current_path = rc let current_path = rc
.evaluate(ctx, "@root/path")? .evaluate(ctx, "@root/path")?
.as_json() .as_json()
.as_str() .as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))? .ok_or_else(|| {
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
})?
.replace('\"', ""); .replace('\"', "");
let current_section = rc let current_section = rc
@ -46,13 +52,17 @@ impl HelperDef for RenderToc {
.evaluate(ctx, "@root/fold_enable")? .evaluate(ctx, "@root/fold_enable")?
.as_json() .as_json()
.as_bool() .as_bool()
.ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?; .ok_or_else(|| {
RenderErrorReason::Other("Type error for `fold_enable`, bool expected".to_owned())
})?;
let fold_level = rc let fold_level = rc
.evaluate(ctx, "@root/fold_level")? .evaluate(ctx, "@root/fold_level")?
.as_json() .as_json()
.as_u64() .as_u64()
.ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?; .ok_or_else(|| {
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
})?;
out.write("<ol class=\"chapter\">")?; out.write("<ol class=\"chapter\">")?;

View File

@ -66,10 +66,23 @@ fn add_doc(
index: &mut Index, index: &mut Index,
doc_urls: &mut Vec<String>, doc_urls: &mut Vec<String>,
anchor_base: &str, anchor_base: &str,
section_id: &Option<String>, heading: &str,
id_counter: &mut HashMap<String, usize>,
section_id: &Option<CowStr<'_>>,
items: &[&str], items: &[&str],
) { ) {
let url = if let Some(ref id) = *section_id { // Either use the explicit section id the user specified, or generate one
// from the heading content.
let section_id = section_id.as_ref().map(|id| id.to_string()).or_else(|| {
if heading.is_empty() {
// In the case where a chapter has no heading, don't set a section id.
None
} else {
Some(utils::unique_id_from_content(heading, id_counter))
}
});
let url = if let Some(id) = section_id {
Cow::Owned(format!("{}#{}", anchor_base, id)) Cow::Owned(format!("{}#{}", anchor_base, id))
} else { } else {
Cow::Borrowed(anchor_base) Cow::Borrowed(anchor_base)
@ -119,7 +132,7 @@ fn render_item(
let mut id_counter = HashMap::new(); let mut id_counter = HashMap::new();
while let Some(event) = p.next() { while let Some(event) = p.next() {
match event { match event {
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { Event::Start(Tag::Heading { level, id, .. }) if level as u32 <= max_section_depth => {
if !heading.is_empty() { if !heading.is_empty() {
// Section finished, the next heading is following now // Section finished, the next heading is following now
// Write the data to the index, and clear it for the next section // Write the data to the index, and clear it for the next section
@ -127,20 +140,21 @@ fn render_item(
index, index,
doc_urls, doc_urls,
&anchor_base, &anchor_base,
&heading,
&mut id_counter,
&section_id, &section_id,
&[&heading, &body, &breadcrumbs.join(" » ")], &[&heading, &body, &breadcrumbs.join(" » ")],
); );
section_id = None;
heading.clear(); heading.clear();
body.clear(); body.clear();
breadcrumbs.pop(); breadcrumbs.pop();
} }
section_id = id;
in_heading = true; in_heading = true;
} }
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => { Event::End(TagEnd::Heading(level)) if level as u32 <= max_section_depth => {
in_heading = false; in_heading = false;
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
breadcrumbs.push(heading.clone()); breadcrumbs.push(heading.clone());
} }
Event::Start(Tag::FootnoteDefinition(name)) => { Event::Start(Tag::FootnoteDefinition(name)) => {
@ -157,9 +171,19 @@ fn render_item(
html_block.push_str(html); html_block.push_str(html);
p.next(); p.next();
} }
body.push_str(&clean_html(&html_block)); body.push_str(&clean_html(&html_block));
} }
Event::InlineHtml(html) => {
// This is not capable of cleaning inline tags like
// `foo <script>…</script>`. The `<script>` tags show up as
// individual InlineHtml events, and the content inside is
// just a regular Text event. There isn't a very good way to
// know how to collect all the content in-between. I'm not
// sure if this is easily fixable. It should be extremely
// rare, since script and style tags should almost always be
// blocks, and worse case you have some noise in the index.
body.push_str(&clean_html(&html));
}
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => { Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
// Insert spaces where HTML output would usually separate text // Insert spaces where HTML output would usually separate text
// to ensure words don't get merged together // to ensure words don't get merged together
@ -186,18 +210,24 @@ fn render_item(
} }
if !body.is_empty() || !heading.is_empty() { if !body.is_empty() || !heading.is_empty() {
if heading.is_empty() { let title = if heading.is_empty() {
if let Some(chapter) = breadcrumbs.first() { if let Some(chapter) = breadcrumbs.first() {
heading = chapter.clone(); chapter
} else {
""
} }
} } else {
&heading
};
// Make sure the last section is added to the index // Make sure the last section is added to the index
add_doc( add_doc(
index, index,
doc_urls, doc_urls,
&anchor_base, &anchor_base,
&heading,
&mut id_counter,
&section_id, &section_id,
&[&heading, &body, &breadcrumbs.join(" » ")], &[title, &body, &breadcrumbs.join(" » ")],
); );
} }

View File

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

View File

@ -68,7 +68,7 @@ function playground_text(playground, hidden = true) {
} }
// updates the visibility of play button based on `no_run` class and // updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org // used crates vs ones available on https://play.rust-lang.org
function update_play_button(pre_block, playground_crates) { function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.querySelector(".play-button"); var play_button = pre_block.querySelector(".play-button");
@ -179,7 +179,7 @@ function playground_text(playground, hidden = true) {
// even if highlighting doesn't apply // even if highlighting doesn't apply
code_nodes.forEach(function (block) { block.classList.add('hljs'); }); code_nodes.forEach(function (block) { block.classList.add('hljs'); });
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
var lines = Array.from(block.querySelectorAll('.boring')); var lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return // If no lines were hidden, return
@ -346,7 +346,7 @@ function playground_text(playground, hidden = true) {
} }
setTimeout(function () { setTimeout(function () {
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor; themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
}, 1); }, 1);
if (window.ace && window.editors) { if (window.ace && window.editors) {
@ -441,7 +441,7 @@ function playground_text(playground, hidden = true) {
})(); })();
(function sidebar() { (function sidebar() {
var html = document.querySelector("html"); var body = document.querySelector("body");
var sidebar = document.getElementById("sidebar"); var sidebar = document.getElementById("sidebar");
var sidebarLinks = document.querySelectorAll('#sidebar a'); var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle"); var sidebarToggleButton = document.getElementById("sidebar-toggle");
@ -449,8 +449,8 @@ function playground_text(playground, hidden = true) {
var firstContact = null; var firstContact = null;
function showSidebar() { function showSidebar() {
html.classList.remove('sidebar-hidden') body.classList.remove('sidebar-hidden')
html.classList.add('sidebar-visible'); body.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function (link) { Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', 0); link.setAttribute('tabIndex', 0);
}); });
@ -471,8 +471,8 @@ function playground_text(playground, hidden = true) {
}); });
function hideSidebar() { function hideSidebar() {
html.classList.remove('sidebar-visible') body.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden'); body.classList.add('sidebar-hidden');
Array.from(sidebarLinks).forEach(function (link) { Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', -1); link.setAttribute('tabIndex', -1);
}); });
@ -483,14 +483,14 @@ function playground_text(playground, hidden = true) {
// Toggle sidebar // Toggle sidebar
sidebarToggleButton.addEventListener('click', function sidebarToggle() { sidebarToggleButton.addEventListener('click', function sidebarToggle() {
if (html.classList.contains("sidebar-hidden")) { if (body.classList.contains("sidebar-hidden")) {
var current_width = parseInt( var current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10); document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
if (current_width < 150) { if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px'); document.documentElement.style.setProperty('--sidebar-width', '150px');
} }
showSidebar(); showSidebar();
} else if (html.classList.contains("sidebar-visible")) { } else if (body.classList.contains("sidebar-visible")) {
hideSidebar(); hideSidebar();
} else { } else {
if (getComputedStyle(sidebar)['transform'] === 'none') { if (getComputedStyle(sidebar)['transform'] === 'none') {
@ -506,14 +506,14 @@ function playground_text(playground, hidden = true) {
function initResize(e) { function initResize(e) {
window.addEventListener('mousemove', resize, false); window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false); window.addEventListener('mouseup', stopResize, false);
html.classList.add('sidebar-resizing'); body.classList.add('sidebar-resizing');
} }
function resize(e) { function resize(e) {
var pos = (e.clientX - sidebar.offsetLeft); var pos = (e.clientX - sidebar.offsetLeft);
if (pos < 20) { if (pos < 20) {
hideSidebar(); hideSidebar();
} else { } else {
if (html.classList.contains("sidebar-hidden")) { if (body.classList.contains("sidebar-hidden")) {
showSidebar(); showSidebar();
} }
pos = Math.min(pos, window.innerWidth - 100); pos = Math.min(pos, window.innerWidth - 100);
@ -522,7 +522,7 @@ function playground_text(playground, hidden = true) {
} }
//on mouseup remove windows functions mousemove & mouseup //on mouseup remove windows functions mousemove & mouseup
function stopResize(e) { function stopResize(e) {
html.classList.remove('sidebar-resizing'); body.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false); window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false); window.removeEventListener('mouseup', stopResize, false);
} }
@ -551,33 +551,41 @@ 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() {
document.addEventListener('keydown', function (e) { document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; } if (window.search && window.search.hasFocus()) { return; }
var html = document.querySelector('html');
function next() {
var nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
}
function prev() {
var previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
}
switch (e.key) { switch (e.key) {
case 'ArrowRight': case 'ArrowRight':
e.preventDefault(); e.preventDefault();
var nextButton = document.querySelector('.nav-chapters.next'); if (html.dir == 'rtl') {
if (nextButton) { prev();
window.location.href = nextButton.href; } else {
next();
} }
break; break;
case 'ArrowLeft': case 'ArrowLeft':
e.preventDefault(); e.preventDefault();
var previousButton = document.querySelector('.nav-chapters.previous'); if (html.dir == 'rtl') {
if (previousButton) { next();
window.location.href = previousButton.href; } else {
prev();
} }
break; break;
} }
@ -676,13 +684,14 @@ function playground_text(playground, hidden = true) {
}, { passive: true }); }, { passive: true });
})(); })();
(function controllBorder() { (function controllBorder() {
menu.classList.remove('bordered'); function updateBorder() {
document.addEventListener('scroll', function () {
if (menu.offsetTop === 0) { if (menu.offsetTop === 0) {
menu.classList.remove('bordered'); menu.classList.remove('bordered');
} else { } else {
menu.classList.add('bordered'); menu.classList.add('bordered');
} }
}, { passive: true }); }
updateBorder();
document.addEventListener('scroll', updateBorder, { passive: true });
})(); })();
})(); })();

View File

@ -22,7 +22,7 @@ a > .hljs {
the screen on small screens. Without it, dragging on mobile Safari the screen on small screens. Without it, dragging on mobile Safari
will want to reposition the viewport in a weird way. will want to reposition the viewport in a weird way.
*/ */
overflow-x: hidden; overflow-x: clip;
} }
/* Menu Bar */ /* Menu Bar */
@ -37,9 +37,9 @@ a > .hljs {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
background-color: var(--bg); background-color: var(--bg);
border-bottom-color: var(--bg); border-block-end-color: var(--bg);
border-bottom-width: 1px; border-block-end-width: 1px;
border-bottom-style: solid; border-block-end-style: solid;
} }
#menu-bar.sticky, #menu-bar.sticky,
.js #menu-bar-hover-placeholder:hover + #menu-bar, .js #menu-bar-hover-placeholder:hover + #menu-bar,
@ -56,7 +56,7 @@ a > .hljs {
height: var(--menu-bar-height); height: var(--menu-bar-height);
} }
#menu-bar.bordered { #menu-bar.bordered {
border-bottom-color: var(--table-border-color); border-block-end-color: var(--table-border-color);
} }
#menu-bar i, #menu-bar .icon-button { #menu-bar i, #menu-bar .icon-button {
position: relative; position: relative;
@ -93,7 +93,7 @@ a > .hljs {
display: flex; display: flex;
margin: 0 5px; margin: 0 5px;
} }
.no-js .left-buttons { .no-js .left-buttons button {
display: none; display: none;
} }
@ -160,7 +160,7 @@ a > .hljs {
} }
.nav-wrapper { .nav-wrapper {
margin-top: 50px; margin-block-start: 50px;
display: none; display: none;
} }
@ -173,23 +173,34 @@ a > .hljs {
background-color: var(--sidebar-bg); background-color: var(--sidebar-bg);
} }
.previous { /* Only Firefox supports flow-relative values */
float: left; .previous { float: left; }
} [dir=rtl] .previous { float: right; }
/* Only Firefox supports flow-relative values */
.next { .next {
float: right; float: right;
right: var(--page-padding); right: var(--page-padding);
} }
[dir=rtl] .next {
float: left;
right: unset;
left: var(--page-padding);
}
/* Use the correct buttons for RTL layouts*/
[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";}
[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; }
@media only screen and (max-width: 1080px) { @media only screen and (max-width: 1080px) {
.nav-wide-wrapper { display: none; } .nav-wide-wrapper { display: none; }
.nav-wrapper { display: block; } .nav-wrapper { display: block; }
} }
/* sidebar-visible */
@media only screen and (max-width: 1380px) { @media only screen and (max-width: 1380px) {
.sidebar-visible .nav-wide-wrapper { display: none; } #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
.sidebar-visible .nav-wrapper { display: block; } #sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
} }
/* Inline code */ /* Inline code */
@ -236,7 +247,7 @@ pre > .buttons :hover {
background-color: var(--theme-hover); background-color: var(--theme-hover);
} }
pre > .buttons i { pre > .buttons i {
margin-left: 8px; margin-inline-start: 8px;
} }
pre > .buttons button { pre > .buttons button {
cursor: inherit; cursor: inherit;
@ -258,8 +269,14 @@ pre > .buttons button {
/* On mobile, make it easier to tap buttons. */ /* On mobile, make it easier to tap buttons. */
padding: 0.3rem 1rem; padding: 0.3rem 1rem;
} }
.sidebar-resize-indicator {
/* Hide resize indicator on devices with limited accuracy */
display: none;
}
} }
pre > code { pre > code {
display: block;
padding: 1rem; padding: 1rem;
} }
@ -273,7 +290,7 @@ pre > code {
} }
pre > .result { pre > .result {
margin-top: 10px; margin-block-start: 10px;
} }
/* Search */ /* Search */
@ -284,8 +301,14 @@ pre > .result {
mark { mark {
border-radius: 2px; border-radius: 2px;
padding: 0 3px 1px 3px; padding-block-start: 0;
margin: 0 -3px -1px -3px; padding-block-end: 1px;
padding-inline-start: 3px;
padding-inline-end: 3px;
margin-block-start: 0;
margin-block-end: -1px;
margin-inline-start: -3px;
margin-inline-end: -3px;
background-color: var(--search-mark-bg); background-color: var(--search-mark-bg);
transition: background-color 300ms linear; transition: background-color 300ms linear;
cursor: pointer; cursor: pointer;
@ -297,14 +320,17 @@ mark.fade-out {
} }
.searchbar-outer { .searchbar-outer {
margin-left: auto; margin-inline-start: auto;
margin-right: auto; margin-inline-end: auto;
max-width: var(--content-max-width); max-width: var(--content-max-width);
} }
#searchbar { #searchbar {
width: 100%; width: 100%;
margin: 5px auto 0px auto; margin-block-start: 5px;
margin-block-end: 0;
margin-inline-start: auto;
margin-inline-end: auto;
padding: 10px 16px; padding: 10px 16px;
transition: box-shadow 300ms ease-in-out; transition: box-shadow 300ms ease-in-out;
border: 1px solid var(--searchbar-border-color); border: 1px solid var(--searchbar-border-color);
@ -320,20 +346,23 @@ mark.fade-out {
.searchresults-header { .searchresults-header {
font-weight: bold; font-weight: bold;
font-size: 1em; font-size: 1em;
padding: 18px 0 0 5px; padding-block-start: 18px;
padding-block-end: 0;
padding-inline-start: 5px;
padding-inline-end: 0;
color: var(--searchresults-header-fg); color: var(--searchresults-header-fg);
} }
.searchresults-outer { .searchresults-outer {
margin-left: auto; margin-inline-start: auto;
margin-right: auto; margin-inline-end: auto;
max-width: var(--content-max-width); max-width: var(--content-max-width);
border-bottom: 1px dashed var(--searchresults-border-color); border-block-end: 1px dashed var(--searchresults-border-color);
} }
ul#searchresults { ul#searchresults {
list-style: none; list-style: none;
padding-left: 20px; padding-inline-start: 20px;
} }
ul#searchresults li { ul#searchresults li {
margin: 10px 0px; margin: 10px 0px;
@ -346,7 +375,10 @@ ul#searchresults li.focus {
ul#searchresults span.teaser { ul#searchresults span.teaser {
display: block; display: block;
clear: both; clear: both;
margin: 5px 0 0 20px; margin-block-start: 5px;
margin-block-end: 0;
margin-inline-start: 20px;
margin-inline-end: 0;
font-size: 0.8em; font-size: 0.8em;
} }
ul#searchresults span.teaser em { ul#searchresults span.teaser em {
@ -369,12 +401,14 @@ ul#searchresults span.teaser em {
background-color: var(--sidebar-bg); background-color: var(--sidebar-bg);
color: var(--sidebar-fg); color: var(--sidebar-fg);
} }
[dir=rtl] .sidebar { left: unset; right: 0; }
.sidebar-resizing { .sidebar-resizing {
-moz-user-select: none; -moz-user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
.no-js .sidebar,
.js:not(.sidebar-resizing) .sidebar { .js:not(.sidebar-resizing) .sidebar {
transition: transform 0.3s; /* Animation: slide away */ transition: transform 0.3s; /* Animation: slide away */
} }
@ -394,16 +428,35 @@ ul#searchresults span.teaser em {
position: absolute; position: absolute;
cursor: col-resize; cursor: col-resize;
width: 0; width: 0;
right: 0; right: calc(var(--sidebar-resize-indicator-width) * -1);
top: 0; top: 0;
bottom: 0; bottom: 0;
display: flex;
align-items: center;
}
.sidebar-resize-handle .sidebar-resize-indicator {
width: 100%;
height: 12px;
background-color: var(--icons);
margin-inline-start: var(--sidebar-resize-indicator-space);
}
[dir=rtl] .sidebar .sidebar-resize-handle {
left: calc(var(--sidebar-resize-indicator-width) * -1);
right: unset;
} }
.js .sidebar .sidebar-resize-handle { .js .sidebar .sidebar-resize-handle {
cursor: col-resize; cursor: col-resize;
width: 5px; width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
} }
.sidebar-hidden .sidebar { /* sidebar-hidden */
transform: translateX(calc(0px - var(--sidebar-width))); #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
z-index: -1;
}
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
} }
.sidebar::-webkit-scrollbar { .sidebar::-webkit-scrollbar {
background: var(--sidebar-bg); background: var(--sidebar-bg);
@ -412,19 +465,26 @@ ul#searchresults span.teaser em {
background: var(--scrollbar); background: var(--scrollbar);
} }
.sidebar-visible .page-wrapper { /* sidebar-visible */
transform: translateX(var(--sidebar-width)); #sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
}
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
} }
@media only screen and (min-width: 620px) { @media only screen and (min-width: 620px) {
.sidebar-visible .page-wrapper { #sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: none;
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
}
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: none; transform: none;
margin-left: var(--sidebar-width);
} }
} }
.chapter { .chapter {
list-style: none outside none; list-style: none outside none;
padding-left: 0; padding-inline-start: 0;
line-height: 2.2em; line-height: 2.2em;
} }
@ -454,7 +514,7 @@ ul#searchresults span.teaser em {
.chapter li > a.toggle { .chapter li > a.toggle {
cursor: pointer; cursor: pointer;
display: block; display: block;
margin-left: auto; margin-inline-start: auto;
padding: 0 10px; padding: 0 10px;
user-select: none; user-select: none;
opacity: 0.68; opacity: 0.68;
@ -471,7 +531,7 @@ ul#searchresults span.teaser em {
.chapter li.chapter-item { .chapter li.chapter-item {
line-height: 1.5em; line-height: 1.5em;
margin-top: 0.6em; margin-block-start: 0.6em;
} }
.chapter li.expanded > a.toggle div { .chapter li.expanded > a.toggle div {
@ -494,7 +554,7 @@ ul#searchresults span.teaser em {
.section { .section {
list-style: none outside none; list-style: none outside none;
padding-left: 20px; padding-inline-start: 20px;
line-height: 1.9em; line-height: 1.9em;
} }
@ -517,6 +577,7 @@ ul#searchresults span.teaser em {
/* Don't let the children's background extend past the rounded corners. */ /* Don't let the children's background extend past the rounded corners. */
overflow: hidden; overflow: hidden;
} }
[dir=rtl] .theme-popup { left: unset; right: 10px; }
.theme-popup .default { .theme-popup .default {
color: var(--icons); color: var(--icons);
} }
@ -527,7 +588,7 @@ ul#searchresults span.teaser em {
padding: 2px 20px; padding: 2px 20px;
line-height: 25px; line-height: 25px;
white-space: nowrap; white-space: nowrap;
text-align: left; text-align: start;
cursor: pointer; cursor: pointer;
color: inherit; color: inherit;
background: inherit; background: inherit;
@ -540,6 +601,6 @@ ul#searchresults span.teaser em {
.theme-selected::before { .theme-selected::before {
display: inline-block; display: inline-block;
content: "✓"; content: "✓";
margin-left: -14px; margin-inline-start: -14px;
width: 14px; width: 14px;
} }

View File

@ -5,6 +5,7 @@
:root { :root {
/* Browser default font-size is 16px, this way 1 rem = 10px */ /* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%; font-size: 62.5%;
color-scheme: var(--color-scheme);
} }
html { html {
@ -24,6 +25,7 @@ body {
code { code {
font-family: var(--mono-font) !important; font-family: var(--mono-font) !important;
font-size: var(--code-font-size); font-size: var(--code-font-size);
direction: ltr !important;
} }
/* make long words/inline code not x overflow */ /* make long words/inline code not x overflow */
@ -47,13 +49,13 @@ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
.hide-boring .boring { display: none; } .hide-boring .boring { display: none; }
.hidden { display: none !important; } .hidden { display: none !important; }
h2, h3 { margin-top: 2.5em; } h2, h3 { margin-block-start: 2.5em; }
h4, h5 { margin-top: 2em; } h4, h5 { margin-block-start: 2em; }
.header + .header h3, .header + .header h3,
.header + .header h4, .header + .header h4,
.header + .header h5 { .header + .header h5 {
margin-top: 1em; margin-block-start: 1em;
} }
h1:target::before, h1:target::before,
@ -64,7 +66,7 @@ h5:target::before,
h6:target::before { h6:target::before {
display: inline-block; display: inline-block;
content: "»"; content: "»";
margin-left: -30px; margin-inline-start: -30px;
width: 30px; width: 30px;
} }
@ -73,28 +75,34 @@ h6:target::before {
https://bugs.webkit.org/show_bug.cgi?id=218076 https://bugs.webkit.org/show_bug.cgi?id=218076
*/ */
:target { :target {
/* Safari does not support logical properties */
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
} }
.page { .page {
outline: 0; outline: 0;
padding: 0 var(--page-padding); padding: 0 var(--page-padding);
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */ margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
} }
.page-wrapper { .page-wrapper {
box-sizing: border-box; box-sizing: border-box;
background-color: var(--bg);
} }
.no-js .page-wrapper,
.js:not(.sidebar-resizing) .page-wrapper { .js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */ transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
} }
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
.content { .content {
overflow-y: auto; overflow-y: auto;
padding: 0 5px 50px 5px; padding: 0 5px 50px 5px;
} }
.content main { .content main {
margin-left: auto; margin-inline-start: auto;
margin-right: auto; margin-inline-end: auto;
max-width: var(--content-max-width); max-width: var(--content-max-width);
} }
.content p { line-height: 1.45em; } .content p { line-height: 1.45em; }
@ -144,8 +152,31 @@ blockquote {
padding: 0 20px; padding: 0 20px;
color: var(--fg); color: var(--fg);
background-color: var(--quote-bg); background-color: var(--quote-bg);
border-top: .1em solid var(--quote-border); border-block-start: .1em solid var(--quote-border);
border-bottom: .1em solid var(--quote-border); border-block-end: .1em solid var(--quote-border);
}
.warning {
margin: 20px;
padding: 0 20px;
border-inline-start: 2px solid var(--warning-border);
}
.warning:before {
position: absolute;
width: 3rem;
height: 3rem;
margin-inline-start: calc(-1.5rem - 21px);
content: "ⓘ";
text-align: center;
background-color: var(--bg);
color: var(--warning-border);
font-weight: bold;
font-size: 2rem;
}
blockquote .warning:before {
background-color: var(--quote-bg);
} }
kbd { kbd {
@ -163,7 +194,7 @@ kbd {
:not(.footnote-definition) + .footnote-definition, :not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) { .footnote-definition + :not(.footnote-definition) {
margin-top: 2em; margin-block-start: 2em;
} }
.footnote-definition { .footnote-definition {
font-size: 0.9em; font-size: 0.9em;

View File

@ -7,8 +7,8 @@
} }
#page-wrapper.page-wrapper { #page-wrapper.page-wrapper {
transform: none; transform: none !important;
margin-left: 0px; margin-inline-start: 0px;
overflow-y: initial; overflow-y: initial;
} }
@ -23,11 +23,7 @@
} }
code { code {
background-color: #666666; direction: ltr !important;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
} }
pre > .buttons { pre > .buttons {

View File

@ -3,6 +3,8 @@
:root { :root {
--sidebar-width: 300px; --sidebar-width: 300px;
--sidebar-resize-indicator-width: 8px;
--sidebar-resize-indicator-space: 2px;
--page-padding: 15px; --page-padding: 15px;
--content-max-width: 750px; --content-max-width: 750px;
--menu-bar-height: 50px; --menu-bar-height: 50px;
@ -38,6 +40,8 @@
--quote-bg: hsl(226, 15%, 17%); --quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%); --quote-border: hsl(226, 15%, 22%);
--warning-border: #ff8e00;
--table-border-color: hsl(210, 25%, 13%); --table-border-color: hsl(210, 25%, 13%);
--table-header-bg: hsl(210, 25%, 28%); --table-header-bg: hsl(210, 25%, 28%);
--table-alternate-bg: hsl(210, 25%, 11%); --table-alternate-bg: hsl(210, 25%, 11%);
@ -50,6 +54,8 @@
--searchresults-border-color: #888; --searchresults-border-color: #888;
--searchresults-li-bg: #252932; --searchresults-li-bg: #252932;
--search-mark-bg: #e3b171; --search-mark-bg: #e3b171;
--color-scheme: dark;
} }
.coal { .coal {
@ -78,6 +84,8 @@
--quote-bg: hsl(234, 21%, 18%); --quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%); --quote-border: hsl(234, 21%, 23%);
--warning-border: #ff8e00;
--table-border-color: hsl(200, 7%, 13%); --table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%); --table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%); --table-alternate-bg: hsl(200, 7%, 11%);
@ -90,6 +98,8 @@
--searchresults-border-color: #98a3ad; --searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f; --searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d; --search-mark-bg: #355c7d;
--color-scheme: dark;
} }
.light { .light {
@ -118,6 +128,8 @@
--quote-bg: hsl(197, 37%, 96%); --quote-bg: hsl(197, 37%, 96%);
--quote-border: hsl(197, 37%, 91%); --quote-border: hsl(197, 37%, 91%);
--warning-border: #ff8e00;
--table-border-color: hsl(0, 0%, 95%); --table-border-color: hsl(0, 0%, 95%);
--table-header-bg: hsl(0, 0%, 80%); --table-header-bg: hsl(0, 0%, 80%);
--table-alternate-bg: hsl(0, 0%, 97%); --table-alternate-bg: hsl(0, 0%, 97%);
@ -130,6 +142,8 @@
--searchresults-border-color: #888; --searchresults-border-color: #888;
--searchresults-li-bg: #e4f2fe; --searchresults-li-bg: #e4f2fe;
--search-mark-bg: #a2cff5; --search-mark-bg: #a2cff5;
--color-scheme: light;
} }
.navy { .navy {
@ -158,6 +172,8 @@
--quote-bg: hsl(226, 15%, 17%); --quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%); --quote-border: hsl(226, 15%, 22%);
--warning-border: #ff8e00;
--table-border-color: hsl(226, 23%, 16%); --table-border-color: hsl(226, 23%, 16%);
--table-header-bg: hsl(226, 23%, 31%); --table-header-bg: hsl(226, 23%, 31%);
--table-alternate-bg: hsl(226, 23%, 14%); --table-alternate-bg: hsl(226, 23%, 14%);
@ -170,6 +186,8 @@
--searchresults-border-color: #5c5c68; --searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430; --searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5; --search-mark-bg: #a2cff5;
--color-scheme: dark;
} }
.rust { .rust {
@ -198,6 +216,8 @@
--quote-bg: hsl(60, 5%, 75%); --quote-bg: hsl(60, 5%, 75%);
--quote-border: hsl(60, 5%, 70%); --quote-border: hsl(60, 5%, 70%);
--warning-border: #ff8e00;
--table-border-color: hsl(60, 9%, 82%); --table-border-color: hsl(60, 9%, 82%);
--table-header-bg: #b3a497; --table-header-bg: #b3a497;
--table-alternate-bg: hsl(60, 9%, 84%); --table-alternate-bg: hsl(60, 9%, 84%);
@ -210,6 +230,8 @@
--searchresults-border-color: #888; --searchresults-border-color: #888;
--searchresults-li-bg: #dec2a2; --searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67; --search-mark-bg: #e69f67;
--color-scheme: light;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -239,6 +261,8 @@
--quote-bg: hsl(234, 21%, 18%); --quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%); --quote-border: hsl(234, 21%, 23%);
--warning-border: #ff8e00;
--table-border-color: hsl(200, 7%, 13%); --table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%); --table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%); --table-alternate-bg: hsl(200, 7%, 11%);

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}"> <html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
<head> <head>
<!-- Book generated using mdBook --> <!-- Book generated using mdBook -->
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ title }}</title> <title>{{ title }}</title>
{{#if is_print }} {{#if is_print }}
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex">
{{/if}} {{/if}}
{{#if base_url}} {{#if base_url}}
<base href="{{ base_url }}"> <base href="{{ base_url }}">
@ -17,7 +17,7 @@
<meta name="description" content="{{ description }}"> <meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff">
{{#if favicon_svg}} {{#if favicon_svg}}
<link rel="icon" href="{{ path_to_root }}favicon.svg"> <link rel="icon" href="{{ path_to_root }}favicon.svg">
@ -53,7 +53,7 @@
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script> <script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}} {{/if}}
</head> </head>
<body> <body class="sidebar-visible no-js">
<div id="body-container"> <div id="body-container">
<!-- Provide site root to javascript --> <!-- Provide site root to javascript -->
<script> <script>
@ -83,41 +83,72 @@
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { } try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; } if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html'); var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('{{ default_theme }}') html.classList.remove('{{ default_theme }}')
html.classList.add(theme); html.classList.add(theme);
html.classList.add('js'); var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script> </script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed --> <!-- Hide / unhide sidebar before it is displayed -->
<script> <script>
var html = document.querySelector('html'); var body = document.querySelector('body');
var sidebar = 'hidden'; var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) { if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { } try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible'; sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
} }
html.classList.remove('sidebar-visible'); sidebar_toggle.checked = sidebar === 'visible';
html.classList.add("sidebar-" + sidebar); body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script> </script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents"> <nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox"> <div class="sidebar-scrollbox">
{{#toc}}{{/toc}} {{#toc}}{{/toc}}
</div> </div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div> <div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</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"> <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" 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>
</button> </label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list"> <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i> <i class="fa fa-paint-brush"></i>
</button> </button>
@ -193,7 +224,7 @@
{{/previous}} {{/previous}}
{{#next}} {{#next}}
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right"> <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
{{/next}} {{/next}}
@ -211,7 +242,7 @@
{{/previous}} {{/previous}}
{{#next}} {{#next}}
<a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right"> <a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i> <i class="fa fa-angle-right"></i>
</a> </a>
{{/next}} {{/next}}

View File

@ -212,7 +212,6 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
mod tests { mod tests {
use super::*; use super::*;
use std::fs; use std::fs;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
#[test] #[test]

View File

@ -316,7 +316,7 @@ window.search = window.search || {};
// Eventhandler for keyevents on `document` // Eventhandler for keyevents on `document`
function globalKeyHandler(e) { function globalKeyHandler(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; } if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; }
if (e.keyCode === ESCAPE_KEYCODE) { if (e.keyCode === ESCAPE_KEYCODE) {
e.preventDefault(); e.preventDefault();

View File

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

View File

@ -1,6 +1,5 @@
use crate::errors::*; use crate::errors::*;
use log::{debug, trace}; use log::{debug, trace};
use std::convert::Into;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
@ -38,7 +37,6 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
/// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues) /// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues)
/// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it. /// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it.
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String { pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
debug!("path_to_root");
// Remove filename and add "../" for every directory // Remove filename and add "../" for every directory
path.into() path.into()
@ -74,14 +72,12 @@ pub fn create_file(path: &Path) -> Result<File> {
/// Removes all the content of a directory but not the directory itself /// Removes all the content of a directory but not the directory itself
pub fn remove_dir_content(dir: &Path) -> Result<()> { pub fn remove_dir_content(dir: &Path) -> Result<()> {
for item in fs::read_dir(dir)? { for item in fs::read_dir(dir)?.flatten() {
if let Ok(item) = item { let item = item.path();
let item = item.path(); if item.is_dir() {
if item.is_dir() { fs::remove_dir_all(item)?;
fs::remove_dir_all(item)?; } else {
} else { fs::remove_file(item)?;
fs::remove_file(item)?;
}
} }
} }
Ok(()) Ok(())
@ -110,77 +106,102 @@ pub fn copy_files_except_ext(
} }
for entry in fs::read_dir(from)? { for entry in fs::read_dir(from)? {
let entry = entry?; let entry = entry?.path();
let metadata = entry let metadata = entry
.path()
.metadata() .metadata()
.with_context(|| format!("Failed to read {:?}", entry.path()))?; .with_context(|| format!("Failed to read {entry:?}"))?;
let entry_file_name = entry.file_name().unwrap();
let target_file_path = to.join(entry_file_name);
// If the entry is a dir and the recursive option is enabled, call itself // If the entry is a dir and the recursive option is enabled, call itself
if metadata.is_dir() && recursive { if metadata.is_dir() && recursive {
if entry.path() == to.to_path_buf() { if entry == to.as_os_str() {
continue; continue;
} }
if let Some(avoid) = avoid_dir { if let Some(avoid) = avoid_dir {
if entry.path() == *avoid { if entry == *avoid {
continue; continue;
} }
} }
// check if output dir already exists // check if output dir already exists
if !to.join(entry.file_name()).exists() { if !target_file_path.exists() {
fs::create_dir(&to.join(entry.file_name()))?; fs::create_dir(&target_file_path)?;
} }
copy_files_except_ext( copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?;
&from.join(entry.file_name()),
&to.join(entry.file_name()),
true,
avoid_dir,
ext_blacklist,
)?;
} else if metadata.is_file() { } else if metadata.is_file() {
// Check if it is in the blacklist // Check if it is in the blacklist
if let Some(ext) = entry.path().extension() { if let Some(ext) = entry.extension() {
if ext_blacklist.contains(&ext.to_str().unwrap()) { if ext_blacklist.contains(&ext.to_str().unwrap()) {
continue; continue;
} }
} }
debug!( debug!("Copying {entry:?} to {target_file_path:?}");
"creating path for file: {:?}", copy(&entry, &target_file_path)?;
&to.join(
entry
.path()
.file_name()
.expect("a file should have a file name...")
)
);
debug!(
"Copying {:?} to {:?}",
entry.path(),
&to.join(
entry
.path()
.file_name()
.expect("a file should have a file name...")
)
);
fs::copy(
entry.path(),
&to.join(
entry
.path()
.file_name()
.expect("a file should have a file name..."),
),
)?;
} }
} }
Ok(()) Ok(())
} }
/// Copies a file.
fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
return copy_inner(from, to)
.with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()));
// This is a workaround for an issue with the macOS file watcher.
// Rust's `std::fs::copy` function uses `fclonefileat`, which creates
// clones on APFS. Unfortunately fs events seem to trigger on both
// sides of the clone, and there doesn't seem to be a way to differentiate
// which side it is.
// https://github.com/notify-rs/notify/issues/465#issuecomment-1657261035
// contains more information.
//
// This is essentially a copy of the simple copy code path in Rust's
// standard library.
#[cfg(target_os = "macos")]
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
use std::fs::OpenOptions;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let mut reader = File::open(from)?;
let metadata = reader.metadata()?;
if !metadata.is_file() {
anyhow::bail!(
"expected a file, `{}` appears to be {:?}",
from.display(),
metadata.file_type()
);
}
let perm = metadata.permissions();
let mut writer = OpenOptions::new()
.mode(perm.mode())
.write(true)
.create(true)
.truncate(true)
.open(to)?;
let writer_metadata = writer.metadata()?;
if writer_metadata.is_file() {
// Set the correct file permissions, in case the file already existed.
// Don't set the permissions on already existing non-files like
// pipes/FIFOs or device nodes.
writer.set_permissions(perm)?;
}
std::io::copy(&mut reader, &mut writer)?;
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
fs::copy(from, to)?;
Ok(())
}
}
pub fn get_404_output_file(input_404: &Option<String>) -> String { pub fn get_404_output_file(input_404: &Option<String>) -> String {
input_404 input_404
.as_ref() .as_ref()
@ -211,39 +232,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);
} }
@ -254,22 +272,22 @@ mod tests {
} }
// Check if the correct files where created // Check if the correct files where created
if !(&tmp.path().join("output/file.txt")).exists() { if !tmp.path().join("output/file.txt").exists() {
panic!("output/file.txt should exist") panic!("output/file.txt should exist")
} }
if (&tmp.path().join("output/file.md")).exists() { if tmp.path().join("output/file.md").exists() {
panic!("output/file.md should not exist") panic!("output/file.md should not exist")
} }
if !(&tmp.path().join("output/file.png")).exists() { if !tmp.path().join("output/file.png").exists() {
panic!("output/file.png should exist") panic!("output/file.png should exist")
} }
if !(&tmp.path().join("output/sub_dir/file.png")).exists() { if !tmp.path().join("output/sub_dir/file.png").exists() {
panic!("output/sub_dir/file.png should exist") panic!("output/sub_dir/file.png should exist")
} }
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() { if !tmp.path().join("output/sub_dir_exists/file.txt").exists() {
panic!("output/sub_dir/file.png should exist") panic!("output/sub_dir/file.png should exist")
} }
if !(&tmp.path().join("output/symlink.png")).exists() { if !tmp.path().join("output/symlink.png").exists() {
panic!("output/symlink.png should exist") panic!("output/symlink.png should exist")
} }
} }

View File

@ -6,7 +6,7 @@ pub(crate) mod toml_ext;
use crate::errors::Error; use crate::errors::Error;
use log::error; use log::error;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
use regex::Regex; use regex::Regex;
use std::borrow::Cow; use std::borrow::Cow;
@ -161,37 +161,59 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
} }
match event { match event {
Event::Start(Tag::Link(link_type, dest, title)) => { Event::Start(Tag::Link {
Event::Start(Tag::Link(link_type, fix(dest, path), title)) link_type,
} dest_url,
Event::Start(Tag::Image(link_type, dest, title)) => { title,
Event::Start(Tag::Image(link_type, fix(dest, path), title)) id,
} }) => Event::Start(Tag::Link {
link_type,
dest_url: fix(dest_url, path),
title,
id,
}),
Event::Start(Tag::Image {
link_type,
dest_url,
title,
id,
}) => Event::Start(Tag::Image {
link_type,
dest_url: fix(dest_url, path),
title,
id,
}),
Event::Html(html) => Event::Html(fix_html(html, path)), Event::Html(html) => Event::Html(fix_html(html, path)),
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
_ => event, _ => event,
} }
} }
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML. /// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String { pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
render_markdown_with_path(text, curly_quotes, None) render_markdown_with_path(text, smart_punctuation, None)
} }
pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> { pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
let mut opts = Options::empty(); let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES); opts.insert(Options::ENABLE_TABLES);
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);
if curly_quotes { opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
if smart_punctuation {
opts.insert(Options::ENABLE_SMART_PUNCTUATION); opts.insert(Options::ENABLE_SMART_PUNCTUATION);
} }
Parser::new_ext(text, opts) Parser::new_ext(text, opts)
} }
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String { pub fn render_markdown_with_path(
text: &str,
smart_punctuation: bool,
path: Option<&Path>,
) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2); let mut s = String::with_capacity(text.len() * 3 / 2);
let p = new_cmark_parser(text, curly_quotes); let p = new_cmark_parser(text, smart_punctuation);
let events = p let events = p
.map(clean_codeblock_headers) .map(clean_codeblock_headers)
.map(|event| adjust_links(event, path)) .map(|event| adjust_links(event, path))
@ -211,7 +233,7 @@ fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
Some(Event::Html(r#"<div class="table-wrapper">"#.into())), Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
Some(event), Some(event),
), ),
Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))), Event::End(TagEnd::Table) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
_ => (Some(event), None), _ => (Some(event), None),
} }
} }

View File

@ -22,6 +22,7 @@
- [Tables](individual/table.md) - [Tables](individual/table.md)
- [Tasks](individual/task.md) - [Tasks](individual/task.md)
- [Strikethrough](individual/strikethrough.md) - [Strikethrough](individual/strikethrough.md)
- [MathJax](individual/mathjax.md)
- [Mixed](individual/mixed.md) - [Mixed](individual/mixed.md)
- [Languages](languages/README.md) - [Languages](languages/README.md)
- [Syntax Highlight](languages/highlight.md) - [Syntax Highlight](languages/highlight.md)

View File

@ -10,7 +10,9 @@ This is a codeblock
--- ---
This line contains `inline code` This line contains `inline code` mixed with some other stuff. (LTR)
ושורה זו מכילה `inline code` אבל עם טקסט בשפה שנכתבת מימין לשמאל. (RTL)
--- ---

View File

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

View File

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

View File

@ -0,0 +1,42 @@
# MathJax
Fourier Transform
\\[
\begin{aligned}
f(x) &= \int_{-\infty}^{\infty}F(s)(-1)^{ 2xs}ds \\\\
F(s) &= \int_{-\infty}^{\infty}f(x)(-1)^{-2xs}dx
\end{aligned}
\\]
The kernel can also be written as \\(e^{2i\pi xs}\\) which is more frequently used in literature.
> Proof that \\(e^{ix} = \cos x + i\sin x\\) a.k.a Euler's Formula:
>
> \\(
\begin{aligned}
e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} \implies e^{ix} = \sum_{n=0}^\infty \frac{(ix)^n}{n!} \\\\
\cos x &= \sum_{m=0}^\infty \frac{(-1)^m x^{2m}}{(2m)!} = \sum_{m=0}^\infty \frac{(ix)^{2m}}{(2m)!} \\\\
\sin x &= \sum_{s=0}^\infty \frac{(-1)^s x^{2s+1}}{(2s+1)!} = \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{i(2s+1)!} \\\\
\cos x + i\sin x &= \sum_{l=0}^\infty \frac{(ix)^{2l}}{(2l)!} + \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{(2s+1)!} = \sum_{n=0}^\infty \frac{(ix)^{n}}{n!} \\\\
&= e^{ix}
\end{aligned}
\\)
>
Pauli Matrices
\\[
\begin{aligned}
\sigma_x &= \begin{pmatrix}
1 & 0 \\\\ 0 & 1
\end{pmatrix} \\\\
\sigma_y &= \begin{pmatrix}
0 & -i \\\\ i & 0
\end{pmatrix} \\\\
\sigma_z &= \begin{pmatrix}
1 & 0 \\\\ 0 & -1
\end{pmatrix}
\end{aligned}
\\]

View File

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

View File

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

View File

@ -27,6 +27,8 @@ This Currently contains following languages
- makefile - makefile
- markdown - markdown
- nginx - nginx
- nim
- nix
- objectivec - objectivec
- perl - perl
- php - php

View File

@ -57,7 +57,7 @@ _start:
## bash ## bash
``` ```bash
#!/bin/bash #!/bin/bash
###### CONFIG ###### CONFIG
@ -529,6 +529,26 @@ http {
} }
``` ```
## nim
```nim
from strutils import `%`
const numDoors = 100
var doors {.compileTime.}: array[1..numDoors, bool]
proc calcDoors(): string =
for pass in 1..numDoors:
for door in countup(pass, numDoors, pass):
doors[door] = not doors[door]
for door in 1..numDoors:
result.add("Door $1 is $2.\n" % [$door, if doors[door]: "open" else: "closed"])
const outputString: string = calcDoors()
echo outputString
```
## objectivec ## objectivec
```objectivec ```objectivec
@ -543,6 +563,15 @@ int main(int argc, const char * argv[]) {
``` ```
## nix
```nix
let
world = "World!";
in
"Hello " + world
```
## perl ## perl
```perl ```perl

View File

@ -39,29 +39,27 @@ fn alternate_backend_with_arguments() {
md.build().unwrap(); md.build().unwrap();
} }
/// Get a command which will pipe `stdin` to the provided file.
#[cfg(not(windows))]
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
let out_file = out_file.as_ref();
if cfg!(windows) {
format!("cmd.exe /c \"type > {}\"", out_file.display())
} else {
format!("tee {}", out_file.display())
}
}
#[test] #[test]
#[cfg(not(windows))]
fn backends_receive_render_context_via_stdin() { fn backends_receive_render_context_via_stdin() {
use mdbook::renderer::RenderContext; use mdbook::renderer::RenderContext;
use std::fs::File; use std::fs::File;
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap(); let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", false);
let out_file = temp.path().join("out.txt");
let cmd = tee_command(&out_file);
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false); let renderers = temp.path().join("renderers");
fs::create_dir(&renderers).unwrap();
rust_exe(
&renderers,
"myrenderer",
r#"fn main() {
use std::io::Read;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
std::fs::write("out.txt", s).unwrap();
}"#,
);
let out_file = temp.path().join("book/out.txt");
assert!(!out_file.exists()); assert!(!out_file.exists());
md.build().unwrap(); md.build().unwrap();
@ -90,7 +88,7 @@ fn relative_command_path() {
.set("output.html", toml::value::Table::new()) .set("output.html", toml::value::Table::new())
.unwrap(); .unwrap();
config.set("output.myrenderer.command", cmd_path).unwrap(); config.set("output.myrenderer.command", cmd_path).unwrap();
let md = MDBook::init(&temp.path()) let md = MDBook::init(temp.path())
.with_config(config) .with_config(config)
.build() .build()
.unwrap(); .unwrap();

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

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@
- [Unicode](first/unicode.md) - [Unicode](first/unicode.md)
- [No Headers](first/no-headers.md) - [No Headers](first/no-headers.md)
- [Duplicate Headers](first/duplicate-headers.md) - [Duplicate Headers](first/duplicate-headers.md)
- [Heading Attributes](first/heading-attributes.md)
- [Second Chapter](second.md) - [Second Chapter](second.md)
- [Nested Chapter](second/nested.md) - [Nested Chapter](second/nested.md)

View File

@ -18,3 +18,7 @@ css looks, like this {
} }
*/ */
</style> </style>
Sneaky inline event <script>alert("inline");</script>.
But regular <b>inline</b> is indexed.

View File

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

View File

@ -35,6 +35,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
"1.5. Unicode", "1.5. Unicode",
"1.6. No Headers", "1.6. No Headers",
"1.7. Duplicate Headers", "1.7. Duplicate Headers",
"1.8. Heading Attributes",
"2.1. Nested Chapter", "2.1. Nested Chapter",
]; ];
@ -275,7 +276,7 @@ fn root_index_html() -> Result<Document> {
.with_context(|| "Book building failed")?; .with_context(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html"); let index_page = temp.path().join("book").join("index.html");
let html = fs::read_to_string(&index_page).with_context(|| "Unable to read index.html")?; let html = fs::read_to_string(index_page).with_context(|| "Unable to read index.html")?;
Ok(Document::from(html.as_str())) Ok(Document::from(html.as_str()))
} }
@ -374,10 +375,7 @@ fn able_to_include_playground_files_in_chapters() {
let second = temp.path().join("book/second.html"); let second = temp.path().join("book/second.html");
let playground_strings = &[ let playground_strings = &[r#"class="playground""#, r#"println!("Hello World!");"#];
r#"class="playground""#,
r#"println!(&quot;Hello World!&quot;);"#,
];
assert_contains_strings(&second, playground_strings); assert_contains_strings(&second, playground_strings);
assert_doesnt_contain_strings(&second, &["{{#playground example.rs}}"]); assert_doesnt_contain_strings(&second, &["{{#playground example.rs}}"]);
@ -412,7 +410,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 +460,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 +626,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.
@ -746,6 +742,7 @@ mod search {
let index = read_book_index(temp.path()); let index = read_book_index(temp.path());
let doc_urls = index["doc_urls"].as_array().unwrap(); let doc_urls = index["doc_urls"].as_array().unwrap();
eprintln!("doc_urls={doc_urls:#?}",);
let get_doc_ref = let get_doc_ref =
|url: &str| -> String { doc_urls.iter().position(|s| s == url).unwrap().to_string() }; |url: &str| -> String { doc_urls.iter().position(|s| s == url).unwrap().to_string() };
@ -756,6 +753,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,13 +766,16 @@ 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"],
"First Chapter » Includes » Summary" "First Chapter » Includes » Summary"
); );
assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!"); // See note about InlineHtml in search.rs. Ideally the `alert()` part
// should not be in the index, but we don't have a way to scrub inline
// html.
assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed.");
assert_eq!( assert_eq!(
docs[&no_headers]["breadcrumbs"], docs[&no_headers]["breadcrumbs"],
"First Chapter » No Headers" "First Chapter » No Headers"
@ -787,6 +788,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 +808,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 +896,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 +905,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 +953,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