Compare commits

..

196 Commits

Author SHA1 Message Date
Eric Huss
f6dd0a4a13 Merge pull request #2753 from ehuss/bump-version
Update to 0.4.52
2025-07-14 22:51:59 +00:00
Eric Huss
432b4296ab Update to 0.4.52 2025-07-14 15:45:55 -07:00
Eric Huss
6b1dc01a3f Merge pull request #2752 from ehuss/update-deps
Update dependencies
2025-07-14 22:38:53 +00:00
Eric Huss
9d8e99f8d7 Update dependencies
Updating adler2 v2.0.0 -> v2.0.1
Updating ammonia v4.1.0 -> v4.1.1
Updating anstream v0.6.18 -> v0.6.19
Updating anstyle v1.0.10 -> v1.0.11
Updating anstyle-lossy v1.1.3 -> v1.1.4
Updating anstyle-parse v0.2.6 -> v0.2.7
Updating anstyle-query v1.1.2 -> v1.1.3
Updating anstyle-svg v0.1.7 -> v0.1.9
Updating anstyle-wincon v3.0.8 -> v3.0.9
Updating autocfg v1.4.0 -> v1.5.0
Updating bumpalo v3.17.0 -> v3.19.0
Updating cc v1.2.24 -> v1.2.29
Updating cfg-if v1.0.0 -> v1.0.1
Updating clap v4.5.38 -> v4.5.41
Updating clap_builder v4.5.38 -> v4.5.41
Updating clap_complete v4.5.50 -> v4.5.55
Updating clap_lex v0.7.4 -> v0.7.5
Updating colorchoice v1.0.3 -> v1.0.4
Updating errno v0.3.12 -> v0.3.13
Updating html5ever v0.31.0 -> v0.35.0
Adding io-uring v0.7.8
Updating jiff v0.2.14 -> v0.2.15
Updating jiff-static v0.2.14 -> v0.2.15
Updating libc v0.2.172 -> v0.2.174
Updating libredox v0.1.3 -> v0.1.4
Updating lock_api v0.4.12 -> v0.4.13
Updating markup5ever v0.16.1 -> v0.35.0
Updating match_token v0.1.0 -> v0.35.0
Updating memchr v2.7.4 -> v2.7.5
Updating miniz_oxide v0.8.8 -> v0.8.9
Updating mio v1.0.3 -> v1.0.4
Updating notify v8.0.0 -> v8.1.0
Updating opener v0.8.1 -> v0.8.2
Updating parking_lot v0.12.3 -> v0.12.4
Updating parking_lot_core v0.9.10 -> v0.9.11
Updating pest v2.8.0 -> v2.8.1
Updating pest_derive v2.8.0 -> v2.8.1
Updating pest_generator v2.8.0 -> v2.8.1
Updating pest_meta v2.8.0 -> v2.8.1
Updating portable-atomic v1.11.0 -> v1.11.1
Updating r-efi v5.2.0 -> v5.3.0
Updating redox_syscall v0.5.12 -> v0.5.13
Updating rustc-demangle v0.1.24 -> v0.1.25
Updating slab v0.4.9 -> v0.4.10
Updating smallvec v1.15.0 -> v1.15.1
Updating socket2 v0.5.9 -> v0.5.10
Updating syn v2.0.101 -> v2.0.104
Updating tokio v1.45.0 -> v1.46.1
Updating tracing-core v0.1.33 -> v0.1.34
Updating unicode-width v0.2.0 -> v0.2.1
Updating wasi v0.11.0+wasi-snapshot-preview1 -> v0.11.1+wasi-snapshot-preview1
Updating web_atoms v0.1.2 -> v0.1.3
Updating windows-link v0.1.1 -> v0.1.3
Adding windows-sys v0.60.2
Adding windows-targets v0.53.2
Adding windows_aarch64_gnullvm v0.53.0
Adding windows_aarch64_msvc v0.53.0
Adding windows_i686_gnu v0.53.0
Adding windows_i686_gnullvm v0.53.0
Adding windows_i686_msvc v0.53.0
Adding windows_x86_64_gnu v0.53.0
Adding windows_x86_64_gnullvm v0.53.0
Adding windows_x86_64_msvc v0.53.0
Updating zerocopy v0.8.25 -> v0.8.26
Updating zerocopy-derive v0.8.25 -> v0.8.26
2025-07-14 15:30:54 -07:00
Eric Huss
e152e197c1 Merge pull request #2702 from capjamesg/patch-1
Add rel=edit attribute to "Suggest an edit" link
2025-07-14 22:10:14 +00:00
Eric Huss
7e68d01e7d Merge pull request #2748 from jelmer/warp-to-axum
Replace warp with axum
2025-07-14 22:06:31 +00:00
James
bd97611eb0 Add rel="edit" for the edit button
rel=edit lets a page indicate that the linked resource can be used to
edit the page. It is defined at https://microformats.org/wiki/rel-edit.
This can then be parsed by tools like the Universal Edit Button and
custom bookmarklets to open the edit page corresponding with a website.
2025-07-14 15:04:42 -07:00
Jelmer Vernooij
8e579072b8 Replace warp with axum
warp is problematic for Debian, since it has some outdated dependencies. Upstream is also fairly dormant.
2025-07-14 15:00:34 -07:00
Eric Huss
a918910a52 Merge pull request #2747 from ehuss/fragment-redirect
Add support for fragment redirects
2025-07-14 21:55:08 +00:00
Eric Huss
1eeb0d23e6 Merge pull request #2750 from ehuss/fix-resize-visible
Fix sidebar animation and other behavior
2025-07-14 21:39:25 +00:00
Eric Huss
c842b5d06e Fix sidebar animation and other behavior
This fixes several issues with how the sidebar was behaving:

- Manually resizing the sidebar was incorrectly applying transition
  animations to the page-wrapper causing awkward movement.
- Clicking the sidebar toggle caused the menu bar to behave differently
  compared to loading a page with the sidebar visible or hidden.
- page-wrapper animation wasn't working when JS was disabled.
- RTL sidebar animation was broken.

Most of these issues stem from
https://github.com/rust-lang/mdBook/pull/2454 which moved `js` and
`sidebar-visible` classes from `<body>` to `<html>`, but failed to
update some of the JS and CSS code that was still assuming it was on the
body.

https://github.com/rust-lang/mdBook/pull/1641 previously moved `js` from
`<html>` to `<body>` with the reasoning
"This will be necessary for using CSS selectors on root attributes.".
However, I don't see how that is absolutely necessary, since selectors
like `[dir=rtl].js` should work to select the root element.
2025-07-14 14:24:32 -07:00
Eric Huss
0ac89dd826 Merge pull request #2746 from notriddle/patch-1
Work around compat break in old index.hbs templates with new searcher.js
2025-07-08 22:54:11 +00:00
Michael Howell
7ec083e426 Work around compat break in old index.hbs templates with new searcher.js
Problem reported in

https://github.com/rust-lang/mdBook/pull/2742#discussion_r2190930557
2025-07-08 15:47:40 -07:00
Eric Huss
15c93b56ed Add support for fragment redirects
This adds the ability to redirect URLs with `#` fragments. This is
useful when section headers get renamed or moved to other pages.

This works both for deleted pages and existing pages.

The implementation requires the use of JavaScript in order to manipulate
the location. (Ideally this would be handled on the server side.)

This also makes it so that deleted page redirects preserve the fragment
ID. Previously if you had a deleted page redirect, and the user went to
something like `page.html#foo`, it would redirect to `bar.html` without
the fragment. I think preserving the fragment is probably a better
behavior. If the new page doesn't have the fragment ID, then no harm is
really done. This is technically an open redirect, but I don't think
that there is too much danger with preserving a fragment ID?
2025-07-08 15:37:46 -07:00
Eric Huss
ec6f26e652 Merge pull request #2744 from GuillaumeGomez/sidebar-text-test
Add check that text in collapsed sidebar cannot be found
2025-07-07 20:39:03 +00:00
Guillaume Gomez
cdce9a7666 Add check that text in collapsed sidebar cannot be found 2025-07-07 21:34:47 +02:00
Guillaume Gomez
63432355e6 Update browser-ui-test version to 0.21.1 2025-07-07 21:34:33 +02:00
Guillaume Gomez
4bf2b472bb Merge pull request #2742 from notriddle/cache-fix-searchindex
Remove direct search index reference from searcher
2025-07-07 19:25:09 +00:00
Eric Huss
1476ec72c3 Merge pull request #2725 from GuillaumeGomez/search-collapsed
Hide the sidebar when collapsed to prevent browser search to find text from it
2025-07-07 17:03:13 +00:00
Eric Huss
d1c09791ab Fix animation bug in safari
When showing the sidebar, Safari was causing the sidebar to snap into
place without animating. This is apparently some well-known issue where
it doesn't like adding new elements (or changing display) and toggling
an animated transition in the same event loop.
2025-07-07 09:47:55 -07:00
Guillaume Gomez
16b99be17f Update sidebar GUI test 2025-07-07 09:47:55 -07:00
Guillaume Gomez
bcd4552bdf Hide the sidebar when collapsed to prevent browser search to find text from it 2025-07-07 09:47:33 -07:00
Guillaume Gomez
f942f3835e Remove unused CSS class sidebar-hidden 2025-07-07 09:47:33 -07:00
Michael Howell
e96c608c11 Remove direct search index reference from searcher
Because `{{resource}}` references don't affect the hash[^1], we need
to avoid referencing dynamic content from within static content.
Otherwise, you get a cached searcher.js referencing a searchindex
that no longer exists.

[^1]: if we made it affect the hash, we'd have to do full dependency
      tracking, and we'd no longer be able to support circular refs
2025-07-07 09:08:06 -07:00
Eric Huss
e6315bf2b1 Merge pull request #2738 from szabgab/test/test-tokenize
add tests to the tokenize() function
2025-06-30 15:01:44 +00:00
Gabor Szabo
6d63343b46 add tests to the tokenize() function 2025-06-30 16:11:35 +03:00
Eric Huss
e1e4518499 Merge pull request #2736 from GuillaumeGomez/update-browser-ui-test
Update browser-ui-test version to `0.21.0`
2025-06-28 20:34:15 +00:00
Guillaume Gomez
34d68403da Update browser-ui-test version to 0.21.0 2025-06-28 13:28:20 -07:00
Eric Huss
e395341210 Merge pull request #2735 from GuillaumeGomez/fix-js-error
Fix JS error when `hasFocus` method is overwritten on search index load
2025-06-28 19:55:37 +00:00
Guillaume Gomez
f4c54178c8 Update tests 2025-06-28 00:04:02 +02:00
Guillaume Gomez
63e1dac122 Fix JS error when hasFocus method is overwritten on search index load 2025-06-25 18:24:33 +02:00
Eric Huss
e8dff6f97e Merge pull request #2726 from Sableoxide/fix-typo-isnt-it
Fix typo 'isn't is?' -> 'isn't it?' in test_book/src/individual/code.md
2025-06-09 15:21:24 +00:00
frank goko
534666f551 fixed typo 'isn't is?' -> 'isn't it?' in test_book/src/individual/code.md 2025-06-08 23:42:42 +03:00
Eric Huss
f3ee794283 Merge pull request #2715 from tshepang/patch-1
add another link type
2025-06-02 15:14:02 +00:00
Eric Huss
94f9a9c5e0 Merge pull request #2553 from GuillaumeGomez/load-on-need
Only load searchindex when needed
2025-06-02 15:01:09 +00:00
Guillaume Gomez
dc6b0a6e58 Update search test 2025-05-31 09:55:58 +02:00
Guillaume Gomez
2fa13cf4e0 Add a spinner when search is in progress 2025-05-31 09:36:27 +02:00
Guillaume Gomez
d64a863223 Add GUI test for search 2025-05-31 09:36:27 +02:00
Guillaume Gomez
1fb91d67f6 Only load searchindex when needed 2025-05-31 09:36:27 +02:00
Eric Huss
1b046e5a90 Merge pull request #2719 from ehuss/update-browser-ui-test
Update browser-ui-test to 0.20.6
2025-05-30 19:39:59 +00:00
Eric Huss
e9f5e3d7c0 Update browser-ui-test to 0.20.6
This updates browser-ui-test to 0.20.6 which has some new features
that could be useful.
2025-05-30 12:23:27 -07:00
Tshepang Mbambo
c6a5d05c64 add another link type 2025-05-30 21:18:11 +02:00
Eric Huss
f93b2675ff Merge pull request #2714 from ehuss/bump-version
Update to 0.4.51
2025-05-26 18:23:28 +00:00
Eric Huss
365918bf89 Merge pull request #2713 from ehuss/fix-s-key
Fix search hotkey
2025-05-26 18:04:46 +00:00
Eric Huss
76be253f76 Update to 0.4.51 2025-05-26 11:01:12 -07:00
Eric Huss
0210e69abc Fix search hotkey
https://github.com/rust-lang/mdBook/pull/2608 accidentally broke the
search "S" keybinding by using the wrong capitalization.
2025-05-26 10:58:53 -07:00
Eric Huss
cdbf6d2806 Merge pull request #2712 from ehuss/bump-version
Update to 0.4.50
2025-05-23 15:04:07 +00:00
Eric Huss
902ded9f89 Merge pull request #2711 from ehuss/update-dependencies
Update dependencies
2025-05-23 15:01:40 +00:00
Eric Huss
851932bd4b Update to 0.4.50 2025-05-23 07:57:51 -07:00
Eric Huss
f38dc687e3 Update dependencies
Also bump MSRV to 1.82

Updating anstyle-wincon v3.0.7 -> v3.0.8
Updating backtrace v0.3.74 -> v0.3.75
Updating bitflags v2.9.0 -> v2.9.1
Updating cc v1.2.21 -> v1.2.24
Updating clap v4.5.37 -> v4.5.38
Updating clap_builder v4.5.37 -> v4.5.38
Updating clap_complete v4.5.48 -> v4.5.50
Updating errno v0.3.11 -> v0.3.12
Updating getrandom v0.3.2 -> v0.3.3
Updating icu_collections v1.5.0 -> v2.0.0
Adding icu_locale_core v2.0.0
Removing icu_locid v1.5.0
Removing icu_locid_transform v1.5.0
Removing icu_locid_transform_data v1.5.1
Updating icu_normalizer v1.5.0 -> v2.0.0
Updating icu_normalizer_data v1.5.1 -> v2.0.0
Updating icu_properties v1.5.1 -> v2.0.1
Updating icu_properties_data v1.5.1 -> v2.0.1
Updating icu_provider v1.5.0 -> v2.0.0
Removing icu_provider_macros v1.5.0
Updating idna_adapter v1.2.0 -> v1.2.1
Updating jiff v0.2.12 -> v0.2.14
Updating jiff-static v0.2.12 -> v0.2.14
Updating kqueue v1.0.8 -> v1.1.1
Updating litemap v0.7.5 -> v0.8.0
Adding once_cell_polyfill v1.70.1
Adding potential_utf v0.1.2
Updating rustversion v1.0.20 -> v1.0.21
Updating tempfile v3.19.1 -> v3.20.0
Updating tinystr v0.7.6 -> v0.8.1
Updating tokio v1.44.2 -> v1.45.0
Removing utf16_iter v1.0.5
Updating web_atoms v0.1.1 -> v0.1.2
Updating windows-core v0.61.0 -> v0.61.2
Updating windows-result v0.3.2 -> v0.3.4
Updating windows-strings v0.4.0 -> v0.4.2
Removing write16 v1.0.0
Updating writeable v0.5.5 -> v0.6.1
Updating yoke v0.7.5 -> v0.8.0
Updating yoke-derive v0.7.5 -> v0.8.0
Adding zerotrie v0.2.2
Updating zerovec v0.10.4 -> v0.11.2
Updating zerovec-derive v0.10.3 -> v0.11.1
2025-05-23 07:55:10 -07:00
Eric Huss
391ee3bae2 Merge pull request #2695 from hamirmahal/fix/invalid-heading-order-in-contributing-md
fix: invalid heading order in `CONTRIBUTING.md`
2025-05-23 14:53:01 +00:00
Hamir Mahal
ad3096e871 fix: invalid heading order in CONTRIBUTING.md 2025-05-23 07:46:40 -07:00
Eric Huss
179bd8dcd5 Merge pull request #2710 from ehuss/fix-mode-rust
Fix loading of mode-rust.js
2025-05-23 14:42:05 +00:00
Eric Huss
426e7bee17 Fix loading of mode-rust.js
The reason the ACE editor was failing to load the rust syntax
highlighting is because the syntax highlighting was being created
*after* the editor was created. If the editor is created first, then ACE
tries to load `ace/mode/rust`. Since it isn't already defined, it tried
to compute the URL and load it manually. However, since the URLs now
have a hash in it (via https://github.com/rust-lang/mdBook/pull/1368),
it was unable to load.

The solution here is to make sure `ace/mode/rust` is defined before
creating the editors. Then ACE knows that it can just load the module
directly instead of trying to fetch it from the server.

Fixes https://github.com/rust-lang/mdBook/issues/2700
2025-05-23 07:19:27 -07:00
Eric Huss
564c80bf6d Merge pull request #2709 from zeenix/master
Update `opener` to 0.8.1
2025-05-21 20:22:49 +00:00
Zeeshan Ali Khan
363c12e9c3 Update opener to 0.8.1
This removes a few indirect deps, especially system deps like `dbus` and
`pkg-config`.
2025-05-21 18:33:43 +02:00
Eric Huss
1ffa9fe830 Merge pull request #2707 from noritada/fix/edition2024-in-guide
Document the `edition2024` code block attribute
2025-05-19 15:38:52 +00:00
Noritada Kobayashi
91d13c390a Document the edition2024 code block attribute 2025-05-19 12:51:11 +09:00
Eric Huss
ce8d8120f8 Merge pull request #2705 from ehuss/help-test
Add help gui test
2025-05-15 12:32:27 +00:00
Eric Huss
038b5fb232 Add help gui test 2025-05-15 05:25:48 -07:00
Eric Huss
fd768efba2 Merge pull request #2703 from ehuss/fix-clippy
Fix clippy lint for 1.88
2025-05-15 01:27:40 +00:00
Eric Huss
481346812c Merge pull request #2608 from szabgab/navigation-help
Add pop-up window showing the keyboard shortcuts
2025-05-15 01:24:55 +00:00
Eric Huss
3d07798832 Fix clippy lint for 1.88 2025-05-14 18:18:45 -07:00
Eric Huss
33da0c26c4 Some updates to the help popup
This makes a few changes to the help popup:

- Move css to chrome.css, since this is a UI element.
- Move HTML code to index.hbs instead of generated in JavaScript.
  In general I prefer to keep HTML out of JavaScript when possible,
  and I didn't see a particular reason to avoid it.
- Added a click handler to dismiss the popup.
- Make sure handlers get removed when dismissed.
- Use `mdbook-` prefixes for IDs to avoid collisions with headers.
- Don't show search if it isn't enabled.
- Add the new `/` shortcut.
- Use flex layout for better positioning.
- Dim out the surrounding text using an overlay.
- Various other styling tweaks.
- Add a GUI test.
2025-05-14 18:17:16 -07:00
Eric Huss
91e94024cd Switch searcher to use keys instead of keyCodes
According to
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
the keyCode is deprecated (and removed). It is causing me some
difficulty with the GUI tests because it can't differentiate between a
slash and question mark correctly. Using keys seems to work.
2025-05-14 18:16:20 -07:00
Gabor Szabo
c9ad6dbf10 eslint 2025-05-14 17:06:49 -07:00
Gabor Szabo
2b455fcd34 Make the help visible in all the themes of mdbook
Also handle the ? key with or without shift being pressed.
2025-05-14 17:06:49 -07:00
Gabor Szabo
4573f4d882 Add pop-up window showing the keyboard shortcuts
Make it display when the user presses `?`.

Implements #2607
2025-05-14 17:06:49 -07:00
Eric Huss
63ae0d5c18 Merge pull request #2698 from traviscross/TC/search-ergonomics
Improve ergonomics of search
2025-05-14 00:43:24 +00:00
Travis Cross
52e406bf95 Use <kbd> tag to describe keyboard shortcuts
When describing, in the guide, the keyboard shortcuts that we accept,
let's use the `<kbd>` element.  This causes the key to render in a box
that people will recognize as conventional.

The way that this is displayed helps to make it clear that, though we
present the key in uppercase, we actually mean for the lowercase
letter to be entered.  Therefore, we present the key in uppercase
since 1) that's how it appears on most keyboards and 2) for some
characters such as `l`, presenting the character in lowercase might be
ambiguous.

We'll spell out "Escape" rather than saying "Esc" (even though many
keyboards spell it that way) since the `KeyboardEvent.keycode`[^1] is
called "Escape", and that's how it would appear in an
`aria-keyshortcuts` attribute[^2].

[^1]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode

[^2]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-keyshortcuts
2025-05-13 22:04:49 +00:00
Travis Cross
3e871d1971 Navigate to first search result on enter
It's common for search boxes like ours to automatically navigate to
the first search result when the `enter` / `select` key is pressed, as
that can allow for rapid navigation.  E.g., the MDN documentation does
this.

Let's similarly navigate to the first result when, in the search box,
the user presses the `enter` key and there is a first result to which
to navigate.
2025-05-13 21:50:04 +00:00
Travis Cross
84a5ba9707 Handle DOWN_KEYCODE with no results
If, when searching, one pressed the down arrow key when there were no
results, this caused an uncaught exception and defocused the search
box.

Let's prevent this and keep the search box focused when pressing down
in this state by checking first whether there is a result for us to
focus instead.
2025-05-13 21:50:04 +00:00
Travis Cross
44d9f4e95b Use / (or s) to open search box
We allow for using `s` to open the search box, but it's more common to
use `/` (forward slash) for this.  E.g., MDN's documentation uses `/`
for search.  Rustdoc and GitHub accept either.

Let's allow either key to be used, and let's switch to "advertising"
`/` rather than `s` in the hover text for the search button.

In making that switch, let's also simplify that hover text a bit.
Previously it had said "Search. (Shortkey: s)".  This was the only top
button on which we had included a period in the hover text.  Let's
remove that, and let's remove the "shortkey" bit of jargon.  It's
enough to just put `/` in a parenthetical, i.e. "Search (`/`)".
People will gleam from that what we mean.

We've also updated the guide accordingly.
2025-05-13 21:48:54 +00:00
Eric Huss
d24c0ca0d7 Merge pull request #2696 from paolobarbolini/once-cell-to-std
Replace `once_cell::sync::Lazy` with `std::sync::LazyLock`
2025-05-12 15:11:11 +00:00
Paolo Barbolini
623fc606a4 Replace once_cell::sync::Lazy with std::sync::LazyLock 2025-05-11 11:46:12 +02:00
Eric Huss
a8aee21cd0 Merge pull request #2691 from notriddle/knurling
sidebar: use the same resize grip as rustdoc and playground
2025-05-10 18:25:55 +00:00
Michael Howell
649a021647 sidebar: use the same resize grip as rustdoc and playground 2025-05-09 14:15:53 -07:00
Eric Huss
785ee564c5 Merge pull request #2690 from ehuss/bump-version
Update to 0.4.49
2025-05-05 21:51:22 +00:00
Eric Huss
006f99ee99 Update to 0.4.49 2025-05-05 14:44:37 -07:00
Eric Huss
2a4e5140c9 Merge pull request #2689 from ehuss/revert-multilingual
Revert: Remove the book.multilingual field and don't serialize it
2025-05-05 21:13:46 +00:00
Eric Huss
3c10b00096 Skip serializing of the multilingual field
This skips serializing of the multilingual field since it is unused, and
we plan to remove it in the future. This helps avoid it showing up in
`mdbook init`.
2025-05-05 14:05:00 -07:00
Eric Huss
c9ddb4dd98 Revert: Remove the book.multilingual field
This reverts https://github.com/rust-lang/mdBook/pull/2646/ because I
overlooked that this is a public field in a public struct, which would
be a breaking API change.
2025-05-05 14:01:49 -07:00
Eric Huss
9822c2a178 Merge pull request #2688 from ehuss/update-dependencies
Update dependencies
2025-05-05 20:52:43 +00:00
Eric Huss
199efd0f2c Update dependencies
Updating ammonia v4.0.0 -> v4.1.0
Updating anyhow v1.0.95 -> v1.0.98
Updating bitflags v2.8.0 -> v2.9.0
Updating bstr v1.11.3 -> v1.12.0
Updating bumpalo v3.16.0 -> v3.17.0
Updating bytes v1.9.0 -> v1.10.1
Updating cc v1.2.10 -> v1.2.21
Updating chrono v0.4.39 -> v0.4.41
Updating clap v4.5.27 -> v4.5.37
Updating clap_builder v4.5.27 -> v4.5.37
Updating clap_complete v4.5.43 -> v4.5.48
  Adding cssparser v0.35.0
  Adding cssparser-macros v0.6.1
Updating darling v0.20.10 -> v0.20.11
Updating darling_core v0.20.10 -> v0.20.11
Updating darling_macro v0.20.10 -> v0.20.11
Updating data-encoding v2.7.0 -> v2.9.0
  Adding dtoa v1.0.10
  Adding dtoa-short v0.3.5
Updating env_logger v0.11.6 -> v0.11.8
Updating equivalent v1.0.1 -> v1.0.2
Updating errno v0.3.10 -> v0.3.11
Removing getrandom v0.2.15
  Adding getrandom v0.2.16
  Adding getrandom v0.3.2
Updating globset v0.4.15 -> v0.4.16
Updating handlebars v6.3.0 -> v6.3.2
Updating hashbrown v0.15.2 -> v0.15.3
Updating html5ever v0.27.0 -> v0.31.0
Updating http v1.2.0 -> v1.3.1
Updating httparse v1.10.0 -> v1.10.1
Removing humantime v2.1.0
Updating iana-time-zone v0.1.61 -> v0.1.63
Updating icu_locid_transform_data v1.5.0 -> v1.5.1
Updating icu_normalizer_data v1.5.0 -> v1.5.1
Updating icu_properties_data v1.5.0 -> v1.5.1
Updating indexmap v2.7.1 -> v2.9.0
Updating itoa v1.0.14 -> v1.0.15
  Adding jiff v0.2.12
  Adding jiff-static v0.2.12
Updating libc v0.2.169 -> v0.2.172
Updating linux-raw-sys v0.4.15 -> v0.9.4
Updating litemap v0.7.4 -> v0.7.5
Updating log v0.4.25 -> v0.4.27
Updating markup5ever v0.12.1 -> v0.16.1
  Adding match_token v0.1.0
Updating miniz_oxide v0.8.3 -> v0.8.8
Updating once_cell v1.20.2 -> v1.21.3
Updating pest v2.7.15 -> v2.8.0
Updating pest_derive v2.7.15 -> v2.8.0
Updating pest_generator v2.7.15 -> v2.8.0
Updating pest_meta v2.7.15 -> v2.8.0
  Adding phf_macros v0.11.3
Updating pin-project v1.1.8 -> v1.1.10
Updating pin-project-internal v1.1.8 -> v1.1.10
Updating pkg-config v0.3.31 -> v0.3.32
  Adding portable-atomic v1.11.0
  Adding portable-atomic-util v0.2.4
Updating ppv-lite86 v0.2.20 -> v0.2.21
Updating proc-macro2 v1.0.93 -> v1.0.95
Updating quote v1.0.38 -> v1.0.40
  Adding r-efi v5.2.0
Updating redox_syscall v0.5.8 -> v0.5.12
Updating rustix v0.38.44 -> v1.0.7
Updating rustversion v1.0.19 -> v1.0.20
Updating ryu v1.0.19 -> v1.0.20
Updating select v0.6.0 -> v0.6.1
Updating semver v1.0.25 -> v1.0.26
Updating serde v1.0.217 -> v1.0.219
Updating serde_derive v1.0.217 -> v1.0.219
Updating serde_json v1.0.137 -> v1.0.140
Updating sha2 v0.10.8 -> v0.10.9
Updating smallvec v1.13.2 -> v1.15.0
Updating socket2 v0.5.8 -> v0.5.9
Updating string_cache v0.8.7 -> v0.8.9
Updating string_cache_codegen v0.5.2 -> v0.5.4
Updating syn v2.0.96 -> v2.0.101
Updating synstructure v0.13.1 -> v0.13.2
Updating tempfile v3.15.0 -> v3.19.1
Updating terminal_size v0.4.1 -> v0.4.2
Updating thiserror v2.0.11 -> v2.0.12
Updating thiserror-impl v2.0.11 -> v2.0.12
Updating tokio v1.43.1 -> v1.44.2
Updating tokio-util v0.7.13 -> v0.7.15
Updating typenum v1.17.0 -> v1.18.0
Updating unicode-ident v1.0.16 -> v1.0.18
  Adding wasi v0.14.2+wasi-0.2.4
  Adding web_atoms v0.1.1
Updating windows-core v0.52.0 -> v0.61.0
  Adding windows-implement v0.60.0
  Adding windows-interface v0.59.1
  Adding windows-link v0.1.1
  Adding windows-result v0.3.2
  Adding windows-strings v0.4.0
  Adding wit-bindgen-rt v0.39.0
Updating zerocopy v0.7.35 -> v0.8.25
Updating zerocopy-derive v0.7.35 -> v0.8.25
Updating zerofrom v0.1.5 -> v0.1.6
Updating zerofrom-derive v0.1.5 -> v0.1.6
2025-05-05 13:44:40 -07:00
Eric Huss
17b197620b Merge pull request #2679 from lolbinarycat/sidebar-size-2678
fix(css): sidebar can no longer be larger than 80% of viewport width
2025-05-03 16:33:32 +00:00
Eric Huss
23abd20589 Merge pull request #2681 from krishanjmistry/issue-2649-footnotes
Warn on and ignore duplicate footnote definitions
2025-04-30 13:48:09 +00:00
Krishan Mistry
7e9be8dee3 Warn on duplicate footnote definition and ignore subsequent definitions 2025-04-30 06:39:48 -07:00
Krishan Mistry
09d22e926f Add a test for duplicate footnote definitions 2025-04-30 06:37:24 -07:00
Eric Huss
1696f5680e Move footnote expected HTML to a separate file
This output is starting to get a little long, so this moves it to a
separate file to keep things a little more tidy.
2025-04-30 06:36:09 -07:00
binarycat
a54ee0215e fix(css): sidebar can no longer be larger than 80% of viewport width 2025-04-25 14:29:58 -05:00
Eric Huss
9b12c5130f Merge pull request #2676 from ehuss/booktest
Introduce new testsuite infrastructure
2025-04-23 04:18:53 +00:00
Eric Huss
084771bcd5 Remove parse_existing_summary_files tests
Although there isn't a direct equivalent in the new testsuite, I felt
like these weren't adding any specific coverage that the existing tests
don't already exercise. If it does turn up that there is specific
coverage missing, then I would prefer to have tests which exercise the
specific thing of interest rather than have these kinds of non-specific
tests.
2025-04-22 21:11:54 -07:00
Eric Huss
7215d60c67 Remove remaining dummy book structure
These tests are now all superseded by the new testsuite.
2025-04-22 21:11:54 -07:00
Eric Huss
0224190ec0 Add testsuite book directories to ignore list
This helps if you are creating new tests, or debugging existing tests by
allowing you to run `mdbook` directly in the test directory to try
things out. But we don't want to ever check these files in.
2025-04-22 21:11:54 -07:00
Eric Huss
ae2fc9a9d1 Remove remaining rendered tests
I don't think these exercise anything in particular that aren't
necessarily covered by other things. If this ends up exposing a lack
of coverage somewhere, I would prefer to add more focused test for
specific things.
2025-04-22 21:11:54 -07:00
Eric Huss
d65d2b2a8e Migrate summary_with_markdown_formatting to BookTest 2025-04-22 21:11:54 -07:00
Eric Huss
69972080f0 Migrate check_link_target_fallback to BookTest 2025-04-22 21:11:54 -07:00
Eric Huss
5f2453e446 Migrate check_link_target_js to BookTest 2025-04-22 21:11:54 -07:00
Eric Huss
20f71af4cb Migrate check_spacers to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
efc5ee4449 Migrate check_first_toc_level to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
14d412b279 Migrate check_second_toc_level to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
707319e004 Migrate custom fonts with filled fonts.css to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
bdd16e25fa Migrate copy-fonts=false empty fonts.css to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
9a1f983e65 Copy copy-fonts=false no theme to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
c2c37705e7 Migrate custom fonts.css to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
5f227613aa Migrate copy theme default fonts to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
0274ad6e87 Migrate (no theme) default fonts to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
dd27c4f8ba Migrate theme_dir_overrides_work_correctly to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
25b9acc321 Migrate empty theme to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
10fae8596c Migrate missing theme to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
909bd1c54e Migrate mdbook_test_chapter_not_found to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
f324aebdec Migrate mdbook_test_chapter to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
5a84d641cd Migrate pass/fail mdbook test to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
0b577ebd76 Migrate chapter_settings_validation_error to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
2056c87e28 Migrate with_no_source_path to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
8bfa6462f8 Migrate can_disable_individual_chapters to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
a8660048ca Migrate search_index_hasnt_changed_accidentally to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
cad8988f8d Migrate book_creates_reasonable_search_index to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
3fce1151dd Migrate first_chapter_is_copied_as_index_even_if_not_first_elem to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
d23bdaa527 Migrate edit-url-template tests to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
2f10831a80 Migrate relative_command_path to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
a38a30da1e Migrate backends_receive_render_context_via_stdin to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
82000d917f Migrate alternate_backend_with_arguments to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
f482aeaca3 Migrate missing_optional_backends_are_not_fatal to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
86638abea9 Migrate missing_backends_are_fatal to BookTest 2025-04-22 21:11:53 -07:00
Eric Huss
a2cf838baf Migrate failing_alternate_backend to BookTest 2025-04-22 21:11:46 -07:00
Eric Huss
5bc25e32eb Remove passing_alternate_backend
After some testing I notice that this test is failing randomly because
the `true` program is exiting before mdbook is able to transmit the
JSON, and it fails with a broken pipe.

This will be replaced with backends_receive_render_context_via_stdin,
which does essentially the same thing, but does suffer from the same
problem.
2025-04-22 20:50:20 -07:00
Eric Huss
15c6f3f318 Migrate mdbook_runs_renderers to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
cb2a63ea0a Migrate redirects_are_emitted_correctly to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
50dfa365c7 Migrate no_index_for_print_html to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
3e22a5cdad Migrate check_correct_relative_links_in_print_page to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
5034707a73 Migrate CmdPreprocessor tests to testsuite 2025-04-22 20:50:20 -07:00
Eric Huss
d815b0cc52 Migrate ask_the_preprocessor_to_blow_up to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
fca149a52c Migrate process_the_dummy_book to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
b4221680e4 Print more context for debugging nop-preprocessor 2025-04-22 20:50:20 -07:00
Eric Huss
ba448a9dd5 Migrate mdbook_runs_preprocessors to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
aa29ef04a2 Migrate rendered_code_does_not_have_playground_stuff_in_html_when_disabled_in_config to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
20d42a53d3 Migrate rendered_code_has_playground_stuff to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
8c8f0a4dbf Add test for smart punctuation 2025-04-22 20:50:20 -07:00
Eric Huss
6904653a82 Migrate custom_header_attributes to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
74e01ea6e3 Migrate markdown_options to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
0732cb47b9 Migrate copy_theme to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
29338b5ade Migrate run_mdbook_init_with_custom_book_and_src_locations to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
4019060ef4 Migrate run_mdbook_init_should_create_content_from_summary to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
3e1d750efa Migrate no_git_config_with_title to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
41bfbc69e6 Migrate base_mdbook_init_can_skip_confirmation_prompts to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
6fdd7b4a17 Migrate base_mdbook_init_should_create_default_content to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
c6d9f15cba Migrate by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
0f397ebdb5 Migrate rustdoc_include_hides_the_unspecified_part_of_the_file to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
342b6ee7b5 Migrate able_to_include_playground_files_in_chapters to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
9952ac15a5 Migrate recursive_includes_are_capped to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
7add0dbf10 Migrate anchors_include_text_between_but_not_anchor_comments to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
03470a7531 Migrate able_to_include_files_in_chapters to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
dd778d50f9 Add some basic help tests 2025-04-22 20:50:20 -07:00
Eric Huss
ac3e4b6c1e Migrate book_toml_isnt_required to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
3706ddc5cc Migrate book_with_a_reserved_filename_does_not_build to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
adcea9b3b9 Migrate create_missing_file_with_config to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
ba8107120c Migrate failure_on_missing_file to BookTest 2025-04-22 20:50:20 -07:00
Eric Huss
b9e433710d Migrate build_the_dummy_book to BookTest (build::basic_build)
This doesn't exercise *everything* that the old test did, but other
tests will take care of those gaps. This is intended as just a smoke
test.
2025-04-22 20:50:20 -07:00
Eric Huss
f10d23e893 Introduce the new BookTest-based testsuite
This is a new testsuite intended to replace the other tests, which
provides an easy facility to update tests, validate output, and more.
2025-04-22 20:50:16 -07:00
Eric Huss
12b4a9631a Merge pull request #2675 from notriddle/sidebar/active-query
Break off query string when comparing url for sidebar
2025-04-22 13:48:42 +00:00
Michael Howell
36fa0064de Break off query string when comparing url for sidebar 2025-04-21 12:03:17 -07:00
Eric Huss
566a42c4f7 Merge pull request #2674 from ehuss/fix-missing-docs
Clean up some missing docs
2025-04-21 02:50:43 +00:00
Eric Huss
6e143ce2a1 Add CI job to check API docs
This ensures that `cargo doc` does not generate any warnings.
2025-04-20 19:43:32 -07:00
Eric Huss
46963ebf65 Fix some missing docs
This removes the `allow(missing_docs)` and fixes any issues.

There's probably more work to be done to improve the API docs. This was
just a minor thing I wanted to clean up.
2025-04-20 19:42:45 -07:00
Eric Huss
c948fe4d6a Merge pull request #2673 from ehuss/clippy
Add clippy in CI
2025-04-21 02:32:31 +00:00
Eric Huss
b0ef5a54cc Fix clippy::redundant_slicing 2025-04-20 19:25:50 -07:00
Eric Huss
fbc875dd9f Fix clippy::only-used-in-recursion 2025-04-20 19:25:50 -07:00
Eric Huss
e6b1413d22 Fix clippy::default_constructed_unit_structs 2025-04-20 19:25:50 -07:00
Eric Huss
1d3b99c0df Fix unused import
This happens because it is only used in the test configuration.
2025-04-20 19:25:50 -07:00
Eric Huss
8181445d99 Add a restricted set of clippy lints, required to pass
This sets up CI to check clippy with a restricted set of clippy groups.
Some of the default groups have some excessive sets of lints that are
either wrong or style choices that I would prefer to not mess over at
this time. The lint groups can be adjusted later if it looks like
something that would be helpful.
2025-04-20 19:25:46 -07:00
Eric Huss
14aeb0cb83 Merge pull request #2633 from GuillaumeGomez/speed-up-loading
Speed up search index loading
2025-04-21 00:10:57 +00:00
Eric Huss
3e6b42cfba Merge pull request #2568 from szabgab/remove-needless-late-init
[refactor] eliminate needless_late_init
2025-04-19 13:12:39 +00:00
Eric Huss
c57a8fcfc4 Merge pull request #2670 from ehuss/require-gui
Require all test jobs to pass
2025-04-17 16:54:42 +00:00
Eric Huss
9d6fcc9afe Require all test jobs to pass 2025-04-17 09:46:17 -07:00
Eric Huss
ea8f0f6161 Merge pull request #2669 from ehuss/fix-searcher-eslint
Fix wrong quotes for eslint
2025-04-17 16:35:48 +00:00
Eric Huss
06e8f6f849 Fix wrong quotes for eslint 2025-04-17 09:24:45 -07:00
Eric Huss
36e5525ea5 Merge pull request #2668 from ehuss/dont-mark-svg
Ignore SVG text elements in search highlighting
2025-04-17 14:53:16 +00:00
Eric Huss
e5d5f5d02b Ignore SVG text elements in search highlighting 2025-04-17 07:39:22 -07:00
Eric Huss
98088c91dd Merge pull request #2659 from szabgab/fix-typo-in-template
fix typo in template
2025-04-16 20:46:35 +00:00
Eric Huss
a4f7d11e92 Merge pull request #2658 from szabgab/fix-typo
fix typo
2025-04-16 20:46:18 +00:00
Gabor Szabo
4c7e85ba82 fix typo 2025-04-10 08:48:51 +03:00
Gabor Szabo
9693c4af05 fix typo 2025-04-10 08:28:13 +03:00
Eric Huss
3052fe3827 Merge pull request #2644 from kg4zow/fonts-binary
Mark more font files as binary
2025-04-08 15:42:52 +00:00
Eric Huss
c85c3eb292 Merge pull request #2646 from szabgab/remove-multilingual
Remove the book.multilingual field
2025-04-08 15:41:55 +00:00
Eric Huss
0d734bbb03 Merge pull request #2650 from rust-lang/dependabot/cargo/tokio-1.43.1
Bump tokio from 1.43.0 to 1.43.1
2025-04-08 03:45:47 +00:00
dependabot[bot]
b9b34f97d9 Bump tokio from 1.43.0 to 1.43.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.43.0 to 1.43.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.43.0...tokio-1.43.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.43.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-08 02:14:28 +00:00
Gabor Szabo
ee59e22603 Remove the book.multilingual field
As it is seems it has never been in real use.

See #2636
2025-04-06 13:27:13 +03:00
John Simpson
22a6dca69b Mark more font files as binary
Not having these files marked as binary causes problems for older
versions of git (like 1.8.3.1 on CentOS/RHEL 7).
2025-04-05 20:41:36 -04:00
Eric Huss
4f698f813c Merge pull request #2622 from szabgab/warn-on-invalid-configuration-field
warn on invalid fields in the root of book.toml
2025-04-03 18:23:26 +00:00
Eric Huss
97f1948681 Make the unexpected case explicit that it is an internal error
The current code has the `else` clause as unreachable, and the text it
has isn't clear that is the case.
2025-04-03 11:17:25 -07:00
Guillaume Gomez
7acc7a03a8 Update JSON loader in search tests 2025-04-02 21:03:12 +02:00
Guillaume Gomez
0ed1cbe486 Fix JS error 2025-04-02 21:03:12 +02:00
Guillaume Gomez
2c382a58d3 Greatly speed up search index load 2025-04-02 21:03:12 +02:00
Gabor Szabo
1bc2ebd775 warn on invalid fields in root of book.toml 2025-03-28 10:56:41 +03:00
Gabor Szabo
0fb814c6d6 eliminate needless_late_init
https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
2025-03-04 17:34:08 +02:00
249 changed files with 4801 additions and 11393 deletions

2
.gitattributes vendored
View File

@@ -6,3 +6,5 @@
*.ttf binary
*.otf binary
*.png binary
*.eot binary
*.woff2 binary

View File

@@ -40,7 +40,7 @@ jobs:
- name: msrv
os: ubuntu-22.04
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
rust: 1.77.0
rust: 1.82.0
target: x86_64-unknown-linux-gnu
name: ${{ matrix.name }}
steps:
@@ -88,6 +88,28 @@ jobs:
- name: Build and run tests (+ GUI)
run: cargo test --locked --target x86_64-unknown-linux-gnu --test gui
# Ensure there are no clippy warnings
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- run: rustup component add clippy
- run: cargo clippy --workspace --all-targets --no-deps -- -D warnings
docs:
name: Check API docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Ensure intradoc links are valid
run: cargo doc --workspace --document-private-items --no-deps
env:
RUSTDOCFLAGS: -D warnings
# 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
@@ -99,6 +121,10 @@ jobs:
needs:
- test
- rustfmt
- aarch64-cross-builds
- gui
- clippy
- docs
runs-on: ubuntu-latest
steps:
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ guide/book
.vscode
tests/dummy_book/book/
test_book/book/
tests/testsuite/*/*/book/
# Ignore Jetbrains specific files.
.idea/

View File

@@ -1,5 +1,100 @@
# Changelog
## mdBook 0.4.52
[v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52)
**Note:** If you have a custom `index.hbs` theme file, it is recommended that you update it to the latest version to pick up the fixes in this release.
### Added
- Added the ability to redirect `#` HTML fragments using the existing `output.html.redirect` table.
[#2747](https://github.com/rust-lang/mdBook/pull/2747)
- Added the `rel="edit"` attribute to the edit page button.
[#2702](https://github.com/rust-lang/mdBook/pull/2702)
### Changed
- The search index is now only loaded when the search input is opened instead of always being loaded.
[#2553](https://github.com/rust-lang/mdBook/pull/2553)
[#2735](https://github.com/rust-lang/mdBook/pull/2735)
- The `mdbook serve` command has switched its underlying server library from warp to axum.
[#2748](https://github.com/rust-lang/mdBook/pull/2748)
- Updated dependencies.
[#2752](https://github.com/rust-lang/mdBook/pull/2752)
### Fixed
- The sidebar is now set to `display:none` when it is hidden in order to prevent the browser's search from thinking the sidebar's text is visible.
[#2725](https://github.com/rust-lang/mdBook/pull/2725)
- Fixed search index URL not updating correctly when `hash-files` is enabled.
[#2742](https://github.com/rust-lang/mdBook/pull/2742)
[#2746](https://github.com/rust-lang/mdBook/pull/2746)
- Fixed several sidebar animation bugs, particularly when manually resizing.
[#2750](https://github.com/rust-lang/mdBook/pull/2750)
## mdBook 0.4.51
[v0.4.50...v0.4.51](https://github.com/rust-lang/mdBook/compare/v0.4.50...v0.4.51)
### Fixed
- Fixed regression that broke the `S` search hotkey.
[#2713](https://github.com/rust-lang/mdBook/pull/2713)
## mdBook 0.4.50
[v0.4.49...v0.4.50](https://github.com/rust-lang/mdBook/compare/v0.4.49...v0.4.50)
### Added
- Added a keyboard shortcut help popup when pressing `?`.
[#2608](https://github.com/rust-lang/mdBook/pull/2608)
### Changed
- Changed the look of the sidebar resize handle to match the new rustdoc format.
[#2691](https://github.com/rust-lang/mdBook/pull/2691)
- `/` can now be used to open the search bar.
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
- Pressing enter from the search bar will navigate to the first entry.
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
- Updated `opener` to drop some dependencies.
[#2709](https://github.com/rust-lang/mdBook/pull/2709)
- Updated dependencies, MSRV raised to 1.82.
[#2711](https://github.com/rust-lang/mdBook/pull/2711)
### Fixed
- Fixed uncaught exception when pressing down when there are no search results.
[#2698](https://github.com/rust-lang/mdBook/pull/2698)
- Fixed syntax highlighting of Rust code in the ACE editor.
[#2710](https://github.com/rust-lang/mdBook/pull/2710)
## mdBook 0.4.49
[v0.4.48...v0.4.49](https://github.com/rust-lang/mdBook/compare/v0.4.48...v0.4.49)
### Added
- Added a warning on unused fields in the root of `book.toml`.
[#2622](https://github.com/rust-lang/mdBook/pull/2622)
### Changed
- Updated dependencies.
[#2650](https://github.com/rust-lang/mdBook/pull/2650)
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
- Updated minimum Rust version to 1.81.
[#2688](https://github.com/rust-lang/mdBook/pull/2688)
- The unused `book.multilingual` field is no longer serialized, or shown in `mdbook init`.
[#2689](https://github.com/rust-lang/mdBook/pull/2689)
- Speed up search index loading by using `JSON.parse` instead of parsing JavaScript.
[#2633](https://github.com/rust-lang/mdBook/pull/2633)
### Fixed
- Search highlighting will not try to highlight in SVG `<text>` elements because it breaks the element.
[#2668](https://github.com/rust-lang/mdBook/pull/2668)
- Fixed scrolling of the sidebar when a search highlight term is in the URL.
[#2675](https://github.com/rust-lang/mdBook/pull/2675)
- Fixed issues when multiple footnote definitions use the same ID. Now, only one definition is used, and a warning is displayed.
[#2681](https://github.com/rust-lang/mdBook/pull/2681)
- The sidebar is now restricted to 80% of the viewport width to make it possible to collapse it when the viewport is very narrow.
[#2679](https://github.com/rust-lang/mdBook/pull/2679)
## mdBook 0.4.48
[v0.4.47...v0.4.48](https://github.com/rust-lang/mdBook/compare/v0.4.47...v0.4.48)

View File

@@ -7,7 +7,7 @@ If you have come here to learn how to contribute to mdBook, we have some tips fo
First of all, don't hesitate to ask questions!
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
### Issue assignment
## Issue assignment
**:warning: Important :warning:**
@@ -16,7 +16,7 @@ The current PR backlog is beyond what we can process at this time.
Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews.
If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review.
### Issues to work on
## Issues to work on
If you are starting out, you might be interested in the
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
@@ -41,7 +41,7 @@ Issues on the issue tracker are categorized with the following labels:
- **S**-prefixed labels show the status of the issue
- **C**-prefixed labels show the category of issue
### Building mdBook
## Building mdBook
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
@@ -56,11 +56,11 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
### Code Quality
## Code Quality
We love code quality and Rust has some excellent tools to assist you with contributions.
#### Formatting Code with rustfmt
### Formatting Code with rustfmt
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
This will ensure we have good quality source code that is better for us all to maintain.
@@ -84,8 +84,7 @@ The quick guide is
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
#### Finding Issues with Clippy
### Finding Issues with Clippy
[Clippy](https://doc.rust-lang.org/clippy/) is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will help us maintain awesome code.
@@ -99,7 +98,7 @@ Like formatting your code with `rustfmt`, running clippy regularly and before yo
cargo clippy
```
### Change requirements
## Change requirements
Please consider the following when making a change:
@@ -124,7 +123,7 @@ Please consider the following when making a change:
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
### Making a pull-request
## Making a pull-request
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
One of the core maintainers will then approve the changes or request some changes before it gets merged.

1542
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,15 @@
[workspace]
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
[workspace.lints.clippy]
all = { level = "allow", priority = -2 }
correctness = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
needless-lifetimes = "allow" # Remove once 1.87 is stable, https://github.com/rust-lang/rust-clippy/issues/13514
[package]
name = "mdbook"
version = "0.4.48"
version = "0.4.52"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
@@ -17,20 +23,19 @@ license = "MPL-2.0"
readme = "README.md"
repository = "https://github.com/rust-lang/mdBook"
description = "Creates a book from markdown files"
rust-version = "1.77" # Keep in sync with installation.md and .github/workflows/main.yml
rust-version = "1.82" # Keep in sync with installation.md and .github/workflows/main.yml
[dependencies]
anyhow = "1.0.71"
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
clap_complete = "4.3.2"
once_cell = "1.17.1"
env_logger = "0.11.1"
handlebars = "6.0"
hex = "0.4.3"
log = "0.4.17"
memchr = "2.5.0"
opener = "0.7.0"
opener = "0.8.1"
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.8.1"
serde = { version = "1.0.163", features = ["derive"] }
@@ -50,25 +55,25 @@ walkdir = { version = "2.3.3", optional = true }
# Serve feature
futures-util = { version = "0.3.28", optional = true }
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"], optional = true }
axum = { version = "0.8.0", features = ["ws"], optional = true }
tower-http = { version = "0.6.0", features = ["fs", "trace"], optional = true }
# Search feature
elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "4.0.0", optional = true }
[dev-dependencies]
assert_cmd = "2.0.11"
predicates = "3.0.3"
select = "0.6.0"
semver = "1.0.17"
snapbox = { version = "0.6.21", features = ["diff", "dir", "term-svg", "regex", "json"] }
pretty_assertions = "1.3.0"
walkdir = "2.3.3"
[features]
default = ["watch", "serve", "search"]
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"]
serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
search = ["dep:elasticlunr-rs", "dep:ammonia"]
[[bin]]
@@ -91,3 +96,6 @@ test = false
name = "gui"
path = "tests/gui/runner.rs"
crate-type = ["bin"]
[lints]
workspace = true

View File

@@ -26,7 +26,7 @@ fn main() {
if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(&preprocessor, sub_args);
} else if let Err(e) = handle_preprocessing(&preprocessor) {
eprintln!("{e}");
eprintln!("{e:?}");
process::exit(1);
}
}

View File

@@ -23,7 +23,7 @@ fn main() {
}
if let Err(e) = handle_preprocessing() {
eprintln!("{}", e);
eprintln!("{e}");
std::process::exit(1);
}
}

View File

@@ -1,12 +1,10 @@
use mdbook::MDBook;
#[test]
fn remove_emphasis_works() {
// Tests that the remove-emphasis example works as expected.
// Workaround for https://github.com/rust-lang/mdBook/issues/1424
std::env::set_current_dir("examples/remove-emphasis").unwrap();
let book = MDBook::load(".").unwrap();
let book = mdbook::MDBook::load(".").unwrap();
book.build().unwrap();
let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap();
assert!(ch1.contains("This has light emphasis and bold emphasis."));

View File

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

View File

@@ -73,7 +73,7 @@ edition = "2015" # the default edition for code blocks
- **edition**: Rust edition to use by default for the code snippets. Default
is `"2015"`. Individual code blocks can be controlled with the `edition2015`,
`edition2018` or `edition2021` annotations, such as:
`edition2018`, `edition2021` or `edition2024` annotations, such as:
~~~text
```rust,edition2015

View File

@@ -310,13 +310,21 @@ This is useful when you move, rename, or remove a page to ensure that links to t
[output.html.redirect]
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
# Fragment redirects also work.
"/some-existing-page.html#old-fragment" = "some-existing-page.html#new-fragment"
# Fragment redirects also work for deleted pages.
"/old-page.html" = "new-page.html"
"/old-page.html#old-fragment" = "new-page.html#new-fragment"
```
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
This will generate an HTML page which will automatically redirect to the given location.
Note that the source location does not support `#` anchor redirects.
When fragment redirects are specified, the page must use JavaScript to redirect to the correct location. This is useful if you rename or move a section header. Fragment redirects work with existing pages and deleted pages.
## Markdown Renderer

View File

@@ -75,15 +75,22 @@ Use [mdBook](https://github.com/rust-lang/mdBook).
Read about [mdBook](mdbook.md).
And now [an mdBook link] that is not inline, unlike the above.
A bare url: <https://www.rust-lang.org>.
[an mdBook link]: https://github.com/rust-lang/mdBook
```
Use [mdBook](https://github.com/rust-lang/mdBook).
Read about [mdBook](mdbook.md).
And now [an mdBook link] that is not inline, unlike the above.
A bare url: <https://www.rust-lang.org>.
[an mdBook link]: https://github.com/rust-lang/mdBook
----
Relative links that end with `.md` will be converted to the `.html` extension.

View File

@@ -122,7 +122,7 @@ These use the same attributes as [rustdoc attributes], with a few additions:
* `no_run` --- The code is compiled when tested, but it is not run.
The play button is also not shown.
* `compile_fail` --- The code should fail to compile.
* `edition2015`, `edition2018`, `edition2021` --- Forces the use of a specific Rust edition.
* `edition2015`, `edition2018`, `edition2021`, `edition2024` --- Forces the use of a specific Rust edition.
See [`rust.edition`] to set this globally.
[`mdbook test`]: ../cli/test.md

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.
Follow the instructions on the [Rust installation page].
mdBook currently requires at least Rust version 1.77.
mdBook currently requires at least Rust version 1.82.
Once you have installed Rust, the following command can be used to build and install mdBook:

View File

@@ -42,14 +42,14 @@ Tapping the menu bar will scroll the page to the top.
## Search
Each book has a built-in search system.
Pressing the search icon (<i class="fa fa-search"></i>) in the menu bar, or pressing the `S` key on the keyboard will open an input box for entering search terms.
Pressing the search icon (<i class="fa fa-search"></i>) in the menu bar, or pressing the <kbd>/</kbd> or <kbd>S</kbd> key on the keyboard will open an input box for entering search terms.
Typing some terms will show matching chapters and sections in real time.
Clicking any of the results will jump to that section.
The up and down arrow keys can be used to navigate the results, and enter will open the highlighted section.
After loading a search result, the matching search terms will be highlighted in the text.
Clicking a highlighted word or pressing the `Esc` key will remove the highlighting.
Clicking a highlighted word or pressing the <kbd>Escape</kbd> key will remove the highlighting.
## Code blocks

View File

@@ -1,10 +1,10 @@
{
"dependencies": {
"browser-ui-test": "0.19.0",
"browser-ui-test": "0.21.1",
"eslint": "^8.57.1"
},
"scripts": {
"lint": "eslint src/front-end/*js src/front-end/**/*js",
"lint-fix": "eslint --fix src/front-end/*js src/front-end/**/*js"
}
}
}

View File

@@ -656,10 +656,10 @@ And here is some \
let got = load_book(&temp_dir, &cfg);
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
let expeceted = format!(
let expected = format!(
r#"Couldn't open SUMMARY.md in {:?} directory"#,
temp_dir.path()
);
assert_eq!(error_message, expeceted);
assert_eq!(error_message, expected);
}
}

View File

@@ -5,7 +5,6 @@
//!
//! [1]: ../index.html
#[allow(clippy::module_inception)]
mod book;
mod init;
mod summary;
@@ -860,7 +859,7 @@ mod tests {
.and_then(Value::as_str)
.unwrap();
assert_eq!(html, "html");
let html_renderer = HtmlHandlebars::default();
let html_renderer = HtmlHandlebars;
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);

View File

@@ -248,7 +248,7 @@ impl<'a> SummaryParser<'a> {
let mut files = HashSet::new();
for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] {
self.check_for_duplicates(&part, &mut files)?;
Self::check_for_duplicates(&part, &mut files)?;
}
Ok(Summary {
@@ -261,7 +261,6 @@ impl<'a> SummaryParser<'a> {
/// Recursively check for duplicate files in the summary items.
fn check_for_duplicates<'b>(
&self,
items: &'b [SummaryItem],
files: &mut HashSet<&'b PathBuf>,
) -> Result<()> {
@@ -276,7 +275,7 @@ impl<'a> SummaryParser<'a> {
}
}
// Recursively check nested items
self.check_for_duplicates(&link.nested_items, files)?;
Self::check_for_duplicates(&link.nested_items, files)?;
}
}
Ok(())

View File

@@ -2,6 +2,9 @@ use super::command_prelude::*;
#[cfg(feature = "watch")]
use super::watch;
use crate::{get_book_dir, open};
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::routing::get;
use axum::Router;
use clap::builder::NonEmptyStringValueParser;
use futures_util::sink::SinkExt;
use futures_util::StreamExt;
@@ -11,8 +14,7 @@ use mdbook::MDBook;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use tokio::sync::broadcast;
use warp::ws::Message;
use warp::Filter;
use tower_http::services::{ServeDir, ServeFile};
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
@@ -116,32 +118,19 @@ async fn serve(
reload_tx: broadcast::Sender<Message>,
file_404: &str,
) {
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
// receive reload messages.
let sender = warp::any().map(move || reload_tx.subscribe());
let reload_tx_clone = reload_tx.clone();
// A warp Filter to handle the livereload endpoint. This upgrades to a
// websocket, and then waits for any filesystem change notifications, and
// relays them over the websocket.
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
.and(warp::ws())
.and(sender)
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
ws.on_upgrade(move |ws| async move {
let (mut user_ws_tx, _user_ws_rx) = ws.split();
trace!("websocket got connection");
if let Ok(m) = rx.recv().await {
trace!("notify of reload");
let _ = user_ws_tx.send(m).await;
}
})
});
// A warp Filter that serves from the filesystem.
let book_route = warp::fs::dir(build_dir.clone());
// The fallback route for 404 errors
let fallback_route = warp::fs::file(build_dir.join(file_404))
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
let routes = livereload.or(book_route).or(fallback_route);
// WebSocket handler for live reload
let websocket_handler = move |ws: WebSocketUpgrade| async move {
let reload_tx = reload_tx_clone.clone();
ws.on_upgrade(move |socket| websocket_connection(socket, reload_tx))
};
let app = Router::new()
.route(&format!("/{LIVE_RELOAD_ENDPOINT}"), get(websocket_handler))
.fallback_service(
ServeDir::new(&build_dir).not_found_service(ServeFile::new(build_dir.join(file_404))),
);
std::panic::set_hook(Box::new(move |panic_info| {
// exit if serve panics
@@ -149,5 +138,20 @@ async fn serve(
std::process::exit(1);
}));
warp::serve(routes).run(address).await;
let listener = tokio::net::TcpListener::bind(&address)
.await
.unwrap_or_else(|e| panic!("Unable to bind to {address}: {e}"));
axum::serve(listener, app).await.unwrap();
}
async fn websocket_connection(ws: WebSocket, reload_tx: broadcast::Sender<Message>) {
let (mut user_ws_tx, _user_ws_rx) = ws.split();
let mut rx = reload_tx.subscribe();
trace!("websocket got connection");
if let Ok(m) = rx.recv().await {
trace!("notify of reload");
let _ = user_ws_tx.send(m).await;
}
}

View File

@@ -312,6 +312,8 @@ impl<'de> serde::Deserialize<'de> for Config {
return Ok(Config::from_legacy(raw));
}
warn_on_invalid_fields(&raw);
use serde::de::Error;
let mut table = match raw {
Value::Table(t) => t,
@@ -376,6 +378,17 @@ fn parse_env(key: &str) -> Option<String> {
.map(|key| key.to_lowercase().replace("__", ".").replace('_', "-"))
}
fn warn_on_invalid_fields(table: &Value) {
let valid_items = ["book", "build", "rust", "output", "preprocessor"];
let table = table.as_table().expect("root must be a table");
for item in table.keys() {
if !valid_items.contains(&item.as_str()) {
warn!("Invalid field {:?} in book.toml", &item);
}
}
}
fn is_legacy_format(table: &Value) -> bool {
let legacy_items = [
"title",
@@ -408,6 +421,9 @@ pub struct BookConfig {
/// Location of the book source relative to the book's root directory.
pub src: PathBuf,
/// Does this book support more than one language?
// TODO: Remove this field in 0.5, it is unused:
// https://github.com/rust-lang/mdBook/issues/2636
#[serde(skip_serializing)]
pub multilingual: bool,
/// The main language of the book.
pub language: Option<String>,

View File

@@ -344,9 +344,34 @@ mark.fade-out {
max-width: var(--content-max-width);
}
#searchbar-outer.searching #searchbar {
padding-right: 30px;
}
#searchbar-outer .spinner-wrapper {
display: none;
}
#searchbar-outer.searching .spinner-wrapper {
display: block;
}
.search-wrapper {
position: relative;
}
.spinner-wrapper {
--spinner-margin: 2px;
position: absolute;
margin-block-start: calc(var(--searchbar-margin-block-start) + var(--spinner-margin));
right: var(--spinner-margin);
top: 0;
bottom: var(--spinner-margin);
padding: 6px;
background-color: var(--bg);
}
#searchbar {
width: 100%;
margin-block-start: 5px;
margin-block-start: var(--searchbar-margin-block-start);
margin-block-end: 0;
margin-inline-start: auto;
margin-inline-end: auto;
@@ -474,9 +499,24 @@ html:not(.sidebar-resizing) .sidebar {
.sidebar-resize-handle .sidebar-resize-indicator {
width: 100%;
height: 12px;
background-color: var(--icons);
height: 16px;
color: var(--icons);
margin-inline-start: var(--sidebar-resize-indicator-space);
display: flex;
align-items: center;
justify-content: flex-start;
}
.sidebar-resize-handle .sidebar-resize-indicator::before {
content: "";
width: 2px;
height: 12px;
border-left: dotted 2px currentColor;
}
.sidebar-resize-handle .sidebar-resize-indicator::after {
content: "";
width: 2px;
height: 16px;
border-left: dotted 2px currentColor;
}
[dir=rtl] .sidebar .sidebar-resize-handle {
@@ -490,7 +530,6 @@ html:not(.sidebar-resizing) .sidebar {
/* sidebar-hidden */
#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)));
@@ -641,3 +680,46 @@ html:not(.sidebar-resizing) .sidebar {
margin-inline-start: -14px;
width: 14px;
}
/* The container for the help popup that covers the whole window. */
#mdbook-help-container {
/* Position and size for the whole window. */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* This uses flex layout (which is set in book.js), and centers the popup
in the window.*/
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
/* Dim out the book while the popup is visible. */
background: var(--overlay-bg);
}
/* The popup help box. */
#mdbook-help-popup {
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
min-width: 300px;
max-width: 500px;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--bg);
color: var(--fg);
border-width: 1px;
border-color: var(--theme-popup-border);
border-style: solid;
border-radius: 8px;
padding: 10px;
}
.mdbook-help-title {
text-align: center;
/* mdbook's margin for h2 is way too large. */
margin: 10px;
}

View File

@@ -86,11 +86,12 @@ h6:target::before {
box-sizing: border-box;
background-color: var(--bg);
}
.no-js .page-wrapper,
html:not(.js) .page-wrapper,
.js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
[dir=rtl]:not(.js) .page-wrapper,
[dir=rtl].js:not(.sidebar-resizing) .page-wrapper {
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}

View File

@@ -2,7 +2,8 @@
/* Globals */
:root {
--sidebar-width: 300px;
--sidebar-target-width: 300px;
--sidebar-width: min(var(--sidebar-target-width), 80vw);
--sidebar-resize-indicator-width: 8px;
--sidebar-resize-indicator-space: 2px;
--page-padding: 15px;
@@ -10,6 +11,7 @@
--menu-bar-height: 50px;
--mono-font: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
--code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
--searchbar-margin-block-start: 5px;
}
/* Themes */
@@ -63,6 +65,8 @@
--copy-button-filter-hover: invert(68%) sepia(55%) saturate(531%) hue-rotate(341deg) brightness(104%) contrast(101%);
--footnote-highlight: #2668a6;
--overlay-bg: rgba(33, 40, 48, 0.4);
}
.coal {
@@ -114,6 +118,8 @@
--copy-button-filter-hover: invert(36%) sepia(70%) saturate(503%) hue-rotate(167deg) brightness(98%) contrast(89%);
--footnote-highlight: #4079ae;
--overlay-bg: rgba(33, 40, 48, 0.4);
}
.light, html:not(.js) {
@@ -165,6 +171,8 @@
--copy-button-filter-hover: invert(14%) sepia(93%) saturate(4250%) hue-rotate(243deg) brightness(99%) contrast(130%);
--footnote-highlight: #7e7eff;
--overlay-bg: rgba(200, 200, 205, 0.4);
}
.navy {
@@ -216,6 +224,8 @@
--copy-button-filter-hover: invert(46%) sepia(20%) saturate(1537%) hue-rotate(156deg) brightness(85%) contrast(90%);
--footnote-highlight: #4079ae;
--overlay-bg: rgba(33, 40, 48, 0.4);
}
.rust {
@@ -265,6 +275,8 @@
--copy-button-filter-hover: invert(77%) sepia(16%) saturate(1798%) hue-rotate(328deg) brightness(98%) contrast(83%);
--footnote-highlight: #d3a17a;
--overlay-bg: rgba(150, 150, 150, 0.25);
}
@media (prefers-color-scheme: dark) {

View File

@@ -515,17 +515,39 @@ aria-label="Show hidden lines"></button>';
})();
(function sidebar() {
const body = document.querySelector('body');
const sidebar = document.getElementById('sidebar');
const sidebarLinks = document.querySelectorAll('#sidebar a');
const sidebarToggleButton = document.getElementById('sidebar-toggle');
const sidebarToggleAnchor = document.getElementById('sidebar-toggle-anchor');
const sidebarResizeHandle = document.getElementById('sidebar-resize-handle');
const sidebarCheckbox = document.getElementById('sidebar-toggle-anchor');
let firstContact = null;
/* Because we cannot change the `display` using only CSS after/before the transition, we
need JS to do it. We change the display to prevent the browsers search to find text inside
the collapsed sidebar. */
if (!document.documentElement.classList.contains('sidebar-visible')) {
sidebar.style.display = 'none';
}
sidebar.addEventListener('transitionend', () => {
/* We only change the display to "none" if we're collapsing the sidebar. */
if (!sidebarCheckbox.checked) {
sidebar.style.display = 'none';
}
});
sidebarToggleButton.addEventListener('click', () => {
/* To allow the sidebar expansion animation, we first need to put back the display. */
if (!sidebarCheckbox.checked) {
sidebar.style.display = '';
// Workaround for Safari skipping the animation when changing
// `display` and a transform in the same event loop. This forces a
// reflow after updating the display.
sidebar.offsetHeight;
}
});
function showSidebar() {
body.classList.remove('sidebar-hidden');
body.classList.add('sidebar-visible');
document.documentElement.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function(link) {
link.setAttribute('tabIndex', 0);
});
@@ -539,8 +561,7 @@ aria-label="Show hidden lines"></button>';
}
function hideSidebar() {
body.classList.remove('sidebar-visible');
body.classList.add('sidebar-hidden');
document.documentElement.classList.remove('sidebar-visible');
Array.from(sidebarLinks).forEach(function(link) {
link.setAttribute('tabIndex', -1);
});
@@ -554,12 +575,12 @@ aria-label="Show hidden lines"></button>';
}
// Toggle sidebar
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
if (sidebarToggleAnchor.checked) {
sidebarCheckbox.addEventListener('change', function sidebarToggle() {
if (sidebarCheckbox.checked) {
const current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px');
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
}
showSidebar();
} else {
@@ -572,23 +593,23 @@ aria-label="Show hidden lines"></button>';
function initResize() {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
body.classList.add('sidebar-resizing');
document.documentElement.classList.add('sidebar-resizing');
}
function resize(e) {
let pos = e.clientX - sidebar.offsetLeft;
if (pos < 20) {
hideSidebar();
} else {
if (body.classList.contains('sidebar-hidden')) {
if (!document.documentElement.classList.contains('sidebar-visible')) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize() {
body.classList.remove('sidebar-resizing');
document.documentElement.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
@@ -623,7 +644,7 @@ aria-label="Show hidden lines"></button>';
(function chapterNavigation() {
document.addEventListener('keydown', function(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
if (e.altKey || e.ctrlKey || e.metaKey) {
return;
}
if (window.search && window.search.hasFocus()) {
@@ -643,6 +664,55 @@ aria-label="Show hidden lines"></button>';
window.location.href = previousButton.href;
}
}
function showHelp() {
const container = document.getElementById('mdbook-help-container');
const overlay = document.getElementById('mdbook-help-popup');
container.style.display = 'flex';
// Clicking outside the popup will dismiss it.
const mouseHandler = event => {
if (overlay.contains(event.target)) {
return;
}
if (event.button !== 0) {
return;
}
event.preventDefault();
event.stopPropagation();
document.removeEventListener('mousedown', mouseHandler);
hideHelp();
};
// Pressing esc will dismiss the popup.
const escapeKeyHandler = event => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
document.removeEventListener('keydown', escapeKeyHandler, true);
hideHelp();
}
};
document.addEventListener('keydown', escapeKeyHandler, true);
document.getElementById('mdbook-help-container')
.addEventListener('mousedown', mouseHandler);
}
function hideHelp() {
document.getElementById('mdbook-help-container').style.display = 'none';
}
// Usually needs the Shift key to be pressed
switch (e.key) {
case '?':
e.preventDefault();
showHelp();
break;
}
// Rest of the keys are only active when the Shift key is not pressed
if (e.shiftKey) {
return;
}
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
@@ -716,7 +786,7 @@ aria-label="Show hidden lines"></button>';
let scrollTop = document.scrollingElement.scrollTop;
let prevScrollTop = scrollTop;
const minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
// When the script loads, the page can be at any scroll (e.g. if you refresh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
let topCache = menu.style.top.slice(0, -2);

View File

@@ -3,7 +3,7 @@
/* global Mark, elasticlunr, path_to_root */
window.search = window.search || {};
(function search(search) {
(function search() {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
@@ -22,6 +22,7 @@ window.search = window.search || {};
}
const search_wrap = document.getElementById('search-wrapper'),
searchbar_outer = document.getElementById('searchbar-outer'),
searchbar = document.getElementById('searchbar'),
searchresults = document.getElementById('searchresults'),
searchresults_outer = document.getElementById('searchresults-outer'),
@@ -29,16 +30,11 @@ window.search = window.search || {};
searchicon = document.getElementById('search-toggle'),
content = document.getElementById('content'),
mark_exclude = [],
// SVG text elements don't render if inside a <mark> tag.
mark_exclude = ['text'],
marker = new Mark(content),
URL_SEARCH_PARAM = 'search',
URL_MARK_PARAM = 'highlight',
SEARCH_HOTKEY_KEYCODE = 83,
ESCAPE_KEYCODE = 27,
DOWN_KEYCODE = 40,
UP_KEYCODE = 38,
SELECT_KEYCODE = 13;
URL_MARK_PARAM = 'highlight';
let current_searchterm = '',
doc_urls = [],
@@ -267,6 +263,18 @@ window.search = window.search || {};
doc_urls = config.doc_urls;
searchindex = elasticlunr.Index.load(config.index);
searchbar_outer.classList.remove('searching');
searchbar.focus();
const searchterm = searchbar.value.trim();
if (searchterm !== '') {
searchbar.classList.add('active');
doSearch(searchterm);
}
}
function initSearchInteractions(config) {
// Set up events
searchicon.addEventListener('click', () => {
searchIconClickHandler();
@@ -288,8 +296,13 @@ window.search = window.search || {};
// If reloaded, do the search or mark again, depending on the current url parameters
doSearchOrMarkFromUrl();
// Exported functions
config.hasFocus = hasFocus;
}
initSearchInteractions(window.search);
function unfocusSearchbar() {
// hacky, but just focusing a div only works once
const tmp = document.createElement('input');
@@ -348,7 +361,7 @@ window.search = window.search || {};
return;
}
if (e.keyCode === ESCAPE_KEYCODE) {
if (e.key === 'Escape') {
e.preventDefault();
searchbar.classList.remove('active');
setSearchUrlParameters('',
@@ -358,31 +371,38 @@ window.search = window.search || {};
}
showSearch(false);
marker.unmark();
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
} else if (!hasFocus() && (e.key === 's' || e.key === '/')) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
} else if (hasFocus() && (e.key === 'ArrowDown'
|| e.key === 'Enter')) {
e.preventDefault();
unfocusSearchbar();
searchresults.firstElementChild.classList.add('focus');
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|| e.keyCode === UP_KEYCODE
|| e.keyCode === SELECT_KEYCODE)) {
const first = searchresults.firstElementChild;
if (first !== null) {
unfocusSearchbar();
first.classList.add('focus');
if (e.key === 'Enter') {
window.location.assign(first.querySelector('a'));
}
}
} else if (!hasFocus() && (e.key === 'ArrowDown'
|| e.key === 'ArrowUp'
|| e.key === 'Enter')) {
// not `:focus` because browser does annoying scrolling
const focused = searchresults.querySelector('li.focus');
if (!focused) {
return;
}
e.preventDefault();
if (e.keyCode === DOWN_KEYCODE) {
if (e.key === 'ArrowDown') {
const next = focused.nextElementSibling;
if (next) {
focused.classList.remove('focus');
next.classList.add('focus');
}
} else if (e.keyCode === UP_KEYCODE) {
} else if (e.key === 'ArrowUp') {
focused.classList.remove('focus');
const prev = focused.previousElementSibling;
if (prev) {
@@ -390,14 +410,34 @@ window.search = window.search || {};
} else {
searchbar.select();
}
} else { // SELECT_KEYCODE
} else { // Enter
window.location.assign(focused.querySelector('a'));
}
}
}
function loadSearchScript(url, id) {
if (document.getElementById(id)) {
return;
}
searchbar_outer.classList.add('searching');
const script = document.createElement('script');
script.src = url;
script.id = id;
script.onload = () => init(window.search);
script.onerror = error => {
console.error(`Failed to load \`${url}\`: ${error}`);
};
document.head.append(script);
}
function showSearch(yes) {
if (yes) {
loadSearchScript(
window.path_to_searchindex_js ||
path_to_root + '{{ resource "searchindex.js" }}',
'search-index');
search_wrap.classList.remove('hidden');
searchicon.setAttribute('aria-expanded', 'true');
} else {
@@ -480,14 +520,14 @@ window.search = window.search || {};
// Don't search the same twice
if (current_searchterm === searchterm) {
return;
} else {
current_searchterm = searchterm;
}
searchbar_outer.classList.add('searching');
if (searchindex === null) {
return;
}
current_searchterm = searchterm;
// Do the actual search
const results = searchindex.search(searchterm, search_options);
const resultcount = Math.min(results.length, results_options.limit_results);
@@ -506,21 +546,9 @@ window.search = window.search || {};
// Display results
showResults(true);
searchbar_outer.classList.remove('searching');
}
function loadScript(url, id) {
const script = document.createElement('script');
script.src = url;
script.id = id;
script.onload = () => init(window.search);
script.onerror = error => {
console.error(`Failed to load \`${url}\`: ${error}`);
};
document.head.append(script);
}
loadScript(path_to_root + '{{ resource "searchindex.js" }}', 'search-index');
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View File

@@ -58,11 +58,27 @@
const path_to_root = "{{ path_to_root }}";
const default_light_theme = "{{ default_theme }}";
const default_dark_theme = "{{ preferred_dark_theme }}";
{{#if search_js}}
window.path_to_searchindex_js = "{{ resource "searchindex.js" }}";
{{/if}}
</script>
<!-- Start loading toc.js asap -->
<script src="{{ resource "toc.js" }}"></script>
</head>
<body>
<div id="mdbook-help-container">
<div id="mdbook-help-popup">
<h2 class="mdbook-help-title">Keyboard shortcuts</h2>
<div>
<p>Press <kbd>←</kbd> or <kbd>→</kbd> to navigate between chapters</p>
{{#if search_enabled}}
<p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
{{/if}}
<p>Press <kbd>?</kbd> to show this help</p>
<p>Press <kbd>Esc</kbd> to hide this help</p>
</div>
</div>
</div>
<div id="body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
@@ -103,10 +119,13 @@
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
sidebar_toggle.checked = false;
}
if (sidebar === 'visible') {
sidebar_toggle.checked = true;
} else {
html.classList.remove('sidebar-visible');
}
sidebar_toggle.checked = sidebar === 'visible';
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
@@ -142,7 +161,7 @@
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
{{/if}}
@@ -162,7 +181,7 @@
</a>
{{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit" rel="edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
{{/if}}
@@ -173,7 +192,12 @@
{{#if search_enabled}}
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
<div class="search-wrapper">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
<div class="spinner-wrapper">
<i class="fa fa-spinner fa-spin"></i>
</div>
</div>
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
@@ -284,8 +308,8 @@
{{#if playground_js}}
<script src="{{ resource "ace.js" }}"></script>
<script src="{{ resource "editor.js" }}"></script>
<script src="{{ resource "mode-rust.js" }}"></script>
<script src="{{ resource "editor.js" }}"></script>
<script src="{{ resource "theme-dawn.js" }}"></script>
<script src="{{ resource "theme-tomorrow_night.js" }}"></script>
{{/if}}
@@ -323,6 +347,21 @@
{{/if}}
{{/if}}
{{#if fragment_map}}
<script>
document.addEventListener('DOMContentLoaded', function() {
const fragmentMap =
{{{fragment_map}}}
;
const target = fragmentMap[window.location.hash];
if (target) {
let url = new URL(target, window.location.href);
window.location.replace(url.href);
}
});
</script>
{{/if}}
</div>
</body>
</html>

View File

@@ -8,5 +8,29 @@
</head>
<body>
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
<script>
// This handles redirects that involve fragments.
document.addEventListener('DOMContentLoaded', function() {
const fragmentMap =
{{{fragment_map}}}
;
const fragment = window.location.hash;
if (fragment) {
let redirectUrl = "{{url}}";
const target = fragmentMap[fragment];
if (target) {
let url = new URL(target, window.location.href);
redirectUrl = url.href;
} else {
let url = new URL(redirectUrl, window.location.href);
url.hash = window.location.hash;
redirectUrl = url.href;
}
window.location.replace(redirectUrl);
}
// else redirect handled by http-equiv
});
</script>
</body>
</html>

View File

@@ -10,7 +10,7 @@ class MDBookSidebarScrollbox extends HTMLElement {
connectedCallback() {
this.innerHTML = '{{#toc}}{{/toc}}';
// Set the current, active page, and reveal it if it's hidden
let current_page = document.location.href.toString().split("#")[0];
let current_page = document.location.href.toString().split("#")[0].split("?")[0];
if (current_page.endsWith("/")) {
current_page += "index.html";
}

View File

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

View File

@@ -7,11 +7,11 @@ use regex::{CaptureMatches, Captures, Regex};
use std::fs;
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem};
use log::{error, warn};
use once_cell::sync::Lazy;
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
@@ -148,7 +148,6 @@ enum RangeOrAnchor {
}
// A range of lines specified with some include directive.
#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
#[derive(PartialEq, Debug, Clone)]
enum LineRange {
Range(Range<usize>),
@@ -410,7 +409,7 @@ impl<'a> Iterator for LinkIter<'a> {
fn find_links(contents: &str) -> LinkIter<'_> {
// lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
static RE: Lazy<Regex> = Lazy::new(|| {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link

View File

@@ -12,18 +12,20 @@ use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::utils::fs::get_404_output_file;
use handlebars::Handlebars;
use log::{debug, trace, warn};
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use serde_json::json;
/// The HTML renderer for mdBook.
#[derive(Default)]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
/// Returns a new instance of [`HtmlHandlebars`].
pub fn new() -> Self {
HtmlHandlebars
}
@@ -109,6 +111,14 @@ impl HtmlHandlebars {
.insert("section".to_owned(), json!(section.to_string()));
}
let redirects = collect_redirects_for_path(&filepath, &ctx.html_config.redirect)?;
if !redirects.is_empty() {
ctx.data.insert(
"fragment_map".to_owned(),
json!(serde_json::to_string(&redirects)?),
);
}
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
@@ -207,7 +217,6 @@ impl HtmlHandlebars {
Ok(())
}
#[allow(clippy::let_and_return)]
fn post_process(
&self,
rendered: String,
@@ -265,15 +274,27 @@ impl HtmlHandlebars {
}
log::debug!("Emitting redirects");
let redirects = combine_fragment_redirects(redirects);
for (original, new) in redirects {
log::debug!("Redirecting \"{}\"\"{}\"", original, new);
for (original, (dest, fragment_map)) in redirects {
// Note: all paths are relative to the build directory, so the
// leading slash in an absolute path means nothing (and would mess
// up `root.join(original)`).
let original = original.trim_start_matches('/');
let filename = root.join(original);
self.emit_redirect(handlebars, &filename, new)?;
if filename.exists() {
// This redirect is handled by the in-page fragment mapper.
continue;
}
if dest.is_empty() {
bail!(
"redirect entry for `{original}` only has source paths with `#` fragments\n\
There must be an entry without the `#` fragment to determine the default \
destination."
);
}
log::debug!("Redirecting \"{}\"\"{}\"", original, dest);
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
}
Ok(())
@@ -284,23 +305,17 @@ impl HtmlHandlebars {
handlebars: &Handlebars<'_>,
original: &Path,
destination: &str,
fragment_map: &BTreeMap<String, String>,
) -> Result<()> {
if original.exists() {
// sanity check to avoid accidentally overwriting a real file.
let msg = format!(
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
original.display(),
destination,
);
return Err(Error::msg(msg));
}
if let Some(parent) = original.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
}
let js_map = serde_json::to_string(fragment_map)?;
let ctx = json!({
"fragment_map": js_map,
"url": destination,
});
let f = File::create(original)?;
@@ -663,10 +678,10 @@ fn make_data(
/// Goes through the rendered HTML, making sure all header tags have
/// an anchor respectively so people can link to sections directly.
fn build_header_links(html: &str) -> String {
static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
static BUILD_HEADER_LINKS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
});
static IGNORE_CLASS: &[&str] = &["menu-title"];
static IGNORE_CLASS: &[&str] = &["menu-title", "mdbook-help-title"];
let mut id_counter = HashMap::new();
@@ -696,7 +711,7 @@ fn build_header_links(html: &str) -> String {
.into_owned()
}
/// Insert a sinle link into a header, making sure each link gets its own
/// Insert a single link into a header, making sure each link gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn insert_link_into_header(
level: usize,
@@ -724,8 +739,8 @@ fn insert_link_into_header(
// ```
// This function replaces all commas by spaces in the code block classes
fn fix_code_blocks(html: &str) -> String {
static FIX_CODE_BLOCKS: Lazy<Regex> =
Lazy::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
static FIX_CODE_BLOCKS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap());
FIX_CODE_BLOCKS
.replace_all(html, |caps: &Captures<'_>| {
@@ -738,8 +753,8 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned()
}
static CODE_BLOCK_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
static CODE_BLOCK_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
fn add_playground_pre(
html: &str,
@@ -807,8 +822,10 @@ fn add_playground_pre(
/// 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());
static LANGUAGE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
static HIDELINES_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
CODE_BLOCK_RE
.replace_all(html, |caps: &Captures<'_>| {
@@ -849,7 +866,8 @@ fn hide_lines(html: &str, code_config: &Code) -> String {
}
fn hide_lines_rust(content: &str) -> String {
static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
static BORING_LINES_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
let mut result = String::with_capacity(content.len());
let mut lines = content.lines().peekable();
@@ -930,6 +948,62 @@ struct RenderItemContext<'a> {
chapter_titles: &'a HashMap<PathBuf, String>,
}
/// Redirect mapping.
///
/// The key is the source path (like `foo/bar.html`). The value is a tuple
/// `(destination_path, fragment_map)`. The `destination_path` is the page to
/// redirect to. `fragment_map` is the map of fragments that override the
/// destination. For example, a fragment `#foo` could redirect to any other
/// page or site.
type CombinedRedirects = BTreeMap<String, (String, BTreeMap<String, String>)>;
fn combine_fragment_redirects(redirects: &HashMap<String, String>) -> CombinedRedirects {
let mut combined: CombinedRedirects = BTreeMap::new();
// This needs to extract the fragments to generate the fragment map.
for (original, new) in redirects {
if let Some((source_path, source_fragment)) = original.rsplit_once('#') {
let e = combined.entry(source_path.to_string()).or_default();
if let Some(old) = e.1.insert(format!("#{source_fragment}"), new.clone()) {
log::error!(
"internal error: found duplicate fragment redirect \
{old} for {source_path}#{source_fragment}"
);
}
} else {
let e = combined.entry(original.to_string()).or_default();
e.0 = new.clone();
}
}
combined
}
/// Collects fragment redirects for an existing page.
///
/// The returned map has keys like `#foo` and the value is the new destination
/// path or URL.
fn collect_redirects_for_path(
path: &Path,
redirects: &HashMap<String, String>,
) -> Result<BTreeMap<String, String>> {
let path = format!("/{}", path.display().to_string().replace('\\', "/"));
if redirects.contains_key(&path) {
bail!(
"redirect found for existing chapter at `{path}`\n\
Either delete the redirect or remove the chapter."
);
}
let key_prefix = format!("{path}#");
let map = redirects
.iter()
.filter_map(|(source, dest)| {
source
.strip_prefix(&key_prefix)
.map(|fragment| (format!("#{fragment}"), dest.to_string()))
})
.collect();
Ok(map)
}
#[cfg(test)]
mod tests {
use crate::config::TextDirection;

View File

@@ -39,12 +39,7 @@ impl HelperDef for ResourceHelper {
let path_to_root = utils::fs::path_to_root(&base_path);
out.write(&path_to_root)?;
out.write(
self.hash_map
.get(&param[..])
.map(|p| &p[..])
.unwrap_or(&param),
)?;
out.write(self.hash_map.get(param).map(|p| &p[..]).unwrap_or(&param))?;
Ok(())
}
}

View File

@@ -107,8 +107,7 @@ impl HelperDef for RenderToc {
}
// Link
let path_exists: bool;
match item.get("path") {
let path_exists = match item.get("path") {
Some(path) if !path.is_empty() => {
out.write("<a href=\"")?;
let tmp = Path::new(path)
@@ -125,13 +124,13 @@ impl HelperDef for RenderToc {
} else {
"\">"
})?;
path_exists = true;
true
}
_ => {
out.write("<div>")?;
path_exists = false;
false
}
}
};
if !self.no_section_label {
// Section does not necessarily exist

View File

@@ -1,5 +1,3 @@
#![allow(missing_docs)] // FIXME: Document this
pub use self::hbs_renderer::HtmlHandlebars;
pub use self::static_files::StaticFiles;

View File

@@ -1,9 +1,9 @@
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use elasticlunr::{Index, IndexBuilder};
use once_cell::sync::Lazy;
use pulldown_cmark::*;
use crate::book::{Book, BookItem, Chapter};
@@ -66,7 +66,13 @@ pub fn create_files(
if search_config.copy_js {
static_files.add_builtin(
"searchindex.js",
format!("Object.assign(window.search, {});", index).as_bytes(),
// To reduce the size of the generated JSON by preventing all `"` characters to be
// escaped, we instead surround the string with much less common `'` character.
format!(
"window.search = Object.assign(window.search, JSON.parse('{}'));",
index.replace("\\", "\\\\").replace("'", "\\'")
)
.as_bytes(),
);
static_files.add_builtin("searcher.js", searcher::JS);
static_files.add_builtin("mark.min.js", searcher::MARK_JS);
@@ -308,7 +314,7 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
}
fn clean_html(html: &str) -> String {
static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
static AMMONIA: LazyLock<ammonia::Builder<'static>> = LazyLock::new(|| {
let mut clean_content = HashSet::new();
clean_content.insert("script");
clean_content.insert("style");
@@ -403,3 +409,92 @@ fn chapter_settings_priority() {
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenize_basic() {
assert_eq!(tokenize("hello world"), vec!["hello", "world"]);
}
#[test]
fn test_tokenize_with_hyphens() {
assert_eq!(
tokenize("hello-world test-case"),
vec!["hello", "world", "test", "case"]
);
}
#[test]
fn test_tokenize_mixed_whitespace() {
assert_eq!(
tokenize("hello\tworld\ntest\r\ncase"),
vec!["hello", "world", "test", "case"]
);
}
#[test]
fn test_tokenize_empty_string() {
assert_eq!(tokenize(""), Vec::<String>::new());
}
#[test]
fn test_tokenize_only_whitespace() {
assert_eq!(tokenize(" \t\n "), Vec::<String>::new());
}
#[test]
fn test_tokenize_case_normalization() {
assert_eq!(tokenize("Hello WORLD Test"), vec!["hello", "world", "test"]);
}
#[test]
fn test_tokenize_trim_whitespace() {
assert_eq!(tokenize(" hello world "), vec!["hello", "world"]);
}
#[test]
fn test_tokenize_long_words_filtered() {
let long_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX + 1);
let short_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
let input = format!("{} hello {}", long_word, short_word);
assert_eq!(tokenize(&input), vec!["hello", &short_word]);
}
#[test]
fn test_tokenize_max_length_word() {
let max_word = "a".repeat(MAX_WORD_LENGTH_TO_INDEX);
assert_eq!(tokenize(&max_word), vec![max_word]);
}
#[test]
fn test_tokenize_special_characters() {
assert_eq!(
tokenize("hello,world.test!case?"),
vec!["hello,world.test!case?"]
);
}
#[test]
fn test_tokenize_unicode() {
assert_eq!(
tokenize("café naïve résumé"),
vec!["café", "naïve", "résumé"]
);
}
#[test]
fn test_tokenize_unicode_rtl_hebre() {
assert_eq!(tokenize("שלום עולם"), vec!["שלום", "עולם"]);
}
#[test]
fn test_tokenize_numbers() {
assert_eq!(
tokenize("test123 456-789 hello"),
vec!["test123", "456", "789", "hello"]
);
}
}

View File

@@ -1,7 +1,6 @@
//! Support for writing static files.
use log::{debug, warn};
use once_cell::sync::Lazy;
use crate::config::HtmlConfig;
use crate::errors::*;
@@ -13,6 +12,7 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
/// Map static files to their final names and contents.
///
@@ -231,8 +231,8 @@ impl StaticFiles {
use regex::bytes::{Captures, Regex};
// The `{{ resource "name" }}` directive in static resources look like
// handlebars syntax, even if they technically aren't.
static RESOURCE: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
static RESOURCE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"\{\{ resource "([^"]+)" \}\}"#).unwrap());
fn replace_all<'a>(
hash_map: &HashMap<String, String>,
data: &'a [u8],

View File

@@ -1,3 +1,5 @@
//! Filesystem utilities and helpers.
use crate::errors::*;
use log::{debug, trace};
use std::fs::{self, File};
@@ -202,6 +204,7 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
}
}
/// Returns the name of the file used for HTTP 404 "not found" with the `.html` extension.
pub fn get_404_output_file(input_404: &Option<String>) -> String {
input_404
.as_ref()

View File

@@ -1,11 +1,10 @@
#![allow(missing_docs)] // FIXME: Document this
//! Various helpers and utilities.
pub mod fs;
mod string;
pub(crate) mod toml_ext;
use crate::errors::Error;
use log::error;
use once_cell::sync::Lazy;
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
use regex::Regex;
@@ -13,6 +12,7 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::Write;
use std::path::Path;
use std::sync::LazyLock;
pub use self::string::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
@@ -21,7 +21,7 @@ pub use self::string::{
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s\s+").unwrap());
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s\s+").unwrap());
RE.replace_all(text, " ")
}
@@ -50,7 +50,7 @@ pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
static HTML: Lazy<Regex> = Lazy::new(|| Regex::new(r"(<.*?>)").unwrap());
static HTML: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(<.*?>)").unwrap());
content = HTML.replace_all(&content, "").into();
const REPL_SUB: &[&str] = &["&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
for sub in REPL_SUB {
@@ -93,9 +93,10 @@ pub fn unique_id_from_content(content: &str, id_counter: &mut HashMap<String, us
/// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult.
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
static SCHEME_LINK: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
static MD_LINK: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
static SCHEME_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap());
static MD_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap());
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
if dest.starts_with('#') {
@@ -148,8 +149,8 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
// There are dozens of HTML tags/attributes that contain paths, so
// feel free to add more tags if desired; these are the only ones I
// care about right now.
static HTML_LINK: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
static HTML_LINK: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap());
HTML_LINK
.replace_all(&html, |caps: &regex::Captures<'_>| {
@@ -194,6 +195,7 @@ pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
render_markdown_with_path(text, smart_punctuation, None)
}
/// Creates a new pulldown-cmark parser of the given text.
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
@@ -207,6 +209,11 @@ pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
Parser::new_ext(text, opts)
}
/// Renders markdown to HTML.
///
/// `path` should only be set if this is being generated for the consolidated
/// print page. It should point to the page being rendered relative to the
/// root of the book.
pub fn render_markdown_with_path(
text: &str,
smart_punctuation: bool,
@@ -229,10 +236,10 @@ pub fn render_markdown_with_path(
// `count` is the number of references to this footnote (used for multiple
// linkbacks, and checking for unused footnotes).
let mut footnote_numbers = HashMap::new();
// This is a list of (name, Vec<Event>)
// This is a map of name -> Vec<Event>
// `name` is the name of the footnote.
// The events list is the list of events needed to build the footnote definition.
let mut footnote_defs = Vec::new();
let mut footnote_defs = HashMap::new();
// The following are used when currently processing a footnote definition.
//
@@ -262,7 +269,16 @@ pub fn render_markdown_with_path(
Event::End(TagEnd::FootnoteDefinition) => {
let def_events = std::mem::take(&mut in_footnote);
let name = std::mem::take(&mut in_footnote_name);
footnote_defs.push((name, def_events));
if footnote_defs.contains_key(&name) {
log::warn!(
"footnote `{name}` in {} defined multiple times - \
not updating to new definition",
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
);
} else {
footnote_defs.insert(name, def_events);
}
None
}
Event::FootnoteReference(name) => {
@@ -298,7 +314,12 @@ pub fn render_markdown_with_path(
html::push_html(&mut body, events);
if !footnote_defs.is_empty() {
add_footnote_defs(&mut body, path, footnote_defs, &footnote_numbers);
add_footnote_defs(
&mut body,
path,
footnote_defs.into_iter().collect(),
&footnote_numbers,
);
}
body

View File

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

View File

@@ -25,4 +25,27 @@ expand = true
heading-split-level = 2
[output.html.redirect]
"/format/config.html" = "configuration/index.html"
"/format/config.html" = "../prefix.html"
# This is a source without a fragment, and one with a fragment that goes to
# the same place. The redirect with the fragment is not necessary, since that
# is the default behavior.
"/pointless-fragment.html" = "prefix.html"
"/pointless-fragment.html#foo" = "prefix.html#foo"
"/rename-page-and-fragment.html" = "prefix.html"
"/rename-page-and-fragment.html#orig" = "prefix.html#new"
"/rename-page-fragment-elsewhere.html" = "prefix.html"
"/rename-page-fragment-elsewhere.html#orig" = "suffix.html#new"
# Rename fragment on an existing page.
"/prefix.html#orig" = "prefix.html#new"
# Rename fragment on an existing page to another page.
"/prefix.html#orig-new-page" = "suffix.html#new"
"/full-url-with-fragment.html" = "https://www.rust-lang.org/#fragment"
"/full-url-with-fragment-map.html" = "https://www.rust-lang.org/"
"/full-url-with-fragment-map.html#a" = "https://www.rust-lang.org/#new1"
"/full-url-with-fragment-map.html#b" = "https://www.rust-lang.org/#new2"

View File

@@ -17,7 +17,7 @@ This line contains `inline code` mixed with some other stuff. (LTR)
---
````
escaping ``` in ```, fun, isn't is?
escaping ``` in ```, fun, isn't it?
````
---

View File

@@ -1,163 +0,0 @@
//! Integration tests to make sure alternative backends work.
use mdbook::config::Config;
use mdbook::MDBook;
use std::fs;
use std::path::Path;
use tempfile::{Builder as TempFileBuilder, TempDir};
#[test]
fn passing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);
md.build().unwrap();
}
#[test]
fn failing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);
md.build().unwrap_err();
}
#[test]
fn missing_backends_are_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
let got = md.build();
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
assert_eq!(error_message, "Rendering failed");
}
#[test]
fn missing_optional_backends_are_not_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
assert!(md.build().is_ok());
}
#[test]
fn alternate_backend_with_arguments() {
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);
md.build().unwrap();
}
#[test]
fn backends_receive_render_context_via_stdin() {
use mdbook::renderer::RenderContext;
use std::fs::File;
let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", 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());
md.build().unwrap();
assert!(out_file.exists());
let got = RenderContext::from_json(File::open(&out_file).unwrap());
assert!(got.is_ok());
}
#[test]
fn relative_command_path() {
// Checks behavior of relative paths for the `command` setting.
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let renderers = temp.path().join("renderers");
fs::create_dir(&renderers).unwrap();
rust_exe(
&renderers,
"myrenderer",
r#"fn main() {
std::fs::write("output", "test").unwrap();
}"#,
);
let do_test = |cmd_path| {
let mut config = Config::default();
config
.set("output.html", toml::value::Table::new())
.unwrap();
config.set("output.myrenderer.command", cmd_path).unwrap();
let md = MDBook::init(temp.path())
.with_config(config)
.build()
.unwrap();
let output = temp.path().join("book/myrenderer/output");
assert!(!output.exists());
md.build().unwrap();
assert!(output.exists());
fs::remove_file(output).unwrap();
};
// Legacy paths work, relative to the output directory.
if cfg!(windows) {
do_test("../../renderers/myrenderer.exe");
} else {
do_test("../../renderers/myrenderer");
}
// Modern path, relative to the book directory.
do_test("renderers/myrenderer");
}
fn dummy_book_with_backend(
name: &str,
command: &str,
backend_is_optional: bool,
) -> (MDBook, TempDir) {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let mut config = Config::default();
config
.set(format!("output.{name}.command"), command)
.unwrap();
if backend_is_optional {
config.set(format!("output.{name}.optional"), true).unwrap();
}
let md = MDBook::init(temp.path())
.with_config(config)
.build()
.unwrap();
(md, temp)
}
fn fail_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 1""#
} else {
"false"
}
}
fn success_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 0""#
} else {
"true"
}
}
fn rust_exe(temp: &Path, name: &str, src: &str) {
let rs = temp.join(name).with_extension("rs");
fs::write(&rs, src).unwrap();
let status = std::process::Command::new("rustc")
.arg(rs)
.current_dir(temp)
.status()
.expect("rustc should run");
assert!(status.success());
}

View File

@@ -1,78 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::renderer::{RenderContext, Renderer};
use mdbook::MDBook;
use std::sync::{Arc, Mutex};
struct Spy(Arc<Mutex<Inner>>);
#[derive(Debug, Default)]
struct Inner {
run_count: usize,
rendered_with: Vec<String>,
}
impl Preprocessor for Spy {
fn name(&self) -> &str {
"dummy"
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
let mut inner = self.0.lock().unwrap();
inner.run_count += 1;
inner.rendered_with.push(ctx.renderer.clone());
Ok(book)
}
}
impl Renderer for Spy {
fn name(&self) -> &str {
"dummy"
}
fn render(&self, _ctx: &RenderContext) -> Result<()> {
let mut inner = self.0.lock().unwrap();
inner.run_count += 1;
Ok(())
}
}
#[test]
fn mdbook_runs_preprocessors() {
let spy: Arc<Mutex<Inner>> = Default::default();
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_preprocessor(Spy(Arc::clone(&spy)));
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
assert_eq!(inner.rendered_with.len(), 1);
assert_eq!(
"html", inner.rendered_with[0],
"We should have been run with the default HTML renderer"
);
}
#[test]
fn mdbook_runs_renderers() {
let spy: Arc<Mutex<Inner>> = Default::default();
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_renderer(Spy(Arc::clone(&spy)));
book.build().unwrap();
let inner = spy.lock().unwrap();
assert_eq!(inner.run_count, 1);
}

View File

@@ -1,28 +0,0 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
#[test]
fn mdbook_cli_dummy_book_generates_index_html() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.arg("build").current_dir(temp.path());
cmd.assert()
.success()
.stderr(
predicates::str::is_match(r##"Stack depth exceeded in first[\\/]recursive.md."##)
.unwrap(),
)
.stderr(predicates::str::contains(
r##"[INFO] (mdbook::book): Running the html backend"##,
));
// exists afterward
assert!(temp.path().join("book").exists());
let index_file = temp.path().join("book/index.html");
assert!(index_file.exists());
}

View File

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

View File

@@ -1,47 +0,0 @@
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());
}
/// Run `mdbook init` with `--title` without git config.
///
/// Regression test for https://github.com/rust-lang/mdBook/issues/2485
#[test]
fn no_git_config_with_title() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.args(["init", "--title", "Example title"])
.current_dir(temp.path())
.env("GIT_CONFIG_GLOBAL", "")
.env("GIT_CONFIG_NOSYSTEM", "1");
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.as_deref(), Some("Example title"));
}

View File

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

View File

@@ -1,46 +0,0 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
use predicates::boolean::PredicateBooleanExt;
#[test]
fn mdbook_cli_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut cmd = mdbook_cmd();
cmd.arg("test").current_dir(temp.path());
cmd.assert().success()
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap().not())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap().not());
}
#[test]
fn mdbook_cli_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut cmd = mdbook_cmd();
cmd.arg("test").current_dir(temp.path());
cmd.assert().failure()
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "README.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "intro.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]index.md""##).unwrap())
.stderr(predicates::str::is_match(r##"Testing chapter [^:]*: "first[\\/]nested.md""##).unwrap())
.stderr(predicates::str::is_match(r##"returned an error:\n\n"##).unwrap())
.stderr(predicates::str::is_match(r##"Nested_Chapter::Rustdoc_include_works_with_anchors_too \(line \d+\) ... FAILED"##).unwrap());
}
#[test]
fn empty_cli() {
let mut cmd = mdbook_cmd();
cmd.assert()
.failure()
.code(2)
.stdout(predicates::str::is_empty())
.stderr(predicates::str::contains(
"Creates a book from markdown files",
));
}

View File

@@ -1,2 +0,0 @@
mod cli;
mod dummy_book;

View File

@@ -1,68 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook::MDBook;
fn example() -> CmdPreprocessor {
CmdPreprocessor::new(
"nop-preprocessor".to_string(),
"cargo run --example nop-preprocessor --".to_string(),
)
}
#[test]
fn example_supports_whatever() {
let cmd = example();
let got = cmd.supports_renderer("whatever");
assert_eq!(got, true);
}
#[test]
fn example_doesnt_support_not_supported() {
let cmd = example();
let got = cmd.supports_renderer("not-supported");
assert_eq!(got, false);
}
#[test]
fn ask_the_preprocessor_to_blow_up() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.config
.set("preprocessor.nop-preprocessor.blow-up", true)
.unwrap();
let got = md.build();
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
let status = if cfg!(windows) {
"exit code: 1"
} else {
"exit status: 1"
};
assert_eq!(
error_message,
format!(
r#"The "nop-preprocessor" preprocessor exited unsuccessfully with {status} status"#
)
);
}
#[test]
fn process_the_dummy_book() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.build().unwrap();
}

View File

@@ -1,145 +0,0 @@
//! This will create an entire book in a temporary directory using some
//! dummy contents from the `tests/dummy-book/` directory.
// Not all features are used in all test crates, so...
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
use anyhow::Context;
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use tempfile::{Builder as TempFileBuilder, TempDir};
use walkdir::WalkDir;
/// Create a dummy book in a temporary directory, using the contents of
/// `SUMMARY_MD` as a guide.
///
/// The "Nested Chapter" file contains a code block with a single
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
/// `false`. This is done using the `passing_test` parameter.
#[derive(Clone, Debug, PartialEq)]
pub struct DummyBook {
passing_test: bool,
}
impl DummyBook {
/// Create a new `DummyBook` with all the defaults.
pub fn new() -> DummyBook {
DummyBook { passing_test: true }
}
/// Whether the doc-test included in the "Nested Chapter" should pass or
/// fail (it passes by default).
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
self.passing_test = test_passes;
self
}
/// Write a book to a temporary directory using the provided settings.
pub fn build(&self) -> Result<TempDir> {
let temp = TempFileBuilder::new()
.prefix("dummy_book-")
.tempdir()
.with_context(|| "Unable to create temp directory")?;
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).with_context(|| {
"Couldn't copy files into a \
temporary directory"
})?;
let sub_pattern = if self.passing_test { "true" } else { "false" };
let files_containing_tests = [
"src/first/nested.md",
"src/first/nested-test.rs",
"src/first/nested-test-with-anchors.rs",
"src/first/partially-included-test.rs",
"src/first/partially-included-test-with-anchors.rs",
];
for file in &files_containing_tests {
let path_containing_tests = temp.path().join(file);
replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
}
Ok(temp)
}
}
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
let contents = fs::read_to_string(filename)?;
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
Ok(())
}
/// Read the contents of the provided file into memory and then iterate through
/// the list of strings asserting that the file contains all of them.
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content
);
}
}
pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
!content.contains(s),
"Found {:?} in {}\n\n{}",
s,
filename.display(),
content
);
}
}
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
for entry in WalkDir::new(from) {
let entry = entry.with_context(|| "Unable to inspect directory entry")?;
let original_location = entry.path();
let relative = original_location
.strip_prefix(from)
.expect("`original_location` is inside the `from` directory");
let new_location = to.join(relative);
if original_location.is_file() {
if let Some(parent) = new_location.parent() {
fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
}
fs::copy(original_location, &new_location)
.with_context(|| "Unable to copy file contents")?;
}
}
Ok(())
}
pub fn new_copy_of_example_book() -> Result<TempDir> {
let temp = TempFileBuilder::new().prefix("guide").tempdir()?;
let guide = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
recursive_copy(guide, temp.path())?;
Ok(temp)
}

View File

@@ -1,5 +0,0 @@
# Dummy Book
This file is just here to cause the index preprocessor to run.
Does a pretty good job, too.

View File

@@ -1,3 +0,0 @@
# Includes
{{#include ../SUMMARY.md::}}

View File

@@ -1,11 +0,0 @@
// The next line will cause a `testing` test to fail if the anchor feature is broken in such a way
// that the whole file gets mistakenly included.
assert!(!$TEST_STATUS);
// ANCHOR: myanchor
// ANCHOR: unendinganchor
// The next line will cause a `rendered_output` test to fail if the anchor feature is broken in
// such a way that the content between anchors isn't included.
// unique-string-for-anchor-test
assert!($TEST_STATUS);
// ANCHOR_END: myanchor

View File

@@ -1 +0,0 @@
assert!($TEST_STATUS);

View File

@@ -1,31 +0,0 @@
# Nested Chapter
This file has some testable code.
```rust
assert!($TEST_STATUS);
```
## Some Section
```rust
{{#include nested-test.rs}}
```
## Anchors include the part of a file between special comments
```rust
{{#include nested-test-with-anchors.rs:myanchor}}
```
## Rustdoc include adds the rest of the file as hidden
```rust
{{#rustdoc_include partially-included-test.rs:5:7}}
```
## Rustdoc include works with anchors too
```rust
{{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}}
```

View File

@@ -1,3 +0,0 @@
# Introduction
Here's some interesting text...

View File

@@ -1,5 +0,0 @@
# Second Chapter
This makes sure you can insert runnable Rust files.
{{#playground example.rs}}

View File

@@ -1 +0,0 @@
# Root README

View File

@@ -1,7 +0,0 @@
# This dummy book is for testing the conversion of README.md to index.html by IndexPreprocessor
[Root README](README.md)
- [1st README](first/README.md)
- [2nd README](second/README.md)
- [2nd index](second/index.md)

View File

@@ -1 +0,0 @@
# First README

View File

@@ -1 +0,0 @@
# Second README

View File

@@ -1 +0,0 @@
# Second index

16
tests/gui/help.goml Normal file
View File

@@ -0,0 +1,16 @@
// This GUI test checks help popup.
go-to: |DOC_PATH| + "index.html"
assert-css: ("#mdbook-help-container", {"display": "none"})
press-key: '?'
wait-for-css: ("#mdbook-help-container", {"display": "flex"})
press-key: 'Escape'
wait-for-css: ("#mdbook-help-container", {"display": "none"})
press-key: '?'
wait-for-css: ("#mdbook-help-container", {"display": "flex"})
// Click inside does nothing.
click: "#mdbook-help-popup"
wait-for-css: ("#mdbook-help-container", {"display": "flex"})
// Click outside dismisses.
click: "*"
wait-for-css: ("#mdbook-help-container", {"display": "none"})

View File

@@ -1,8 +1,5 @@
// This tests pressing the left and right arrows moving to previous and next page.
// We disable the requests checks because `mode-rust.js` is not found.
fail-on-request-error: false
go-to: |DOC_PATH| + "index.html"
// default page is the first numbered page

51
tests/gui/redirect.goml Normal file
View File

@@ -0,0 +1,51 @@
go-to: |DOC_PATH| + "format/config.html"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
// Check that it preserves fragments when redirecting.
go-to: |DOC_PATH| + "format/config.html#fragment"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#fragment"})
// The fragment one here isn't necessary, but should still work.
go-to: |DOC_PATH| + "pointless-fragment.html"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
go-to: |DOC_PATH| + "pointless-fragment.html#foo"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#foo"})
// Page rename, and a fragment rename.
go-to: |DOC_PATH| + "rename-page-and-fragment.html"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
go-to: |DOC_PATH| + "rename-page-and-fragment.html#orig"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#new"})
// Page rename, and the fragment goes to a *different* page from the default.
go-to: |DOC_PATH| + "rename-page-fragment-elsewhere.html"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
go-to: |DOC_PATH| + "rename-page-fragment-elsewhere.html#orig"
assert-window-property: ({"location": |DOC_PATH| + "suffix.html#new"})
// Goes to an external site.
go-to: |DOC_PATH| + "full-url-with-fragment.html"
assert-window-property: ({"location": "https://www.rust-lang.org/#fragment"})
// External site with fragment renames.
go-to: |DOC_PATH| + "full-url-with-fragment-map.html#a"
assert-window-property: ({"location": "https://www.rust-lang.org/#new1"})
go-to: |DOC_PATH| + "full-url-with-fragment-map.html#b"
assert-window-property: ({"location": "https://www.rust-lang.org/#new2"})
// Rename fragment on an existing page.
go-to: |DOC_PATH| + "prefix.html#orig"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#new"})
// Other fragments aren't affected.
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
go-to: |DOC_PATH| + "prefix.html"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html"})
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
go-to: |DOC_PATH| + "prefix.html#dont-change"
assert-window-property: ({"location": |DOC_PATH| + "prefix.html#dont-change"})
// Rename fragment on an existing page to another page.
go-to: |DOC_PATH| + "index.html" // Reset page since redirects are processed on load.
go-to: |DOC_PATH| + "prefix.html#orig-new-page"
assert-window-property: ({"location": |DOC_PATH| + "suffix.html#new"})

View File

@@ -1,5 +1,6 @@
// This tests basic search behavior.
fail-on-js-error: true
go-to: |DOC_PATH| + "index.html"
define-function: (
@@ -7,7 +8,7 @@ define-function: (
[],
block {
assert-css: ("#search-wrapper", {"display": "none"})
press-key: 'S'
press-key: 's'
wait-for-css-false: ("#search-wrapper", {"display": "none"})
}
)
@@ -28,3 +29,48 @@ assert-text: ("#searchresults-header", "")
call-function: ("open-search", {})
write: "strikethrough"
wait-for-text: ("#searchresults-header", "2 search results for 'strikethrough':")
// Now we test search shortcuts and more page changes.
go-to: |DOC_PATH| + "index.html"
// This check is to ensure that the search bar is inside the search wrapper.
assert: "#search-wrapper #searchbar"
assert-css: ("#search-wrapper", {"display": "none"})
// Now we make sure the search input appear with the `S` shortcut.
press-key: 's'
wait-for-css-false: ("#search-wrapper", {"display": "none"})
// We ensure the search bar has the focus.
assert: "#searchbar:focus"
// Pressing a key will therefore update the search input.
press-key: 't'
assert-text: ("#searchbar", "t")
// Now we press `Escape` to ensure that the search input disappears again.
press-key: 'Escape'
wait-for-css: ("#search-wrapper", {"display": "none"})
// Making it appear by clicking on the search button.
click: "#search-toggle"
wait-for-css: ("#search-wrapper", {"display": "block"})
// We ensure the search bar has the focus.
assert: "#searchbar:focus"
// We input "test".
write: "test"
// The results should now appear.
wait-for-text: ("#searchresults-header", "search results for 'test':", ENDS_WITH)
assert: "#searchresults"
// Ensure that the URL was updated as well.
assert-document-property: ({"URL": "?search=test"}, ENDS_WITH)
// Now we ensure that when we land on the page with a "search in progress", the search results are
// loaded and that the search input has focus.
go-to: |DOC_PATH| + "index.html?search=test"
wait-for-text: ("#searchresults-header", "search results for 'test':", ENDS_WITH)
assert: "#searchbar:focus"
assert: "#searchresults"
// And now we press `Escape` to close everything.
press-key: 'Escape'
wait-for-css: ("#search-wrapper", {"display": "none"})

View File

@@ -0,0 +1,17 @@
// This GUI test checks the active page sidebar highlight.
go-to: |DOC_PATH| + "index.html"
assert-text: ("mdbook-sidebar-scrollbox a.active", "Prefix Chapter")
go-to: |DOC_PATH| + "individual/index.html"
assert-text: ("mdbook-sidebar-scrollbox a.active", "3. Markdown Individual tags")
go-to: |DOC_PATH| + "index.html?highlight=test"
assert-text: ("mdbook-sidebar-scrollbox a.active", "Prefix Chapter")
go-to: |DOC_PATH| + "individual/index.html?highlight=test"
assert-text: ("mdbook-sidebar-scrollbox a.active", "3. Markdown Individual tags")

View File

@@ -7,23 +7,20 @@ set-window-size: (1100, 600)
reload:
store-value: (content_indent, 308)
store-value: (sidebar_storage_value, "mdbook-sidebar")
store-value: (sidebar_storage_hidden_value, "hidden")
store-value: (sidebar_storage_displayed_value, "visible")
define-function: (
"hide-sidebar",
[],
block {
// The content should be "moved" to the right because of the sidebar.
assert-css: ("#sidebar", {"transform": "none"})
assert-position: ("#page-wrapper", {"x": |content_indent|})
// We now hide the sidebar.
click: "#sidebar-toggle"
wait-for: "body.sidebar-hidden"
// `transform` is 0.3s so we need to wait a bit (0.5s) to ensure the animation is done.
wait-for: 5000
assert-css-false: ("#sidebar", {"transform": "none"})
// The page content should now be on the left.
assert-position: ("#page-wrapper", {"x": 0})
wait-for-css: ("#sidebar", {"display": "none"})
assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_hidden_value|}
},
)
@@ -31,26 +28,42 @@ define-function: (
"show-sidebar",
[],
block {
// The page content should be on the left and the sidebar "moved out".
assert-css: ("#sidebar", {"transform": "matrix(1, 0, 0, 1, -308, 0)"})
assert-css: ("#sidebar", {"display": "none"})
assert-position: ("#page-wrapper", {"x": 0})
// We expand the sidebar.
click: "#sidebar-toggle"
wait-for: "body.sidebar-visible"
wait-for-css-false: ("#sidebar", {"display": "none"})
// `transform` is 0.3s so we need to wait a bit (0.5s) to ensure the animation is done.
wait-for: 5000
assert-css-false: ("#sidebar", {"transform": "matrix(1, 0, 0, 1, -308, 0)"})
// The page content should be moved to the right.
assert-position: ("#page-wrapper", {"x": |content_indent|})
assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|}
},
)
// Since the sidebar is visible, we should be able to find this text.
assert-find-text: ("3.9. Links and Horizontal Rule", {"case-sensitive": true})
call-function: ("hide-sidebar", {})
// Text should not be findeable anymore since the sidebar is collapsed.
assert-find-text-false: ("3.9. Links and Horizontal Rule", {"case-sensitive": true})
call-function: ("show-sidebar", {})
// We should be able to find this text again.
assert-find-text: ("3.9. Links and Horizontal Rule", {"case-sensitive": true})
// We now test on smaller width to ensure that the sidebar is collapsed by default.
set-window-size: (900, 600)
reload:
call-function: ("show-sidebar", {})
call-function: ("hide-sidebar", {})
// We now test that if the sidebar is considered open and we reload the page, since
// the width is small, it will still be collapsed.
set-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|}
reload:
// The stored value shouldn't have changed.
assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|}
// But the sidebar should be hidden anyway.
assert-css: ("#sidebar", {"display": "none"})
assert-position: ("#page-wrapper", {"x": 0})

View File

@@ -1,157 +0,0 @@
use mdbook::config::Config;
use mdbook::MDBook;
use pretty_assertions::assert_eq;
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
/// Run `mdbook init` in an empty directory and make sure the default files
/// are created.
#[test]
fn base_mdbook_init_should_create_default_content() {
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists());
}
MDBook::init(temp.path()).build().unwrap();
for file in &created_files {
let target = temp.path().join(file);
println!("{}", target.display());
assert!(target.exists(), "{file} doesn't exist");
}
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!(
contents,
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\n"
);
}
/// Run `mdbook init` in a directory containing a SUMMARY.md should create the
/// files listed in the summary.
#[test]
fn run_mdbook_init_should_create_content_from_summary() {
let created_files = vec!["intro.md", "first.md", "outro.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let src_dir = temp.path().join("src");
fs::create_dir_all(src_dir.clone()).unwrap();
static SUMMARY: &str = r#"# Summary
[intro](intro.md)
- [First chapter](first.md)
[outro](outro.md)
"#;
let mut summary = File::create(src_dir.join("SUMMARY.md")).unwrap();
summary.write_all(SUMMARY.as_bytes()).unwrap();
MDBook::init(temp.path()).build().unwrap();
for file in &created_files {
let target = src_dir.join(file);
println!("{}", target.display());
assert!(target.exists(), "{file} doesn't exist");
}
}
/// Set some custom arguments for where to place the source and destination
/// files, then call `mdbook init`.
#[test]
fn run_mdbook_init_with_custom_book_and_src_locations() {
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
for file in &created_files {
assert!(
!temp.path().join(file).exists(),
"{file} shouldn't exist yet!"
);
}
let mut cfg = Config::default();
cfg.book.src = PathBuf::from("in");
cfg.build.build_dir = PathBuf::from("out");
MDBook::init(temp.path()).with_config(cfg).build().unwrap();
for file in &created_files {
let target = temp.path().join(file);
assert!(
target.exists(),
"{file} should have been created by `mdbook init`"
);
}
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!(
contents,
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"in\"\n\n[build]\nbuild-dir = \"out\"\ncreate-missing = true\nextra-watch-dirs = []\nuse-default-preprocessors = true\n"
);
}
#[test]
fn book_toml_isnt_required() {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let md = MDBook::init(temp.path()).build().unwrap();
let _ = fs::remove_file(temp.path().join("book.toml"));
md.build().unwrap();
}
#[test]
fn copy_theme() {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
MDBook::init(temp.path()).copy_theme(true).build().unwrap();
let expected = vec![
"book.js",
"css/chrome.css",
"css/general.css",
"css/print.css",
"css/variables.css",
"favicon.png",
"favicon.svg",
"fonts/OPEN-SANS-LICENSE.txt",
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
"fonts/fonts.css",
"fonts/open-sans-v17-all-charsets-300.woff2",
"fonts/open-sans-v17-all-charsets-300italic.woff2",
"fonts/open-sans-v17-all-charsets-600.woff2",
"fonts/open-sans-v17-all-charsets-600italic.woff2",
"fonts/open-sans-v17-all-charsets-700.woff2",
"fonts/open-sans-v17-all-charsets-700italic.woff2",
"fonts/open-sans-v17-all-charsets-800.woff2",
"fonts/open-sans-v17-all-charsets-800italic.woff2",
"fonts/open-sans-v17-all-charsets-italic.woff2",
"fonts/open-sans-v17-all-charsets-regular.woff2",
"fonts/source-code-pro-v11-all-charsets-500.woff2",
"highlight.css",
"highlight.js",
"index.hbs",
];
let theme_dir = temp.path().join("theme");
let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| !e.file_type().is_dir())
.map(|e| {
e.path()
.strip_prefix(&theme_dir)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/")
})
.collect();
actual.sort();
assert_eq!(actual, expected);
}

View File

@@ -1,43 +0,0 @@
//! Some integration tests to make sure the `SUMMARY.md` parser can deal with
//! some real-life examples.
use mdbook::book;
use std::fs::File;
use std::io::Read;
use std::path::Path;
macro_rules! summary_md_test {
($name:ident, $filename:expr) => {
#[test]
fn $name() {
env_logger::try_init().ok();
let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("summary_md_files")
.join($filename);
if !filename.exists() {
panic!("{} Doesn't exist", filename.display());
}
let mut content = String::new();
File::open(&filename)
.unwrap()
.read_to_string(&mut content)
.unwrap();
if let Err(e) = book::parse_summary(&content) {
eprintln!("Error parsing {}", filename.display());
eprintln!();
eprintln!("{:?}", e);
panic!();
}
}
};
}
summary_md_test!(rust_by_example, "rust_by_example.md");
summary_md_test!(rust_ffi_guide, "rust_ffi_guide.md");
summary_md_test!(example_book, "example_book.md");
summary_md_test!(the_book_2nd_edition, "the_book-2nd_edition.md");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
# Summary
- [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md)
- [build](cli/build.md)
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [Format](format/format.md)
- [SUMMARY.md](format/summary.md)
- [Configuration](format/config.md)
- [Theme](format/theme/theme.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View File

@@ -1,191 +0,0 @@
# Summary
[Introduction](index.md)
- [Hello World](hello.md)
- [Comments](hello/comment.md)
- [Formatted print](hello/print.md)
- [Debug](hello/print/print_debug.md)
- [Display](hello/print/print_display.md)
- [Testcase: List](hello/print/print_display/testcase_list.md)
- [Formatting](hello/print/fmt.md)
- [Primitives](primitives.md)
- [Literals and operators](primitives/literals.md)
- [Tuples](primitives/tuples.md)
- [Arrays and Slices](primitives/array.md)
- [Custom Types](custom_types.md)
- [Structures](custom_types/structs.md)
- [Enums](custom_types/enum.md)
- [use](custom_types/enum/enum_use.md)
- [C-like](custom_types/enum/c_like.md)
- [Testcase: linked-list](custom_types/enum/testcase_linked_list.md)
- [constants](custom_types/constants.md)
- [Variable Bindings](variable_bindings.md)
- [Mutability](variable_bindings/mut.md)
- [Scope and Shadowing](variable_bindings/scope.md)
- [Declare first](variable_bindings/declare.md)
- [Types](types.md)
- [Casting](types/cast.md)
- [Literals](types/literals.md)
- [Inference](types/inference.md)
- [Aliasing](types/alias.md)
- [Conversion](conversion.md)
- [From and Into](conversion/from_into.md)
- [To and From String](conversion/string.md)
- [Expressions](expression.md)
- [Flow Control](flow_control.md)
- [if/else](flow_control/if_else.md)
- [loop](flow_control/loop.md)
- [Nesting and labels](flow_control/loop/nested.md)
- [Returning from loops](flow_control/loop/return.md)
- [while](flow_control/while.md)
- [for and range](flow_control/for.md)
- [match](flow_control/match.md)
- [Destructuring](flow_control/match/destructuring.md)
- [tuples](flow_control/match/destructuring/destructure_tuple.md)
- [enums](flow_control/match/destructuring/destructure_enum.md)
- [pointers/ref](flow_control/match/destructuring/destructure_pointers.md)
- [structs](flow_control/match/destructuring/destructure_structures.md)
- [Guards](flow_control/match/guard.md)
- [Binding](flow_control/match/binding.md)
- [if let](flow_control/if_let.md)
- [while let](flow_control/while_let.md)
- [Functions](fn.md)
- [Methods](fn/methods.md)
- [Closures](fn/closures.md)
- [Capturing](fn/closures/capture.md)
- [As input parameters](fn/closures/input_parameters.md)
- [Type anonymity](fn/closures/anonymity.md)
- [Input functions](fn/closures/input_functions.md)
- [As output parameters](fn/closures/output_parameters.md)
- [Examples in `std`](fn/closures/closure_examples.md)
- [Iterator::any](fn/closures/closure_examples/iter_any.md)
- [Iterator::find](fn/closures/closure_examples/iter_find.md)
- [Higher Order Functions](fn/hof.md)
- [Modules](mod.md)
- [Visibility](mod/visibility.md)
- [Struct visibility](mod/struct_visibility.md)
- [The `use` declaration](mod/use.md)
- [`super` and `self`](mod/super.md)
- [File hierarchy](mod/split.md)
- [Crates](crates.md)
- [Library](crates/lib.md)
- [`extern crate`](crates/link.md)
- [Attributes](attribute.md)
- [`dead_code`](attribute/unused.md)
- [Crates](attribute/crate.md)
- [`cfg`](attribute/cfg.md)
- [Custom](attribute/cfg/custom.md)
- [Generics](generics.md)
- [Functions](generics/gen_fn.md)
- [Implementation](generics/impl.md)
- [Traits](generics/gen_trait.md)
- [Bounds](generics/bounds.md)
- [Testcase: empty bounds](generics/bounds/testcase_empty.md)
- [Multiple bounds](generics/multi_bounds.md)
- [Where clauses](generics/where.md)
- [New Type Idiom](generics/new_types.md)
- [Associated items](generics/assoc_items.md)
- [The Problem](generics/assoc_items/the_problem.md)
- [Associated types](generics/assoc_items/types.md)
- [Phantom type parameters](generics/phantom.md)
- [Testcase: unit clarification](generics/phantom/testcase_units.md)
- [Scoping rules](scope.md)
- [RAII](scope/raii.md)
- [Ownership and moves](scope/move.md)
- [Mutability](scope/move/mut.md)
- [Borrowing](scope/borrow.md)
- [Mutability](scope/borrow/mut.md)
- [Freezing](scope/borrow/freeze.md)
- [Aliasing](scope/borrow/alias.md)
- [The ref pattern](scope/borrow/ref.md)
- [Lifetimes](scope/lifetime.md)
- [Explicit annotation](scope/lifetime/explicit.md)
- [Functions](scope/lifetime/fn.md)
- [Methods](scope/lifetime/methods.md)
- [Structs](scope/lifetime/struct.md)
- [Bounds](scope/lifetime/lifetime_bounds.md)
- [Coercion](scope/lifetime/lifetime_coercion.md)
- [static](scope/lifetime/static_lifetime.md)
- [elision](scope/lifetime/elision.md)
- [Traits](trait.md)
- [Derive](trait/derive.md)
- [Operator Overloading](trait/ops.md)
- [Drop](trait/drop.md)
- [Iterators](trait/iter.md)
- [Clone](trait/clone.md)
- [macro_rules!](macros.md)
- [Syntax](macro/syntax.md)
- [Designators](macros/designators.md)
- [Overload](macros/overload.md)
- [Repeat](macros/repeat.md)
- [DRY (Don't Repeat Yourself)](macros/dry.md)
- [DSL (Domain Specific Languages)](macros/dsl.md)
- [Variadics](macros/variadics.md)
- [Error handling](error.md)
- [`panic`](error/panic.md)
- [`Option` & `unwrap`](error/option_unwrap.md)
- [Combinators: `map`](error/option_unwrap/map.md)
- [Combinators: `and_then`](error/option_unwrap/and_then.md)
- [`Result`](error/result.md)
- [`map` for `Result`](error/result/result_map.md)
- [aliases for `Result`](error/result/result_alias.md)
- [Early returns](error/result/early_returns.md)
- [Introducing `?`](error/result/enter_question_mark.md)
- [Multiple error types](error/multiple_error_types.md)
- [Pulling `Result`s out of `Option`s](error/multiple_error_types/option_result.md)
- [Defining an error type](error/multiple_error_types/define_error_type.md)
- [`Box`ing errors](error/multiple_error_types/boxing_errors.md)
- [Other uses of `?`](error/multiple_error_types/reenter_question_mark.md)
- [Wrapping errors](error/multiple_error_types/wrap_error.md)
- [Iterating over `Result`s](error/iter_result.md)
- [Std library types](std.md)
- [Box, stack and heap](std/box.md)
- [Vectors](std/vec.md)
- [Strings](std/str.md)
- [`Option`](std/option.md)
- [`Result`](std/result.md)
- [`?`](std/result/question_mark.md)
- [`panic!`](std/panic.md)
- [HashMap](std/hash.md)
- [Alternate/custom key types](std/hash/alt_key_types.md)
- [HashSet](std/hash/hashset.md)
- [Std misc](std_misc.md)
- [Threads](std_misc/threads.md)
- [Testcase: map-reduce](std_misc/threads/testcase_mapreduce.md)
- [Channels](std_misc/channels.md)
- [Path](std_misc/path.md)
- [File I/O](std_misc/file.md)
- [`open`](std_misc/file/open.md)
- [`create`](std_misc/file/create.md)
- [Child processes](std_misc/process.md)
- [Pipes](std_misc/process/pipe.md)
- [Wait](std_misc/process/wait.md)
- [Filesystem Operations](std_misc/fs.md)
- [Program arguments](std_misc/arg.md)
- [Argument parsing](std_misc/arg/matching.md)
- [Foreign Function Interface](std_misc/ffi.md)
- [Meta](meta.md)
- [Documentation](meta/doc.md)
- [Testing](meta/test.md)
- [Unsafe Operations](unsafe.md)

View File

@@ -1,19 +0,0 @@
# Summary
- [Overview](./overview.md)
- [Setting Up](./setting_up.md)
- [Core Client Library](./client.md)
- [Constructing a Basic Request](./basic_request.md)
- [Sending the Request](./send_basic.md)
- [Generating a Header File](./cbindgen.md)
- [Better Error Handling](./error_handling.md)
- [Asynchronous Operations](./async.md)
- [More Complex Requests](./complex_request.md)
- [Testing](./testing.md)
- [Dynamic Loading & Plugins](./dynamic_loading.md)
---
- [Break All The Things!!1!](./fun/index.md)
- [Problems](./fun/problems.md)
- [Solutions](./fun/solutions.md)

View File

@@ -1,130 +0,0 @@
# The Rust Programming Language
## Getting started
- [Introduction](ch01-00-introduction.md)
- [Installation](ch01-01-installation.md)
- [Hello, World!](ch01-02-hello-world.md)
- [Guessing Game Tutorial](ch02-00-guessing-game-tutorial.md)
- [Common Programming Concepts](ch03-00-common-programming-concepts.md)
- [Variables and Mutability](ch03-01-variables-and-mutability.md)
- [Data Types](ch03-02-data-types.md)
- [How Functions Work](ch03-03-how-functions-work.md)
- [Comments](ch03-04-comments.md)
- [Control Flow](ch03-05-control-flow.md)
- [Understanding Ownership](ch04-00-understanding-ownership.md)
- [What is Ownership?](ch04-01-what-is-ownership.md)
- [References & Borrowing](ch04-02-references-and-borrowing.md)
- [Slices](ch04-03-slices.md)
- [Using Structs to Structure Related Data](ch05-00-structs.md)
- [Defining and Instantiating Structs](ch05-01-defining-structs.md)
- [An Example Program Using Structs](ch05-02-example-structs.md)
- [Method Syntax](ch05-03-method-syntax.md)
- [Enums and Pattern Matching](ch06-00-enums.md)
- [Defining an Enum](ch06-01-defining-an-enum.md)
- [The `match` Control Flow Operator](ch06-02-match.md)
- [Concise Control Flow with `if let`](ch06-03-if-let.md)
## Basic Rust Literacy
- [Modules](ch07-00-modules.md)
- [`mod` and the Filesystem](ch07-01-mod-and-the-filesystem.md)
- [Controlling Visibility with `pub`](ch07-02-controlling-visibility-with-pub.md)
- [Referring to Names in Different Modules](ch07-03-importing-names-with-use.md)
- [Common Collections](ch08-00-common-collections.md)
- [Vectors](ch08-01-vectors.md)
- [Strings](ch08-02-strings.md)
- [Hash Maps](ch08-03-hash-maps.md)
- [Error Handling](ch09-00-error-handling.md)
- [Unrecoverable Errors with `panic!`](ch09-01-unrecoverable-errors-with-panic.md)
- [Recoverable Errors with `Result`](ch09-02-recoverable-errors-with-result.md)
- [To `panic!` or Not To `panic!`](ch09-03-to-panic-or-not-to-panic.md)
- [Generic Types, Traits, and Lifetimes](ch10-00-generics.md)
- [Generic Data Types](ch10-01-syntax.md)
- [Traits: Defining Shared Behavior](ch10-02-traits.md)
- [Validating References with Lifetimes](ch10-03-lifetime-syntax.md)
- [Testing](ch11-00-testing.md)
- [Writing tests](ch11-01-writing-tests.md)
- [Running tests](ch11-02-running-tests.md)
- [Test Organization](ch11-03-test-organization.md)
- [An I/O Project: Building a Command Line Program](ch12-00-an-io-project.md)
- [Accepting Command Line Arguments](ch12-01-accepting-command-line-arguments.md)
- [Reading a File](ch12-02-reading-a-file.md)
- [Refactoring to Improve Modularity and Error Handling](ch12-03-improving-error-handling-and-modularity.md)
- [Developing the Librarys Functionality with Test Driven Development](ch12-04-testing-the-librarys-functionality.md)
- [Working with Environment Variables](ch12-05-working-with-environment-variables.md)
- [Writing Error Messages to Standard Error Instead of Standard Output](ch12-06-writing-to-stderr-instead-of-stdout.md)
## Thinking in Rust
- [Functional Language Features: Iterators and Closures](ch13-00-functional-features.md)
- [Closures: Anonymous Functions that Can Capture Their Environment](ch13-01-closures.md)
- [Processing a Series of Items with Iterators](ch13-02-iterators.md)
- [Improving Our I/O Project](ch13-03-improving-our-io-project.md)
- [Comparing Performance: Loops vs. Iterators](ch13-04-performance.md)
- [More about Cargo and Crates.io](ch14-00-more-about-cargo.md)
- [Customizing Builds with Release Profiles](ch14-01-release-profiles.md)
- [Publishing a Crate to Crates.io](ch14-02-publishing-to-crates-io.md)
- [Cargo Workspaces](ch14-03-cargo-workspaces.md)
- [Installing Binaries from Crates.io with `cargo install`](ch14-04-installing-binaries.md)
- [Extending Cargo with Custom Commands](ch14-05-extending-cargo.md)
- [Smart Pointers](ch15-00-smart-pointers.md)
- [`Box<T>` Points to Data on the Heap and Has a Known Size](ch15-01-box.md)
- [The `Deref` Trait Allows Access to the Data Through a Reference](ch15-02-deref.md)
- [The `Drop` Trait Runs Code on Cleanup](ch15-03-drop.md)
- [`Rc<T>`, the Reference Counted Smart Pointer](ch15-04-rc.md)
- [`RefCell<T>` and the Interior Mutability Pattern](ch15-05-interior-mutability.md)
- [Creating Reference Cycles and Leaking Memory is Safe](ch15-06-reference-cycles.md)
- [Fearless Concurrency](ch16-00-concurrency.md)
- [Threads](ch16-01-threads.md)
- [Message Passing](ch16-02-message-passing.md)
- [Shared State](ch16-03-shared-state.md)
- [Extensible Concurrency: `Sync` and `Send`](ch16-04-extensible-concurrency-sync-and-send.md)
- [Is Rust an Object-Oriented Programming Language?](ch17-00-oop.md)
- [What Does Object-Oriented Mean?](ch17-01-what-is-oo.md)
- [Trait Objects for Using Values of Different Types](ch17-02-trait-objects.md)
- [Object-Oriented Design Pattern Implementations](ch17-03-oo-design-patterns.md)
## Advanced Topics
- [Patterns Match the Structure of Values](ch18-00-patterns.md)
- [All the Places Patterns May be Used](ch18-01-all-the-places-for-patterns.md)
- [Refutability: Whether a Pattern Might Fail to Match](ch18-02-refutability.md)
- [All the Pattern Syntax](ch18-03-pattern-syntax.md)
- [Advanced Features](ch19-00-advanced-features.md)
- [Unsafe Rust](ch19-01-unsafe-rust.md)
- [Advanced Lifetimes](ch19-02-advanced-lifetimes.md)
- [Advanced Traits](ch19-03-advanced-traits.md)
- [Advanced Types](ch19-04-advanced-types.md)
- [Advanced Functions & Closures](ch19-05-advanced-functions-and-closures.md)
- [Final Project: Building a Multithreaded Web Server](ch20-00-final-project-a-web-server.md)
- [A Single Threaded Web Server](ch20-01-single-threaded.md)
- [How Slow Requests Affect Throughput](ch20-02-slow-requests.md)
- [Designing the Thread Pool Interface](ch20-03-designing-the-interface.md)
- [Creating the Thread Pool and Storing Threads](ch20-04-storing-threads.md)
- [Sending Requests to Threads Via Channels](ch20-05-sending-requests-via-channels.md)
- [Graceful Shutdown and Cleanup](ch20-06-graceful-shutdown-and-cleanup.md)
- [Appendix](appendix-00.md)
- [A - Keywords](appendix-01-keywords.md)
- [B - Operators and Symbols](appendix-02-operators.md)
- [C - Derivable Traits](appendix-03-derivable-traits.md)
- [D - Macros](appendix-04-macros.md)
- [E - Translations](appendix-05-translation.md)
- [F - Newest Features](appendix-06-newest-features.md)

View File

@@ -1,47 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::MDBook;
#[test]
fn mdbook_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
let result = md.test(vec![]);
assert!(
result.is_ok(),
"Tests failed with {}",
result.err().unwrap()
);
}
#[test]
fn mdbook_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test(vec![]).is_err());
}
#[test]
fn mdbook_test_chapter() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
let result = md.test_chapter(vec![], Some("Introduction"));
assert!(
result.is_ok(),
"test_chapter failed with {}",
result.err().unwrap()
);
}
#[test]
fn mdbook_test_chapter_not_found() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test_chapter(vec![], Some("Bogus Chapter Name")).is_err());
}

43
tests/testsuite/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Testsuite
## Introduction
This is the main testsuite for exercising all functionality of mdBook.
Tests should be organized into modules based around major features. Tests should use `BookTest` to drive the test. `BookTest` will set up a temp directory, and provides a variety of methods to help create a build books.
## Basic structure of a test
Using `BookTest`, you typically use it to copy a directory into a temp directory, and then run mdbook commands in that temp directory. You can run the `mdbook` executable, or use the mdbook API to perform whatever tasks you need. Running the executable has the benefit of being able to validate the console output.
See `build::basic_build` for a simple test example. I recommend reviewing the methods on `BookTest` to learn more, and reviewing some of the existing tests to get a feel for how they are structured.
For example, let's say you are creating a new theme test. In the `testsuite/theme` directory, create a new directory with the book source that you want to exercise. At a minimum, this needs a `src/SUMMARY.md`, but often you'll also want `book.toml`. Then, in `testsuite/theme.rs`, add a test with `BookTest::from_dir("theme/mytest")`, and then use the methods to perform whatever actions you want.
`BookTest` is designed to be able to chain a series of actions. For example, you can do something like:
```rust
BookTest::from_dir("theme/mytest")
.build()
.check_main_file("book/index.html", str![["file contents"]])
.change_file("src/index.md", "new contents")
.build()
.check_main_file("book/index.html", str![["new contents"]]);
```
## Snapbox
The testsuite uses [`snapbox`] to drive most of the tests. This library provides the ability to compare strings using a variety of methods. These strings are written in the source code using either the [`str!`] or [`file!`] macros.
The magic is that you can set the `SNAPSHOTS=overwrite` environment variable, and snapbox will automatically update the strings contents of `str!`, or the file contents of `file!`. This makes it easier to update tests. Snapbox provides nice diffing output, and quite a few other features.
Expected contents can have wildcards like `...` (matches any lines) or `[..]` (matches any characters on a line). See [snapbox filters] for more info and other filters.
Typically when writing a test, I'll just start with an empty `str!` or `file!`, and let snapbox fill it in. Then I review the contents to make sure they are what I expect.
Note that there is some normalization applied to the strings. See `book_test::assert` for how some of these normalizations happen.
[`snapbox`]: https://docs.rs/snapbox/latest/snapbox/
[`str!`]: https://docs.rs/snapbox/latest/snapbox/macro.str.html
[`file!`]: https://docs.rs/snapbox/latest/snapbox/macro.file.html
[snapbox filters]: https://docs.rs/snapbox/latest/snapbox/assert/struct.Assert.html#method.eq

View File

@@ -0,0 +1,442 @@
//! Utility for building and running tests against mdbook.
use mdbook::book::BookBuilder;
use mdbook::MDBook;
use snapbox::IntoData;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
/// Test number used for generating unique temp directory names.
static NEXT_TEST_ID: AtomicU32 = AtomicU32::new(0);
#[derive(Clone, Copy, Eq, PartialEq)]
enum StatusCode {
Success,
Failure,
Code(i32),
}
/// Main helper for driving mdbook tests.
pub struct BookTest {
/// The temp directory where the test should perform its work.
pub dir: PathBuf,
assert: snapbox::Assert,
/// This indicates whether or not the book has been built.
built: bool,
}
impl BookTest {
/// Creates a new test, copying the contents from the given directory into
/// a temp directory.
pub fn from_dir(dir: &str) -> BookTest {
// Copy this test book to a temp directory.
let dir = Path::new("tests/testsuite").join(dir);
assert!(dir.exists(), "{dir:?} should exist");
let tmp = Self::new_tmp();
mdbook::utils::fs::copy_files_except_ext(
&dir,
&tmp,
true,
Some(&PathBuf::from("book")),
&[],
)
.unwrap_or_else(|e| panic!("failed to copy test book {dir:?} to {tmp:?}: {e:?}"));
Self::new(tmp)
}
/// Creates a new test with an empty temp directory.
pub fn empty() -> BookTest {
Self::new(Self::new_tmp())
}
/// Creates a new test with the given function to initialize a new book.
///
/// The book itself is not built.
pub fn init(f: impl Fn(&mut BookBuilder)) -> BookTest {
let tmp = Self::new_tmp();
let mut bb = MDBook::init(&tmp);
f(&mut bb);
bb.build()
.unwrap_or_else(|e| panic!("failed to initialize book at {tmp:?}: {e:?}"));
Self::new(tmp)
}
fn new_tmp() -> PathBuf {
let id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst);
let tmp = Path::new(env!("CARGO_TARGET_TMPDIR"))
.join("ts")
.join(format!("t{id}"));
if tmp.exists() {
std::fs::remove_dir_all(&tmp)
.unwrap_or_else(|e| panic!("failed to remove {tmp:?}: {e:?}"));
}
std::fs::create_dir_all(&tmp).unwrap_or_else(|e| panic!("failed to create {tmp:?}: {e:?}"));
tmp
}
fn new(dir: PathBuf) -> BookTest {
let assert = assert(&dir);
BookTest {
dir,
assert,
built: false,
}
}
/// Checks the contents of an HTML file that it has the given contents
/// between the `<main>` tag.
///
/// Normally the contents outside of the `<main>` tag aren't interesting,
/// and they add a significant amount of noise.
pub fn check_main_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
if !self.built {
self.build();
}
let full_path = self.dir.join(path);
let actual = read_to_string(&full_path);
let start = actual
.find("<main>")
.unwrap_or_else(|| panic!("didn't find <main> in:\n{actual}"));
let end = actual.find("</main>").unwrap();
let contents = actual[start + 6..end - 7].trim();
self.assert.eq(contents, expected);
self
}
/// Checks the summary contents of `toc.js` against the expected value.
pub fn check_toc_js(&mut self, expected: impl IntoData) -> &mut Self {
if !self.built {
self.build();
}
let inner = self.toc_js_html();
// Would be nice if this were prettified, but a primitive wrapping will do for now.
let inner = inner.replace("><", ">\n<");
self.assert.eq(inner, expected);
self
}
/// Returns the summary contents from `toc.js`.
pub fn toc_js_html(&self) -> String {
let full_path = self.dir.join("book/toc.js");
let actual = read_to_string(&full_path);
let inner = actual
.lines()
.filter_map(|line| {
let line = line.trim().strip_prefix("this.innerHTML = '")?;
let line = line.strip_suffix("';")?;
Some(line)
})
.next()
.expect("should have innerHTML");
inner.to_string()
}
/// Checks that the contents of the given file matches the expected value.
pub fn check_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
if !self.built {
self.build();
}
let path = self.dir.join(path);
let actual = read_to_string(&path);
self.assert.eq(actual, expected);
self
}
/// Checks that the given file contains the given string somewhere.
pub fn check_file_contains(&mut self, path: &str, expected: &str) -> &mut Self {
if !self.built {
self.build();
}
let path = self.dir.join(path);
let actual = read_to_string(&path);
assert!(
actual.contains(expected),
"Did not find {expected:?} in {path:?}\n\n{actual}",
);
self
}
/// Checks that the given file does not contain the given string anywhere.
///
/// Beware that using this is fragile, as it may be unable to catch
/// regressions (it can't tell the difference between success, or the
/// string being looked for changed).
pub fn check_file_doesnt_contain(&mut self, path: &str, string: &str) -> &mut Self {
if !self.built {
self.build();
}
let path = self.dir.join(path);
let actual = read_to_string(&path);
assert!(
!actual.contains(string),
"Unexpectedly found {string:?} in {path:?}\n\n{actual}",
);
self
}
/// Checks that the list of files at the given path matches the given value.
pub fn check_file_list(&mut self, path: &str, expected: impl IntoData) -> &mut Self {
let mut all_paths: Vec<_> = walkdir::WalkDir::new(&self.dir.join(path))
.into_iter()
// Skip the outer directory.
.skip(1)
.map(|e| {
e.unwrap()
.into_path()
.strip_prefix(&self.dir)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/")
})
.collect();
all_paths.sort();
let actual = all_paths.join("\n");
self.assert.eq(actual, expected);
self
}
/// Loads an [`MDBook`] from the temp directory.
pub fn load_book(&self) -> MDBook {
MDBook::load(&self.dir).unwrap_or_else(|e| panic!("book failed to load: {e:?}"))
}
/// Builds the book in the temp directory.
pub fn build(&mut self) -> &mut Self {
let book = self.load_book();
book.build()
.unwrap_or_else(|e| panic!("book failed to build: {e:?}"));
self.built = true;
self
}
/// Runs the `mdbook` binary in the temp directory.
///
/// This runs `mdbook` with the given args. The args are split on spaces
/// (if you need args with spaces, use the `args` method). The given
/// callback receives a [`BookCommand`] for you to customize how the
/// executable is run.
pub fn run(&mut self, args: &str, f: impl Fn(&mut BookCommand)) -> &mut Self {
let mut cmd = BookCommand {
assert: self.assert.clone(),
dir: self.dir.clone(),
args: split_args(args),
env: BTreeMap::new(),
expect_status: StatusCode::Success,
expect_stderr_data: None,
expect_stdout_data: None,
};
f(&mut cmd);
cmd.run();
self
}
/// Change a file's contents in the given path.
pub fn change_file(&mut self, path: impl AsRef<Path>, body: &str) -> &mut Self {
let path = self.dir.join(path);
std::fs::write(&path, body).unwrap_or_else(|e| panic!("failed to write {path:?}: {e:?}"));
self
}
/// Builds a Rust program with the given src.
///
/// The given path should be the path where to output the executable in
/// the temp directory.
pub fn rust_program(&mut self, path: &str, src: &str) -> &mut Self {
let rs = self.dir.join(path).with_extension("rs");
let parent = rs.parent().unwrap();
if !parent.exists() {
std::fs::create_dir_all(&parent).unwrap();
}
std::fs::write(&rs, src).unwrap_or_else(|e| panic!("failed to write {rs:?}: {e:?}"));
let status = std::process::Command::new("rustc")
.arg(&rs)
.current_dir(&parent)
.status()
.expect("rustc should run");
assert!(status.success());
self
}
}
/// A builder for preparing to run the `mdbook` executable.
///
/// By default, it expects the process to succeed.
pub struct BookCommand {
pub dir: PathBuf,
assert: snapbox::Assert,
args: Vec<String>,
env: BTreeMap<String, Option<String>>,
expect_status: StatusCode,
expect_stderr_data: Option<snapbox::Data>,
expect_stdout_data: Option<snapbox::Data>,
}
impl BookCommand {
/// Indicates that the process should fail.
pub fn expect_failure(&mut self) -> &mut Self {
self.expect_status = StatusCode::Failure;
self
}
/// Indicates the process should fail with the given exit code.
pub fn expect_code(&mut self, code: i32) -> &mut Self {
self.expect_status = StatusCode::Code(code);
self
}
/// Verifies that stderr matches the given value.
pub fn expect_stderr(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
self.expect_stderr_data = Some(expected.into_data());
self
}
/// Verifies that stdout matches the given value.
pub fn expect_stdout(&mut self, expected: impl snapbox::IntoData) -> &mut Self {
self.expect_stdout_data = Some(expected.into_data());
self
}
/// Adds arguments to the command to run.
pub fn args(&mut self, args: &[&str]) -> &mut Self {
self.args.extend(args.into_iter().map(|t| t.to_string()));
self
}
/// Specifies an environment variable to set on the executable.
pub fn env<T: Into<String>>(&mut self, key: &str, value: T) -> &mut Self {
self.env.insert(key.to_string(), Some(value.into()));
self
}
/// Runs the command, and verifies the output.
fn run(&mut self) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mdbook"));
cmd.current_dir(&self.dir)
.args(&self.args)
.env_remove("RUST_LOG")
// Don't read the system git config which is out of our control.
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", &self.dir)
.env("GIT_CONFIG_SYSTEM", &self.dir)
.env_remove("GIT_AUTHOR_EMAIL")
.env_remove("GIT_AUTHOR_NAME")
.env_remove("GIT_COMMITTER_EMAIL")
.env_remove("GIT_COMMITTER_NAME");
for (k, v) in &self.env {
match v {
Some(v) => cmd.env(k, v),
None => cmd.env_remove(k),
};
}
let output = cmd.output().expect("mdbook should be runnable");
let stdout = std::str::from_utf8(&output.stdout).expect("stdout is not utf8");
let stderr = std::str::from_utf8(&output.stderr).expect("stderr is not utf8");
let render_output = || format!("\n--- stdout\n{stdout}\n--- stderr\n{stderr}");
match (self.expect_status, output.status.success()) {
(StatusCode::Success, false) => {
panic!("mdbook failed, but expected success{}", render_output())
}
(StatusCode::Failure, true) => {
panic!("mdbook succeeded, but expected failure{}", render_output())
}
(StatusCode::Code(expected), _) => match output.status.code() {
Some(actual) => assert_eq!(
actual, expected,
"process exit code did not match as expected"
),
None => panic!("process exited via signal {:?}", output.status),
},
_ => {}
}
self.expect_status = StatusCode::Success; // Reset to default.
if let Some(expect_stderr_data) = &self.expect_stderr_data {
if let Err(e) = self.assert.try_eq(
Some(&"stderr"),
stderr.into_data(),
expect_stderr_data.clone(),
) {
panic!("{e}");
}
}
if let Some(expect_stdout_data) = &self.expect_stdout_data {
if let Err(e) = self.assert.try_eq(
Some(&"stdout"),
stdout.into_data(),
expect_stdout_data.clone(),
) {
panic!("{e}");
}
}
}
}
fn split_args(s: &str) -> Vec<String> {
s.split_whitespace()
.map(|arg| {
if arg.contains(&['"', '\''][..]) {
panic!("shell-style argument parsing is not supported");
}
String::from(arg)
})
.collect()
}
static LITERAL_REDACTIONS: &[(&str, &str)] = &[
// Unix message for an entity was not found
("[NOT_FOUND]", "No such file or directory (os error 2)"),
// Windows message for an entity was not found
(
"[NOT_FOUND]",
"The system cannot find the file specified. (os error 2)",
),
(
"[NOT_FOUND]",
"The system cannot find the path specified. (os error 3)",
),
("[NOT_FOUND]", "program not found"),
// Unix message for exit status
("[EXIT_STATUS]", "exit status"),
// Windows message for exit status
("[EXIT_STATUS]", "exit code"),
("[TAB]", "\t"),
("[EXE]", std::env::consts::EXE_SUFFIX),
];
/// This makes it easier to write regex replacements that are guaranteed to only
/// get compiled once
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
fn assert(root: &Path) -> snapbox::Assert {
let mut subs = snapbox::Redactions::new();
subs.insert("[ROOT]", root.to_path_buf()).unwrap();
subs.insert(
"[TIMESTAMP]",
regex!(r"(?m)(?<redacted>20\d\d-\d{2}-\d{2} \d{2}:\d{2}:\d{2})"),
)
.unwrap();
subs.insert("[VERSION]", mdbook::MDBOOK_VERSION).unwrap();
subs.extend(LITERAL_REDACTIONS.into_iter().cloned())
.unwrap();
snapbox::Assert::new()
.action_env(snapbox::assert::DEFAULT_ACTION_ENV)
.redact_with(subs)
}
/// Helper to read a string from the filesystem.
#[track_caller]
pub fn read_to_string<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref();
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("could not read file {path:?}: {e:?}"))
}

67
tests/testsuite/build.rs Normal file
View File

@@ -0,0 +1,67 @@
//! General build tests.
//!
//! More specific tests should usually go into a module based on the feature.
//! This module should just have general build tests, or misc small things.
use crate::prelude::*;
// Simple smoke test that building works.
#[test]
fn basic_build() {
BookTest::from_dir("build/basic_build").run("build", |cmd| {
cmd.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
"#]]);
});
}
// Ensure building fails if `create-missing` is false and one of the files does
// not exist.
#[test]
fn failure_on_missing_file() {
BookTest::from_dir("build/missing_file").run("build", |cmd| {
cmd.expect_failure().expect_stderr(str![[r#"
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Chapter file not found, ./chapter_1.md
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: [NOT_FOUND]
"#]]);
});
}
// Ensure a missing file is created if `create-missing` is true.
#[test]
fn create_missing() {
let test = BookTest::from_dir("build/create_missing");
assert!(test.dir.join("src/SUMMARY.md").exists());
assert!(!test.dir.join("src/chapter_1.md").exists());
test.load_book();
assert!(test.dir.join("src/chapter_1.md").exists());
}
// Checks that it fails if the summary has a reserved filename.
#[test]
fn no_reserved_filename() {
BookTest::from_dir("build/no_reserved_filename").run("build", |cmd| {
cmd.expect_failure().expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
[TIMESTAMP] [ERROR] (mdbook::utils): Error: Rendering failed
[TIMESTAMP] [ERROR] (mdbook::utils): [TAB]Caused By: print.md is reserved for internal use
"#]]);
});
}
// Build without book.toml should be OK.
#[test]
fn book_toml_isnt_required() {
let mut test = BookTest::init(|_| {});
std::fs::remove_file(test.dir.join("book.toml")).unwrap();
test.build();
test.check_main_file(
"book/chapter_1.html",
str![[r##"<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>"##]],
);
}

View File

@@ -0,0 +1,2 @@
[book]
title = "basic_build"

View File

@@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View File

@@ -0,0 +1,2 @@
[book]
title = "create_missing"

View File

@@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View File

@@ -0,0 +1,5 @@
[book]
title = "missing_file"
[build]
create-missing = false

View File

@@ -0,0 +1,3 @@
# Summary
- [Chapter 1](./chapter_1.md)

View File

@@ -0,0 +1,2 @@
[book]
title = "no_reserved_filename"

View File

@@ -0,0 +1,3 @@
# Summary
- [Print](print.md)

View File

@@ -0,0 +1 @@
# Print

36
tests/testsuite/cli.rs Normal file
View File

@@ -0,0 +1,36 @@
//! Basic tests for mdbook's CLI.
use crate::prelude::*;
use snapbox::file;
// Test with no args.
#[test]
#[cfg_attr(
not(all(feature = "watch", feature = "serve")),
ignore = "needs all features"
)]
fn no_args() {
BookTest::empty().run("", |cmd| {
cmd.expect_code(2)
.expect_stdout(str![[""]])
.expect_stderr(file!["cli/no_args.term.svg"]);
});
}
// Help command.
#[test]
#[cfg_attr(
not(all(feature = "watch", feature = "serve")),
ignore = "needs all features"
)]
fn help() {
BookTest::empty()
.run("help", |cmd| {
cmd.expect_stdout(file!["cli/help.term.svg"])
.expect_stderr(str![[""]]);
})
.run("--help", |cmd| {
cmd.expect_stdout(file!["cli/help.term.svg"])
.expect_stderr(str![[""]]);
});
}

View File

@@ -0,0 +1,63 @@
<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.container {
padding: 0 10px;
line-height: 18px;
}
tspan {
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: pre;
line-height: 18px;
}
</style>
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan>Creates a book from markdown files</tspan>
</tspan>
<tspan x="10px" y="46px">
</tspan>
<tspan x="10px" y="64px"><tspan>Usage: mdbook[EXE] [COMMAND]</tspan>
</tspan>
<tspan x="10px" y="82px">
</tspan>
<tspan x="10px" y="100px"><tspan>Commands:</tspan>
</tspan>
<tspan x="10px" y="118px"><tspan> init Creates the boilerplate structure and files for a new book</tspan>
</tspan>
<tspan x="10px" y="136px"><tspan> build Builds a book from its markdown files</tspan>
</tspan>
<tspan x="10px" y="154px"><tspan> test Tests that a book's Rust code samples compile</tspan>
</tspan>
<tspan x="10px" y="172px"><tspan> clean Deletes a built book</tspan>
</tspan>
<tspan x="10px" y="190px"><tspan> completions Generate shell completions for your shell to stdout</tspan>
</tspan>
<tspan x="10px" y="208px"><tspan> watch Watches a book's files and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="226px"><tspan> serve Serves a book at http://localhost:3000, and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="244px"><tspan> help Print this message or the help of the given subcommand(s)</tspan>
</tspan>
<tspan x="10px" y="262px">
</tspan>
<tspan x="10px" y="280px"><tspan>Options:</tspan>
</tspan>
<tspan x="10px" y="298px"><tspan> -h, --help Print help</tspan>
</tspan>
<tspan x="10px" y="316px"><tspan> -V, --version Print version</tspan>
</tspan>
<tspan x="10px" y="334px">
</tspan>
<tspan x="10px" y="352px"><tspan>For more information about a specific command, try `mdbook &lt;command&gt; --help`</tspan>
</tspan>
<tspan x="10px" y="370px"><tspan>The source code for mdBook is available at: https://github.com/rust-lang/mdBook</tspan>
</tspan>
<tspan x="10px" y="388px">
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,63 @@
<svg width="740px" height="398px" xmlns="http://www.w3.org/2000/svg">
<style>
.fg { fill: #AAAAAA }
.bg { background: #000000 }
.container {
padding: 0 10px;
line-height: 18px;
}
tspan {
font: 14px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
white-space: pre;
line-height: 18px;
}
</style>
<rect width="100%" height="100%" y="0" rx="4.5" class="bg" />
<text xml:space="preserve" class="container fg">
<tspan x="10px" y="28px"><tspan>Creates a book from markdown files</tspan>
</tspan>
<tspan x="10px" y="46px">
</tspan>
<tspan x="10px" y="64px"><tspan>Usage: mdbook[EXE] [COMMAND]</tspan>
</tspan>
<tspan x="10px" y="82px">
</tspan>
<tspan x="10px" y="100px"><tspan>Commands:</tspan>
</tspan>
<tspan x="10px" y="118px"><tspan> init Creates the boilerplate structure and files for a new book</tspan>
</tspan>
<tspan x="10px" y="136px"><tspan> build Builds a book from its markdown files</tspan>
</tspan>
<tspan x="10px" y="154px"><tspan> test Tests that a book's Rust code samples compile</tspan>
</tspan>
<tspan x="10px" y="172px"><tspan> clean Deletes a built book</tspan>
</tspan>
<tspan x="10px" y="190px"><tspan> completions Generate shell completions for your shell to stdout</tspan>
</tspan>
<tspan x="10px" y="208px"><tspan> watch Watches a book's files and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="226px"><tspan> serve Serves a book at http://localhost:3000, and rebuilds it on changes</tspan>
</tspan>
<tspan x="10px" y="244px"><tspan> help Print this message or the help of the given subcommand(s)</tspan>
</tspan>
<tspan x="10px" y="262px">
</tspan>
<tspan x="10px" y="280px"><tspan>Options:</tspan>
</tspan>
<tspan x="10px" y="298px"><tspan> -h, --help Print help</tspan>
</tspan>
<tspan x="10px" y="316px"><tspan> -V, --version Print version</tspan>
</tspan>
<tspan x="10px" y="334px">
</tspan>
<tspan x="10px" y="352px"><tspan>For more information about a specific command, try `mdbook &lt;command&gt; --help`</tspan>
</tspan>
<tspan x="10px" y="370px"><tspan>The source code for mdBook is available at: https://github.com/rust-lang/mdBook</tspan>
</tspan>
<tspan x="10px" y="388px">
</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

112
tests/testsuite/includes.rs Normal file
View File

@@ -0,0 +1,112 @@
//! Tests for include preprocessor.
use crate::prelude::*;
// Basic test for #include.
#[test]
fn include() {
BookTest::from_dir("includes/all_includes")
.check_main_file(
"book/includes.html",
str![[r##"
<h1 id="basic-includes"><a class="header" href="#basic-includes">Basic Includes</a></h1>
<h2 id="sample"><a class="header" href="#sample">Sample</a></h2>
<p>This is a sample include.</p>
"##]],
)
.check_main_file(
"book/relative/includes.html",
str![[r##"
<h1 id="relative-includes"><a class="header" href="#relative-includes">Relative Includes</a></h1>
<h2 id="sample"><a class="header" href="#sample">Sample</a></h2>
<p>This is a sample include.</p>
"##]],
);
}
// Checks for anchored includes.
#[test]
fn anchored_include() {
BookTest::from_dir("includes/all_includes").check_main_file(
"book/anchors.html",
str![[r##"
<h1 id="include-anchors"><a class="header" href="#include-anchors">Include Anchors</a></h1>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 1;
<span class="boring">}</span></code></pre></pre>
"##]],
);
}
// Checks behavior of recursive include.
#[test]
fn recursive_include() {
BookTest::from_dir("includes/all_includes")
.run("build", |cmd| {
cmd.expect_stderr(str![[r#"
[TIMESTAMP] [INFO] (mdbook::book): Book building has started
[TIMESTAMP] [ERROR] (mdbook::preprocess::links): Stack depth exceeded in recursive.md. Check for cyclic includes
[TIMESTAMP] [INFO] (mdbook::book): Running the html backend
"#]]);
})
.check_main_file(
"book/recursive.html",
str![[r#"
<p>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
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
Around the world, around the world</p>
"#]],
);
}
// Checks the behavior of `{{#playground}}` include.
#[test]
fn playground_include() {
BookTest::from_dir("includes/all_includes")
.check_main_file("book/playground.html",
str![[r##"
<h1 id="playground-includes"><a class="header" href="#playground-includes">Playground Includes</a></h1>
<pre><pre class="playground"><code class="language-rust">fn main() {
println!("Hello World!");
<span class="boring">
</span><span class="boring"> // You can even hide lines! :D
</span><span class="boring"> println!("I am hidden! Expand the code snippet to see me");
</span>}</code></pre></pre>
"##]]);
}
// Checks the behavior of `{{#rustdoc_include}}`.
#[test]
fn rustdoc_include() {
BookTest::from_dir("includes/all_includes")
.check_main_file("book/rustdoc.html",
str![[r##"
<h1 id="rustdoc-includes"><a class="header" href="#rustdoc-includes">Rustdoc Includes</a></h1>
<h2 id="rustdoc-include-adds-the-rest-of-the-file-as-hidden"><a class="header" href="#rustdoc-include-adds-the-rest-of-the-file-as-hidden">Rustdoc include adds the rest of the file as hidden</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">fn some_function() {
</span><span class="boring"> println!("some function");
</span><span class="boring">}
</span><span class="boring">
</span>fn main() {
some_function();
}</code></pre></pre>
<h2 id="rustdoc-include-works-with-anchors-too"><a class="header" href="#rustdoc-include-works-with-anchors-too">Rustdoc include works with anchors too</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">fn some_other_function() {
</span><span class="boring"> println!("unused anchor");
</span><span class="boring">}
</span><span class="boring">
</span>fn main() {
some_other_function();
}</code></pre></pre>
"##]]);
}

View File

@@ -0,0 +1,6 @@
[book]
authors = ["Eric Huss"]
language = "en"
multilingual = false
src = "src"
title = "all_includes"

View File

@@ -0,0 +1,8 @@
# Summary
- [Basic Includes](./includes.md)
- [Relative Includes](./relative/includes.md)
- [Recursive Includes](./recursive.md)
- [Include Anchors](./anchors.md)
- [Rustdoc Includes](./rustdoc.md)
- [Playground Includes](./playground.md)

Some files were not shown because too many files have changed in this diff Show More