Compare commits

...

650 Commits

Author SHA1 Message Date
Eric Huss
405f407260 Fix typo in changelog 2025-10-29 20:23:31 -07:00
Eric Huss
eaa778bebd Merge pull request #2908 from rust-lang/ehuss-patch-1
mdbook-compare: fix duplicate "diff" print
2025-10-30 02:51:01 +00:00
Eric Huss
a17c1d1b95 mdbook-compare: fix duplicate "diff" print
The "diff" arg is already in the args list.
2025-10-29 19:45:22 -07:00
Eric Huss
8a27d1b7ac Merge pull request #2904 from traviscross/TC/fix-ayu-comments
Remove italics from `ayu` quotes/comments for alignment
2025-10-28 19:50:07 +00:00
Eric Huss
d6cd50b601 Merge pull request #2907 from ehuss/search-feature
Expose "search" feature from mdbook-driver
2025-10-28 18:37:03 +00:00
Eric Huss
68d9bcfec4 Expose "search" feature from mdbook-driver
This allows users of mdbook-driver to easily enable the search feature.
2025-10-28 11:29:48 -07:00
Eric Huss
3fa49214ad Merge pull request #2905 from ehuss/fix-indented-code-block
Fix rust fenced code blocks with an indent
2025-10-28 01:44:41 +00:00
Eric Huss
ddf02e0c0c Fix rust fenced code blocks with an indent
This fixes a bug in the Rust code block partitioning that was
incorrectly removing the whitespace from the beginning of a code block.
2025-10-27 18:38:27 -07:00
Eric Huss
3992bc18f5 Add a test for a fenced code block with an indent 2025-10-27 18:35:39 -07:00
Travis Cross
49f9c9741e Remove italics from ayu quotes/comments for alignment
Comments in code examples often rely on exact column alignment,
e.g. for ASCII-art.  This alignment often relies on both code and
comment characters having exactly the same width.

Setting `font-style: italic` seems to break these invariants with
common monospace fonts used by browsers.  This may be due to font
synthesis when the monospace font does not have a native italic
variant.

E.g., see these code examples when using the `ayu` theme:

- https://doc.rust-lang.org/1.90.0/reference/types/closure.html#r-type.closure.drop-order
- https://doc.rust-lang.org/1.90.0/reference/types/impl-trait.html#r-type.impl-trait.generic-capture.precise.use

It seems more important to have correct alignment than to style these
elements in italics, so let's drop the italic styling.

One alternative would be to set `font-synthesis: none` instead.  This
would prevent font synthesis-related misalignment while still
rendering italics when a font supports italics natively.  This might
correct the alignment issue, but ASCII-art in comments often wants
vertical bars to actually be vertical, so it still seems better to
just turn off italics entirely.

A more minimal change might be to only drop this from comments and not
from `hljs-quote`, but it seems the styling for these classes are
usually kept in sync, so we preserve that here.
2025-10-27 20:26:04 +00:00
Eric Huss
f84b1a15b6 Merge pull request #2903 from ehuss/bump-version
Update to 0.5.0-beta.1
2025-10-26 20:02:56 +00:00
Eric Huss
ac11e00aa2 Update to 0.5.0-beta.1 2025-10-26 12:55:06 -07:00
Eric Huss
860e8d109e Merge pull request #2902 from ehuss/fix-missing-format
Fix error message for config.get deserialization error
2025-10-25 23:52:46 +00:00
Eric Huss
adcbd117da Fix error message for config.get deserialization error
The error message for a deserialization failure was missing a call to
`format!`.
2025-10-25 16:46:51 -07:00
Eric Huss
08d9fddfc9 Add a test for config.get deserialization error 2025-10-25 16:45:48 -07:00
Eric Huss
118c1096ea Merge pull request #2899 from ehuss/filtered-headings
Filter mark tags from sidebar heading nav
2025-10-22 00:24:37 +00:00
Eric Huss
18813516e1 Fix avoiding the mark header in the sidebar nav
This makes sure that the sidebar headings don't have the `<mark>` tag.
When these are created, the Marker is unable to remove them from the
sidebar (and we don't want them there in the first place).

I suspect we'll want more filtering in the future, but I'm not sure
exactly what to filter. Alternatively, it could have an allow list of
tags, and filter all others out.
2025-10-21 17:19:00 -07:00
Eric Huss
a6944683e6 Add a test that shows heading nav conflict with search mark
The search marker is getting copied into the sidebar, but it cannot be
dismissed.
2025-10-21 17:17:25 -07:00
Eric Huss
f27ae21825 Merge pull request #2898 from ehuss/on-this-page
Rework the look of the header navigation
2025-10-21 23:12:49 +00:00
Eric Huss
58af25384d Rework the look of the header navigation
This updates the header navigation so that:

- Added a colored bar to break it apart from the chapter navigation.
- Removed the colored circle and just use link color to make it
  look cleaner.
2025-10-21 16:06:17 -07:00
Eric Huss
51a80febb3 Merge pull request #2896 from rust-lang/update-dependencies
Update cargo dependencies
2025-10-21 01:22:50 +00:00
github-actions[bot]
7945ac8e8c Update cargo dependencies
```
name          old req compatible latest  new req
====          ======= ========== ======  =======
anyhow        1.0.98  1.0.100    1.0.100 1.0.100
axum          0.8.4   0.8.6      0.8.6   0.8.6
clap          4.5.41  4.5.50     4.5.50  4.5.50
clap_complete 4.5.55  4.5.59     4.5.59  4.5.59
indexmap      2.10.0  2.12.0     2.12.0  2.12.0
ignore        0.4.23  0.4.24     0.4.24  0.4.24
memchr        2.7.5   2.7.6      2.7.6   2.7.6
notify        8.1.0   8.2.0      8.2.0   8.2.0
opener        0.8.2   0.8.3      0.8.3   0.8.3
regex         1.11.1  1.12.2     1.12.2  1.12.2
semver        1.0.26  1.0.27     1.0.27  1.0.27
serde         1.0.219 1.0.228    1.0.228 1.0.228
serde_json    1.0.140 1.0.145    1.0.145 1.0.145
snapbox       0.6.21  0.6.22     0.6.22  0.6.22
tempfile      3.20.0  3.23.0     3.23.0  3.23.0
tokio         1.46.1  1.48.0     1.48.0  1.48.0
toml          0.9.2   0.9.8      0.9.8   0.9.8
```
2025-10-21 01:16:53 +00:00
Eric Huss
a097fe6232 Merge pull request #2891 from ehuss/divide-by-zero-heading-bug
Avoid divide-by-zero in heading nav computation
2025-10-21 00:52:17 +00:00
Eric Huss
4b5004b621 Avoid divide-by-zero in heading nav computation
This particular value can go to zero when the document height and the
window height are exactly the same value. This causes a NaN which causes
the "current" heading nav bug to not update properly. This clamps the
value to 1 to avoid that.
2025-10-20 17:43:34 -07:00
Eric Huss
7b7dee4a4e Merge pull request #2893 from ehuss/fix-fold-sub-chapter
Fix heading nav with folded chapters
2025-10-21 00:37:48 +00:00
Eric Huss
5282083dec Fix heading nav with folded chapters
This fixes an issue when folding is enabled. The folding was not
properly hiding the sub-chapters because it was assuming it could hide
the next list element. However, the heading nav was the next list
element, so the remaining chapters remained visible.

The solution required some deeper changes to how the chapters were
organized in the sidebar. Instead of nested chapters being a list
element *sibling*, the nested chapter's `ol` is now a *child* of its
parent chapter. This makes it much easier to just hide everything
without regard of the exact sibling order.

This required wrapping the chapter title and the toggle chevron inside a
span so that the flex layout could be localized to just those elements,
and allow the following `ol` elements to lay out regularly.

Closes https://github.com/rust-lang/mdBook/issues/2880
2025-10-20 17:31:40 -07:00
Eric Huss
816913bd72 Merge pull request #2892 from ehuss/heading-nav-debug-update
Improve the heading nav debug
2025-10-21 00:11:39 +00:00
Eric Huss
1620858032 Improve the heading nav debug
This updates the heading nav debug code with a few changes:

- Now enabled with the `mdbookEnableThresholdDebug` function.
- Adds a table with the relevant internal variables.
2025-10-20 17:05:44 -07:00
Eric Huss
4a07076896 Merge pull request #2890 from ehuss/remove-chrome-tabs
Remove tabs in chrome.css
2025-10-20 22:47:42 +00:00
Eric Huss
3a2705d742 Remove tabs in chrome.css
This causes awkwardness with some editors which want to replace tabs
with spaces.
2025-10-20 15:41:33 -07:00
Eric Huss
b0229e76a5 Merge pull request #2888 from ehuss/rustfmt-ignore-blame
Add more rustfmt commits to git blame ignore
2025-10-20 20:36:54 +00:00
Eric Huss
a43ef4ca4b Add more rustfmt commits to git blame ignore
These generally aren't interesting when blaming.
2025-10-20 13:31:03 -07:00
Eric Huss
780fd83cac Merge pull request #2886 from ehuss/simplify-runner
Simplify GUI runner
2025-10-15 21:11:50 +00:00
Eric Huss
d5406c8dff Simplify GUI runner
It didn't click that this was in the same project, and thus can access
the exe directly.
2025-10-15 14:05:23 -07:00
Eric Huss
5e7b6d7d9d Merge pull request #2885 from ehuss/multiple-gui-books
Support multiple books in the GUI tests
2025-10-15 14:09:15 +00:00
Eric Huss
07d2989486 Merge pull request #2884 from ehuss/fix-xtask-format
Fix incorrect string formatting in xtask error message
2025-10-15 14:04:36 +00:00
Eric Huss
5fe7e9531d Remove test_book
This is no longer used, as individual books have been added to support
different GUI tests. If there is anything here that we later decide we
need to keep for whatever reason, the needed content can be brought back
as new GUI test books. Otherwise, the guide and other books should cover
most things here.
2025-10-15 07:00:35 -07:00
Eric Huss
f958a0e8cf Add a syntax-highlighting GUI test
This adds a test specific to highlight.js syntax highlighting.
2025-10-15 07:00:35 -07:00
Eric Huss
dbad189b26 Add a gui test book for search
This isolates the search test with its own test book.
2025-10-15 07:00:35 -07:00
Eric Huss
4a06e067c5 Add a general-purpose summary GUI book
This adds the all-summary GUI test book which can be used for general
purpose tests that need a few pages to exercise all the different kinds
of items.
2025-10-15 07:00:35 -07:00
Eric Huss
98a093a0ff Move redirect gui tests to its own book 2025-10-15 07:00:35 -07:00
Eric Huss
9b5c57bf48 Add a basic book for GUI tests
This is the default book with a single chapter.
2025-10-15 07:00:35 -07:00
Eric Huss
87e9cc0ac3 Move heading-nav gui tests to a dedicated book 2025-10-15 07:00:35 -07:00
Eric Huss
e37e5314f8 Support multiple books in the GUI tests
This adds the ability to use multiple books for the GUI tests. This is
helpful since some tests need special configuration, and sharing the
same book can make it difficult or impossible to test different
configurations. It also makes it difficult to make changes to the
test_book since it can affect other tests.

This works by placing the books in the tests/gui/books directory. The
test runner will automatically build all the books. The gui tests can
then just access the DOC_PATH with the name of the book.

Books are now saved in a temp directory to make it easier to use the
DOC_PATH variable, instead of being tests/gui/books/book_name/book which
is a little awkward.

Following commits will restructure the existing book. This is just a
mechanical move.
2025-10-15 07:00:33 -07:00
Eric Huss
4913bf82f1 Fix incorrect string formatting in xtask error message 2025-10-15 06:57:39 -07:00
Eric Huss
4e41c844c3 Merge pull request #2876 from ehuss/xtask-bump
Add `cargo xtask bump`
2025-09-28 23:21:34 +00:00
Eric Huss
0ae202ea85 Add cargo xtask bump
This is a simple script to assist with bumping the version.
2025-09-28 16:14:41 -07:00
Eric Huss
71083144e8 Merge pull request #2875 from ehuss/0.5-alpha.1-changelog
Add 0.5 migration guide
2025-09-28 22:31:29 +00:00
Eric Huss
7eb63bf0e3 Merge pull request #2874 from ehuss/cmd-docs
Remove partially outdated CmdPreprocessor/CmdRenderer docs
2025-09-28 22:28:45 +00:00
Eric Huss
283b2798e4 Add 0.5 migration guide
This adds a migration guide for the breaking change in 0.5. This also
includes the changelog entries for 0.5-alpha.1.
2025-09-28 15:24:55 -07:00
Eric Huss
91842c3db6 Remove partially outdated CmdPreprocessor/CmdRenderer docs
These docs were slightly drifting from the user guide docs. Instead of
trying to maintain multiple copies of this, I have changed it so that
it just links out to the guide.

(The guide docs could be cleaned up a little, but that's a separate
issue.)
2025-09-28 15:22:49 -07:00
Eric Huss
fd88719b68 Merge pull request #2869 from rust-lang/renovate/notify-debouncer-mini-0.x
Update Rust crate notify-debouncer-mini to 0.7.0
2025-09-28 17:06:49 +00:00
renovate[bot]
c438e13027 Update Rust crate notify-debouncer-mini to 0.7.0 2025-09-28 10:00:01 -07:00
Eric Huss
8e9697bd8b Merge pull request #2870 from rust-lang/renovate/actions-checkout-5.x
Update actions/checkout action to v5
2025-09-28 16:46:48 +00:00
Eric Huss
dcf7ae45c2 Merge pull request #2872 from rust-lang/renovate/node-22.x
Update dependency node to v22
2025-09-28 16:43:35 +00:00
Eric Huss
38fa5b5ebc Merge pull request #2871 from rust-lang/renovate/actions-setup-node-5.x
Update actions/setup-node action to v5
2025-09-28 16:42:02 +00:00
Eric Huss
41262665e8 Merge pull request #2868 from rust-lang/renovate/browser-ui-test-0.x
Update dependency browser-ui-test to v0.22.2
2025-09-28 16:40:45 +00:00
Eric Huss
21dc3b0eb1 Merge pull request #2867 from ehuss/xtask-changelog
Add a script to help update the changelog
2025-09-28 16:19:00 +00:00
renovate[bot]
44cd371066 Update dependency node to v22 2025-09-28 16:17:55 +00:00
renovate[bot]
1c766160c3 Update actions/setup-node action to v5 2025-09-28 16:17:52 +00:00
renovate[bot]
2cb4093cc8 Update actions/checkout action to v5 2025-09-28 16:17:49 +00:00
renovate[bot]
dad941454e Update dependency browser-ui-test to v0.22.2 2025-09-28 16:17:11 +00:00
Eric Huss
2a93606727 Add a script to help update the changelog
This adds `cargo xtask changelog` to automatically add a new changelog
entry for a release.
2025-09-28 09:13:16 -07:00
Eric Huss
78819f4525 Merge pull request #2866 from ehuss/xtask-test
Add an xtask to help with running tests
2025-09-27 02:01:09 +00:00
Eric Huss
2c7d192b50 Add an xtask to help with running tests
During development I often need to run a bunch of tests. Instead of
having some unwieldy shell command, I have added this xtask to help with
running the testing commands.
2025-09-26 18:55:21 -07:00
Eric Huss
e15f80407d Merge pull request #2865 from ehuss/new-publish-workflow
Set up new workspace publish workflow
2025-09-27 00:46:02 +00:00
Eric Huss
4fc72e8d9f Set up new workspace publish workflow
This sets up the publish workflow to use the new OIDC authentication,
and to publish the whole workspace at once.
2025-09-26 17:40:34 -07:00
Eric Huss
b4c53b9e9c Merge pull request #2864 from rust-lang/renovate/cargo-semver-checks-0.x
Update cargo-semver-checks to v0.44.0
2025-09-26 21:33:50 +00:00
renovate[bot]
2011ddb479 Update cargo-semver-checks to v0.44.0 2025-09-26 17:48:43 +00:00
Eric Huss
4a28995641 Merge pull request #2858 from ehuss/add-semver-checks
Add cargo-semver-checks
2025-09-26 17:42:43 +00:00
Eric Huss
4c397a9be0 Merge pull request #2862 from ehuss/add-range-diff
Add triagebot range-diff feature
2025-09-25 00:07:51 +00:00
Eric Huss
f1b413444b Add triagebot range-diff feature
This adds a comment on a PR when the author rebases to have a link
to a better diff.
2025-09-24 17:00:52 -07:00
Eric Huss
cd1b54f41f Merge pull request #2861 from ehuss/add-update-dependencies
Add job to automatically update dependencies
2025-09-24 23:13:53 +00:00
Eric Huss
83c307be3c Add job to automatically update dependencies
This adds a job to automatically update cargo dependencies once a month.
I've added this script instead of using Renovate because I couldn't get
Renovate to update versions in `Cargo.toml`. I also wanted to batch
transitive dependency updates all in one PR.
2025-09-24 16:05:14 -07:00
Eric Huss
9ec49d978f Merge pull request #2860 from ehuss/add-renovate
Add Renovate configuration
2025-09-24 23:05:00 +00:00
Eric Huss
7dee816838 Add Renovate configuration
This adds a configuration to enable Renovate to do some basic automated
updates.
2025-09-24 15:55:16 -07:00
Eric Huss
5b79ed4144 Add cargo-semver-checks
This adds cargo-semver-checks to CI to help catch any unintended
breaking changes to the API.
2025-09-20 18:19:48 -07:00
Eric Huss
aa96e1174e Merge pull request #2857 from ehuss/move-copy-theme
Move theme copy to the Theme type and reduce visibility
2025-09-21 01:02:42 +00:00
Eric Huss
7fcacf3386 Move theme copy to the Theme type and reduce visibility
This moves the code for copying the theme to the theme directory to the
Theme type so that the code lives closer to the data definition. This
also then reduces the public API surface of the Theme to give a little
more flexibility for updating it in the future.
2025-09-20 17:55:12 -07:00
Eric Huss
2aa2b95f0f Merge pull request #2856 from ehuss/fs-update
Clean up some fs-related utilities
2025-09-21 00:18:51 +00:00
Eric Huss
797112ef36 Clean up some fs-related utilities
This does a little cleanup around the usage of filesystem functions:

- Add `mdbook_core::utils::fs::read_to_string` as a wrapper around
  `std::fs::read_to_string` to provide better error messages. Use
  this wherever a file is read.
- Add `mdbook_core::utils::fs::create_dir_all` as a wrapper around
  `std::fs::create_dir_all` to provide better error messages. Use
  this wherever a file is read.
- Replace `mdbook_core::utils::fs::write_file` with `write` to mirror
  the `std::fs::write` API.
- Remove `mdbook_core::utils::fs::create_file`. It was generally not
  used anymore.
- Scrub the usage of `std::fs` to use the new wrappers. This doesn't
  remove it 100%, but it is now significantly reduced.
2025-09-20 17:13:31 -07:00
Eric Huss
f24221a1d7 Merge pull request #2855 from ehuss/move-get_404_output_file
Move get_404_output_file to HtmlConfig
2025-09-20 01:18:12 +00:00
Eric Huss
6223189b95 Move get_404_output_file to HtmlConfig
This function was essentially only operating on data from HtmlConfig. It
wasn't really a "filesystem" function. So this moves it to be more
logically associated with the data it works on.
2025-09-19 18:11:29 -07:00
Eric Huss
73aeed48d7 Merge pull request #2854 from ehuss/move-take-lines
Move take_lines functions to mdbook-driver and make private
2025-09-20 01:07:23 +00:00
Eric Huss
09c3e542da Move take_lines functions to mdbook-driver and make private
These functions are only used by the links preprocessor. I'm moving
these functions to put them closer to the code that they are associated
with, and to reduce the public API surface.
2025-09-19 18:01:33 -07:00
Eric Huss
9fae3c88eb Merge pull request #2853 from ehuss/markdown-outdated-comment
Remove outdated comment in mdbook-markdown
2025-09-20 00:53:05 +00:00
Eric Huss
8159dea9ea Remove outdated comment in mdbook-markdown
I missed this in https://github.com/rust-lang/mdBook/pull/2844.
2025-09-19 17:46:08 -07:00
Eric Huss
e5386f9230 Merge pull request #2852 from ehuss/add-def-list-cfg-link
Add link to `output.html.definition-lists`
2025-09-19 03:26:17 +00:00
Eric Huss
5fafc3f1f6 Add link to output.html.definition-lists
I forgot to include this link in the description of definition lists.
2025-09-18 20:20:25 -07:00
Eric Huss
39a148fc8d Merge pull request #2851 from ehuss/admonitions
Add support for admonitions
2025-09-19 03:00:27 +00:00
Eric Huss
873e4fe40f Add support for admonitions
This enables the admonitions support from pulldown-cmark. This includes
a config option in case it causes problems with existing books.

I would like to make this extensible in the future, though I'm not sure
what that would look like. There's also some concerns with how this will
affect translations like mdbook-i18n-helpers, which we may need to work
out in a different way.

Closes https://github.com/rust-lang/mdBook/issues/2771
2025-09-18 19:54:20 -07:00
Eric Huss
604d4dd78a Merge pull request #2850 from ehuss/fix-nojs-css-vars
Fix missing css vars for no-js dark mode
2025-09-19 01:59:48 +00:00
Eric Huss
ddeb3ce54f Fix missing css vars for no-js dark mode
These various were inadvertently missing from the no-js dark mode.
2025-09-18 18:53:46 -07:00
Eric Huss
15958773d5 Merge pull request #2847 from ehuss/definition-list
Add support for definition lists
2025-09-17 23:50:16 +00:00
Eric Huss
ba4c3ed873 Add support for definition lists
This enables the definition lists support from pulldown-cmark.
This includes a config option in case it causes problems with existing
books.

Closes https://github.com/rust-lang/mdBook/issues/2770
2025-09-17 16:44:45 -07:00
Eric Huss
53d39a8654 Merge pull request #2846 from ehuss/fix-unique-id-loop
Fix ID collisions when the numeric suffix gets used
2025-09-17 21:42:08 +00:00
Eric Huss
f1731329e1 Fix ID collisions when the numeric suffix gets used
This fixes a collision with the ID generation where it a previous entry
could generate a unique ID like "foo-1", but then a header with the text
"Foo 1" would collide with it. This fixes it so that when generating the
ID for "Foo 1", it will loop unit it finds an ID that doesn't collide
(in this case, `foo-1-1`).
2025-09-17 14:36:16 -07:00
Eric Huss
51d7998ba4 Merge pull request #2845 from ehuss/script-in-block
Fix raw status ending in the HTML tokenizer
2025-09-17 21:27:14 +00:00
Eric Huss
d27a2bdd1d Fix raw status ending in the HTML tokenizer
This fixes a small mistake where the "raw" status wasn't being reset
once exiting the script or style tags. That means any text nodes that
followed would be misinterpreted as being raw.
2025-09-17 14:21:01 -07:00
Eric Huss
cd3e26fb90 Add a test for a script inside an HTML block 2025-09-17 14:19:38 -07:00
Eric Huss
1c034bdd9a Merge pull request #2844 from ehuss/html-tokenize
Add a new HTML rendering pipeline
2025-09-17 03:36:21 +00:00
Eric Huss
2b242494b0 Add a new HTML rendering pipeline
This rewrites the HTML rendering pipeline to use a tree data structure,
and implements a custom HTML serializer. The intent is to make it easier
to make changes and to manipulate the output. This should make some
future changes much easier.

This is a large change, but I'll try to briefly summarize what's
changing:

- All of the HTML rendering support has been moved out of
  mdbook-markdown into mdbook-html. For now, all of the API surface is
  private, though we may consider ways to safely expose it in the
  future.
- Instead of using pulldown-cmark's html serializer, this takes the
  pulldown-cmark events and translates them into a tree data structure
  (using the ego-tree crate to define the tree). See `tree.rs`.
- HTML in the markdown document is parsed using html5ever, and then
  lives inside the same tree data structure. See `tokenizer.rs`.
- Transformations are then applied to the tree data structure. For
  example, adding header links or hiding code lines.
- Serialization is a simple process of writing out the nodes to a
  string. See `serialize.rs`.
- The search indexer works on the tree structure instead of re-rendering
  every chapter twice. See `html_handlebars/search.rs`.
- The print page now takes a very different approach of taking the
  same tree structure built for rendering the chapters, and applies
  transformations to it. This avoid re-parsing everything again. See
  `print.rs`.
    - I changed the linking behavior so that links on the print page
      link to items on the print page instead of outside the print page.
- There are a variety of small changes to how it serializes as can be
  seen in the changes to the tests. Some highlights:
	- Code blocks no longer have a second layer of `<pre>` tags wrapping
      it.
    - Fixed a minor issue where a rust code block with a specific
      edition was having the wrong classes when there was a default
      edition.
- Drops the ammonia dependency, which significantly reduces the number
  of dependencies. It was only being used for a very minor task, and
  we can handle it much more easily now.
- Drops `pretty_assertions`, they are no longer used (mostly being
  migrated to the testsuite).

There's obviously a lot of risk trying to parse everything to such a low
level, but I think the benefits are worth it. Also, the API isn't super
ergonomic compared to say javascript (there are no selectors), but it
works well enough so far.

I have not run this through rigorous benchmarking, but it does have a
very noticeable performance improvement, especially in a debug build.

I expect in the future that we'll want to expose some kind of
integration with extensions so they have access to this tree structure
(or some kind of tree structure).

Closes https://github.com/rust-lang/mdBook/issues/1736
2025-09-16 20:26:35 -07:00
Eric Huss
d4763d2c90 Merge pull request #2843 from ehuss/new-html-tests
Add more comprehensive tests for HTML rendering
2025-09-16 21:14:58 +00:00
Eric Huss
03443f723c Add more comprehensive tests for HTML rendering
This adds a bunch of tests to better exercise the HTML rendering and to
be able to track any changes in its behavior.

This includes a new `check_all_main_files` to more conveniently check
the HTML content of every chapter in a book.
2025-09-16 14:07:54 -07:00
Eric Huss
c3f4b8114a Merge pull request #2842 from ehuss/mdbook-compare
Add a basic utility to compare different versions of mdbook
2025-09-16 21:05:18 +00:00
Eric Huss
21e8c08827 Add a basic utility to compare different versions of mdbook
This is a very simplistic utility to compare the output of different
versions of mdbook. This is useful when making changes to compare
real-world books to see what changes actually happen.

This is a variation of scripts that I have been using for a few years.
This could definitely use some improvements, but this seems like it
could be useful as-is.
2025-09-16 13:58:20 -07:00
Eric Huss
de155f859b Merge pull request #2841 from ehuss/fix-missing-a
Fix broken a tag
2025-09-16 03:24:38 +00:00
Eric Huss
737090abf1 Fix broken a tag
This fixes the missing close `</a>` tag for the "previous" button.
2025-09-15 20:17:11 -07:00
Eric Huss
fb4fa867d1 Merge pull request #2840 from ehuss/partition-source
Rewrite partition_source to return slices
2025-09-16 01:50:14 +00:00
Eric Huss
c606a010c7 Rewrite partition_source to return slices
This changes partition_source so that instead of allocating new strings,
it just returns slices into the original string. It probably doesn't
make a big difference perf-wise, but I felt more comfortable with this,
and also felt it was a little easier to understand exactly what it was
doing.

This is generally equivalent except for the possibility of not having a
newline at the end. In practice that doesn't matter because markdown
code blocks always have a newline. However, to be defensive, the caller
will check for this.
2025-09-15 18:42:43 -07:00
Eric Huss
09f05e8a86 Add a test for partition_source 2025-09-15 18:08:25 -07:00
Eric Huss
1daa650d61 Merge pull request #2839 from ehuss/to_url_path
Add ToUrlPath helper trait
2025-09-15 14:50:47 +00:00
Eric Huss
2474ae799b Add ToUrlPath helper trait
This adds the `ToUrlPath` helper trait to convert a Path to a path
suitable for use in HTML (replacing `normalize_path`).

This also fixes a minor bug where on Windows the next/prev links were
using a double forward slash. I don't think this is possible, since
chapter links are derived from the summary, but I'm noting just in case.
It's also not too much of an issue since double slashes are normally
just treated as a single.
2025-09-15 07:44:10 -07:00
Eric Huss
f393e22896 Merge pull request #2838 from ehuss/chapters-iterator
Add an iterator over chapters
2025-09-15 14:18:39 +00:00
Eric Huss
3629e2c051 Add an iterator over chapters
This adds the `Book::chapters` iterator (and `for_each_chapter_mut`) to
iterate over non-draft chapters. This is a common pattern I keep
encountering, and I figure it might simplify things. It runs a little
risk that callers may not be properly handling every item type, but I
think it should be ok.
2025-09-15 07:11:19 -07:00
Eric Huss
e7b15274b5 Merge pull request #2837 from ehuss/remove-chrono
Remove chrono
2025-09-15 13:58:28 +00:00
Eric Huss
d15a40123c Remove chrono
I accidentally missed this in https://github.com/rust-lang/mdBook/pull/2829.
2025-09-15 06:50:50 -07:00
Eric Huss
166a972e9a Merge pull request #2833 from ehuss/static-regex
Add a helper for defining a regex
2025-09-12 13:57:30 +00:00
Eric Huss
f2db034587 Merge pull request #2832 from ehuss/remove-clap-macro_use
Remove clap macro_use
2025-09-12 13:55:51 +00:00
Eric Huss
a4140ba535 Remove clap macro_use
This removes the macro_use for clap just because I'm not a big fan of
glob-style imports like this. I think being a little more explicit here
makes it a little clearer where these macros come from.
2025-09-12 06:49:54 -07:00
Eric Huss
e3bb655663 Add a helper for defining a regex
This adds the `static_regex` macro to help with defining a regex.
2025-09-12 06:48:50 -07:00
Eric Huss
8bb9a7ff42 Merge pull request #2829 from ehuss/log-to-tracing
Switch from log to tracing
2025-09-12 13:20:49 +00:00
Eric Huss
3e673ce424 Switch from log to tracing
This switches to using the tracing crate instead of log. Tracing
provides a lot of nice features which we can take advantage of moving
forward.

This also adjusts the output fairly significantly. This includes:

- Switched the environment variable from RUST_LOG to MDBOOK_LOG.
- Dropped the timestamp. I experimented with various different time
  displays, but ultimately decided to omit it for now. I don't think
  I've ever found it to be useful, and it takes up a very significant
  amount of space. It could potentially be useful for basic profiling,
  but I think there are other, better mechanisms for that. We could
  consider leveraging tracing itself for doing some basic profiling
  (like using something like tracing-chrome).
- Dropped the target unless MDBOOK_LOG is set. The target tends to be
  pretty noisy, and doesn't really convey much information unless you
  are debugging or otherwise trying to adjust the log output.
- Added color.
- Slightly reworked the way the error cause trace is displayed.
- Slightly changed the way html5ever filtering is done, as well as add
  handlebars to the list since they both are very noisy. You can
  override this now by explicitly listing them as targets.

I still expect that mdbook will eventually change how it displays things
to the console, possibly switching away from tracing and printing things
itself. However, that is a larger project for the future.
2025-09-12 06:13:45 -07:00
Eric Huss
787882069e Merge pull request #2828 from GuillaumeGomez/simplify-gui
Simplify GUI tests runner
2025-09-08 17:35:25 +00:00
Guillaume Gomez
e6fffcf9ec Simplify GUI tests runner 2025-09-08 17:30:48 +02:00
Guillaume Gomez
c78d463864 Update browser-ui-test version to 0.22.1 2025-09-08 17:25:08 +02:00
Eric Huss
14f249071c Merge pull request #2827 from ehuss/publish-pre-release-guide
Support publishing a pre-release version of the guide
2025-09-05 00:14:49 +00:00
Eric Huss
0dc65a1ac4 Support publishing a pre-release version of the guide
This changes the publishing process so that when publishing the guide
and the current version is a pre-release, it will be pushed to a
directory called `/pre-release/`.

This also switches from using simpleinfra's SSH-based script to a simple
push using normal git commands.
2025-09-04 17:08:49 -07:00
Eric Huss
8f46ad8575 Merge pull request #2826 from ehuss/guide-version
Add the mdbook version to the guide
2025-09-03 22:15:33 +00:00
Eric Huss
6dd8be2ab2 Use the helper for the CI version string
This uses the new guide-helper preprocessor to insert the version string
on the continuous integration guide page. This should make it easier to
bump new versions.
2025-09-03 15:07:26 -07:00
Eric Huss
29b71be0a5 Add the mdbook version to the first page of the guide
This displays the version of mdBook that the guide is for.
2025-09-03 15:06:33 -07:00
Eric Huss
f3bb6ce7ff Merge pull request #2825 from GuillaumeGomez/update-dep
Update browser-ui-test version to `0.21.3`
2025-09-02 14:56:48 +00:00
Guillaume Gomez
0ce670d124 Update browser-ui-test version to 0.21.3 2025-08-31 16:02:18 +02:00
Eric Huss
ebcd293fee Merge pull request #2824 from ehuss/compatibility-test
Add a test for extension compatibility
2025-08-30 01:49:51 +00:00
Eric Huss
5eaae38bf4 Add a test for extension compatibility
This adds a test to ensure that the interface for preprocessors and
renderers does not change unexpectedly, particularly in a semver
compatible release.

Closes https://github.com/rust-lang/mdBook/issues/1574
2025-08-29 18:40:26 -07:00
Eric Huss
be63b44038 Merge pull request #2823 from ehuss/non_exhaustive_remove
Remove non_exhaustive from Book
2025-08-30 01:34:06 +00:00
Eric Huss
30d3aeb691 Remove non_exhaustive from Book
This removes the `non_exhaustive` attribute from the `Book` and its
inner types `BookItem` and `Chapter`. These were added in
https://github.com/rust-lang/mdBook/pull/2779. After thinking about it
more, I realized that these types cannot be extended in a
semver-compatible way, so I am fine with allowing them be exhaustive.

The problem is that with CmdPreprocessor, the `Book` will be
re-serialized by a preprocessor, which could potentially be on an older
version. Attempting to add any new fields/variants means that either the
deserialization will fail, or the new fields will be stripped by the
preprocessor.

These could potentially be structured such that they have a
`serde(flatten)` or Other/Unknown variant so that a preprocessor would
at least see the extra fields/variants and pass them along back to the
output. However, a preprocessor or renderer wouldn't know what to do
with those new fields/variants (particularly `BookItem`) which would
itself be a problem. It's still possible to do something like this in
the future, but for now I think it's fine to restrict these to
semver-major changes.
2025-08-29 18:24:44 -07:00
Eric Huss
06af133838 Merge pull request #2822 from ehuss/dynamic-toc
Add sidebar heading navigation
2025-08-27 22:24:50 +00:00
Eric Huss
327417373f Try to adjust search test to pass in CI
This test is failing on CI. I don't know why, as I cannot reproduce
locally. Perhaps it is due to the update to browser-ui-test? Or perhaps
some events from the new nav bar are delaying something?
2025-08-27 15:15:08 -07:00
Eric Huss
1b55d4a389 Add sidebar heading navigation
This adds dynamic navigation of headers of the current page in the
sidebar. This is intended to help the user see what is on the current
page, and to be able to more easily navigate it. The "current" header is
tracked based on the scrolling behavior of the user, and is marked with
a small circle. This includes automatic folding to help keep it from
being too unwieldy on a page with a lot of nested headers.

This includes the `output.html.sidebar-header-nav` option to disable it.

I'm sure there are tweaks, fixes, and improvements that can be made. I'd
like to get this out now, and iterate on it over time to make
improvements.
2025-08-27 14:44:12 -07:00
Eric Huss
ac1674845f Merge pull request #2821 from ehuss/browser-ui-test-args
Add the ability to pass options to browser-ui-test
2025-08-27 03:44:17 +00:00
Eric Huss
148d282065 Add the ability to pass options to browser-ui-test
This adds the ability to pass options to browser-ui-test, which can help
with debugging or doing things like snapshot work. It's maybe not the
cleanest since it doesn't support space-separated args, but should be
good enough.
2025-08-26 20:38:28 -07:00
Eric Huss
c0dc4b7367 Merge pull request #2820 from ehuss/hash-files-default
Enable hash-files by default
2025-08-26 23:32:19 +00:00
Eric Huss
6f3fac763c Enable hash-files by default
This enables the hash-files setting by default. We have been running it
for a while, and it seems most of the issues have been resolved. This
should help with more reliably loading content like the toc contents.
2025-08-26 16:25:49 -07:00
Eric Huss
73a1652b64 Merge pull request #2819 from ehuss/test-improvements
Various test improvements
2025-08-26 22:54:09 +00:00
Eric Huss
9b5e5e7c0f Add snapbox pattern matching on check_file_contains
This is helpful for matching patterns within a larger file. The error
message isn't quite as good, since it doesn't explicitly say "pattern
not found", but I think you can figure it out from the context.
2025-08-26 15:47:24 -07:00
Eric Huss
d071d127ef Add globs to test path names
This adds the ability for some test functions to use a glob pattern to
match a single file. This will be helpful when testing hash-files
support.
2025-08-26 15:44:01 -07:00
Eric Huss
321a76bd27 Add track_caller to more test functions
This sprinkles track_caller on some more test functions to give more
useful line numbers on errors when a test fails.

read_to_string was changed since it couldn't track caller on a closure.
2025-08-26 15:38:53 -07:00
Eric Huss
8c5b72ca3f Merge pull request #2818 from ehuss/esline-hbs-js
Lint HBS JS templates
2025-08-25 22:22:16 +00:00
Eric Huss
3e421d353d Lint HBS JS templates
This updates eslint to lint on the toc.js.hbs file. This file uses a
small amount of hbs templating which eslint can't handle. This adds a
hacky preprocessor which will strips out the handlebars tags so that the
file can be linted.
2025-08-25 15:15:47 -07:00
Eric Huss
3cd055c508 Merge pull request #2817 from ehuss/update-eslint-9
Update eslint to 9.34.0
2025-08-25 21:35:52 +00:00
Eric Huss
189eea5beb Update eslint to 9.34.0
This requires a switch to the configuration file format described at
https://eslint.org/docs/latest/use/configure/configuration-files. I used
the automatic tool to generate the new file, with some simplifications
around defaults.

- Removed no-cond-assign override, it is no longer needed.
- Fixed `catch (e)` where `e` is not used. This seems a little pedantic
  to me, but seems like a relatively easy fix to just remove it, and I
  believe the syntax without the variable has been supported for quite
  some time. Alternatively it could also be `_e`, or explicitly allowed.
- Added `--no-warn-ignored` to the script since it was very noisy.
2025-08-25 14:29:00 -07:00
Eric Huss
3f45b024f2 Merge pull request #2816 from szabgab/test/verify-more-specific-exception
test case: verify the more specific exception message
2025-08-25 16:49:13 +00:00
Eric Huss
a40f1f281b Merge pull request #2815 from szabgab/test/invalid-field-in-rust-table
add test: invalid field in the top level rust table in config
2025-08-25 16:47:08 +00:00
Eric Huss
6c847275c5 Merge pull request #2814 from szabgab/test/invalid-table-at-top-level
add test: invalid table at the top level config
2025-08-25 16:46:42 +00:00
Gabor Szabo
aced9f609c test case: verify the more specific exception message 2025-08-24 11:18:57 +03:00
Gabor Szabo
82a7df41a6 add test: invalid field in the top level rust table in config 2025-08-24 09:54:59 +03:00
Gabor Szabo
f6c11d12e0 add test: invalid table at the top level config 2025-08-24 09:49:43 +03:00
Eric Huss
313be7162f Merge pull request #2813 from ehuss/rename-book-sections
Rename Book.sections to Book.items
2025-08-23 01:59:06 +00:00
Eric Huss
800fb54aeb Rename Book.sections to Book.items
This renames the "sections" list to "items". In practice, this list has
contained more than just "sections" since parts were added. Also, the
rest of the code consistently uses the term "items", since the values it
contains are called `BookItem`s. Finally, the naming has always been a
little confusing to me.

This is a very disruptive change, and I'm not doing it lightly. However,
since there are a number of other API changes going into 0.5, I think
now is an ok time to change this.
2025-08-22 18:51:04 -07:00
Eric Huss
45e700db00 Merge pull request #2811 from ehuss/core-book-tests
Fix mdbook-core book tests
2025-08-23 00:11:25 +00:00
Eric Huss
24c7ffcd62 Fix mdbook-core book tests
These tests were moved in https://github.com/rust-lang/mdBook/pull/2766,
but the `mod tests` was missing. This fixes this missing `mod`, and
updates the tests so that they pass.
2025-08-22 17:04:21 -07:00
Eric Huss
ec436adca2 Merge pull request #2810 from ehuss/smart-punctuation-default
Enable smart-punctuation by default
2025-08-22 23:58:42 +00:00
Eric Huss
b8ad85c16f Enable smart-punctuation by default
This enables the smart-punctuation setting by default. The long term
plan is to continue to enable more markdown extensions by default across
semver breaking releases.
2025-08-22 16:52:08 -07:00
Eric Huss
6be8e526d6 Merge pull request #2809 from ehuss/markdown-options
Introduce options struct for markdown rendering
2025-08-22 23:23:58 +00:00
Eric Huss
f4012757a7 Introduce options struct for markdown rendering
This adds `MarkdownOptions` for creating the pulldown-cmark parser, and
`HtmlRenderOptions` for converting markdown to HTML. These types should
help make it easier to extend the rendering options while remaining
semver compatible. It should also help with just general ergonomics of
using these functions.
2025-08-22 16:17:41 -07:00
Eric Huss
0722d81295 Merge pull request #2808 from ehuss/mdbook-id
Change all HTML IDs to have a prefix
2025-08-20 02:45:29 +00:00
Eric Huss
402d11414c Change all HTML IDs to have a prefix
This changes all HTML IDs so that they have the `mdbook-` prefix. This
should help avoid ID conflicts between internal IDs and IDs from user
content such as section headers.

This is a relatively disruptive change and has a high risk of breaking
something. However, I think I have covered everything, and if anything
is missed, hopefully it will get detected.

I did not change class names since the chance of a collision is much
smaller than with IDs. However, that is something that could be
considered in the future.

Closes https://github.com/rust-lang/mdBook/issues/880
2025-08-19 19:38:22 -07:00
Eric Huss
988ed9b5bc Merge pull request #2807 from ehuss/footnotes-in-a-row
Test and add a fix for multiple footnotes in a row
2025-08-19 00:18:40 +00:00
Eric Huss
4a47b3d18a Add space between consecutive footnotes
This fixes it so that consecutive footnotes have a little space between
them so they aren't jammed together.
2025-08-18 17:11:18 -07:00
Eric Huss
82a457b548 Add a test for multiple footnotes in a row
These were previously broken in older versions of pulldown-cmark.
2025-08-18 16:38:34 -07:00
Eric Huss
54a5d749fc Merge pull request #2806 from ehuss/dest-dir-relative
Change CLI dest-dir to be relative to the current directory
2025-08-18 23:35:34 +00:00
Eric Huss
c177081104 Change CLI dest-dir to be relative to the current directory
This changes the `--dest-dir` flag so that it is relative to the current
directory, not the book root. This has been a source of confusion for
several people.

Fixes https://github.com/rust-lang/mdBook/issues/698
2025-08-18 16:28:08 -07:00
Eric Huss
1b00525574 Add test for relative dest-dir 2025-08-18 16:28:08 -07:00
Eric Huss
dbb51d32db Merge pull request #2805 from ehuss/remove-test-dest-dir
Remove `test --dest-dir`
2025-08-18 23:21:34 +00:00
Eric Huss
b4641d2830 Remove test --dest-dir
This removes the `--dest-dir` flag from the `mdbook test` subcommand
because it is unused. The test command does not generate output, so it
doesn't need an output directory.
2025-08-18 16:15:18 -07:00
Eric Huss
03f2806cae Merge pull request #2804 from ehuss/remove-external-gui-test
Remove external website tests
2025-08-18 21:01:48 +00:00
Eric Huss
4498739095 Remove external website tests
These tests have been flaky, and in general it was probably unwise to
try to rely on an external site like this. I was unable to determine
exactly why the test is failing. The page loads, and then puppeteer
throws an error.

I don't know if it is really feasible to bring these back in some form.
It's probably more effort than it is worth.

Closes https://github.com/rust-lang/mdBook/issues/2765
2025-08-18 13:55:45 -07:00
Eric Huss
29ae40c3ec Merge pull request #2801 from HollowMan6/btreemap
Keep preprocessors/backends execution order deterministic
2025-08-18 19:06:03 +00:00
Eric Huss
6746df7ce9 Add tests for preprocessor/output default sort order 2025-08-18 11:59:15 -07:00
Hollow Man
a0a01ecd60 Keep preprocessors/backends execution order deterministic
There's a regression caused by recent refactor work, as it used to execute preprocessors/backends in a deterministic way, but now this is not the case, which causes trouble when some backends implicitly depend on the result from another backend and happen to work (e.g. mdbook-pdf). The root cause is that a HashMap has no order, so this PR switches this into `BTreeMap` instead.

Signed-off-by: Hollow Man <hollowman@opensuse.org>
2025-08-18 11:58:23 -07:00
Eric Huss
21f2435182 Merge pull request #2802 from ehuss/with-replace
Change with_renderer/with_preprocessor to overwrite
2025-08-18 18:25:35 +00:00
Eric Huss
338a9b424e Change with_renderer/with_preprocessor to overwrite
This changes `with_renderer` and `with_preprocessor` to replace any
extensions of the same name instead of just appending to the list. This
is necessary for rust-lang's build process, because we replace the
preprocessors with local ones. Previously, mdbook would just print an
error, but continue working. With the change that preprocessors are no
longer optional by default, it is now required that we have a way to
replace the existing entries.
2025-08-18 11:18:31 -07:00
Eric Huss
25c47ed0bc Add tests for with_renderer and with_preprocessor collisions
This adds tests with with_renderer and with_preprocessor are used with
extensions that have the same name.
2025-08-18 11:18:30 -07:00
Eric Huss
0f0d1f3377 Merge pull request #2800 from ehuss/no-default-src
Don't serialize the default for `book.src`
2025-08-16 22:27:53 +00:00
Eric Huss
ae1a8c362c Don't serialize the default for book.src
This changes the serialization so that `book.src` is not serialized if
it is the default. This removes the somewhat pointless `src = "src"`
which shows up in the default `mdbook init` output. Deserialization
should still default to `"src"`.
2025-08-16 15:21:46 -07:00
Eric Huss
0043043bb3 Merge pull request #2799 from ehuss/guide-case
Use consistent sentence case for section headers
2025-08-16 22:05:24 +00:00
Eric Huss
e9e3bb6ddd Use consistent sentence case for section headers
There was a mix of different capitalization here, so I'm just going to
pick the one we use most often.
2025-08-16 14:59:05 -07:00
Eric Huss
03ba7d9089 Merge pull request #2797 from ehuss/optional-preprocessor
Add `optional` field for preprocessors
2025-08-16 20:46:36 +00:00
Eric Huss
d7892f5601 Add optional field for preprocessors
This adds the `optional` field to the preprocessor configuration to
mirror the same option for the `output` table. Missing preprocessors are
now an error unless the `optional` field is set. This should help with
inadvertently building a book when a missing preprocessor that you
expect to be installed.
2025-08-16 13:39:54 -07:00
Eric Huss
0a29ba6eb6 Add a test for a missing preprocessor 2025-08-16 13:33:38 -07:00
Eric Huss
4d9095b603 Change PreProcessor::supports_renderer to return a Result
This changes `PreProcessor::supports_renderer` to return a `Result` in
preparation to allow preprocessors to be optional when the command
fails.
2025-08-16 13:26:01 -07:00
Eric Huss
235c1f87f0 Factor out handle_render_command_error
This moves `handle_render_command_error` out to the crate root so that
it can later be shared with `CmdPreprocessor`.
2025-08-16 13:23:18 -07:00
Eric Huss
5d44ef91dc Merge pull request #2796 from ehuss/relative-cmd-preprocessor
Change CmdPreprocessor to use paths relative to the book root
2025-08-16 19:33:00 +00:00
Eric Huss
e7084e5548 Change CmdPreprocessor to use paths relative to the book root
This changes preprocessors so that:

- Relative paths in the `command` value are relative to the book root.
- The process current directory is the book root.

This makes it so that it isn't dependent on the directory where `mdbook`
is executed.

Fixes https://github.com/rust-lang/mdBook/issues/1424
2025-08-16 12:25:54 -07:00
Eric Huss
4637d5f5d1 Add a test for a relative preprocessor path 2025-08-16 12:17:50 -07:00
Eric Huss
4bac54883b Merge pull request #1330 from notriddle/embed-svg
Use embedded SVG instead of fonts for icons, font-awesome 6.2
2025-08-15 03:26:53 +00:00
Michael Howell
2dc8c5e686 Use embedded SVG instead of fonts for icons
The [downsides of icon fonts] are well-documented, and also, why ship all of
the icons when it only uses 14?

[downsides of icon fonts]: https://speakerdeck.com/ninjanails/death-to-icon-fonts
2025-08-14 20:14:55 -07:00
Eric Huss
7b3e6973be Merge pull request #2795 from ehuss/remove-theme-option
Remove theme_option
2025-08-14 02:48:39 +00:00
Eric Huss
9fce4ad74c Remove theme_option
This helper is no longer used.
2025-08-13 19:43:00 -07:00
Eric Huss
76c5b6967a Remove default_theme comment
I don't remember why I added this comment. The default_theme is
definitely still used.
2025-08-13 19:41:44 -07:00
Eric Huss
04ff12a206 Fix copy/paste error in resource error message 2025-08-13 19:41:44 -07:00
Eric Huss
3236ec0aea Merge pull request #2794 from ehuss/nav-helper
Replace navigation helpers with objects
2025-08-14 01:02:55 +00:00
Eric Huss
ff5e85af51 Replace navigation helpers with objects
This replaces the `{{#previous}}` and `{{#next}}` handelbars helpers
with simple objects that contain the previous and next values. These
helpers have been a bit fussy to work with and have caused issues in the
past. This drops a large amount of somewhat fragile code with something
that is a bit simpler.

Additionally, this switches the previous/next arrows to use an `{{#if}}`
instead CSS trickery which may help with upcoming changes to
font-awesome.
2025-08-13 17:56:18 -07:00
Eric Huss
c1b631d086 Merge pull request #2793 from rust-lang/dependabot/cargo/slab-0.4.11
Bump slab from 0.4.10 to 0.4.11
2025-08-13 20:48:15 +00:00
dependabot[bot]
39ce6123b6 Bump slab from 0.4.10 to 0.4.11
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 04:48:48 +00:00
Eric Huss
01da420f76 Merge pull request #2792 from ehuss/remove-relative-renderer-command
Remove legacy relative renderer command paths
2025-08-13 01:14:05 +00:00
Eric Huss
df037d132d Remove legacy relative renderer command paths
This removes the deprecated support for renderer paths that are relative
to the destination. Relative renderer command paths now must always be
relative to the book root.
2025-08-12 18:08:29 -07:00
Eric Huss
534725cbb8 Merge pull request #2791 from ehuss/un-pub-unique-id
Move id_from_content private
2025-08-13 01:07:38 +00:00
Eric Huss
4c948b4547 Merge pull request #2790 from ehuss/remove-copy-fonts
Remove copy-fonts
2025-08-13 01:02:33 +00:00
Eric Huss
bba1216df1 Move id_from_content private
This follows through with the deprecation of `id_from_content` which was
deprecated in https://github.com/rust-lang/mdBook/pull/1749.
2025-08-12 18:01:45 -07:00
Eric Huss
3e5ec749ba Remove copy-fonts
This removes the deprecated `output.html.copy-fonts` option. This was
deprecated in https://github.com/rust-lang/mdBook/pull/1987. The
behavior now is that the default fonts are copied over unless there is a
custom `theme/fonts/fonts.css` file.
2025-08-12 17:56:14 -07:00
Eric Huss
7e0949175a Merge pull request #2789 from ehuss/remove-book-json
Remove book.json warning
2025-08-13 00:38:39 +00:00
Eric Huss
b51d3e5106 Merge pull request #2788 from ehuss/remove-curly-quotes
Remove curly-quotes
2025-08-13 00:36:33 +00:00
Eric Huss
f47066f68f Remove book.json warning
This warning was added in https://github.com/rust-lang/mdBook/pull/510
in 2017. I think plenty enough time has passed for projects to update.
2025-08-12 17:33:05 -07:00
Eric Huss
9ac0eb288a Remove curly-quotes
This removes the deprecated alias `curly-quotes` for
`smart-punctuation`.
2025-08-12 17:29:59 -07:00
Eric Huss
79e9ae48a1 Merge pull request #2787 from ehuss/deny-unknown-fields
Deny all unknown config fields
2025-08-12 22:20:20 +00:00
Eric Huss
d29072783f Deny all unknown config fields
This changes it so that it is an error if there is ever an unknown
configuration field. This is intended to help avoid things like typos,
or using an outdated version of mdbook. Although it is possible that new
fields could potentially safely be ignored, setting up a warning system
is a bit more of a hassle. I don't think mdbook needs to have the same
kind of multi-version support as something like cargo does. However, if
this ends up being too much of a pain point, we can try to add a warning
system instead.

There are a variety of changes here:

- The top-level config namespace is now closed so that it only accepts
  the keys defined in `Config`.
- All config tables now reject unknown fields.
- Added `Config::outputs` and `Config::preprocessors` for convenience
  to access the entire `output` and `preprocessor` tables.
- Moved the unit-tests that were setting environment variables to the
  testsuite where it launches a process instead.

Closes https://github.com/rust-lang/mdBook/issues/1595
2025-08-12 15:14:36 -07:00
Eric Huss
e284eb1c30 Merge pull request #2786 from ehuss/read_to_string-prelude
Add read_to_string to the prelude
2025-08-12 02:31:44 +00:00
Eric Huss
b09c588ca9 Merge pull request #2785 from ehuss/ensure-test-built
Don't rebuild books in tests
2025-08-12 02:27:02 +00:00
Eric Huss
a6a60eef31 Add read_to_string to the prelude
This adds the `read_to_string` test helper to the test prelude so that
it is easier to use.
2025-08-11 19:26:05 -07:00
Eric Huss
95128b6662 Merge pull request #2784 from ehuss/debug-testsuite
Add debug option to the testsuite
2025-08-12 02:21:35 +00:00
Eric Huss
824e2a7681 Don't rebuild books in tests
This fixes an issue where the `check` methods would inadvertently
rebuild the books if the `mdbook build` command is run. Normally this
isn't too much of an issue unless the `build` command is run with
options that affect the output.
2025-08-11 19:21:03 -07:00
Eric Huss
43e690dd13 Add debug option to the testsuite
This adds a basic debug method to the testsuite command to help with
diagnosing tests that aren't working as expected.
2025-08-11 19:15:53 -07:00
Eric Huss
4a2d3a7e85 Merge pull request #2783 from ehuss/no-legacy
Remove legacy config support
2025-08-11 15:31:07 +00:00
Eric Huss
eda77b81db Remove legacy config support
This removes the old legacy config that allowed certain things at the
top level.

The legacy switch was added in https://github.com/rust-lang/mdBook/pull/457

Closes https://github.com/rust-lang/mdBook/issues/2653
2025-08-11 08:25:04 -07:00
Eric Huss
8befcc74d1 Merge pull request #2779 from ehuss/non_exhaustive
Switch all public types to non_exhaustive
2025-08-10 00:07:29 +00:00
Eric Huss
5956092b4b Switch all public types to non_exhaustive
This switches all public types to use non_exhaustive to make it easier
to make additions without a semver-breaking change.

Some of the ergonomics are hampered due to the lack of exhaustiveness
checking. Hopefully some day in the future,
non_exhaustive_omitted_patterns_lint or something like it will get
stabilized.

Closes https://github.com/rust-lang/mdBook/issues/1835
2025-08-09 17:02:01 -07:00
Eric Huss
c25e866796 Make SectionNumber field private
This removes the `pub` status of the SectionNumber field. The intent is
to make this potentially extensible in the future if we decide to add
more fields, or change its internal representation. With the existence
of the deref impls, generally this change shouldn't be visible except
for the constructor, which hopefully shouldn't be too cumbersome to use
`SectionNumber::new` instead.
2025-08-09 17:02:01 -07:00
Eric Huss
1d1274e53a Merge pull request #2780 from ehuss/fix-nightly-panic-message
Fix test for nightly panic message change
2025-08-09 23:51:19 +00:00
Eric Huss
841c68d05e Fix test for nightly panic message change
A recent nightly changed the format of the panic message. This updates
the test that was matching against this so it doesn't match the text
that has changed.
2025-08-09 16:45:41 -07:00
Eric Huss
37273ba8e0 Merge pull request #2401 from Roms1383/chore/upgrade-pulldown-cmark
Upgrade pulldown cmark to 0.13.0
2025-07-26 15:39:13 +00:00
Roms1383
91f04bc7ba Upgrade pulldown-cmark to 0.13.0 2025-07-26 08:33:28 -07:00
Eric Huss
7cce45818a Merge pull request #2776 from ehuss/remove-google-analytics
Remove google-analytics
2025-07-26 15:22:19 +00:00
Eric Huss
84fbd679e7 Merge pull request #2775 from ehuss/remove-multilingual
Remove the book.multilingual field
2025-07-26 15:17:00 +00:00
Eric Huss
aef12bbb20 Remove google-analytics
This was deprecated in https://github.com/rust-lang/mdBook/pull/1675 in
2021.

Closes https://github.com/rust-lang/mdBook/issues/2720
2025-07-26 08:15:37 -07:00
Gabor Szabo
00eba964ce Remove the book.multilingual field
As it is seems it has never been in real use.

See #2636
2025-07-26 08:10:20 -07:00
Eric Huss
cf7762f57f Merge pull request #2774 from ehuss/update-toml
Update toml to 0.9.2
2025-07-25 20:30:08 +00:00
Eric Huss
26319f4124 Update toml to 0.9.2 2025-07-25 13:24:27 -07:00
Eric Huss
f8e66db41c Merge pull request #2773 from ehuss/remove-public-toml
Remove toml as a public dependency
2025-07-25 18:35:41 +00:00
Eric Huss
529cfc34ec Remove toml as a public dependency
This removes toml as a public dependency. This reduces the exposure of
the public API, reduces exposure of internal implementation, and makes
it easier to make semver-incompatible changes to toml.

This is accomplished through a variety of changes:

- `get` and `get_mut` are removed.
- `get_deserialized_opt` is renamed to `get`.
- Dropped the AsRef for `get_deserialized_opt` for ergonomics, since
  using an `&` for a String is not too much to ask, and the other
  generic arg needs to be specified in a fair number of situations.
- Removed deprecated `get_deserialized`.
- Dropped `TomlExt` from the public API.
- Removed `get_renderer` and `get_preprocessor` since they were trivial
  wrappers over `get`.
2025-07-25 11:29:07 -07:00
Eric Huss
8053774ba3 Fix Config.set working with the rust table 2025-07-25 11:29:07 -07:00
Eric Huss
fe76ee626f Merge pull request #2772 from ehuss/unreachable-pub
Enable unreachable_pub
2025-07-25 16:08:22 +00:00
Eric Huss
f6c062fc98 Enable unreachable_pub
This lint can help make it clearer which items are actually exposed in
the public API.
2025-07-25 09:02:55 -07:00
Eric Huss
97d9078a32 Merge pull request #2766 from ehuss/crate-split
Split mdbook into multiple crates
2025-07-24 00:54:10 +00:00
Eric Huss
a397f64356 Add a section on how to run tests 2025-07-23 17:47:31 -07:00
Eric Huss
fad53f720f Update guide to accommodate crate split
This updates the docs now that the crate has been split into multiple
crates.
2025-07-23 17:47:31 -07:00
Eric Huss
dcfb527342 Update crate docs
This updates the crate-level docs to add a little more detail for each
crate.

This also drops the `mdbook` library crate, as it is no longer needed.
2025-07-23 17:47:31 -07:00
Eric Huss
e0a4fb1ea9 Add READMEs for all new crates 2025-07-23 17:47:31 -07:00
Eric Huss
6e6518a7ae Move the remaining dependencies to the workspace table
This is intended to have all dependencies only defined in the workspace
table, and crates can then refer to it.
2025-07-23 17:47:31 -07:00
Eric Huss
12fc0ff5c3 Clean up dependencies of mdbook
These are no longer used, or are dev-dependencies only.
2025-07-23 17:47:31 -07:00
Eric Huss
b8a7b6e846 Simplify MDBook::iter doc
The original was a little awkward, and I'm not sure what the tuple
syntax was intending to convey.
2025-07-23 17:47:31 -07:00
Eric Huss
9229e80499 Clean up some remaining uses of mdbook_core
These should be using the appropriate high-level crate.
2025-07-23 17:47:31 -07:00
Eric Huss
ae6c4522bb Publicly re-export mdbook-core modules from mdbook-driver
The intent here is to make mdbook-core a private dependency that the
user shouldn't need.
2025-07-23 17:47:31 -07:00
Eric Huss
fdebbfdce2 Remove pulldown-cmark from mdbook-core
This dependency is no longer directly used.
2025-07-23 17:47:31 -07:00
Eric Huss
40745600a3 Finish move of MDBook to mdbook-driver 2025-07-23 17:47:31 -07:00
Eric Huss
5a31947eb7 Move MDBook to mdbook-driver
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-driver.
2025-07-23 17:47:31 -07:00
Eric Huss
d758753551 Finish moving builtin renderers to mdbook-driver 2025-07-23 17:47:28 -07:00
Eric Huss
9b27b14985 Move built-in renderers to mdbook-driver
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-driver.
2025-07-23 17:40:57 -07:00
Eric Huss
f5fc54461a Finish moving built-in preprocessors to mdbook-driver 2025-07-23 17:40:57 -07:00
Eric Huss
6aac696ee1 Move built-in preprocessors to mdbook-driver
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-driver.
2025-07-23 17:40:57 -07:00
Eric Huss
c8571f592c Add mdbook-driver
This is intended to hold the high-level MDBook type.
2025-07-23 17:40:57 -07:00
Eric Huss
7eccd1d556 Finish move of hbs_renderer to mdbook-html
This updates everything for the move of hbs_renderer to mdbook-html.
2025-07-23 17:40:57 -07:00
Eric Huss
06324d8b24 Move hbs_renderer to mdbook-html
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-html.
Additional commits will refactor/move/remove items.
2025-07-23 17:40:57 -07:00
Eric Huss
753780f653 Finish move of theme to mdbook-html
This updates everything for the move of theme to mdbook-html. There
will be followup commits that will be doing more cleanup here.
2025-07-23 17:40:57 -07:00
Eric Huss
3087686559 Move theme to mdbook-html
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-html.
Additional commits will refactor/move/remove items.
2025-07-23 17:40:52 -07:00
Eric Huss
6805740e20 Add mdbook-html
This new crate will hold the HTML renderer and related front-end parts.
2025-07-23 17:29:55 -07:00
Eric Huss
8f3b6b4776 Move markdown support to mdbook-markdown
This moves all the code responsible for markdown processing to the
mdbook-markdown crate.
2025-07-23 17:29:55 -07:00
Eric Huss
3278f84373 Move renderer types to mdbook-renderer
This sets up mdbook-renderer with the intent of being the core
library that renderers use to implement the necessary interactions.
2025-07-23 17:29:55 -07:00
Eric Huss
12285f505d Move preprocessor types to mdbook-preprocessor
This sets up mdbook-preprocessor with the intent of being the core
library that preprocessors use to implement the necessary interactions.
2025-07-23 17:29:55 -07:00
Eric Huss
e123879c8c Move Book to mdbook-core
This moves the Book definition to mdbook-core, along with related types
it needs.
2025-07-23 17:29:55 -07:00
Eric Huss
7bcdfe6f0f Finish move of summary to mdbook-summary
This updates everything for the move of summary to mdbook-summary. There
will be followup commits that will be doing more cleanup here.
2025-07-23 17:29:55 -07:00
Eric Huss
29f936b1eb Move summary to mdbook-summary
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-summary.
Additional commits will refactor/move/remove items.
2025-07-23 17:29:55 -07:00
Eric Huss
bd3e555962 Add mdbook-summary
This new crate will hold the Summary types and parsing support.
2025-07-23 17:29:55 -07:00
Eric Huss
02b6628048 Finish move of config to mdbook-core
This updates everything for the move of config to mdbook-core. There
will be followup commits that will be moving and refactoring the config.
This simply moves it over unchanged.
2025-07-23 17:29:55 -07:00
Eric Huss
4ae5a53791 Move config to mdbook-core
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-core.
Additional commits will refactor/move/remove items.
2025-07-23 17:29:55 -07:00
Eric Huss
fc76a47d6e Finish move of utils to mdbook-core
This updates everything for the move of utils to mdbook-core. There will
be followup commits that will be moving and refactoring these utils.
This simply moves them over unchanged (except visibility).
2025-07-23 17:29:55 -07:00
Eric Huss
a224bfd7d7 Move utils to mdbook-core
This is a pure git rename in order to make sure that git can follow
history. The next commit will integrate these into mdbook-core.
Additional commits will refactor/move/remove items.
2025-07-23 17:29:55 -07:00
Eric Huss
f51d89ba02 Move error types to mdbook-core
This moves Result and Error to mdbook-core with the anticipation of
using them in user crates. For now, the internal APIs will be using
anyhow directly, but the intent is to transition more of these to
mdbook-core where it makes sense.
2025-07-23 17:29:55 -07:00
Eric Huss
461884f109 Add mdbook-preprocessor and mdbook-renderer
These are two new crates intended to support implementing preprocessors
and renderers. Currently these stubs just have MDBOOK_VERSION, but
future commits will migrate more code to these crates.
2025-07-23 17:29:55 -07:00
Eric Huss
bc3399cc22 Update version to 0.5.0-alpha.1
This bumps the main crate to 0.5.0-alpha.1 in preparation for the 0.5
release.
2025-07-23 17:29:55 -07:00
Eric Huss
4a655ff2a3 Add mdbook-core
This is intended as a shared, internal library that will be used by
other mdbook crates. The intention is that those crates will either
directly use, or reexport items from this crate.

Initially this includes MDBOOK_VERSION, which will get reexported from
the preprocessor and renderer crates.
2025-07-23 17:29:55 -07:00
Eric Huss
877a3af671 Add rustfmt.toml
This is intended to help with editor integration for using the correct
style edition.
2025-07-23 17:29:55 -07:00
Eric Huss
1499e850ac Add .git-blame-ignore-revs
This is to help make it easier to traverse blame history for things like
formatting commits.
2025-07-23 17:29:53 -07:00
Eric Huss
c7b67e363b Rustfmt for 2024 2025-07-23 17:29:12 -07:00
Eric Huss
d5a505e0c6 Update to Rust 2024 2025-07-23 17:29:12 -07:00
Eric Huss
0de13cf5a9 Update CI to test the whole workspace
This updates the CI jobs to ensure that all crates in the workspace are
tested. This will be needed when more crates are added.
2025-07-23 17:29:12 -07:00
Eric Huss
d6d5d6e674 Move common package settings to shared workspace table
This moves common settings that can be shared across crates to the
shared workspace table. This will make it easier to maintain these
settings when adding more crates.
2025-07-23 17:29:12 -07:00
Eric Huss
5264074c1b Move common lint controls to Cargo.toml
This moves lint overrides to Cargo.toml so that they can more easily be
shared across crates.
2025-07-23 17:29:12 -07:00
Eric Huss
702c676107 Merge pull request #2758 from ehuss/remove-release-toml
Remove release.toml
2025-07-22 00:58:38 +00:00
Eric Huss
a387846b20 Remove release.toml
This hasn't been used in many years.
2025-07-21 17:53:03 -07:00
Eric Huss
0eabdb0169 Merge pull request #2757 from ehuss/clippy-needless-lifetimes
Remove clippy needless-lifetimes workaround
2025-07-21 16:41:17 +00:00
Eric Huss
92836f3988 Remove clippy needless-lifetimes workaround
This is no longer needed now that 1.87 has reached the stable channel.
2025-07-21 09:35:54 -07:00
Eric Huss
05c6a99446 Merge pull request #2729 from GuillaumeGomez/info-log-location
Show where the book was generated
2025-07-15 22:29:51 +00:00
Guillaume Gomez
68893f785f Show where the book was generated 2025-07-15 15:24:35 -07:00
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
Eric Huss
b7a27d2759 Merge pull request #2629 from ehuss/bump-version
Update to 0.4.48
2025-03-31 20:17:02 +00:00
Eric Huss
d67dbc74fd Update to 0.4.48 2025-03-31 12:55:28 -07:00
Eric Huss
d9d27f38c3 Merge pull request #2625 from GuillaumeGomez/simplify-resources-location
Simplify resources location
2025-03-31 19:47:15 +00:00
Guillaume Gomez
4886c92fa4 Finish moving resources around 2025-03-31 21:18:22 +02:00
Guillaume Gomez
195d97a514 Move JS files into front-end/js 2025-03-31 21:18:22 +02:00
Guillaume Gomez
e954e872f0 Move css and font files into front-end 2025-03-31 21:18:22 +02:00
Guillaume Gomez
e74b4b0507 Move template files into front-end/templates folder 2025-03-31 21:18:22 +02:00
Eric Huss
4946c78e8c Merge pull request #2576 from tmandry/default-auto-switch
Add "Auto" theme selection which switches between default light and dark theme automatically
2025-03-31 18:56:39 +00:00
Eric Huss
54d8d37b77 Fix eslint errors
This updates the ecmaVersion due to the ?? nullish coalescing operator.
2025-03-31 11:50:13 -07:00
Tyler Mandry
20eea0b41e Add "Auto" option to theme menu
This switches between light and dark based on the OS, and provides a way
to remove a saved preference.
2025-03-31 11:37:06 -07:00
Tyler Mandry
8835bdc47e Switch theme when preferred color scheme changes 2025-03-31 11:34:40 -07:00
Eric Huss
7a1977a78c Merge pull request #2613 from szabgab/avoid-duplicate-entries
Avoid using the same file twice in SUMMARY.md
2025-03-31 18:15:16 +00:00
Eric Huss
629c09df4d Merge pull request #2627 from szabgab/test/empty-cli
test the command line without any parameters #1568
2025-03-31 18:06:55 +00:00
Gabor Szabo
7247e5f9a1 test the command line without any parameters #1568 2025-03-31 11:39:38 +03:00
Eric Huss
a3c0ecdb45 Merge pull request #2626 from ehuss/footnote-backrefs-style
Add footnote backreferences, and update styling
2025-03-30 13:51:46 +00:00
Eric Huss
b20b1757a9 Add footnote backreferences, and update styling
This makes several changes to how footnotes are rendered:

- Backlinks are now included, which links back to the reference so you
  can continue reading where you left off.
- Footnotes are moved to the bottom of the page. This helps with the
  implementation of numbering, and is a style some have requested. I
  waffled a lot on this change, but supporting the in-place style was
  just adding too much complexity.
- Footnotes are now highlighted when you click on a reference.
- Some of the spacing for elements within a footnote has now been fixed
  (such as supporting multiple paragraphs).
- Footnote navigation now scrolls to the middle of the page.

This is an alternative to https://github.com/rust-lang/mdBook/pull/2475

Closes https://github.com/rust-lang/mdBook/issues/1927
Closes https://github.com/rust-lang/mdBook/issues/2169
Closes https://github.com/rust-lang/mdBook/issues/2595
2025-03-30 06:44:59 -07:00
Gabor Szabo
1bc2ebd775 warn on invalid fields in root of book.toml 2025-03-28 10:56:41 +03:00
Eric Huss
a56cffeb4e Merge pull request #2616 from rust-lang/ehuss-patch-2
Add more triagebot features
2025-03-23 20:10:38 +00:00
Eric Huss
c908ac8cc5 Add more triagebot features 2025-03-23 13:04:10 -07:00
Eric Huss
b47d1cff33 Merge pull request #2554 from GuillaumeGomez/eslint
Fix eslint warnings and add eslint check in CI
2025-03-23 19:50:44 +00:00
Eric Huss
780daa73ae Merge pull request #2609 from szabgab/gh-pages-explanation
add explanation I learned in #2606
2025-03-23 19:43:33 +00:00
Guillaume Gomez
9114905a93 Use serde_json instead of json to get browser-ui-test version 2025-03-23 10:06:12 +01:00
Guillaume Gomez
a7aaef1e85 Add information on eslint and how to install and run it 2025-03-23 10:06:12 +01:00
Guillaume Gomez
9823246ecd Add eslint check in the CI 2025-03-23 10:06:12 +01:00
Guillaume Gomez
861940ba4b Fix eslint warnings 2025-03-23 10:06:12 +01:00
Eric Huss
3ed302467e Merge pull request #2552 from GuillaumeGomez/deduplicate-search-indexes
Remove JSON search file
2025-03-22 23:07:37 +00:00
Guillaume Gomez
f54356da10 Remove fail-on-request-error in GUI tests as they are not needed anymore 2025-03-22 17:56:22 +01:00
Guillaume Gomez
418d677584 Improve warning message when search index is too big 2025-03-22 17:49:30 +01:00
Guillaume Gomez
a0eb8c0a0e Remove JSON search file 2025-03-22 17:48:16 +01:00
Gabor Szabo
67b4260021 Avoid using the same file twice in SUMMARY.md
See #2612
2025-03-22 14:55:49 +02:00
Gabor Szabo
7c6d47e8b6 add explanation I learned in #2606 2025-03-21 16:32:07 +02:00
Eric Huss
43281c85c5 Merge pull request #2604 from szabgab/test/arrow-keys
Add GUI test to check the left and right arrow keys
2025-03-21 01:27:47 +00:00
Eric Huss
e73d3b7cfa Merge pull request #2602 from szabgab/gui-tests
Select all the GUI tests if no filter was provided
2025-03-21 00:25:11 +00:00
Gabor Szabo
1de8cf8ba6 try the last pages as well 2025-03-20 22:07:08 +02:00
Gabor Szabo
85afbe466e Add GUI test to check the left and right arrow keys 2025-03-20 21:58:21 +02:00
Gabor Szabo
63b312948a Select all the GUI tests if no filter was provided
The name of excutable was taken as a filter and because of
that nothing was selected by default.
2025-03-20 18:13:59 +02:00
Eric Huss
3a8faba645 Merge pull request #2579 from szabgab/ask_the_preprocessor_to_blow_up
[test] Check content of error message
2025-03-16 17:36:38 +00:00
Eric Huss
6d6bee0dc9 Merge pull request #2589 from szabgab/test/cant_open_summary_md
[test] error Couldn't open SUMMARY.md in load_book
2025-03-16 17:35:31 +00:00
Eric Huss
4b266f1ebc Merge pull request #2590 from GuillaumeGomez/filter-gui-tests
Allow to run only some specific GUI tests
2025-03-10 16:38:50 +00:00
Guillaume Gomez
fc7ef59dee Allow to run only some specific GUI tests 2025-03-10 13:31:40 +01:00
Gabor Szabo
5fa9f12427 try to fix expected error on windows 2025-03-09 19:13:53 +02:00
Eric Huss
07b25cdb64 Merge pull request #2587 from ehuss/bump-version
Update to 0.4.47
2025-03-09 16:28:26 +00:00
Eric Huss
105d836fbc Merge pull request #2586 from ehuss/fix-search-subchapter
Fix search not showing in sub-directories
2025-03-09 16:18:36 +00:00
Eric Huss
74fcaf5273 Update to 0.4.47 2025-03-09 09:14:57 -07:00
Eric Huss
74200f7395 Fix search not showing in sub-directories
This fixes a problem where the search was not displaying in
sub-directories. The problem was that `searcher.js` only exists in one
place, and was loading `searchindex.json` with a relative path. However,
when loading from a subdirectory, it needs the appropriate `..` to reach
the root of the book.
2025-03-09 09:10:50 -07:00
Gabor Szabo
a7ca2e169f [test] error Couldn't open SUMMARY.md in load_book 2025-03-09 14:40:00 +02:00
Eric Huss
1a5286b25c Merge pull request #2578 from ehuss/bump-version
Update to 0.4.46
2025-03-08 22:06:23 +00:00
Eric Huss
c493d3b5e3 Update to 0.4.46 2025-03-08 14:00:42 -08:00
Eric Huss
a68091a84c Merge pull request #2571 from szabgab/missing_backends_are_fatal
Check content of the error message.
2025-03-08 20:46:53 +00:00
Gabor Szabo
32daca669a Accomodate for different status message on Windows
Alternatively we could change the error message to
only include take the status code from the os by
using

output.status.code().unwrap() in preprocess/cmd.rs
where this error message is generated.

In that case we could have the exact same error
message on all the OS-es.
2025-03-06 07:25:51 +02:00
Gabor Szabo
cf5a78c0e1 Check content of the error message
in ask_the_preprocessor_to_blow_up
2025-03-05 23:24:18 +02:00
Eric Huss
b0cf568ba4 Merge pull request #2569 from szabgab/suffix_items_cannot_be_followed_by_a_list
check content of the error message
2025-03-05 17:56:16 +00:00
Gabor Szabo
bf544be282 Check content of the error message.
In missing_backends_are_fatal
2025-03-05 17:38:23 +02:00
Gabor Szabo
4f0dba8fdb check content of the error message
in suffix_items_cannot_be_followed_by_a_list
2025-03-05 17:27:15 +02:00
Eric Huss
5390e44dec Merge pull request #2566 from szabgab/remove-dots-from-docs
remove unnecessary dots from docs
2025-03-04 17:34:42 +00: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
Gabor Szabo
e7e3317ff0 remove unnecessary dots from docs 2025-03-04 17:06:21 +02:00
Eric Huss
d68a596455 Merge pull request #2561 from szabgab/test-failure-in-summary
Test failure in SUMMARY.md when item is not a link
2025-03-03 18:43:36 +00:00
Eric Huss
ace2abff34 Merge pull request #2563 from jofas/patch-1
Enhanced wording for editable code blocks docs
2025-03-03 18:41:44 +00:00
Jonas Fassbender
0c6439faad Enhanced wording for editable code blocks docs 2025-03-03 17:21:34 +01:00
Gabor Szabo
e7418f21f9 Test failure in SUMMARY.md when item is not a link 2025-03-03 10:33:27 +02:00
Eric Huss
19146c403e Merge pull request #2557 from ehuss/fix-playground-edition
Fix playground edition detection
2025-02-26 14:02:34 +00:00
Eric Huss
66ded2302f Fix playground edition detection 2025-02-26 05:50:25 -08:00
Eric Huss
98abb22be1 Merge pull request #1368 from notriddle/hash-files
feat(html): cache bust static files by adding hashes to file names
2025-02-20 18:32:17 +00:00
Eric Huss
ab304e7d38 More code simplification 2025-02-20 10:25:14 -08:00
Eric Huss
fbc21592af Some clippy cleanup 2025-02-20 10:23:47 -08:00
Eric Huss
e7b69114ed Remove some code duplication 2025-02-20 10:19:04 -08:00
Michael Howell
8a9ecd212d Fix, and test, the no-js toc sidebar with hashed resources
To make this work, I need to break the circular dependency and
stop hashing toc.html itself.
2025-02-20 10:27:18 -07:00
Eric Huss
ec157cd1cd Use full patch description for hex 2025-02-20 08:54:01 -08:00
Eric Huss
d3bcb359fa Update sha2 to latest 2025-02-20 08:52:58 -08:00
Eric Huss
2a4e5583ab Rewrite test to use tempfile
We don't want to be writing to arbitrary directories, and this
seems to make the test a little simpler.
2025-02-20 08:48:16 -08:00
Eric Huss
3978612611 Update some comments and formatting 2025-02-20 08:47:03 -08:00
Eric Huss
4941acdb87 Merge pull request #2551 from ehuss/bump-version
Update to 0.4.45
2025-02-17 18:26:17 +00:00
Eric Huss
7e3d2f96ab Update to 0.4.45 2025-02-17 10:18:04 -08:00
Eric Huss
ddba36b24c Merge pull request #2524 from WaffleLapkin/first-last-of-type-footnote
nicer style rules for margin around footnote defs
2025-02-17 18:12:15 +00:00
Eric Huss
35cf96a064 Merge pull request #2550 from ehuss/fix-expected-source-path
Fix issue with None source_path
2025-02-17 17:52:50 +00:00
Eric Huss
5777a0edc4 Fix issue with None source_path
This fixes an issue where mdbook would panic if a non-draft chapter has
a None source_path when generating the search index. The code was
assuming that only draft chapters would have that behavior. However, API
users can inject synthetic chapters that have no path on disk.

This updates it to fall back to the path, or skip if neither is set.
2025-02-17 09:41:52 -08:00
Eric Huss
53c3a92285 Add test for a chapter with no source path 2025-02-17 08:20:16 -08:00
Michael Howell
82db7f5b93 Add a bit more to the configuration docs 2025-02-13 14:22:54 -07:00
Michael Howell
879449447f feat(html): cache bust static files by adding hashes to file names
Closes rust-lang#1254
2025-02-13 10:39:22 -07:00
Eric Huss
132ca0dca3 Merge pull request #2548 from tamird/patch-1
README.md: update workflow status badge
2025-02-13 16:25:11 +00:00
Tamir Duberstein
56c2b9ba3a README.md: update workflow status badge
The previous badge was broken.

Link: https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/monitoring-workflows/adding-a-workflow-status-badge
2025-02-13 11:01:08 -05:00
Eric Huss
542b6feed1 Merge pull request #2545 from ehuss/rustdoc-missing-error
Add context when `rustdoc` command is not found
2025-02-03 19:10:48 +00:00
Eric Huss
2af44a396f Add context when rustdoc command is not found 2025-02-03 11:02:53 -08:00
Eric Huss
40d91fff29 Merge pull request #2540 from ehuss/bump-version
Update to 0.4.44
2025-01-28 17:58:16 +00:00
Eric Huss
59eab7cfc2 Update to 0.4.44 2025-01-28 09:50:04 -08:00
Eric Huss
1b524ff356 Merge pull request #2539 from ehuss/update-notify
Update notify to 8.0.0
2025-01-28 17:41:05 +00:00
Eric Huss
9b873e9d97 Bump rust-version to 1.77 2025-01-28 09:35:11 -08:00
Eric Huss
b6d6cb2711 Update notify to 8.0.0 2025-01-28 09:32:17 -08:00
Eric Huss
c8095160d0 Merge pull request #2538 from ehuss/update-dependencies
Update dependencies
2025-01-28 17:19:41 +00:00
Eric Huss
ae6db3a87e Update dependencies
Updating anstyle-wincon v3.0.6 -> v3.0.7
Updating anyhow v1.0.93 -> v1.0.95
Updating bitflags v2.6.0 -> v2.8.0
Updating bstr v1.10.0 -> v1.11.3
Updating bytes v1.8.0 -> v1.9.0
Updating cc v1.1.36 -> v1.2.10
Updating chrono v0.4.38 -> v0.4.39
Updating clap v4.5.20 -> v4.5.27
Updating clap_builder v4.5.20 -> v4.5.27
Updating clap_complete v4.5.37 -> v4.5.43
Updating clap_lex v0.7.2 -> v0.7.4
Updating cpufeatures v0.2.14 -> v0.2.17
Updating crossbeam-channel v0.5.13 -> v0.5.14
Updating crossbeam-deque v0.8.5 -> v0.8.6
Updating crossbeam-utils v0.8.20 -> v0.8.21
  Adding darling v0.20.10
  Adding darling_core v0.20.10
  Adding darling_macro v0.20.10
Updating data-encoding v2.6.0 -> v2.7.0
  Adding derive_builder v0.20.2
  Adding derive_builder_core v0.20.2
  Adding derive_builder_macro v0.20.2
Updating env_filter v0.1.2 -> v0.1.3
Updating env_logger v0.11.5 -> v0.11.6
Updating errno v0.3.9 -> v0.3.10
Updating fastrand v2.1.1 -> v2.3.0
Updating float-cmp v0.9.0 -> v0.10.0
Updating handlebars v6.2.0 -> v6.3.0
Updating hashbrown v0.15.1 -> v0.15.2
Removing hermit-abi v0.3.9
Updating http v1.1.0 -> v1.2.0
Updating httparse v1.9.5 -> v1.10.0
Updating hyper v0.14.31 -> v0.14.32
  Adding ident_case v1.0.1
Updating indexmap v2.6.0 -> v2.7.1
Updating itoa v1.0.11 -> v1.0.14
Updating js-sys v0.3.72 -> v0.3.77
Updating libc v0.2.161 -> v0.2.169
Updating linux-raw-sys v0.4.14 -> v0.4.15
Updating litemap v0.7.3 -> v0.7.4
Updating log v0.4.22 -> v0.4.25
Updating miniz_oxide v0.8.0 -> v0.8.3
Updating mio v1.0.2 -> v1.0.3
Updating object v0.36.5 -> v0.36.7
Updating pathdiff v0.2.2 -> v0.2.3
Updating pest v2.7.14 -> v2.7.15
Updating pest_derive v2.7.14 -> v2.7.15
Updating pest_generator v2.7.14 -> v2.7.15
Updating pest_meta v2.7.14 -> v2.7.15
Updating phf v0.11.2 -> v0.11.3
Updating phf_codegen v0.11.2 -> v0.11.3
Updating phf_generator v0.11.2 -> v0.11.3
Updating phf_shared v0.11.2 -> v0.11.3
Updating pin-project v1.1.7 -> v1.1.8
Updating pin-project-internal v1.1.7 -> v1.1.8
Updating pin-project-lite v0.2.15 -> v0.2.16
Updating predicates v3.1.2 -> v3.1.3
Updating predicates-core v1.0.8 -> v1.0.9
Updating predicates-tree v1.0.11 -> v1.0.12
Updating proc-macro2 v1.0.89 -> v1.0.93
Updating quote v1.0.37 -> v1.0.38
Updating redox_syscall v0.5.7 -> v0.5.8
Updating regex-automata v0.4.8 -> v0.4.9
Updating rustix v0.38.39 -> v0.38.44
  Adding rustversion v1.0.19
Updating ryu v1.0.18 -> v1.0.19
Updating semver v1.0.23 -> v1.0.25
Updating serde v1.0.214 -> v1.0.217
Updating serde_derive v1.0.214 -> v1.0.217
Updating serde_json v1.0.132 -> v1.0.137
  Adding siphasher v1.0.1
Updating socket2 v0.5.7 -> v0.5.8
Updating syn v2.0.87 -> v2.0.96
Updating tempfile v3.13.0 -> v3.15.0
Updating terminal_size v0.4.0 -> v0.4.1
Updating termtree v0.4.1 -> v0.5.1
Removing thiserror v1.0.68
  Adding thiserror v1.0.69
  Adding thiserror v2.0.11
Removing thiserror-impl v1.0.68
  Adding thiserror-impl v1.0.69
  Adding thiserror-impl v2.0.11
Updating tokio v1.41.0 -> v1.43.0
Updating tokio-macros v2.4.0 -> v2.5.0
Updating tokio-util v0.7.12 -> v0.7.13
Updating tracing v0.1.40 -> v0.1.41
Updating tracing-core v0.1.32 -> v0.1.33
Updating unicase v2.8.0 -> v2.8.1
Updating unicode-ident v1.0.13 -> v1.0.16
Updating url v2.5.3 -> v2.5.4
Updating wasm-bindgen v0.2.95 -> v0.2.100
Updating wasm-bindgen-backend v0.2.95 -> v0.2.100
Updating wasm-bindgen-macro v0.2.95 -> v0.2.100
Updating wasm-bindgen-macro-support v0.2.95 -> v0.2.100
Updating wasm-bindgen-shared v0.2.95 -> v0.2.100
Updating yoke v0.7.4 -> v0.7.5
Updating yoke-derive v0.7.4 -> v0.7.5
Updating zerofrom v0.1.4 -> v0.1.5
Updating zerofrom-derive v0.1.4 -> v0.1.5
2025-01-28 09:11:17 -08:00
Eric Huss
18f57f5bd9 Merge pull request #2533 from ehuss/search-chapter-settings
Add output.html.search.chapter
2025-01-28 14:43:02 +00:00
Eric Huss
09a37284b0 Add output.html.search.chapter
This config setting provides the ability to disable search indexing on a
per-chapter (or sub-path) basis.

This is structured to possibly add additional settings, such as perhaps
a score multiplier or other settings.
2025-01-27 19:45:50 -08:00
Eric Huss
dff5ac64e5 Merge pull request #2458 from dcampbell24/display-for-clean
Display what is removed from mdbook clean.
2025-01-25 21:54:30 +00:00
Eric Huss
0ee565a5ff Merge pull request #2530 from max-heller/rust-hidelines
fix: make line hiding in Rust code blocks consistent with `rustdoc`
2025-01-25 21:50:47 +00:00
Eric Huss
9e4854f349 Merge pull request #2532 from notriddle/sync-toggle
Prevent the real sidebar position from becoming unsynced from the JS
2025-01-25 21:17:31 +00:00
Michael Howell
74d48f5ad2 Prevent the real sidebar position from becoming unsynced from the JS
This way, whatever behavior the browser might use for checkboxes
will apply to the CSS class, localStorage, and the visible state.
2025-01-23 10:18:21 -07:00
Eric Huss
0b51a74c16 Merge pull request #2531 from GuillaumeGomez/regression-test-2529
Add GUI regression test for #2529
2025-01-23 14:33:22 +00:00
Guillaume Gomez
ce63cc31f4 Add GUI regression test for #2529 2025-01-23 14:01:38 +01:00
Guillaume Gomez
d6720fc671 Update browser-ui-test version to 0.19.0 2025-01-23 13:58:35 +01:00
Waffle Lapkin
64cca1399b nicer style rules for margin around footnote defs
previous implementation used `:not(.fd) + .fd` and `.fd + :not(.fd)`.
the latter selector caused many problems:
- it doesn't select footnote defs which are last children
  (this can be easily triggered in a blockquote)
- it changes the margin of the next sibling, rather than the footnote def
  itself, which can also *shrink* margin for elements with big margins
  (this happens to headings)
- because it applies to the next sibling it is also quite hard to
  override in user styles, since it may apply to any element
  
this commit replaces the latter selector with `:not(:has(+ .fd))`,
which fixes all of the mentioned problems.
2025-01-21 01:21:53 +01:00
Eric Huss
629c2ad2fd Merge pull request #2529 from GuillaumeGomez/fix-sidebar-display
Fix display of sidebar when JS is disabled
2025-01-20 17:42:49 +00:00
Max Heller
d325e821cd fix: make line hiding in Rust code blocks consistent with rustdoc
Requires a space following a `#` for a line to be hidden.
2025-01-20 11:43:39 -05:00
Guillaume Gomez
ac3a7faa54 Fix display of sidebar when JS is disabled 2025-01-20 17:29:07 +01:00
Eric Huss
35ed24cd18 Merge pull request #2523 from marcoieni/ubuntu-22
ci: move ubuntu-20 jobs to ubuntu-22
2025-01-15 14:37:44 +00:00
MarcoIeni
81d42f1c6e ci: move ubuntu-20 jobs to ubuntu-22 2025-01-15 10:21:10 +01:00
Eric Huss
618a2fa78b Merge pull request #2476 from GuillaumeGomez/gui-tests
Add base for GUI tests
2025-01-06 22:46:26 +00:00
Eric Huss
0bf6751eed Merge pull request #2517 from notriddle/master
Ignore fragment when figuring out sidebar items
2025-01-02 20:25:15 +00:00
Michael Howell
f92eac4acd Ignore fragment when figuring out sidebar items 2025-01-02 10:34:03 -07:00
Guillaume Gomez
69ef52fd13 Disable sandbox when running GUI tests 2024-12-19 20:01:25 +01:00
Guillaume Gomez
cc8ce35b4d Run GUI tests as a separate testsuite 2024-12-18 11:25:11 +01:00
Guillaume Gomez
2a13ca2fbf Add base for GUI tests 2024-12-16 17:45:36 +01:00
Eric Huss
59e6afcaad Merge pull request #2500 from rukai/release_for_aarch64_macos
Add aarch64-apple-darwin release target
2024-12-02 14:51:39 +00:00
Lucas Kent
4d9a455a27 Add aarch64-apple-darwin release target 2024-12-02 11:43:57 +11:00
Eric Huss
74b2c79d46 Merge pull request #2497 from ehuss/bump-version
Update to 0.4.43
2024-11-25 17:21:18 +00:00
Eric Huss
ed407b091c Update to 0.4.43 2024-11-25 09:14:43 -08:00
Eric Huss
6c8020a3b9 Merge pull request #2495 from ehuss/stabilize-2024
Stabilize 2024 flag
2024-11-25 16:05:27 +00:00
Eric Huss
42f18d1e51 Stabilize 2024 flag
The 2024 edition is now stable on nightly, so the `-Z` flag is no longer necessary.
2024-11-23 15:25:29 -08:00
David Campbell
abf3e4ab50 Display what is removed from mdbook clean.
This is based off of [cargo's][1] clean command. cargo is licensed
under MIT or Apache-2.0.

[1]: https://github.com/rust-lang/cargo
2024-11-22 15:10:51 -05:00
Eric Huss
d1078434af Merge pull request #2486 from eureka-cpu/eureka-cpu/2485
fix `init --title` option failure when git user is not configured
2024-11-18 19:25:00 +00:00
eureka-cpu
8f024dabc3 fix init --title option failure when git user is not configured 2024-11-18 11:10:11 -08:00
Eric Huss
0c580c32c4 Add regression test for mdbook init title with no git config
Regression test for https://github.com/rust-lang/mdBook/issues/2485
2024-11-18 11:08:26 -08:00
Eric Huss
90960126e8 Merge pull request #2478 from rust-lang/ehuss-patch-1
Add note about updating `index.hbs`
2024-11-09 13:47:04 +00:00
Eric Huss
aa37f24fc1 Add note about updating index.hbs 2024-11-09 05:40:12 -08:00
Eric Huss
3f4f287e6e Merge pull request #2474 from ehuss/bump-version
Update to 0.4.42
2024-11-07 14:49:21 +00:00
Eric Huss
55fe75c716 Update to 0.4.42 2024-11-07 06:42:05 -08:00
Eric Huss
c6236ead67 Merge pull request #2473 from notriddle/notriddle/folding
Fix inadvertently broken folding behavior
2024-11-07 14:39:31 +00:00
Michael Howell
68e3572278 Fix inadvertently broken folding behavior 2024-11-06 15:47:12 -07:00
Eric Huss
27ab7eb2f0 Merge pull request #2470 from ehuss/update-deps
Update dependencies
2024-11-06 17:42:16 +00:00
Eric Huss
6d183be0ec Merge pull request #2471 from ehuss/bump-version
Update to 0.4.41
2024-11-06 17:41:56 +00:00
Eric Huss
c83a34b473 Update to 0.4.41 2024-11-06 09:36:21 -08:00
Eric Huss
d3e0e597d2 Update dependencies 2024-11-06 09:34:07 -08:00
Eric Huss
271bbba7dd Merge pull request #2414 from notriddle/on2
Load the sidebar toc from a shared JS file or iframe
2024-11-02 23:56:19 +00:00
Eric Huss
86ff2e1e6b Merge pull request #2465 from ehuss/footnote-line-height
Set line-height of superscripts to 0
2024-11-02 23:19:27 +00:00
Eric Huss
6ef7cc0ccb Set line-height of superscripts to 0
This changes it so that superscript (and in particular footnote tags)
do not bump the line spacing of previous lines.
2024-11-02 16:12:07 -07:00
Eric Huss
f4cf32e768 Merge pull request #2464 from ehuss/remove-emphasis
Add a real example of remove-emphasis
2024-11-02 22:49:49 +00:00
Eric Huss
47384c1f18 Merge pull request #2463 from Pistonight/bug/theme_popup
fix: themes broken when localStorage has invalid theme id stored
2024-11-02 22:48:17 +00:00
Eric Huss
9e3d533acc Add a real example of remove-emphasis 2024-11-02 15:41:55 -07:00
Eric Huss
5ec4f65ac3 Merge pull request #2454 from GuillaumeGomez/theme-noscript
Improve theme support when JS is disabled
2024-11-02 21:33:57 +00:00
Pistonight
4a330ae36f fix: themes broken when localStorage has invalid theme id stored 2024-10-31 19:02:35 -07:00
Guillaume Gomez
d93fbc0f6b Improve theme support when JS is disabled 2024-10-29 16:20:41 +01:00
Eric Huss
684bb78897 Merge pull request #2448 from jackieh/enhance-syntax-highlighting
Enhance syntax highlighting
2024-10-22 15:49:01 +00:00
Jackie Harris
d0dd16c527 Enhance syntax highlighting
Add syntax highlighting for `hljs-attr` and `hljs-section` CSS classes,
consistent with the Ayu theme.
2024-10-17 12:25:15 -05:00
Eric Huss
f4805343f8 Merge pull request #2442 from hamirmahal/style/simplify-string-formatting-for-readability
style: simplify string formatting for readability
2024-09-25 20:49:56 +00:00
Hamir Mahal
f9add3e936 fix: formatting in src/ and tests/ directories 2024-09-21 15:56:13 -07:00
Hamir Mahal
1fd9656291 style: simplify string formatting for readability 2024-09-21 15:53:59 -07:00
Eric Huss
6f281a6401 Merge pull request #2416 from campeis/update_handlebars_to_v6
chore: update handlebars to v6
2024-09-07 16:11:07 +00:00
Eric Huss
5194d2b3cd Merge pull request #2421 from GuillaumeGomez/copy-code
Unify copy to clipboard icon with docs.rs, rustdoc and crates.io
2024-08-14 15:10:39 +00:00
Guillaume Gomez
b3c23c5f88 Add credits for clipboard image 2024-08-11 16:18:19 +02:00
Eric Huss
a15134cc2f Merge pull request #2423 from radeksvarz/patch-1
added update how to
2024-08-11 12:51:44 +00:00
Eric Huss
b51bb101f2 Tweak heading wording 2024-08-11 05:46:48 -07:00
Eric Huss
59d26dbbe7 Move "upgrade mdbook" description to the build-from-source section 2024-08-11 05:45:26 -07:00
Radek
94baf19e6a added update how to
Updating workflow is not clear for non rust users.
2024-08-07 12:01:19 +02:00
Guillaume Gomez
f1a446fb02 Unify copy to clipboard icon with docs.rs, rustdoc and crates.io 2024-08-02 11:55:17 +02:00
Alessandro Campeis
01d1242753 chore: update handlebars to v6 2024-07-23 10:32:47 +02:00
Michael Howell
203685e91c Make the sidebar work without JS
Uses an iframe instead. The downside of iframes comes from them
not necessarily being same-origin as the main page (particularly
with `file:///` URLs), which can cause themes to fall out of sync,
but that's not a problem here since themes don't work without JS
anyway.
2024-07-16 12:38:00 -07:00
Michael Howell
2cb5b85ab2 Load the sidebar toc from a shared JS file
Before this change, the Rust `unstable-book` is 88MiB.
With this change, it becomes 15MiB. Other pages might not be
as extreme, but it's expected to help any book like this.

This change is so drastic because, if every chapter has a link to
every other chapter, the result is *O*(n<sup>2</sup>) text output.
2024-07-15 18:51:32 -07:00
Eric Huss
ec996d3509 Merge pull request #2406 from ehuss/fix-smart-link
Fix broken link to "Smart Punctuation"
2024-06-24 21:40:46 +00:00
Eric Huss
5ed3223185 Fix broken link to "Smart Punctuation" 2024-06-24 14:32:55 -07:00
Eric Huss
3bdcc0a5a6 Merge pull request #2398 from ehuss/edition2024
Add support for Rust Edition 2024
2024-06-12 22:59:25 +00:00
Eric Huss
1e4d4887e1 Add support for Rust Edition 2024 2024-06-12 15:53:56 -07:00
574 changed files with 19290 additions and 21708 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[alias]
xtask = "run --manifest-path=crates/xtask/Cargo.toml --"

26
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,26 @@
# Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits.
# rustfmt
ad0794a0bd692e4f2ff23b85e361889620e93f51
# rustfmt and use_try_shorthand
75bbd55128083897d40c3f5265cc5b1f10314ddb
# rustfmt
382fc4139b96bde3c4b8875b499c720eabc89c6a
# rustfmt
154e0fb3080c6ffc225b0d47b5d835e589789892
# rustfmt
5835da243244bfc5c95c6c6db96f453da4bb5740
# rustfmt
fd9d27e082f5e9eea50e4fa9fa3a22060d02c66b
# rustfmt
1d69ccae4854f13552d452d0bffef95cbff70364
# rustfmt
3688f73052454bf510a5acc85cf55aae450c6e46
# rustfmt
742dbbc91700dce1b7d910bca6b3e10a5ae46b86
# rustfmt 1.38
b88839cc25a6fd1c782101e94318959e8079bb20
# rustfmt 1.40
2f59943c04f0aa204a9238d6a699ba9cc06c88d9
# Rustfmt for 2024
c7b67e363bb9ce3383636ee615e8e761bf185b33

2
.gitattributes vendored
View File

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

71
.github/renovate.json5 vendored Normal file
View File

@@ -0,0 +1,71 @@
{
schedule: ['before 5am on the first day of the month'],
// Raise from default of 2 to reduce trickle.
prHourlyLimit: 6,
dependencyDashboard: true,
// Creates PRs if this renovate config file needs updating.
configMigration: true,
ignorePaths: [
'guide/src/for_developers/mdbook-wordcount/',
],
customManagers: [
// Custom manager to extract the version of cargo-semver-checks from the workflow.
{
customType: 'regex',
managerFilePatterns: [
'/^.github.workflows.main.yml$/',
],
matchStrings: [
'cargo-semver-checks.releases.download.v(?<currentValue>\\d+\\.\\d+(\\.\\d+)?)',
],
depNameTemplate: 'cargo-semver-checks',
packageNameTemplate: 'obi1kenobi/cargo-semver-checks',
datasourceTemplate: 'github-releases',
},
],
packageRules: [
// The next two rules disable compatible dependency updates. I wasn't
// able to get Renovate to be able to update Cargo.toml for compatible
// updates only, update all transitive dependencies, and do that all
// in a single PR. Instead, the `update-dependencies.sh` will handle
// that.
{
matchManagers: ['cargo'],
matchUpdateTypes: ['patch'],
enabled: false,
},
{
matchManagers: ['cargo'],
matchCurrentVersion: '>=1.0.0',
matchUpdateTypes: ['minor'],
enabled: false,
},
// Allow minor updates for pre-1.0 dependencies (semver-breaking)
{
matchManagers: ['cargo'],
matchCurrentVersion: '<1.0.0',
matchUpdateTypes: ['minor'],
},
// Allow major updates for stable dependencies (semver-breaking)
{
matchManagers: ['cargo'],
matchCurrentVersion: '>=1.0.0',
matchUpdateTypes: ['major'],
},
// Update cargo-semver-checks when a new version is available.
{
commitMessageTopic: 'cargo-semver-checks',
matchManagers: [
'custom.regex',
],
matchDepNames: [
'cargo-semver-checks',
],
extractVersion: '^v(?<version>\\d+\\.\\d+\\.\\d+)',
schedule: [
'* * * * *',
],
internalChecksFilter: 'strict',
},
]
}

View File

@@ -17,18 +17,20 @@ jobs:
matrix:
include:
- target: aarch64-unknown-linux-musl
os: ubuntu-20.04
os: ubuntu-22.04
- target: x86_64-unknown-linux-gnu
os: ubuntu-20.04
os: ubuntu-22.04
- target: x86_64-unknown-linux-musl
os: ubuntu-20.04
os: ubuntu-22.04
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
name: Deploy ${{ matrix.target }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust
run: ci/install-rust.sh stable ${{ matrix.target }}
- name: Build asset
@@ -41,27 +43,25 @@ jobs:
name: GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable
- name: Build book
run: cargo run -- build guide
- name: Deploy to GitHub
env:
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
run: |
touch guide/book/.nojekyll
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
cd guide/book
/tmp/deploy
- name: Deploy the User Guide to GitHub Pages using the gh-pages branch
run: ci/publish-guide.sh
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
permissions:
# Required for OIDC token exchange
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable
- name: Authenticate with crates.io
id: auth
uses: rust-lang/crates-io-auth-action@v1
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --no-verify
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: cargo publish --workspace --no-verify

View File

@@ -22,7 +22,7 @@ jobs:
rust: nightly
target: x86_64-unknown-linux-gnu
- name: stable x86_64-unknown-linux-musl
os: ubuntu-20.04
os: ubuntu-22.04
rust: stable
target: x86_64-unknown-linux-musl
- name: stable x86_64 macos
@@ -38,24 +38,24 @@ jobs:
rust: stable
target: x86_64-pc-windows-msvc
- name: msrv
os: ubuntu-20.04
os: ubuntu-22.04
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
rust: 1.74.0
rust: 1.88.0
target: x86_64-unknown-linux-gnu
name: ${{ matrix.name }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust
run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }}
- name: Build and run tests
run: cargo test --locked --target ${{ matrix.target }}
run: cargo test --workspace --locked --target ${{ matrix.target }}
- name: Test no default
run: cargo test --no-default-features --target ${{ matrix.target }}
run: cargo test --workspace --no-default-features --target ${{ matrix.target }}
aarch64-cross-builds:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust
run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl
- name: Build
@@ -65,11 +65,65 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install Rust
run: rustup update stable && rustup default stable && rustup component add rustfmt
- run: cargo fmt --check
gui:
name: GUI tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Install npm
uses: actions/setup-node@v5
with:
node-version: 22
- name: Install browser-ui-test
run: npm install
- name: Run eslint
run: npm run lint
- 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@v5
- 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@v5
- 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
check-version-bump:
name: Check version bump
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: rustup update stable && rustup default stable
- name: Install cargo-semver-checks
run: |
mkdir installed-bins
curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.44.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz --directory=./installed-bins
echo `pwd`/installed-bins >> $GITHUB_PATH
- run: cargo semver-checks --workspace
# 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
@@ -81,6 +135,11 @@ jobs:
needs:
- test
- rustfmt
- aarch64-cross-builds
- gui
- clippy
- docs
- check-version-bump
runs-on: ubuntu-latest
steps:
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'

View File

@@ -0,0 +1,20 @@
name: Update dependencies
on:
schedule:
- cron: '0 0 1 * *'
workflow_dispatch:
jobs:
update:
name: Update dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Install cargo-edit
run: cargo install cargo-edit --locked
- name: Update dependencies
run: ci/update-dependencies.sh
env:
GH_TOKEN: ${{ github.token }}

8
.gitignore vendored
View File

@@ -8,7 +8,8 @@ guide/book
.vscode
tests/dummy_book/book/
test_book/book/
tests/gui/books/*/book/
tests/testsuite/*/*/book/
# Ignore Jetbrains specific files.
.idea/
@@ -16,3 +17,8 @@ test_book/book/
# Ignore Vim temporary and swap files.
*.sw?
*~
# GUI tests
node_modules
package-lock.json
package.json

View File

@@ -1,5 +1,474 @@
# Changelog
## 0.5 Migration Guide
During the pre-release phase of the 0.5 release, the documentation may be found at <https://rust-lang.github.io/mdBook/pre-release/>.
The 0.5 release contains several breaking changes from the 0.4 release. Preprocessors and renderers will need to be migrated to continue to work with this release. After updating your configuration, it is recommended to carefully compare and review how your book renders to ensure everything is working correctly.
If you have overridden any of the theme files, you will likely need to update them to match the current version.
The following is a summary of the changes that may require your attention when updating to 0.5:
### Config changes
- Unknown fields in config are now an error.
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
- Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
- Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
- If the `command` path for a renderer or preprocessor is relative, it is now always relative to the book root.
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
- Added the `optional` field for preprocessors. The default is `false`, so this also means it is an error by default if the preprocessor is missing.
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
- `output.html.smart-punctuation` is now `true` by default.
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
- `output.html.hash-files` is now `true` by default.
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
- Removed support for google-analytics. Use a theme extension (like `head.hbs`) if you need to continue to support this.
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
- Removed the `book.multilingual` field. This was never used.
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
- Removed the very old legacy config support. Warnings have been displayed in previous versions on how to migrate.
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
### Theme changes
- Replaced the `{{#previous}}` and `{{#next}}` handlebars helpers with simple objects that contain the previous and next values.
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
- Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
### Rendering changes
- Updated to a newer version of `pulldown-cmark`. This brings a large number of fixes to markdown processing.
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
- The font-awesome font is no longer loaded as a font. Instead, the corresponding SVG is embedded in the output for the corresponding `<i>` tags. Additionally, a handlebars helper has been added for the `hbs` files.
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
- Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
- There is a new internal HTML rendering pipeline. This is primarily intended to give mdBook more flexibility in generating its HTML output. This resulted in some small changes to the HTML structure. HTML parsing may now be more strict than before.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
- Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
### CLI changes
- Removed the `--dest-dir` option to `mdbook test`. It was unused since `mdbook test` does not generate output.
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
- Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
### Rust API
- The Rust API has been split into several crates ([#2766](https://github.com/rust-lang/mdBook/pull/2766)). In summary, the different crates are:
- `mdbook` — The CLI binary.
- [`mdbook-driver`](https://docs.rs/mdbook-driver/latest/mdbook_driver/) — The high-level library for running mdBook, primarily through the `MDBook` type. If you are driving mdBook programmatically, this is the crate you want.
- [`mdbook-preprocessor`](https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/) — Support for implementing preprocessors. If you have a preprocessor, then this is the crate you should depend on.
- [`mdbook-renderer`](https://docs.rs/mdbook-renderer/latest/mdbook_renderer/) — Support for implementing renderers. If you have a custom renderer, this is the crate you should depend on.
- [`mdbook-markdown`](https://docs.rs/mdbook-markdown/latest/mdbook_markdown/) — The Markdown renderer. If you are processing markdown, this is the crate you should depend on. This is essentially a thin wrapper around `pulldown-cmark`, and re-exports that crate so that you can ensure the version stays in sync with mdBook.
- [`mdbook-summary`](https://docs.rs/mdbook-summary/latest/mdbook_summary/) — The `SUMMARY.md` parser.
- [`mdbook-html`](https://docs.rs/mdbook-html/latest/mdbook_html/) — The HTML renderer.
- [`mdbook-core`](https://docs.rs/mdbook-core/latest/mdbook_core/) — An internal library that is used by the other crates for shared types. You should not depend on this crate directly since types from this crate are re-exported from the other crates as appropriate.
- Changes to `Config`:
- [`Config::get`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.get) is now generic over the return value, using `serde` to deserialize the value. It also returns a `Result` to handle deserialization errors. [#2773](https://github.com/rust-lang/mdBook/pull/2773)
- Removed `Config::get_deserialized`. Use `Config::get` instead.
- Removed `Config::get_deserialized_opt`. Use `Config::get` instead.
- Removed `Config::get_mut`. Use `Config::set` instead.
- Removed deprecated `Config::get_deserialized_opt`. Use `Config::get` instead.
- Removed `Config::get_renderer`. Use `Config::get` instead.
- Removed `Config::get_preprocessor`. Use `Config::get` instead.
- Public types have been switch to use the `#[non_exhaustive]` attribute to help allow them to change in a backwards-compatible way.
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
- Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded. This allows the caller to replace an entry.
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
- Added `MarkdownOptions` struct to specify settings for markdown rendering for `mdbook_markdown::new_cmark_parser`.
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
- Renamed `Book::sections` to `Book::items`.
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
- `mdbook::book::load_book` is now private. Instead, use one of the `MDBook` load functions like `MDBook::load_with_config`.
- Removed `HtmlConfig::smart_punctuation` method, use the field of the same name.
- `CmdPreprocessor::parse_input` moved to `mdbook_preprocessor::parse_input`.
- `Preprocessor::supports_renderer` now returns a `Result<bool>` instead of `bool` to be able to handle errors.
- Most of the types from the `theme` module are now private. The `Theme` struct is still exposed for working with themes.
- Various functions in the `utils::fs` module have been removed, renamed, or reworked.
- Most of the functions in the `utils` module have been moved, removed, or made private.
## mdBook 0.5.0-beta.1
[v0.5.0-alpha.1...v0.5.0-beta.1](https://github.com/rust-lang/mdBook/compare/v0.5.0-alpha.1...v0.5.0-beta.1)
### Changed
- Reworked the look of the header navigation.
[#2898](https://github.com/rust-lang/mdBook/pull/2898)
- Update cargo dependencies.
[#2896](https://github.com/rust-lang/mdBook/pull/2896)
- Improved the heading nav debug.
[#2892](https://github.com/rust-lang/mdBook/pull/2892)
### Fixed
- Fixed error message for config.get deserialization error.
[#2902](https://github.com/rust-lang/mdBook/pull/2902)
- Filter `<mark>` tags from sidebar heading nav.
[#2899](https://github.com/rust-lang/mdBook/pull/2899)
- Avoid divide-by-zero in heading nav computation
[#2891](https://github.com/rust-lang/mdBook/pull/2891)
- Fixed heading nav with folded chapters.
[#2893](https://github.com/rust-lang/mdBook/pull/2893)
## mdBook 0.5.0-alpha.1
[v0.4.52...v0.5.0-alpha.1](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0-alpha.1)
### Added
- The location of the generated HTML book is now displayed on the console.
[#2729](https://github.com/rust-lang/mdBook/pull/2729)
- ❗ Added the `optional` field for preprocessors. The default is `false`, so this also changes it so that it is an error if the preprocessor is missing.
[#2797](https://github.com/rust-lang/mdBook/pull/2797)
- ❗ Added `MarkdownOptions` struct to specify settings for markdown rendering.
[#2809](https://github.cocm/rust-lang/mdBook/pull/2809)
- Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it.
[#2822](https://github.com/rust-lang/mdBook/pull/2822)
- Added the mdbook version to the guide.
[#2826](https://github.com/rust-lang/mdBook/pull/2826)
- Added `Book::chapters` and `Book::for_each_chapter_mut` to more conveniently iterate over chapters (instead of all items).
[#2838](https://github.com/rust-lang/mdBook/pull/2838)
- ❗ Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it.
[#2847](https://github.com/rust-lang/mdBook/pull/2847)
- ❗ Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it.
[#2851](https://github.com/rust-lang/mdBook/pull/2851)
### Changed
- ❗ The `mdbook` crate has been split into multiple crates.
[#2766](https://github.com/rust-lang/mdBook/pull/2766)
- The minimum Rust version has been updated to 1.88.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
-`pulldown-cmark` has been upgraded to 0.13.0, bringing a large number of fixes to markdown processing.
[#2401](https://github.com/rust-lang/mdBook/pull/2401)
- ❗ Switched public types to `non_exhaustive` to help allow them to change in a backwards-compatible way.
[#2779](https://github.com/rust-lang/mdBook/pull/2779)
[#2823](https://github.com/rust-lang/mdBook/pull/2823)
- ❗ Unknown fields in config are now an error.
[#2787](https://github.com/rust-lang/mdBook/pull/2787)
[#2801](https://github.com/rust-lang/mdBook/pull/2801)
- ❗ Changed `id_from_content` to be private.
[#2791](https://github.com/rust-lang/mdBook/pull/2791)
- ❗ Changed preprocessor `command` to use paths relative to the book root.
[#2796](https://github.com/rust-lang/mdBook/pull/2796)
- ❗ Replaced the `{{#previous}}` and `{{#next}}` handelbars navigation helpers with objects.
[#2794](https://github.com/rust-lang/mdBook/pull/2794)
- ❗ Use embedded SVG instead of fonts for icons, font-awesome 6.2.
[#1330](https://github.com/rust-lang/mdBook/pull/1330)
- The `book.src` field is no longer serialized if it is the default of "src".
[#2800](https://github.com/rust-lang/mdBook/pull/2800)
- ❗ Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded.
[#2802](https://github.com/rust-lang/mdBook/pull/2802)
- ❗ Changed CLI `--dest-dir` to be relative to the current directory, not the book root.
[#2806](https://github.com/rust-lang/mdBook/pull/2806)
- ❗ Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs.
[#2808](https://github.com/rust-lang/mdBook/pull/2808)
-`output.html.smart-punctuation` is now `true` by default.
[#2810](https://github.com/rust-lang/mdBook/pull/2810)
- ❗ Renamed `Book::sections` to `Book::items`.
[#2813](https://github.com/rust-lang/mdBook/pull/2813)
-`output.html.hash-files` is now `true` by default.
[#2820](https://github.com/rust-lang/mdBook/pull/2820)
- Switched from `log` to `tracing`.
[#2829](https://github.com/rust-lang/mdBook/pull/2829)
- ❗ Rewrote the HTML rendering pipeline.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- ❗ Links on the print page now link to elements on the print page instead of linking out to the individual chapters.
[#2844](https://github.com/rust-lang/mdBook/pull/2844)
- ❗ Moved theme copy to the Theme type and reduced visibility.
[#2857](https://github.com/rust-lang/mdBook/pull/2857)
- ❗ Cleaned up some fs-related utilities.
[#2856](https://github.com/rust-lang/mdBook/pull/2856)
- ❗ Moved `get_404_output_file` to `HtmlConfig`.
[#2855](https://github.com/rust-lang/mdBook/pull/2855)
- ❗ Moved `take_lines` functions to `mdbook-driver` and made private.
[#2854](https://github.com/rust-lang/mdBook/pull/2854)
- Updated dependencies.
[#2793](https://github.com/rust-lang/mdBook/pull/2793)
[#2869](https://github.com/rust-lang/mdBook/pull/2869)
### Removed
- ❗ Removed `toml` as a public dependency.
[#2773](https://github.com/rust-lang/mdBook/pull/2773)
- ❗ Removed the `book.multilingual` field. This was never used.
[#2775](https://github.com/rust-lang/mdBook/pull/2775)
- ❗ Removed support for google-analytics.
[#2776](https://github.com/rust-lang/mdBook/pull/2776)
- ❗ Removed the very old legacy config support.
[#2783](https://github.com/rust-lang/mdBook/pull/2783)
- ❗ Removed `curly-quotes`, use `output.html.smart-punctuation` instead.
[#2788](https://github.com/rust-lang/mdBook/pull/2788)
- Removed old warning about `book.json`.
[#2789](https://github.com/rust-lang/mdBook/pull/2789)
- ❗ Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file.
[#2790](https://github.com/rust-lang/mdBook/pull/2790)
- ❗ Removed legacy relative renderer command paths. Relative renderer command paths now must always be relative to the book root.
[#2792](https://github.com/rust-lang/mdBook/pull/2792)
- ❗ Removed the `{{theme_option}}` handlebars helper. It has not been used for a while.
[#2795](https://github.com/rust-lang/mdBook/pull/2795)
- ❗ Removed the `--dest-dir` option to `mdbook test`.
[#2805](https://github.com/rust-lang/mdBook/pull/2805)
### Fixed
- Fixed handling of multiple footnotes in a row.
[#2807](https://github.com/rust-lang/mdBook/pull/2807)
- Fixed ID collisions when the numeric suffix gets used.
[#2846](https://github.com/rust-lang/mdBook/pull/2846)
- Fixed missing css vars for no-js dark mode.
[#2850](https://github.com/rust-lang/mdBook/pull/2850)
## 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)
### Added
- Footnotes now have back-reference links. These links bring the reader back to the original location. As part of this change, footnotes are now only rendered at the bottom of the page. This also includes some styling updates and fixes for footnote rendering.
[#2626](https://github.com/rust-lang/mdBook/pull/2626)
- Added an "Auto" theme selection option which will default to the system-preferred mode. This will also automatically switch when the system changes the preferred mode.
[#2576](https://github.com/rust-lang/mdBook/pull/2576)
### Changed
- The `searchindex.json` file has been removed; only the `searchindex.js` file will be generated.
[#2552](https://github.com/rust-lang/mdBook/pull/2552)
- Updated Javascript code to use eslint.
[#2554](https://github.com/rust-lang/mdBook/pull/2554)
- An error is generated if there are duplicate files in `SUMMARY.md`.
[#2613](https://github.com/rust-lang/mdBook/pull/2613)
## mdBook 0.4.47
[v0.4.46...v0.4.47](https://github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47)
### Fixed
- Fixed search not showing up in sub-directories.
[#2586](https://github.com/rust-lang/mdBook/pull/2586)
## mdBook 0.4.46
[v0.4.45...v0.4.46](https://github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46)
### Changed
- The `output.html.hash-files` config option has been added to add hashes to static filenames to bust any caches when a book is updated. `{{resource}}` template tags have been added so that links can be properly generated to those files.
[#1368](https://github.com/rust-lang/mdBook/pull/1368)
### Fixed
- Playground links for Rust 2024 now set the edition correctly.
[#2557](https://github.com/rust-lang/mdBook/pull/2557)
## mdBook 0.4.45
[v0.4.44...v0.4.45](https://github.com/rust-lang/mdBook/compare/v0.4.44...v0.4.45)
### Changed
- Added context to error message when rustdoc is not found.
[#2545](https://github.com/rust-lang/mdBook/pull/2545)
- Slightly changed the styling rules around margins of footnotes.
[#2524](https://github.com/rust-lang/mdBook/pull/2524)
### Fixed
- Fixed an issue where it would panic if a source_path is not set.
[#2550](https://github.com/rust-lang/mdBook/pull/2550)
## mdBook 0.4.44
[v0.4.43...v0.4.44](https://github.com/rust-lang/mdBook/compare/v0.4.43...v0.4.44)
### Added
- Added pre-built aarch64-apple-darwin binaries to the releases.
[#2500](https://github.com/rust-lang/mdBook/pull/2500)
- `mdbook clean` now shows a summary of what it did.
[#2458](https://github.com/rust-lang/mdBook/pull/2458)
- Added the `output.html.search.chapter` config setting to disable search indexing of individual chapters.
[#2533](https://github.com/rust-lang/mdBook/pull/2533)
### Fixed
- Fixed auto-scrolling the side-bar when loading a page with a `#` fragment URL.
[#2517](https://github.com/rust-lang/mdBook/pull/2517)
- Fixed display of sidebar when javascript is disabled.
[#2529](https://github.com/rust-lang/mdBook/pull/2529)
- Fixed the sidebar visibility getting out of sync with the button.
[#2532](https://github.com/rust-lang/mdBook/pull/2532)
### Changed
- ❗ Rust code block hidden lines now follow the same logic as rustdoc. This requires a space after the `#` symbol.
[#2530](https://github.com/rust-lang/mdBook/pull/2530)
- ❗ Updated the Linux pre-built binaries which requires a newer version of glibc (2.34).
[#2523](https://github.com/rust-lang/mdBook/pull/2523)
- Updated dependencies
[#2538](https://github.com/rust-lang/mdBook/pull/2538)
[#2539](https://github.com/rust-lang/mdBook/pull/2539)
## mdBook 0.4.43
[v0.4.42...v0.4.43](https://github.com/rust-lang/mdBook/compare/v0.4.42...v0.4.43)
### Fixed
- Fixed setting the title in `mdbook init` when no git user is configured.
[#2486](https://github.com/rust-lang/mdBook/pull/2486)
### Changed
- The Rust 2024 edition no longer needs `-Zunstable-options`.
[#2495](https://github.com/rust-lang/mdBook/pull/2495)
## mdBook 0.4.42
[v0.4.41...v0.4.42](https://github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)
### Fixed
- Fixed chapter list folding.
[#2473](https://github.com/rust-lang/mdBook/pull/2473)
## mdBook 0.4.41
[v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)
**Note:** If you have a custom `index.hbs` theme file, you will need to update it to the latest version.
### Added
- Added preliminary support for Rust 2024 edition.
[#2398](https://github.com/rust-lang/mdBook/pull/2398)
- Added a full example of the remove-emphasis preprocessor.
[#2464](https://github.com/rust-lang/mdBook/pull/2464)
### Changed
- Adjusted styling of clipboard/play icons.
[#2421](https://github.com/rust-lang/mdBook/pull/2421)
- Updated to handlebars v6.
[#2416](https://github.com/rust-lang/mdBook/pull/2416)
- Attr and section rules now have specific code highlighting.
[#2448](https://github.com/rust-lang/mdBook/pull/2448)
- The sidebar is now loaded from a common file, significantly reducing the book size when there are many chapters.
[#2414](https://github.com/rust-lang/mdBook/pull/2414)
- Updated dependencies.
[#2470](https://github.com/rust-lang/mdBook/pull/2470)
### Fixed
- Improved theme support when JavaScript is disabled.
[#2454](https://github.com/rust-lang/mdBook/pull/2454)
- Fixed broken themes when localStorage has an invalid theme id.
[#2463](https://github.com/rust-lang/mdBook/pull/2463)
- Adjusted the line-height of superscripts (and footnotes) to avoid adding extra space between lines.
[#2465](https://github.com/rust-lang/mdBook/pull/2465)
## mdBook 0.4.40
[v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40)

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,34 @@ 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
## Tests
The main test harness is described in the [testsuite documentation](tests/testsuite/README.md). There are several different commands to run different kinds of tests:
- `cargo test --workspace` — This runs all of the unit and integration tests, except for the GUI tests.
- `cargo test --test gui` — This runs the [GUI test harness](#browser-compatibility-and-testing). This does not get run automatically due to its extra requirements.
- `npm run lint` — [Checks the `.js` files](#checking-changes-in-js-files)
- `cargo test --workspace --no-default-features` — Testing without default features helps check that all feature checks are implemented correctly.
- `cargo clippy --workspace --all-targets --no-deps -- -D warnings` — This makes sure that there are no clippy warnings.
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --document-private-items --no-deps` — This verifies that there aren't any rustdoc warnings.
- `cargo fmt --check` — Verifies that everything is formatted correctly.
- `cargo +stable semver-checks` — Verifies that no SemVer breaking changes have been made. You must install [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks) first.
To help simplify running all these commands, you can run the following cargo command:
```sh
cargo xtask test-all
```
It is useful to run all tests before submitting a PR. While developing I recommend to run some subset of that command based on what you are working on. There are individual arguments for each one. For example:
```sh
cargo xtask test-workspace clippy doc eslint fmt gui semver-checks
```
While developing, remove any of those arguments that are not relevant to what you are changing, or are really slow.
## 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.
@@ -138,8 +164,43 @@ We generally strive to keep mdBook compatible with a relatively recent browser o
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
If possible, do your best to avoid breaking older browser releases.
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
GUI tests are checked with the GUI testsuite. To run it, you need to install `npm` first. Then run:
```
cargo test --test gui
```
If you want to only run some tests, you can filter them by passing (part of) their name:
```
cargo test --test gui -- search
```
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it with the provided
command then re-run the tests.
If you want to disable the headless mode, use the `--disable-headless-test` option:
```
cargo test --test gui -- --disable-headless-test
```
The GUI tests are in the directory `tests/gui` in text files with the `.goml` extension. The books that the tests use are located in the `tests/gui/books` directory. These tests are run using a `node.js` framework called `browser-ui-test`. You can find documentation for this language on its [repository](https://github.com/GuillaumeGomez/browser-UI-test/blob/master/goml-script.md).
### Checking changes in `.js` files
The `.js` files source code is checked using [`eslint`](https://eslint.org/). This is a linter (just like `clippy` in Rust)
for the Javascript language. You can install it with `npm` by running the following command:
```
npm install
```
Then you can run it using:
```
npm run lint
```
## Updating highlight.js
@@ -152,20 +213,24 @@ The following are instructions for updating [highlight.js](https://highlightjs.o
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [syntax GUI test](https://github.com/rust-lang/mdBook/tree/master/tests/gui/books/highlighting) contains a chapter with many languages to examine. Update the test (`highlighting.goml`) to add any new languages.
## Publishing new releases
Instructions for mdBook maintainers to publish a new release:
1. Create a PR to update the version and update the CHANGELOG:
1. Update the version in `Cargo.toml`
2. Run `cargo test` to verify that everything is passing, and to update `Cargo.lock`.
3. Double-check for any SemVer breaking changes.
Try [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks), though beware that the current version of mdBook isn't properly adhering to SemVer due to the lack of `#[non_exhaustive]` and other issues. See https://github.com/rust-lang/mdBook/issues/1835.
4. Update `CHANGELOG.md` with any changes that users may be interested in.
5. Update `continuous-integration.md` to update the version number for the installation instructions.
6. Commit the changes, and open a PR.
1. Create a PR that bumps the version and updates the changelog:
1. `git fetch upstream`
2. `git checkout -B bump-version upstream/master`
3. `cargo xtask bump <BUMP>`
- This will update the version of all the crates.
- `cargo set-version` must first be installed with `cargo install cargo-edit`.
- Replace `<BUMP>` with the kind of bump (patch, alpha, etc.)
4. `cargo xtask changelog`
- This will update `CHANGELOG.md` to add a list of all changes at the top. You will need to move those into the appropriate categories. Most changes that are generally not relevant to a user should be removed. Rewrite the descriptions so that a user can reasonably figure out what it means.
5. `git add --update .`
6. `git commit`
7. `git push`
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
```bash
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \

2039
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +1,136 @@
[workspace]
members = [
".",
"crates/*",
"examples/remove-emphasis/mdbook-remove-emphasis", "guide/guide-helper",
]
[workspace.lints.clippy]
all = { level = "allow", priority = -2 }
correctness = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
exhaustive_enums = "warn"
exhaustive_structs = "warn"
manual_non_exhaustive = "warn"
[workspace.lints.rust]
missing_docs = "warn"
rust_2018_idioms = "warn"
unreachable_pub = "warn"
[workspace.package]
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/rust-lang/mdBook"
rust-version = "1.88.0" # Keep in sync with installation.md and .github/workflows/main.yml
[workspace.dependencies]
anyhow = "1.0.100"
axum = "0.8.6"
clap = { version = "4.5.50", features = ["cargo", "wrap_help"] }
clap_complete = "4.5.59"
ego-tree = "0.10.0"
elasticlunr-rs = "3.0.2"
font-awesome-as-a-crate = "0.3.0"
futures-util = "0.3.31"
glob = "0.3.3"
handlebars = "6.3.2"
hex = "0.4.3"
html5ever = "0.35.0"
indexmap = "2.12.0"
ignore = "0.4.24"
mdbook-core = { path = "crates/mdbook-core", version = "0.5.0-beta.1" }
mdbook-driver = { path = "crates/mdbook-driver", version = "0.5.0-beta.1" }
mdbook-html = { path = "crates/mdbook-html", version = "0.5.0-beta.1" }
mdbook-markdown = { path = "crates/mdbook-markdown", version = "0.5.0-beta.1" }
mdbook-preprocessor = { path = "crates/mdbook-preprocessor", version = "0.5.0-beta.1" }
mdbook-renderer = { path = "crates/mdbook-renderer", version = "0.5.0-beta.1" }
mdbook-summary = { path = "crates/mdbook-summary", version = "0.5.0-beta.1" }
memchr = "2.7.6"
notify = "8.2.0"
notify-debouncer-mini = "0.7.0"
opener = "0.8.3"
pathdiff = "0.2.3"
pulldown-cmark = { version = "0.13.0", default-features = false, features = ["html"] } # Do not update, part of the public api.
regex = "1.12.2"
select = "0.6.1"
semver = "1.0.27"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
sha2 = "0.10.9"
shlex = "1.3.0"
snapbox = "0.6.22"
tempfile = "3.23.0"
tokio = "1.48.0"
toml = "0.9.8"
topological-sort = "0.2.2"
tower-http = "0.6.6"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
walkdir = "2.5.0"
[package]
name = "mdbook"
version = "0.4.40"
version = "0.5.0-beta.1"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>"
]
documentation = "https://rust-lang.github.io/mdBook/index.html"
edition = "2021"
edition.workspace = true
exclude = ["/guide/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0"
license.workspace = true
readme = "README.md"
repository = "https://github.com/rust-lang/mdBook"
repository.workspace = true
description = "Creates a book from markdown files"
rust-version = "1.74"
rust-version.workspace = true
[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 = "5.0"
log = "0.4.17"
memchr = "2.5.0"
opener = "0.7.0"
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"] }
serde_json = "1.0.96"
shlex = "1.3.0"
tempfile = "3.4.0"
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
topological-sort = "0.2.2"
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
mdbook-core.workspace = true
mdbook-driver.workspace = true
mdbook-html.workspace = true
mdbook-markdown.workspace = true
mdbook-preprocessor.workspace = true
mdbook-renderer.workspace = true
mdbook-summary.workspace = true
opener.workspace = true
toml.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
# Watch feature
notify = { version = "6.1.1", optional = true }
notify-debouncer-mini = { version = "0.4.1", optional = true }
ignore = { version = "0.4.20", optional = true }
pathdiff = { version = "0.2.1", optional = true }
walkdir = { version = "2.3.3", optional = true }
ignore = { workspace = true, optional = true }
notify = { workspace = true, optional = true }
notify-debouncer-mini = { workspace = true, optional = true }
pathdiff = { workspace = true, optional = true }
walkdir = { workspace = true, 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 }
# Search feature
elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "4.0.0", optional = true }
axum = { workspace = true, features = ["ws"], optional = true }
futures-util = { workspace = true, optional = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"], optional = true }
tower-http = { workspace = true, features = ["fs", "trace"], optional = true }
[dev-dependencies]
assert_cmd = "2.0.11"
predicates = "3.0.3"
select = "0.6.0"
semver = "1.0.17"
pretty_assertions = "1.3.0"
walkdir = "2.3.3"
glob.workspace = true
regex.workspace = true
select.workspace = true
semver.workspace = true
serde_json.workspace = true
snapbox = { workspace = true, features = ["diff", "dir", "term-svg", "regex", "json"] }
tempfile.workspace = true
walkdir.workspace = true
[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"]
search = ["dep:elasticlunr-rs", "dep:ammonia"]
serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"]
search = ["mdbook-html/search"]
[[bin]]
doc = false
@@ -73,3 +139,19 @@ name = "mdbook"
[[example]]
name = "nop-preprocessor"
test = true
[[example]]
name = "remove-emphasis"
path = "examples/remove-emphasis/test.rs"
crate-type = ["lib"]
test = true
[[test]]
harness = false
test = false
name = "gui"
path = "tests/gui/runner.rs"
crate-type = ["bin"]
[lints]
workspace = true

View File

@@ -1,6 +1,6 @@
# mdBook
[![Build Status](https://github.com/rust-lang/mdBook/workflows/CI/badge.svg?event=push)](https://github.com/rust-lang/mdBook/actions?workflow=CI)
[![CI Status](https://github.com/rust-lang/mdBook/actions/workflows/main.yml/badge.svg)](https://github.com/rust-lang/mdBook/actions/workflows/main.yml)
[![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook)
[![LICENSE](https://img.shields.io/github/license/rust-lang/mdBook.svg)](LICENSE)

38
ci/publish-guide.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# This publishes the user guide to GitHub Pages.
#
# If this is a pre-release, then it goes in a separate directory called "pre-release".
# Commits are amended to avoid keeping history which can balloon the repo size.
set -ex
cargo run --no-default-features -F search -- build guide
VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[] | select(.name == "mdbook") | .version')
if [[ "$VERSION" == *-* ]]; then
PRERELEASE=true
else
PRERELEASE=false
fi
git fetch origin gh-pages
git worktree add gh-pages gh-pages
git config user.name "Deploy from CI"
git config user.email ""
cd gh-pages
if [[ "$PRERELEASE" == "true" ]]
then
rm -rf pre-release
mv ../guide/book pre-release
git add pre-release
git commit --amend -m "Deploy $GITHUB_SHA pre-release to gh-pages"
else
# Delete everything except pre-release and .git.
find . -mindepth 1 -maxdepth 1 -not -name "pre-release" -not -name ".git" -exec rm -rf {} +
# Copy the guide here.
find ../guide/book/ -mindepth 1 -maxdepth 1 -exec mv {} . \;
git add .
git commit --amend -m "Deploy $GITHUB_SHA to gh-pages"
fi
git push --force origin +gh-pages

44
ci/update-dependencies.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Updates all compatible Cargo dependencies.
#
# I wasn't able to get Renovate to update compatible dependencies in a way
# that I like, so this script takes care of it. This uses `cargo upgrade` to
# ensure that `Cargo.toml` also gets updated. This also makes sure that all
# transitive dependencies are updated.
set -ex
git fetch origin update-dependencies
if git checkout update-dependencies
then
git reset --hard origin/master
else
git checkout -b update-dependencies
fi
cat > commit-message << 'EOF'
Update cargo dependencies
```
EOF
cargo upgrade >> commit-message
echo '```' >> commit-message
if git diff --quiet
then
echo "No changes detected, exiting."
exit 0
fi
# Also update any transitive dependencies.
cargo update
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
git commit -F commit-message
git push --force origin update-dependencies
gh pr create --fill \
--head update-dependencies \
--base master

View File

@@ -0,0 +1,12 @@
[package]
name = "mdbook-compare"
publish = false
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
[lints]
workspace = true

View File

@@ -0,0 +1,26 @@
# mdbook-compare
This is a simple utility to compare the output of two different versions of mdbook.
To use this:
1. Install [`tidy`](https://www.html-tidy.org/).
2. Install or build the initial version of mdbook that you want to compare.
3. Install or build the new version of mdbook that you want to compare.
4. Run `mdbook-compare` with the arguments to the mdbook executables and the books to build.
```sh
cargo run --manifest-path /path/to/mdBook/Cargo.toml -p mdbook-compare -- \
/path/to/orig/mdbook /path/to/my-book /path/to/new/mdbook /path/to/my-book
```
It takes two separate paths for the book to use for "before" and "after" in case you need to customize the book to run on older versions. If you don't need that, then you can use the same directory for both the before and after.
`mdbook-compare` will do the following:
1. Clean up any book directories.
2. Build the book with the first mdbook.
3. Build the book with the second mdbook.
4. The output of those two commands are stored in directories called `compare1` and `compare2`.
5. The HTML in those directories is normalized using `tidy`.
6. Runs `git diff` to compare the output.

View File

@@ -0,0 +1,113 @@
//! Utility to compare the output of two different versions of mdbook.
use std::path::Path;
use std::process::Command;
macro_rules! error {
($msg:literal $($arg:tt)*) => {
eprint!("error: ");
eprintln!($msg $($arg)*);
std::process::exit(1);
};
}
fn main() {
let mut args = std::env::args().skip(1);
let (Some(mdbook1), Some(book1), Some(mdbook2), Some(book2)) =
(args.next(), args.next(), args.next(), args.next())
else {
eprintln!("error: Expected four arguments: <exe1> <dir1> <exe2> <dir2>");
std::process::exit(1);
};
let mdbook1 = Path::new(&mdbook1);
let mdbook2 = Path::new(&mdbook2);
let book1 = Path::new(&book1);
let book2 = Path::new(&book2);
let compare1 = Path::new("compare1");
let compare2 = Path::new("compare2");
clean(compare1);
clean(compare2);
clean(&book1.join("book"));
clean(&book2.join("book"));
build(mdbook1, book1);
std::fs::rename(book1.join("book"), compare1).unwrap();
build(mdbook2, book2);
std::fs::rename(book2.join("book"), compare2).unwrap();
diff(compare1, compare2);
}
fn clean(path: &Path) {
if path.exists() {
println!("removing {path:?}");
std::fs::remove_dir_all(path).unwrap();
}
}
fn build(mdbook: &Path, book: &Path) {
println!("running `{mdbook:?} build` in `{book:?}`");
let status = Command::new(mdbook)
.arg("build")
.current_dir(book)
.status()
.unwrap_or_else(|e| {
error!("expected {mdbook:?} executable to exist: {e}");
});
if !status.success() {
error!("process {mdbook:?} failed");
}
process(&book.join("book"));
}
fn process(path: &Path) {
for entry in std::fs::read_dir(path).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
process(&path);
} else {
if path.extension().is_some_and(|ext| ext == "html") {
tidy(&path);
process_html(&path);
} else {
std::fs::remove_file(path).unwrap();
}
}
}
}
fn process_html(path: &Path) {
let content = std::fs::read_to_string(path).unwrap();
let Some(start_index) = content.find("<main>") else {
return;
};
let end_index = content.rfind("</main>").unwrap();
let new_content = &content[start_index..end_index + 8];
std::fs::write(path, new_content).unwrap();
}
fn tidy(path: &Path) {
// quiet, no wrap, modify in place
let args = "-q -w 0 -m --custom-tags yes --drop-empty-elements no";
println!("running `tidy {args}` in `{path:?}`");
let status = Command::new("tidy")
.args(args.split(' '))
.arg(path)
.status()
.expect("tidy should be installed");
if !status.success() {
// Exit code 1 is a warning.
if status.code() != Some(1) {
error!("tidy failed: {status}");
}
}
}
fn diff(a: &Path, b: &Path) {
let args = "diff --no-index";
println!("running `git {args} {a:?} {b:?}`");
Command::new("git")
.args(args.split(' '))
.args([a, b])
.status()
.unwrap();
}

View File

@@ -0,0 +1,22 @@
[package]
name = "mdbook-core"
version = "0.5.0-beta.1"
description = "The base support library for mdbook, intended for internal use only"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,13 @@
# mdbook-core
[![Documentation](https://img.shields.io/docsrs/mdbook-core)](https://docs.rs/mdbook-core)
[![crates.io](https://img.shields.io/crates/v/mdbook-core.svg)](https://crates.io/crates/mdbook-core)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the base support library for [mdBook](https://rust-lang.github.io/mdBook/). It is intended for internal use only. Other mdBook crates depend on this for any types that are shared across the crates.
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View File

@@ -0,0 +1,288 @@
//! A tree structure representing a book.
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::fmt::{self, Display, Formatter};
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
#[cfg(test)]
mod tests;
/// A tree structure representing a book.
///
/// A book is just a collection of [`BookItems`] which are accessible by
/// either iterating (immutably) over the book with [`iter()`], or recursively
/// applying a closure to each item to mutate the chapters, using
/// [`for_each_mut()`].
///
/// [`iter()`]: #method.iter
/// [`for_each_mut()`]: #method.for_each_mut
#[allow(
clippy::exhaustive_structs,
reason = "This cannot be extended without breaking preprocessors."
)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Book {
/// The items in this book.
pub items: Vec<BookItem>,
}
impl Book {
/// Create an empty book.
pub fn new() -> Self {
Default::default()
}
/// Creates a new book with the given items.
pub fn new_with_items(items: Vec<BookItem>) -> Book {
Book { items }
}
/// Get a depth-first iterator over the items in the book.
pub fn iter(&self) -> BookItems<'_> {
BookItems {
items: self.items.iter().collect(),
}
}
/// A depth-first iterator over each [`Chapter`], skipping draft chapters.
pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
self.iter().filter_map(|item| match item {
BookItem::Chapter(ch) if !ch.is_draft_chapter() => Some(ch),
_ => None,
})
}
/// Recursively apply a closure to each item in the book, allowing you to
/// mutate them.
///
/// # Note
///
/// Unlike the `iter()` method, this requires a closure instead of returning
/// an iterator. This is because using iterators can possibly allow you
/// to have iterator invalidation errors.
pub fn for_each_mut<F>(&mut self, mut func: F)
where
F: FnMut(&mut BookItem),
{
for_each_mut(&mut func, &mut self.items);
}
/// Recursively apply a closure to each non-draft chapter in the book,
/// allowing you to mutate them.
pub fn for_each_chapter_mut<F>(&mut self, mut func: F)
where
F: FnMut(&mut Chapter),
{
for_each_mut(
&mut |item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
func(ch)
},
&mut self.items,
);
}
/// Append a `BookItem` to the `Book`.
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
self.items.push(item.into());
self
}
}
fn for_each_mut<'a, F, I>(func: &mut F, items: I)
where
F: FnMut(&mut BookItem),
I: IntoIterator<Item = &'a mut BookItem>,
{
for item in items {
if let BookItem::Chapter(ch) = item {
for_each_mut(func, &mut ch.sub_items);
}
func(item);
}
}
/// Enum representing any type of item which can be added to a book.
#[allow(
clippy::exhaustive_enums,
reason = "This cannot be extended without breaking preprocessors."
)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BookItem {
/// A nested chapter.
Chapter(Chapter),
/// A section separator.
Separator,
/// A part title.
PartTitle(String),
}
impl From<Chapter> for BookItem {
fn from(other: Chapter) -> BookItem {
BookItem::Chapter(other)
}
}
/// The representation of a "chapter", usually mapping to a single file on
/// disk however it may contain multiple sub-chapters.
#[allow(
clippy::exhaustive_structs,
reason = "This cannot be extended without breaking preprocessors."
)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Chapter {
/// The chapter's name.
pub name: String,
/// The chapter's contents.
pub content: String,
/// The chapter's section number, if it has one.
pub number: Option<SectionNumber>,
/// Nested items.
pub sub_items: Vec<BookItem>,
/// The chapter's location, relative to the `SUMMARY.md` file.
///
/// **Note**: After the index preprocessor runs, any README files will be
/// modified to be `index.md`. If you need access to the actual filename
/// on disk, use [`Chapter::source_path`] instead.
///
/// This is `None` for a draft chapter.
pub path: Option<PathBuf>,
/// The chapter's source file, relative to the `SUMMARY.md` file.
///
/// **Note**: Beware that README files will internally be treated as
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
/// exists if you need access to the true file path.
///
/// This is `None` for a draft chapter, or a synthetically generated
/// chapter that has no file on disk.
pub source_path: Option<PathBuf>,
/// An ordered list of the names of each chapter above this one in the hierarchy.
pub parent_names: Vec<String>,
}
impl Chapter {
/// Create a new chapter with the provided content.
pub fn new<P: Into<PathBuf>>(
name: &str,
content: String,
p: P,
parent_names: Vec<String>,
) -> Chapter {
let path: PathBuf = p.into();
Chapter {
name: name.to_string(),
content,
path: Some(path.clone()),
source_path: Some(path),
parent_names,
..Default::default()
}
}
/// Create a new draft chapter that is not attached to a source markdown file (and thus
/// has no content).
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
Chapter {
name: name.to_string(),
content: String::new(),
path: None,
source_path: None,
parent_names,
..Default::default()
}
}
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
pub fn is_draft_chapter(&self) -> bool {
self.path.is_none()
}
}
impl Display for Chapter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(ref section_number) = self.number {
write!(f, "{section_number} ")?;
}
write!(f, "{}", self.name)
}
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
/// a pretty `Display` impl.
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct SectionNumber(Vec<u32>);
impl SectionNumber {
/// Creates a new [`SectionNumber`].
pub fn new(numbers: impl Into<Vec<u32>>) -> SectionNumber {
SectionNumber(numbers.into())
}
}
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
for item in &self.0 {
write!(f, "{item}.")?;
}
Ok(())
}
}
}
impl Deref for SectionNumber {
type Target = Vec<u32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SectionNumber {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<u32> for SectionNumber {
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
SectionNumber(it.into_iter().collect())
}
}
/// A depth-first iterator over the items in a book.
///
/// # Note
///
/// This struct shouldn't be created directly, instead prefer the
/// [`Book::iter()`] method.
pub struct BookItems<'a> {
items: VecDeque<&'a BookItem>,
}
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;
fn next(&mut self) -> Option<Self::Item> {
let item = self.items.pop_front();
if let Some(BookItem::Chapter(ch)) = item {
// if we wanted a breadth-first iterator we'd `extend()` here
for sub_item in ch.sub_items.iter().rev() {
self.items.push_front(sub_item);
}
}
item
}
}

View File

@@ -0,0 +1,123 @@
use super::*;
#[test]
fn section_number_has_correct_dotted_representation() {
let inputs = vec![
(vec![0], "0."),
(vec![1, 3], "1.3."),
(vec![1, 2, 3], "1.2.3."),
];
for (input, should_be) in inputs {
let section_number = SectionNumber(input).to_string();
assert_eq!(section_number, should_be);
}
}
#[test]
fn book_iter_iterates_over_sequential_items() {
let items = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
..Default::default()
}),
BookItem::Separator,
];
let book = Book::new_with_items(items);
let should_be: Vec<_> = book.items.iter().collect();
let got: Vec<_> = book.iter().collect();
assert_eq!(got, should_be);
}
#[test]
fn for_each_mut_visits_all_items() {
let items = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
source_path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
];
let mut book = Book::new_with_items(items);
let num_items = book.iter().count();
let mut visited = 0;
book.for_each_mut(|_| visited += 1);
assert_eq!(visited, num_items);
}
#[test]
fn iterate_over_nested_book_items() {
let items = vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from("# Chapter 1"),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
source_path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
];
let book = Book::new_with_items(items);
let got: Vec<_> = book.iter().collect();
assert_eq!(got.len(), 5);
// checking the chapter names are in the order should be sufficient here...
let chapter_names: Vec<String> = got
.into_iter()
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
})
.collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),
String::from("Goodbye World"),
];
assert_eq!(chapter_names, should_be);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
//! The base support library for mdbook, intended for internal use only.
/// The current version of `mdbook`.
///
/// This is provided as a way for custom preprocessors and renderers to do
/// compatibility checks.
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub mod book;
pub mod config;
pub mod utils;
/// The error types used in mdbook.
pub mod errors {
pub use anyhow::{Error, Result};
}

View File

@@ -1,22 +1,38 @@
use crate::errors::*;
use log::{debug, trace};
use std::fs::{self, File};
use std::io::Write;
use std::path::{Component, Path, PathBuf};
//! Filesystem utilities and helpers.
/// Naively replaces any path separator with a forward-slash '/'
pub fn normalize_path(path: &str) -> String {
use std::path::is_separator;
path.chars()
.map(|ch| if is_separator(ch) { '/' } else { ch })
.collect::<String>()
use anyhow::{Context, Result};
use std::fs;
use std::path::{Component, Path, PathBuf};
use tracing::debug;
/// Reads a file into a string.
///
/// Equivalent to [`std::fs::read_to_string`] with better error messages.
pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
fs::read_to_string(path).with_context(|| format!("failed to read `{}`", path.display()))
}
/// Write the given data to a file, creating it first if necessary
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
let path = build_dir.join(filename);
/// Writes a file to disk.
///
/// Equivalent to [`std::fs::write`] with better error messages. This will
/// also create the parent directory if it doesn't exist.
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
let path = path.as_ref();
debug!("Writing `{}`", path.display());
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
fs::write(path, contents.as_ref())
.with_context(|| format!("failed to write `{}`", path.display()))
}
create_file(&path)?.write_all(content).map_err(Into::into)
/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
let p = p.as_ref();
fs::create_dir_all(p)
.with_context(|| format!("failed to create directory `{}`", p.display()))?;
Ok(())
}
/// Takes a path and returns a path containing just enough `../` to point to
@@ -27,7 +43,7 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
///
/// ```rust
/// # use std::path::Path;
/// # use mdbook::utils::fs::path_to_root;
/// # use mdbook_core::utils::fs::path_to_root;
/// let path = Path::new("some/relative/path");
/// assert_eq!(path_to_root(path), "../../");
/// ```
@@ -54,30 +70,19 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
})
}
/// This function creates a file and returns it. But before creating the file
/// it checks every directory in the path to see if it exists,
/// and if it does not it will be created.
pub fn create_file(path: &Path) -> Result<File> {
debug!("Creating {}", path.display());
// Construct path
if let Some(p) = path.parent() {
trace!("Parent directory is: {:?}", p);
fs::create_dir_all(p)?;
}
File::create(path).map_err(Into::into)
}
/// Removes all the content of a directory but not the directory itself
/// Removes all the content of a directory but not the directory itself.
pub fn remove_dir_content(dir: &Path) -> Result<()> {
for item in fs::read_dir(dir)?.flatten() {
for item in fs::read_dir(dir)
.with_context(|| format!("failed to read directory `{}`", dir.display()))?
.flatten()
{
let item = item.path();
if item.is_dir() {
fs::remove_dir_all(item)?;
fs::remove_dir_all(&item)
.with_context(|| format!("failed to remove `{}`", item.display()))?;
} else {
fs::remove_file(item)?;
fs::remove_file(&item)
.with_context(|| format!("failed to remove `{}`", item.display()))?;
}
}
Ok(())
@@ -168,7 +173,7 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
use std::fs::OpenOptions;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let mut reader = File::open(from)?;
let mut reader = std::fs::File::open(from)?;
let metadata = reader.metadata()?;
if !metadata.is_file() {
anyhow::bail!(
@@ -202,17 +207,11 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
}
}
pub fn get_404_output_file(input_404: &Option<String>) -> String {
input_404
.as_ref()
.unwrap_or(&"404.md".to_string())
.replace(".md", ".html")
}
#[cfg(test)]
mod tests {
use super::copy_files_except_ext;
use std::{fs, io::Result, path::Path};
use super::*;
use std::io::Result;
use std::path::Path;
#[cfg(target_os = "windows")]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
@@ -228,47 +227,27 @@ mod tests {
fn copy_files_except_ext_test() {
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(e) => panic!("Could not create a temp dir: {}", e),
Err(e) => panic!("Could not create a temp dir: {e}"),
};
// Create a couple of files
if let Err(err) = fs::File::create(tmp.path().join("file.txt")) {
panic!("Could not create file.txt: {}", err);
}
if let Err(err) = fs::File::create(tmp.path().join("file.md")) {
panic!("Could not create file.md: {}", err);
}
if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
panic!("Could not create file.png: {}", err);
}
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
panic!("Could not create sub_dir: {}", err);
}
if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
panic!("Could not create sub_dir/file.png: {}", err);
}
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
panic!("Could not create sub_dir_exists: {}", err);
}
if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
panic!("Could not create sub_dir_exists/file.txt: {}", err);
}
write(tmp.path().join("file.txt"), "").unwrap();
write(tmp.path().join("file.md"), "").unwrap();
write(tmp.path().join("file.png"), "").unwrap();
write(tmp.path().join("sub_dir/file.png"), "").unwrap();
write(tmp.path().join("sub_dir_exists/file.txt"), "").unwrap();
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
panic!("Could not symlink file.png: {}", err);
panic!("Could not symlink file.png: {err}");
}
// Create output dir
if let Err(err) = fs::create_dir(tmp.path().join("output")) {
panic!("Could not create output: {}", err);
}
if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
panic!("Could not create output/sub_dir_exists: {}", err);
}
create_dir_all(tmp.path().join("output")).unwrap();
create_dir_all(tmp.path().join("output/sub_dir_exists")).unwrap();
if let Err(e) =
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])
{
panic!("Error while executing the function:\n{:?}", e);
panic!("Error while executing the function:\n{e:?}");
}
// Check if the correct files where created

View File

@@ -0,0 +1,78 @@
//! Utilities for dealing with HTML.
use std::borrow::Cow;
/// Escape characters to make it safe for an HTML string.
pub fn escape_html_attribute(text: &str) -> Cow<'_, str> {
let needs_escape: &[char] = &['<', '>', '\'', '"', '\\', '&'];
let mut s = text;
let mut output = String::new();
while let Some(next) = s.find(needs_escape) {
output.push_str(&s[..next]);
match s.as_bytes()[next] {
b'<' => output.push_str("&lt;"),
b'>' => output.push_str("&gt;"),
b'\'' => output.push_str("&#39;"),
b'"' => output.push_str("&quot;"),
b'\\' => output.push_str("&#92;"),
b'&' => output.push_str("&amp;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
if output.is_empty() {
Cow::Borrowed(text)
} else {
output.push_str(s);
Cow::Owned(output)
}
}
/// Escape `<`, `>`, and '&' for HTML.
pub fn escape_html(text: &str) -> Cow<'_, str> {
let needs_escape: &[char] = &['<', '>', '&'];
let mut s = text;
let mut output = String::new();
while let Some(next) = s.find(needs_escape) {
output.push_str(&s[..next]);
match s.as_bytes()[next] {
b'<' => output.push_str("&lt;"),
b'>' => output.push_str("&gt;"),
b'&' => output.push_str("&amp;"),
_ => unreachable!(),
}
s = &s[next + 1..];
}
if output.is_empty() {
Cow::Borrowed(text)
} else {
output.push_str(s);
Cow::Owned(output)
}
}
#[test]
fn attributes_are_escaped() {
assert_eq!(escape_html_attribute(""), "");
assert_eq!(escape_html_attribute("<"), "&lt;");
assert_eq!(escape_html_attribute(">"), "&gt;");
assert_eq!(escape_html_attribute("<>"), "&lt;&gt;");
assert_eq!(escape_html_attribute("<test>"), "&lt;test&gt;");
assert_eq!(escape_html_attribute("a<test>b"), "a&lt;test&gt;b");
assert_eq!(escape_html_attribute("'"), "&#39;");
assert_eq!(escape_html_attribute("\\"), "&#92;");
assert_eq!(escape_html_attribute("&"), "&amp;");
}
#[test]
fn html_is_escaped() {
assert_eq!(escape_html(""), "");
assert_eq!(escape_html("<"), "&lt;");
assert_eq!(escape_html(">"), "&gt;");
assert_eq!(escape_html("&"), "&amp;");
assert_eq!(escape_html("<>"), "&lt;&gt;");
assert_eq!(escape_html("<test>"), "&lt;test&gt;");
assert_eq!(escape_html("a<test>b"), "a&lt;test&gt;b");
assert_eq!(escape_html("'"), "'");
assert_eq!(escape_html("\\"), "\\");
}

View File

@@ -0,0 +1,37 @@
//! Various helpers and utilities.
use anyhow::Error;
use std::fmt::Write;
use tracing::error;
pub mod fs;
mod html;
mod toml_ext;
pub(crate) use self::toml_ext::TomlExt;
pub use self::html::{escape_html, escape_html_attribute};
/// Defines a `static` with a [`regex::Regex`].
#[macro_export]
macro_rules! static_regex {
($name:ident, $regex:literal) => {
static $name: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new($regex).unwrap());
};
($name:ident, bytes, $regex:literal) => {
static $name: std::sync::LazyLock<regex::bytes::Regex> =
std::sync::LazyLock::new(|| regex::bytes::Regex::new($regex).unwrap());
};
}
/// Prints a "backtrace" of some `Error`.
pub fn log_backtrace(e: &Error) {
let mut message = format!("{e}");
for cause in e.chain().skip(1) {
write!(message, "\n\tCaused by: {cause}").unwrap();
}
error!("{message}");
}

View File

@@ -1,10 +1,13 @@
//! Helper for working with toml types.
use toml::value::{Table, Value};
/// Helper for working with toml types.
pub(crate) trait TomlExt {
/// Read a dotted key.
fn read(&self, key: &str) -> Option<&Value>;
fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
/// Insert with a dotted key.
fn insert(&mut self, key: &str, value: Value);
fn delete(&mut self, key: &str) -> Option<Value>;
}
impl TomlExt for Value {
@@ -16,14 +19,6 @@ impl TomlExt for Value {
}
}
fn read_mut(&mut self, key: &str) -> Option<&mut Value> {
if let Some((head, tail)) = split(key) {
self.get_mut(head)?.read_mut(tail)
} else {
self.get_mut(key)
}
}
fn insert(&mut self, key: &str, value: Value) {
if !self.is_table() {
*self = Value::Table(Table::new());
@@ -40,16 +35,6 @@ impl TomlExt for Value {
table.insert(key.to_string(), value);
}
}
fn delete(&mut self, key: &str) -> Option<Value> {
if let Some((head, tail)) = split(key) {
self.get_mut(head)?.delete(tail)
} else if let Some(table) = self.as_table_mut() {
table.remove(key)
} else {
None
}
}
}
fn split(key: &str) -> Option<(&str, &str)> {
@@ -65,12 +50,11 @@ fn split(key: &str) -> Option<(&str, &str)> {
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn read_simple_table() {
let src = "[table]";
let value = Value::from_str(src).unwrap();
let value: Value = toml::from_str(src).unwrap();
let got = value.read("table").unwrap();
@@ -80,7 +64,7 @@ mod tests {
#[test]
fn read_nested_item() {
let src = "[table]\nnested=true";
let value = Value::from_str(src).unwrap();
let value: Value = toml::from_str(src).unwrap();
let got = value.read("table.nested").unwrap();
@@ -107,24 +91,4 @@ mod tests {
let inserted = value.read("first.second").unwrap();
assert_eq!(inserted, &item);
}
#[test]
fn delete_a_top_level_item() {
let src = "top = true";
let mut value = Value::from_str(src).unwrap();
let got = value.delete("top").unwrap();
assert_eq!(got, Value::Boolean(true));
}
#[test]
fn delete_a_nested_item() {
let src = "[table]\n nested = true";
let mut value = Value::from_str(src).unwrap();
let got = value.delete("table.nested").unwrap();
assert_eq!(got, Value::Boolean(true));
}
}

View File

@@ -0,0 +1,32 @@
[package]
name = "mdbook-driver"
version = "0.5.0-beta.1"
description = "High-level library for running mdBook"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
indexmap.workspace = true
mdbook-core.workspace = true
mdbook-html.workspace = true
mdbook-markdown.workspace = true
mdbook-preprocessor.workspace = true
mdbook-renderer.workspace = true
mdbook-summary.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true
tempfile.workspace = true
toml.workspace = true
topological-sort.workspace = true
tracing.workspace = true
[lints]
workspace = true
[features]
search = ["mdbook-html/search"]

View File

@@ -0,0 +1,13 @@
# mdbook-driver
[![Documentation](https://img.shields.io/docsrs/mdbook-driver)](https://docs.rs/mdbook-driver)
[![crates.io](https://img.shields.io/crates/v/mdbook-driver.svg)](https://crates.io/crates/mdbook-driver)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the high-level Rust library for running [mdBook](https://rust-lang.github.io/mdBook/). New books can be created using [`BookBuilder`](https://docs.rs/mdbook-driver/latest/mdbook_driver/init/struct.BookBuilder.html). The primary type [`MDBook`](https://docs.rs/mdbook-driver/latest/mdbook_driver/struct.MDBook.html) can be used to manage and render books.
> This crate is maintained by the mdBook team for use by the wider ecosystem. This crate follows [semver compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for its APIs.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View File

@@ -1,50 +1,32 @@
use super::{Preprocessor, PreprocessorContext};
use crate::book::Book;
use crate::errors::*;
use log::{debug, trace, warn};
use shlex::Shlex;
use std::io::{self, Read, Write};
use std::process::{Child, Command, Stdio};
use anyhow::{Context, Result, ensure};
use mdbook_core::book::Book;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use std::io::Write;
use std::path::PathBuf;
use std::process::{Child, Stdio};
use tracing::{debug, trace, warn};
/// A custom preprocessor which will shell out to a 3rd-party program.
///
/// # Preprocessing Protocol
///
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
/// execute the shell command `$cmd supports $renderer`. If the renderer is
/// supported, custom preprocessors should exit with a exit code of `0`,
/// any other exit code be considered as unsupported.
///
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
/// should then "return" a processed book by printing it to `stdout` as JSON.
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
/// to parse the input provided by `mdbook`.
///
/// Exiting with a non-zero exit code while preprocessing is considered an
/// error. `stderr` is passed directly through to the user, so it can be used
/// for logging or emitting warnings if desired.
///
/// # Examples
///
/// An example preprocessor is available in this project's `examples/`
/// directory.
/// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html>
/// for a description of the preprocessor protocol.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdPreprocessor {
name: String,
cmd: String,
root: PathBuf,
optional: bool,
}
impl CmdPreprocessor {
/// Create a new `CmdPreprocessor`.
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
CmdPreprocessor { name, cmd }
}
/// A convenience function custom preprocessors can use to parse the input
/// written to `stdin` by a `CmdRenderer`.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
CmdPreprocessor {
name,
cmd,
root,
optional,
}
}
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
@@ -70,22 +52,6 @@ impl CmdPreprocessor {
pub fn cmd(&self) -> &str {
&self.cmd
}
fn command(&self) -> Result<Command> {
let mut words = Shlex::new(&self.cmd);
let executable = match words.next() {
Some(e) => e,
None => bail!("Command string was empty"),
};
let mut cmd = Command::new(executable);
for arg in words {
cmd.arg(arg);
}
Ok(cmd)
}
}
impl Preprocessor for CmdPreprocessor {
@@ -94,19 +60,31 @@ impl Preprocessor for CmdPreprocessor {
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
let mut cmd = self.command()?;
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
let mut child = cmd
let mut child = match cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.current_dir(&self.root)
.spawn()
.with_context(|| {
format!(
"Unable to start the \"{}\" preprocessor. Is it installed?",
self.name()
)
})?;
{
Ok(c) => c,
Err(e) => {
crate::handle_command_error(
e,
self.optional,
"preprocessor",
"preprocessor",
&self.name,
&self.cmd,
)?;
// This should normally not be reached, since the validation
// for NotFound should have already happened when running the
// "supports" command.
return Ok(book);
}
};
self.write_input_to_child(&mut child, &book, ctx);
@@ -134,45 +112,37 @@ impl Preprocessor for CmdPreprocessor {
})
}
fn supports_renderer(&self, renderer: &str) -> bool {
fn supports_renderer(&self, renderer: &str) -> Result<bool> {
debug!(
"Checking if the \"{}\" preprocessor supports \"{}\"",
self.name(),
renderer
);
let mut cmd = match self.command() {
Ok(c) => c,
Err(e) => {
warn!(
"Unable to create the command for the \"{}\" preprocessor, {}",
self.name(),
e
);
return false;
}
};
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
let outcome = cmd
match cmd
.arg("supports")
.arg(renderer)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&self.root)
.status()
.map(|status| status.code() == Some(0));
if let Err(ref e) = outcome {
if e.kind() == io::ErrorKind::NotFound {
warn!(
"The command wasn't found, is the \"{}\" preprocessor installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
{
Ok(status) => Ok(status.code() == Some(0)),
Err(e) => {
crate::handle_command_error(
e,
self.optional,
"preprocessor",
"preprocessor",
&self.name,
&self.cmd,
)?;
Ok(false)
}
}
outcome.unwrap_or(false)
}
}
@@ -183,14 +153,19 @@ mod tests {
use std::path::Path;
fn guide() -> MDBook {
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../guide");
MDBook::load(example).unwrap()
}
#[test]
fn round_trip_write_and_parse_input() {
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
let md = guide();
let cmd = CmdPreprocessor::new(
"test".to_string(),
"test".to_string(),
md.root.clone(),
false,
);
let ctx = PreprocessorContext::new(
md.root.clone(),
md.config.clone(),
@@ -200,7 +175,7 @@ mod tests {
let mut buffer = Vec::new();
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
let (got_ctx, got_book) = mdbook_preprocessor::parse_input(buffer.as_slice()).unwrap();
assert_eq!(got_book, md.book);
assert_eq!(got_ctx, ctx);

View File

@@ -1,19 +1,19 @@
use regex::Regex;
use anyhow::Result;
use mdbook_core::book::{Book, BookItem};
use mdbook_core::static_regex;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use std::path::Path;
use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem};
use crate::errors::*;
use log::warn;
use once_cell::sync::Lazy;
use tracing::warn;
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in markdown-based documentation.
#[derive(Default)]
#[non_exhaustive]
pub struct IndexPreprocessor;
impl IndexPreprocessor {
pub(crate) const NAME: &'static str = "index";
/// Name of this preprocessor.
pub const NAME: &'static str = "index";
/// Create a new `IndexPreprocessor`.
pub fn new() -> Self {
@@ -68,9 +68,9 @@ 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_regex!(README, r"(?i)^readme$");
RE.is_match(
README.is_match(
path.as_ref()
.file_stem()
.and_then(std::ffi::OsStr::to_str)

View File

@@ -1,17 +1,18 @@
use crate::errors::*;
use crate::utils::{
use self::take_lines::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
use regex::{CaptureMatches, Captures, Regex};
use std::fs;
use anyhow::{Context, Result};
use mdbook_core::book::{Book, BookItem};
use mdbook_core::static_regex;
use mdbook_core::utils::fs;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use regex::{CaptureMatches, Captures};
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use tracing::{error, warn};
use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem};
use log::{error, warn};
use once_cell::sync::Lazy;
mod take_lines;
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
@@ -19,18 +20,20 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
///
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
///. lines, or only between the specified anchors.
/// lines, or only between the specified anchors.
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
/// specified or the lines between specified anchors, and include the rest of the file behind `#`.
/// This hides the lines from initial display but shows them when the reader expands the code
/// block and provides them to Rustdoc for testing.
/// - `{{# playground}}` - Insert runnable Rust files
/// - `{{# title}}` - Override \<title\> of a webpage.
#[derive(Default)]
#[non_exhaustive]
pub struct LinkPreprocessor;
impl LinkPreprocessor {
pub(crate) const NAME: &'static str = "links";
/// Name of this preprocessor.
pub const NAME: &'static str = "links";
/// Create a new `LinkPreprocessor`.
pub fn new() -> Self {
@@ -148,7 +151,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>),
@@ -408,23 +410,19 @@ 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(|| {
Regex::new(
r"(?x) # insignificant whitespace mode
static_regex!(
LINK,
r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9_]+) # link type
\s+ # separating whitespace
([^}]+) # link target path and space separated properties
\}\} # link closing parens",
)
.unwrap()
});
\}\} # link closing parens"
);
LinkIter(RE.captures_iter(contents))
LinkIter(LINK.captures_iter(contents))
}
#[cfg(test)]
@@ -493,7 +491,7 @@ mod tests {
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
@@ -519,7 +517,7 @@ mod tests {
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
@@ -536,7 +534,7 @@ mod tests {
fn test_find_links_with_range() {
let s = "Some random text with {{#include file.rs:10:20}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -555,7 +553,7 @@ mod tests {
fn test_find_links_with_line_number() {
let s = "Some random text with {{#include file.rs:10}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -574,7 +572,7 @@ mod tests {
fn test_find_links_with_from_range() {
let s = "Some random text with {{#include file.rs:10:}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -593,7 +591,7 @@ mod tests {
fn test_find_links_with_to_range() {
let s = "Some random text with {{#include file.rs::20}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -612,7 +610,7 @@ mod tests {
fn test_find_links_with_full_range() {
let s = "Some random text with {{#include file.rs::}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -631,7 +629,7 @@ mod tests {
fn test_find_links_with_no_range_specified() {
let s = "Some random text with {{#include file.rs}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -650,7 +648,7 @@ mod tests {
fn test_find_links_with_anchor() {
let s = "Some random text with {{#include file.rs:anchor}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![Link {
@@ -670,7 +668,7 @@ mod tests {
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
@@ -685,12 +683,11 @@ mod tests {
#[test]
fn test_find_playgrounds_with_properties() {
let s =
"Some random text with escaped playground {{#playground file.rs editable }} and some \
let s = "Some random text with escaped playground {{#playground file.rs editable }} and some \
more\n text {{#playground my.rs editable no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(
res,
vec![
@@ -715,13 +712,12 @@ mod tests {
#[test]
fn test_find_all_link_types() {
let s =
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
let s = "Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
println!("\nOUTPUT: {res:?}\n");
assert_eq!(res.len(), 3);
assert_eq!(
res[0],

View File

@@ -1,10 +1,9 @@
use once_cell::sync::Lazy;
use regex::Regex;
use mdbook_core::static_regex;
use std::ops::Bound::{Excluded, Included, Unbounded};
use std::ops::RangeBounds;
/// Take a range of lines from a string.
pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
pub(super) fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
let start = match range.start_bound() {
Excluded(&n) => n + 1,
Included(&n) => n,
@@ -24,14 +23,12 @@ 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_regex!(ANCHOR_START, r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)");
static_regex!(ANCHOR_END, r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)");
/// Take anchored lines from a string.
/// Lines containing anchor are ignored.
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
pub(super) fn take_anchored_lines(s: &str, anchor: &str) -> String {
let mut retained = Vec::<&str>::new();
let mut anchor_found = false;
@@ -63,7 +60,7 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
/// For any lines not in the range, include them but use `#` at the beginning. This will hide the
/// lines from initial display but include them when expanding the code snippet or testing with
/// rustdoc.
pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
pub(super) fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
let mut output = String::with_capacity(s.len());
for (index, line) in s.lines().enumerate() {
@@ -81,7 +78,7 @@ pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> S
/// For any lines not between the anchors, include them but use `#` at the beginning. This will
/// hide the lines from initial display but include them when expanding the code snippet or testing
/// with rustdoc.
pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
pub(super) fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
let mut output = String::with_capacity(s.len());
let mut within_anchored_section = false;

View File

@@ -0,0 +1,9 @@
//! Built-in preprocessors.
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod cmd;
mod index;
mod links;

View File

@@ -1,13 +1,12 @@
use crate::book::BookItem;
use crate::errors::*;
use crate::renderer::{RenderContext, Renderer};
use crate::utils;
use log::trace;
use std::fs;
use anyhow::{Context, Result};
use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer};
use tracing::trace;
#[derive(Default)]
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
/// when debugging preprocessors.
#[derive(Default)]
#[non_exhaustive]
pub struct MarkdownRenderer;
impl MarkdownRenderer {
@@ -27,21 +26,16 @@ impl Renderer for MarkdownRenderer {
let book = &ctx.book;
if destination.exists() {
utils::fs::remove_dir_content(destination)
fs::remove_dir_content(destination)
.with_context(|| "Unable to remove stale Markdown output")?;
}
trace!("markdown render");
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
if !ch.is_draft_chapter() {
utils::fs::write_file(
&ctx.destination,
ch.path.as_ref().expect("Checked path exists before"),
ch.content.as_bytes(),
)?;
}
}
for ch in book.chapters() {
let path = ctx
.destination
.join(ch.path.as_ref().expect("Checked path exists before"));
fs::write(path, &ch.content)?;
}
fs::create_dir_all(destination)

View File

@@ -0,0 +1,88 @@
//! Built-in renderers.
//!
//! The HTML renderer can be found in the [`mdbook_html`] crate.
use anyhow::{Context, Result, bail};
use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer};
use std::process::Stdio;
use tracing::{error, info, trace, warn};
pub use self::markdown_renderer::MarkdownRenderer;
mod markdown_renderer;
/// A generic renderer which will shell out to an arbitrary executable.
///
/// See <https://rust-lang.github.io/mdBook/for_developers/backends.html>
/// for a description of the renderer protocol.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdRenderer {
name: String,
cmd: String,
}
impl CmdRenderer {
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
pub fn new(name: String, cmd: String) -> CmdRenderer {
CmdRenderer { name, cmd }
}
}
impl Renderer for CmdRenderer {
fn name(&self) -> &str {
&self.name
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
info!("Invoking the \"{}\" renderer", self.name);
let optional_key = format!("output.{}.optional", self.name);
let optional = match ctx.config.get(&optional_key) {
Ok(Some(value)) => value,
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
Ok(None) => false,
};
let _ = fs::create_dir_all(&ctx.destination);
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
let mut child = match cmd
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&ctx.destination)
.spawn()
{
Ok(c) => c,
Err(e) => {
return crate::handle_command_error(
e, optional, "output", "backend", &self.name, &self.cmd,
);
}
};
let mut stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
// explicitly close the `stdin` file handle
drop(stdin);
let status = child
.wait()
.with_context(|| "Error waiting for the backend to complete")?;
trace!("{} exited with output: {:?}", self.cmd, status);
if !status.success() {
error!("Renderer exited with non-zero return code.");
bail!("The \"{}\" renderer failed", self.name);
} else {
Ok(())
}
}
}

View File

@@ -1,13 +1,12 @@
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
//! Support for initializing a new book.
use super::MDBook;
use crate::config::Config;
use crate::errors::*;
use crate::theme;
use crate::utils::fs::write_file;
use log::{debug, error, info, trace};
use anyhow::{Context, Result};
use mdbook_core::config::Config;
use mdbook_core::utils::fs;
use mdbook_html::theme::Theme;
use std::path::PathBuf;
use tracing::{debug, error, info, trace};
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]
@@ -99,12 +98,10 @@ impl BookBuilder {
fn write_book_toml(&self) -> Result<()> {
debug!("Writing book.toml");
let book_toml = self.root.join("book.toml");
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
let cfg =
toml::to_string(&self.config).with_context(|| "Unable to serialize the config")?;
File::create(book_toml)
.with_context(|| "Couldn't create book.toml")?
.write_all(&cfg)
.with_context(|| "Unable to write config to book.toml")?;
fs::write(&book_toml, cfg)?;
Ok(())
}
@@ -112,76 +109,15 @@ impl BookBuilder {
debug!("Copying theme");
let html_config = self.config.html_config().unwrap_or_default();
let themedir = html_config.theme_dir(&self.root);
if !themedir.exists() {
debug!(
"{} does not exist, creating the directory",
themedir.display()
);
fs::create_dir(&themedir)?;
}
let mut index = File::create(themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?;
let cssdir = themedir.join("css");
if !cssdir.exists() {
fs::create_dir(&cssdir)?;
}
let mut general_css = File::create(cssdir.join("general.css"))?;
general_css.write_all(theme::GENERAL_CSS)?;
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
chrome_css.write_all(theme::CHROME_CSS)?;
if html_config.print.enable {
let mut print_css = File::create(cssdir.join("print.css"))?;
print_css.write_all(theme::PRINT_CSS)?;
}
let mut variables_css = File::create(cssdir.join("variables.css"))?;
variables_css.write_all(theme::VARIABLES_CSS)?;
let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON_PNG)?;
let mut favicon = File::create(themedir.join("favicon.svg"))?;
favicon.write_all(theme::FAVICON_SVG)?;
let mut js = File::create(themedir.join("book.js"))?;
js.write_all(theme::JS)?;
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
write_file(&themedir.join("fonts"), "fonts.css", theme::fonts::CSS)?;
for (file_name, contents) in theme::fonts::LICENSES {
write_file(&themedir, file_name, contents)?;
}
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
write_file(&themedir, file_name, contents)?;
}
write_file(
&themedir,
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1,
)?;
Theme::copy_theme(&html_config, &self.root)?;
Ok(())
}
fn build_gitignore(&self) -> Result<()> {
debug!("Creating .gitignore");
let mut f = File::create(self.root.join(".gitignore"))?;
writeln!(f, "{}", self.config.build.build_dir.display())?;
fs::write(
self.root.join(".gitignore"),
format!("{}", self.config.build.build_dir.display()),
)?;
Ok(())
}
@@ -192,14 +128,14 @@ impl BookBuilder {
let summary = src_dir.join("SUMMARY.md");
if !summary.exists() {
trace!("No summary found creating stub summary and chapter_1.md.");
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
writeln!(f, "# Summary")?;
writeln!(f)?;
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
fs::write(
summary,
"# Summary\n\
\n\
- [Chapter 1](./chapter_1.md)\n",
)?;
let chapter_1 = src_dir.join("chapter_1.md");
let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
writeln!(f, "# Chapter 1")?;
fs::write(src_dir.join("chapter_1.md"), "# Chapter 1\n")?;
} else {
trace!("Existing summary found, no need to create stub files.");
}

View File

@@ -0,0 +1,131 @@
//! High-level library for running mdBook.
//!
//! This is the high-level library for running
//! [mdBook](https://rust-lang.github.io/mdBook/). There are several
//! reasons for using the programmatic API (over the CLI):
//!
//! - Integrate mdBook in a current project.
//! - Extend the capabilities of mdBook.
//! - Do some processing or test before building your book.
//! - Accessing the public API to help create a new Renderer.
//!
//! ## Additional crates
//!
//! In addition to `mdbook-driver`, there are several other crates available
//! for using and extending mdBook:
//!
//! - [`mdbook_preprocessor`]: Provides support for implementing preprocessors.
//! - [`mdbook_renderer`]: Provides support for implementing renderers.
//! - [`mdbook_markdown`]: The Markdown renderer.
//! - [`mdbook_summary`]: The `SUMMARY.md` parser.
//! - [`mdbook_html`]: The HTML renderer.
//! - [`mdbook_core`]: An internal library that is used by the other crates
//! for shared types. Types from this crate are rexported from the other
//! crates as appropriate.
//!
//! ## Cargo features
//!
//! The following cargo features are available:
//!
//! - `search`: Enables the search index in the HTML renderer.
//!
//! ## Examples
//!
//! If creating a new book from scratch, you'll want to get a [`init::BookBuilder`] via
//! the [`MDBook::init()`] method.
//!
//! ```rust,no_run
//! use mdbook_driver::MDBook;
//! use mdbook_driver::config::Config;
//!
//! let root_dir = "/path/to/book/root";
//!
//! // create a default config and change a couple things
//! let mut cfg = Config::default();
//! cfg.book.title = Some("My Book".to_string());
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
//!
//! MDBook::init(root_dir)
//! .create_gitignore(true)
//! .with_config(cfg)
//! .build()
//! .expect("Book generation failed");
//! ```
//!
//! You can also load an existing book and build it.
//!
//! ```rust,no_run
//! use mdbook_driver::MDBook;
//!
//! let root_dir = "/path/to/book/root";
//!
//! let mut md = MDBook::load(root_dir)
//! .expect("Unable to load the book");
//! md.build().expect("Building failed");
//! ```
pub mod builtin_preprocessors;
pub mod builtin_renderers;
pub mod init;
mod load;
mod mdbook;
use anyhow::{Context, Result, bail};
pub use mdbook::MDBook;
pub use mdbook_core::{book, config, errors};
use shlex::Shlex;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{error, warn};
/// Creates a [`Command`] for command renderers and preprocessors.
fn compose_command(cmd: &str, root: &Path) -> Result<Command> {
let mut words = Shlex::new(cmd);
let exe = match words.next() {
Some(e) => PathBuf::from(e),
None => bail!("Command string was empty"),
};
let exe = if exe.components().count() == 1 {
// Search PATH for the executable.
exe
} else {
// Relative path is relative to book root.
root.join(&exe)
};
let mut cmd = Command::new(exe);
for arg in words {
cmd.arg(arg);
}
Ok(cmd)
}
/// Handles a failure for a preprocessor or renderer.
fn handle_command_error(
error: std::io::Error,
optional: bool,
key: &str,
what: &str,
name: &str,
cmd: &str,
) -> Result<()> {
if let std::io::ErrorKind::NotFound = error.kind() {
if optional {
warn!(
"The command `{cmd}` for {what} `{name}` was not found, \
but is marked as optional.",
);
return Ok(());
} else {
error!(
"The command `{cmd}` wasn't found, is the `{name}` {what} installed? \
If you want to ignore this error when the `{name}` {what} is not installed, \
set `optional = true` in the `[{key}.{name}]` section of the book.toml configuration file.",
);
}
}
Err(error).with_context(|| format!("Unable to run the {what} `{name}`"))?
}

View File

@@ -0,0 +1,309 @@
use anyhow::{Context, Result};
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::BuildConfig;
use mdbook_core::utils::{escape_html, fs};
use mdbook_summary::{Link, Summary, SummaryItem, parse_summary};
use std::path::Path;
use tracing::debug;
/// Load a book into memory from its `src/` directory.
pub(crate) fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md");
let summary_content = fs::read_to_string(&summary_md)?;
let summary = parse_summary(&summary_content)
.with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?;
if cfg.create_missing {
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
}
load_book_from_disk(&summary, src_dir)
}
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut items: Vec<_> = summary
.prefix_chapters
.iter()
.chain(summary.numbered_chapters.iter())
.chain(summary.suffix_chapters.iter())
.collect();
while let Some(next) = items.pop() {
if let SummaryItem::Link(ref link) = *next {
if let Some(ref location) = link.location {
let filename = src_dir.join(location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
debug!("Creating missing file {}", filename.display());
let title = escape_html(&link.name);
fs::write(&filename, format!("# {title}\n"))?;
}
}
items.extend(&link.nested_items);
}
}
Ok(())
}
/// Use the provided `Summary` to load a `Book` from disk.
///
/// You need to pass in the book's source directory because all the links in
/// `SUMMARY.md` give the chapter locations relative to it.
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
debug!("Loading the book from disk");
let src_dir = src_dir.as_ref();
let prefix = summary.prefix_chapters.iter();
let numbered = summary.numbered_chapters.iter();
let suffix = summary.suffix_chapters.iter();
let summary_items = prefix.chain(numbered).chain(suffix);
let mut chapters = Vec::new();
for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
chapters.push(chapter);
}
Ok(Book::new_with_items(chapters))
}
fn load_summary_item<P: AsRef<Path> + Clone>(
item: &SummaryItem,
src_dir: P,
parent_names: Vec<String>,
) -> Result<BookItem> {
match item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(link) => load_chapter(link, src_dir, parent_names).map(BookItem::Chapter),
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
_ => panic!("SummaryItem {item:?} not covered"),
}
}
fn load_chapter<P: AsRef<Path>>(
link: &Link,
src_dir: P,
parent_names: Vec<String>,
) -> Result<Chapter> {
let src_dir = src_dir.as_ref();
let mut ch = if let Some(ref link_location) = link.location {
debug!("Loading {} ({})", link.name, link_location.display());
let location = if link_location.is_absolute() {
link_location.clone()
} else {
src_dir.join(link_location)
};
let mut content = std::fs::read_to_string(&location)
.with_context(|| format!("failed to read chapter `{}`", link_location.display()))?;
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
content.replace_range(..3, "");
}
let stripped = location
.strip_prefix(src_dir)
.expect("Chapters are always inside a book");
Chapter::new(&link.name, content, stripped, parent_names.clone())
} else {
Chapter::new_draft(&link.name, parent_names.clone())
};
let mut sub_item_parents = parent_names;
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
let sub_items = link
.nested_items
.iter()
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
.collect::<Result<Vec<_>>>()?;
ch.sub_items = sub_items;
Ok(ch)
}
#[cfg(test)]
mod tests {
use super::*;
use mdbook_core::book::SectionNumber;
use std::path::PathBuf;
use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &str = "
# Dummy Chapter
this is some dummy text.
And here is some \
more text.
";
/// Create a dummy `Link` in a temporary directory.
fn dummy_link() -> (Link, TempDir) {
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp.path().join("chapter_1.md");
fs::write(&chapter_path, DUMMY_SRC).unwrap();
let link = Link::new("Chapter 1", chapter_path);
(link, temp)
}
/// Create a nested `Link` written to a temporary directory.
fn nested_links() -> (Link, TempDir) {
let (mut root, temp_dir) = dummy_link();
let second_path = temp_dir.path().join("second.md");
fs::write(&second_path, "Hello World!").unwrap();
let mut second = Link::new("Nested Chapter 1", &second_path);
second.number = Some(SectionNumber::new([1, 2]));
root.nested_items.push(second.clone().into());
root.nested_items.push(SummaryItem::Separator);
root.nested_items.push(second.into());
(root, temp_dir)
}
#[test]
fn load_a_single_chapter_from_disk() {
let (link, temp_dir) = dummy_link();
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_single_chapter_with_utf8_bom_from_disk() {
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp_dir.path().join("chapter_1.md");
fs::write(&chapter_path, format!("\u{feff}{DUMMY_SRC}")).unwrap();
let link = Link::new("Chapter 1", chapter_path);
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_a_nonexistent_chapter() {
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
let got = load_chapter(&link, "", Vec::new());
assert!(got.is_err());
}
#[test]
fn load_recursive_link_with_separators() {
let (root, temp) = nested_links();
let mut nested = Chapter::new(
"Nested Chapter 1",
String::from("Hello World!"),
"second.md",
vec![String::from("Chapter 1")],
);
nested.number = Some(SectionNumber::new([1, 2]));
let mut chapter =
Chapter::new("Chapter 1", String::from(DUMMY_SRC), "chapter_1.md", vec![]);
chapter.sub_items = vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested),
];
let should_be = BookItem::Chapter(chapter);
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_book_with_a_single_chapter() {
let (link, temp) = dummy_link();
let mut summary = Summary::default();
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let chapter = Chapter::new(
"Chapter 1",
String::from(DUMMY_SRC),
PathBuf::from("chapter_1.md"),
vec![],
);
let items = vec![BookItem::Chapter(chapter)];
let should_be = Book::new_with_items(items);
let got = load_book_from_disk(&summary, temp.path()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let mut summary = Summary::default();
let link = Link::new("Empty", "");
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_load_chapters_when_the_link_is_a_directory() {
let (_, temp) = dummy_link();
let dir = temp.path().join("nested");
fs::create_dir_all(&dir).unwrap();
let mut summary = Summary::default();
let link = Link::new("nested", dir);
summary.numbered_chapters = vec![SummaryItem::Link(link)];
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_open_summary_md() {
let cfg = BuildConfig::default();
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let got = load_book(&temp_dir, &cfg);
assert!(got.is_err());
let error_message = got.err().unwrap().to_string();
let expected = format!(
r#"failed to read `{}`"#,
temp_dir.path().join("SUMMARY.md").display()
);
assert_eq!(error_message, expected);
}
}

View File

@@ -0,0 +1,574 @@
//! The high-level interface for loading and rendering books.
use crate::builtin_preprocessors::{CmdPreprocessor, IndexPreprocessor, LinkPreprocessor};
use crate::builtin_renderers::{CmdRenderer, MarkdownRenderer};
use crate::init::BookBuilder;
use crate::load::{load_book, load_book_from_disk};
use anyhow::{Context, Error, Result, bail};
use indexmap::IndexMap;
use mdbook_core::book::{Book, BookItem, BookItems};
use mdbook_core::config::{Config, RustEdition};
use mdbook_core::utils::fs;
use mdbook_html::HtmlHandlebars;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use mdbook_renderer::{RenderContext, Renderer};
use mdbook_summary::Summary;
use serde::Deserialize;
use std::ffi::OsString;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::Builder as TempFileBuilder;
use topological_sort::TopologicalSort;
use tracing::{debug, error, info, trace, warn};
#[cfg(test)]
mod tests;
/// The object used to manage and build a book.
pub struct MDBook {
/// The book's root directory.
pub root: PathBuf,
/// The configuration used to tweak now a book is built.
pub config: Config,
/// A representation of the book's contents in memory.
pub book: Book,
/// Renderers to execute.
renderers: IndexMap<String, Box<dyn Renderer>>,
/// Pre-processors to be run on the book.
preprocessors: IndexMap<String, Box<dyn Preprocessor>>,
}
impl MDBook {
/// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
let book_root = book_root.into();
let config_location = book_root.join("book.toml");
let mut config = if config_location.exists() {
debug!("Loading config from {}", config_location.display());
Config::from_disk(&config_location)?
} else {
Config::default()
};
config.update_from_env();
if tracing::enabled!(tracing::Level::TRACE) {
for line in format!("Config: {config:#?}").lines() {
trace!("{}", line);
}
}
MDBook::load_with_config(book_root, config)
}
/// Load a book from its root directory using a custom `Config`.
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = load_book(src_dir, &config.build)?;
let renderers = determine_renderers(&config)?;
let preprocessors = determine_preprocessors(&config, &root)?;
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
}
/// Load a book from its root directory using a custom `Config` and a custom summary.
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
book_root: P,
config: Config,
summary: Summary,
) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = load_book_from_disk(&summary, src_dir)?;
let renderers = determine_renderers(&config)?;
let preprocessors = determine_preprocessors(&config, &root)?;
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
}
/// Returns a flat depth-first iterator over the [`BookItem`]s of the book.
///
/// ```no_run
/// # use mdbook_driver::MDBook;
/// # use mdbook_driver::book::BookItem;
/// # let book = MDBook::load("mybook").unwrap();
/// for item in book.iter() {
/// match *item {
/// BookItem::Chapter(ref chapter) => {},
/// BookItem::Separator => {},
/// BookItem::PartTitle(ref title) => {}
/// _ => {}
/// }
/// }
///
/// // would print something like this:
/// // 1. Chapter 1
/// // 1.1 Sub Chapter
/// // 1.2 Sub Chapter
/// // 2. Chapter 2
/// //
/// // etc.
/// ```
pub fn iter(&self) -> BookItems<'_> {
self.book.iter()
}
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
/// and its accompanying directory structure.
///
/// The `BookBuilder` creates some boilerplate files and directories to get
/// you started with your book.
///
/// ```text
/// book-test/
/// ├── book
/// └── src
/// ├── chapter_1.md
/// └── SUMMARY.md
/// ```
///
/// It uses the path provided as the root directory for your book, then adds
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
/// to get you started.
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
BookBuilder::new(book_root)
}
/// Tells the renderer to build our book and put it in the build directory.
pub fn build(&self) -> Result<()> {
info!("Book building has started");
for renderer in self.renderers.values() {
self.execute_build_process(&**renderer)?;
}
Ok(())
}
/// Run preprocessors and return the final book.
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
let preprocess_ctx = PreprocessorContext::new(
self.root.clone(),
self.config.clone(),
renderer.name().to_string(),
);
let mut preprocessed_book = self.book.clone();
for preprocessor in self.preprocessors.values() {
if preprocessor_should_run(&**preprocessor, renderer, &self.config)? {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
}
}
Ok((preprocessed_book, preprocess_ctx))
}
/// Run the entire build process for a particular [`Renderer`].
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
let name = renderer.name();
let build_dir = self.build_dir_for(name);
let mut render_context = RenderContext::new(
self.root.clone(),
preprocessed_book,
self.config.clone(),
build_dir,
);
render_context
.chapter_titles
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
info!("Running the {} backend", renderer.name());
renderer
.render(&render_context)
.with_context(|| "Rendering failed")
}
/// You can change the default renderer to another one by using this method.
/// The only requirement is that your renderer implement the [`Renderer`]
/// trait.
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers
.insert(renderer.name().to_string(), Box::new(renderer));
self
}
/// Register a [`Preprocessor`] to be used when rendering the book.
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
self.preprocessors
.insert(preprocessor.name().to_string(), Box::new(preprocessor));
self
}
/// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// test_chapter with chapter:None will run all tests.
self.test_chapter(library_paths, None)
}
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
/// If `chapter` is `None`, all tests will be run.
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
let cwd = std::env::current_dir()?;
let library_args: Vec<OsString> = library_paths
.into_iter()
.flat_map(|path| {
let path = Path::new(path);
let path = if path.is_relative() {
cwd.join(path).into_os_string()
} else {
path.to_path_buf().into_os_string()
};
[OsString::from("-L"), path]
})
.collect();
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let mut chapter_found = false;
struct TestRenderer;
impl Renderer for TestRenderer {
// FIXME: Is "test" the proper renderer name to use here?
fn name(&self) -> &str {
"test"
}
fn render(&self, _: &RenderContext) -> Result<()> {
Ok(())
}
}
// Index Preprocessor is disabled so that chapter paths
// continue to point to the actual markdown files.
self.preprocessors = determine_preprocessors(&self.config, &self.root)?;
self.preprocessors
.shift_remove_entry(IndexPreprocessor::NAME);
let (book, _) = self.preprocess_book(&TestRenderer)?;
let color_output = std::io::stderr().is_terminal();
let mut failed = false;
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
let chapter_path = match ch.path {
Some(ref path) if !path.as_os_str().is_empty() => path,
_ => continue,
};
if let Some(chapter) = chapter {
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
if chapter == "?" {
info!("Skipping chapter '{}'...", ch.name);
}
continue;
}
}
chapter_found = true;
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
// write preprocessed file to tempdir
let path = temp_dir.path().join(chapter_path);
fs::write(&path, &ch.content)?;
let mut cmd = Command::new("rustdoc");
cmd.current_dir(temp_dir.path())
.arg(chapter_path)
.arg("--test")
.args(&library_args);
if let Some(edition) = self.config.rust.edition {
match edition {
RustEdition::E2015 => {
cmd.args(["--edition", "2015"]);
}
RustEdition::E2018 => {
cmd.args(["--edition", "2018"]);
}
RustEdition::E2021 => {
cmd.args(["--edition", "2021"]);
}
RustEdition::E2024 => {
cmd.args(["--edition", "2024"]);
}
_ => panic!("RustEdition {edition:?} not covered"),
}
}
if color_output {
cmd.args(["--color", "always"]);
}
debug!("running {:?}", cmd);
let output = cmd
.output()
.with_context(|| "failed to execute `rustdoc`")?;
if !output.status.success() {
failed = true;
error!(
"rustdoc returned an error:\n\
\n--- stdout\n{}\n--- stderr\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
}
}
if failed {
bail!("One or more tests failed");
}
if let Some(chapter) = chapter {
if !chapter_found {
bail!("Chapter not found: {}", chapter);
}
}
Ok(())
}
/// The logic for determining where a backend should put its build
/// artefacts.
///
/// If there is only 1 renderer, put it in the directory pointed to by the
/// `build.build_dir` key in [`Config`]. If there is more than one then the
/// renderer gets its own directory within the main build dir.
///
/// i.e. If there were only one renderer (in this case, the HTML renderer):
///
/// - build/
/// - index.html
/// - ...
///
/// Otherwise if there are multiple:
///
/// - build/
/// - epub/
/// - my_awesome_book.epub
/// - html/
/// - index.html
/// - ...
/// - latex/
/// - my_awesome_book.tex
///
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
let build_dir = self.root.join(&self.config.build.build_dir);
if self.renderers.len() <= 1 {
build_dir
} else {
build_dir.join(backend_name)
}
}
/// Get the directory containing this book's source files.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
/// Get the directory containing the theme resources for the book.
pub fn theme_dir(&self) -> PathBuf {
self.config
.html_config()
.unwrap_or_default()
.theme_dir(&self.root)
}
}
/// An `output` table.
#[derive(Deserialize)]
struct OutputConfig {
command: Option<String>,
}
/// Look at the `Config` and try to figure out what renderers to use.
fn determine_renderers(config: &Config) -> Result<IndexMap<String, Box<dyn Renderer>>> {
let mut renderers = IndexMap::new();
let outputs = config.outputs::<OutputConfig>()?;
renderers.extend(outputs.into_iter().map(|(key, table)| {
let renderer = if key == "html" {
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
} else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} else {
let command = table.command.unwrap_or_else(|| format!("mdbook-{key}"));
Box::new(CmdRenderer::new(key.clone(), command))
};
(key, renderer)
}));
// if we couldn't find anything, add the HTML renderer as a default
if renderers.is_empty() {
renderers.insert("html".to_string(), Box::new(HtmlHandlebars::new()));
}
Ok(renderers)
}
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name();
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
}
/// A `preprocessor` table.
#[derive(Deserialize)]
struct PreprocessorConfig {
command: Option<String>,
#[serde(default)]
before: Vec<String>,
#[serde(default)]
after: Vec<String>,
#[serde(default)]
optional: bool,
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(
config: &Config,
root: &Path,
) -> Result<IndexMap<String, Box<dyn Preprocessor>>> {
// Collect the names of all preprocessors intended to be run, and the order
// in which they should be run.
let mut preprocessor_names = TopologicalSort::<String>::new();
if config.build.use_default_preprocessors {
for name in DEFAULT_PREPROCESSORS {
preprocessor_names.insert(name.to_string());
}
}
let preprocessor_table = config.preprocessors::<PreprocessorConfig>()?;
for (name, table) in preprocessor_table.iter() {
preprocessor_names.insert(name.to_string());
let exists = |name| {
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|| preprocessor_table.contains_key(name)
};
for after in &table.before {
if !exists(&after) {
// Only warn so that preprocessors can be toggled on and off (e.g. for
// troubleshooting) without having to worry about order too much.
warn!(
"preprocessor.{}.after contains \"{}\", which was not found",
name, after
);
} else {
preprocessor_names.add_dependency(name, after);
}
}
for before in &table.after {
if !exists(&before) {
// See equivalent warning above for rationale
warn!(
"preprocessor.{}.before contains \"{}\", which was not found",
name, before
);
} else {
preprocessor_names.add_dependency(before, name);
}
}
}
// Now that all links have been established, queue preprocessors in a suitable order
let mut preprocessors = IndexMap::with_capacity(preprocessor_names.len());
// `pop_all()` returns an empty vector when no more items are not being depended upon
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
.take_while(|names| !names.is_empty())
{
// The `topological_sort` crate does not guarantee a stable order for ties, even across
// runs of the same program. Thus, we break ties manually by sorting.
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
// values ([1]), which may not be an alphabetical sort.
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
// preprocessor execution order.
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
names.sort();
for name in names {
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
"links" => Box::new(LinkPreprocessor::new()),
"index" => Box::new(IndexPreprocessor::new()),
_ => {
// The only way to request a custom preprocessor is through the `preprocessor`
// table, so it must exist, be a table, and contain the key.
let table = &preprocessor_table[&name];
let command = table
.command
.to_owned()
.unwrap_or_else(|| format!("mdbook-{name}"));
Box::new(CmdPreprocessor::new(
name.clone(),
command,
root.to_owned(),
table.optional,
))
}
};
preprocessors.insert(name, preprocessor);
}
}
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
if preprocessor_names.is_empty() {
Ok(preprocessors)
} else {
Err(Error::msg("Cyclic dependency detected in preprocessors"))
}
}
/// Check whether we should run a particular `Preprocessor` in combination
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
/// method if the user doesn't say anything.
///
/// The `build.use-default-preprocessors` config option can be used to ensure
/// default preprocessors always run if they support the renderer.
fn preprocessor_should_run(
preprocessor: &dyn Preprocessor,
renderer: &dyn Renderer,
cfg: &Config,
) -> Result<bool> {
// default preprocessors should be run by default (if supported)
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
return preprocessor.supports_renderer(renderer.name());
}
let key = format!("preprocessor.{}.renderers", preprocessor.name());
let renderer_name = renderer.name();
match cfg.get::<Vec<String>>(&key) {
Ok(Some(explicit_renderers)) => {
Ok(explicit_renderers.iter().any(|name| name == renderer_name))
}
Ok(None) => preprocessor.supports_renderer(renderer_name),
Err(e) => bail!("failed to get `{key}`: {e}"),
}
}

View File

@@ -0,0 +1,284 @@
use super::*;
use std::str::FromStr;
use toml::value::{Table, Value};
#[test]
fn config_defaults_to_html_renderer_if_empty() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.outputs::<toml::Value>().unwrap().is_empty());
let got = determine_renderers(&cfg).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "html");
}
#[test]
fn add_a_random_renderer_to_the_config() {
let mut cfg = Config::default();
cfg.set("output.random", Table::new()).unwrap();
let got = determine_renderers(&cfg).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn add_a_random_renderer_with_custom_command_to_the_config() {
let mut cfg = Config::default();
let mut table = Table::new();
table.insert("command".to_string(), Value::String("false".to_string()));
cfg.set("output.random", table).unwrap();
let got = determine_renderers(&cfg).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `preprocessor` table
assert!(cfg.preprocessors::<toml::Value>().unwrap().is_empty());
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
let names: Vec<_> = got.values().map(|p| p.name()).collect();
assert_eq!(names, ["index", "links"]);
}
#[test]
fn use_default_preprocessors_works() {
let mut cfg = Config::default();
cfg.build.use_default_preprocessors = false;
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert_eq!(got.len(), 0);
}
#[test]
fn can_determine_third_party_preprocessors() {
let cfg_str = r#"
[book]
title = "Some Book"
[preprocessor.random]
[build]
build-dir = "outputs"
create-missing = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
assert!(cfg.get::<Value>("preprocessor.random").unwrap().is_some());
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
assert!(got.contains_key("random"));
}
#[test]
fn preprocessors_can_provide_their_own_commands() {
let cfg_str = r#"
[preprocessor.random]
command = "python random.py"
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
let random = cfg
.get::<OutputConfig>("preprocessor.random")
.unwrap()
.unwrap();
assert_eq!(random.command, Some("python random.py".to_string()));
}
#[test]
fn preprocessor_before_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
before = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
}
#[test]
fn preprocessor_after_must_be_array() {
let cfg_str = r#"
[preprocessor.random]
after = 0
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
}
#[test]
fn preprocessor_order_is_honored() {
let cfg_str = r#"
[preprocessor.random]
before = [ "last" ]
after = [ "index" ]
[preprocessor.last]
after = [ "links", "index" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
let index = |name| preprocessors.get_index_of(name).unwrap();
let assert_before = |before, after| {
if index(before) >= index(after) {
eprintln!("Preprocessor order:");
for preprocessor in preprocessors.keys() {
eprintln!(" {}", preprocessor);
}
panic!("{before} should come before {after}");
}
};
assert_before("index", "random");
assert_before("index", "last");
assert_before("random", "last");
assert_before("links", "last");
}
#[test]
fn cyclic_dependencies_are_detected() {
let cfg_str = r#"
[preprocessor.links]
before = [ "index" ]
[preprocessor.index]
before = [ "links" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
assert!(determine_preprocessors(&cfg, Path::new("")).is_err());
}
#[test]
fn dependencies_dont_register_undefined_preprocessors() {
let cfg_str = r#"
[preprocessor.links]
before = [ "random" ]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
// Does not contain "random"
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["index", "links"]);
}
#[test]
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
let cfg_str = r#"
[preprocessor.random]
before = [ "links" ]
[build]
use-default-preprocessors = false
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let preprocessors = determine_preprocessors(&cfg, Path::new("")).unwrap();
// Does not contain "links"
assert_eq!(preprocessors.keys().collect::<Vec<_>>(), ["random"]);
}
#[test]
fn config_respects_preprocessor_selection() {
let cfg_str = r#"
[preprocessor.links]
renderers = ["html"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let html_renderer = HtmlHandlebars::default();
let pre = LinkPreprocessor::new();
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg).unwrap();
assert!(should_run);
}
struct BoolPreprocessor(bool);
impl Preprocessor for BoolPreprocessor {
fn name(&self) -> &str {
"bool-preprocessor"
}
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
unimplemented!()
}
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
Ok(self.0)
}
}
#[test]
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
let cfg = Config::default();
let html = HtmlHandlebars::new();
let should_be = true;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
assert_eq!(got, should_be);
let should_be = false;
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg).unwrap();
assert_eq!(got, should_be);
}
// Default is to sort preprocessors alphabetically.
#[test]
fn preprocessor_sorted_by_name() {
let cfg_str = r#"
[preprocessor.xyz]
[preprocessor.abc]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let got = determine_preprocessors(&cfg, Path::new("")).unwrap();
let names: Vec<_> = got.values().map(|p| p.name()).collect();
assert_eq!(names, ["abc", "index", "links", "xyz"]);
}
// Default is to sort renderers alphabetically.
#[test]
fn renderers_sorted_by_name() {
let cfg_str = r#"
[output.xyz]
[output.abc]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
let got = determine_renderers(&cfg).unwrap();
let names: Vec<_> = got.values().map(|p| p.name()).collect();
assert_eq!(names, ["abc", "xyz"]);
}

View File

@@ -0,0 +1,37 @@
[package]
name = "mdbook-html"
version = "0.5.0-beta.1"
description = "mdBook HTML renderer"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
ego-tree.workspace = true
elasticlunr-rs = { workspace = true, optional = true }
font-awesome-as-a-crate.workspace = true
handlebars.workspace = true
hex.workspace = true
html5ever.workspace = true
indexmap.workspace = true
mdbook-core.workspace = true
mdbook-markdown.workspace = true
mdbook-renderer.workspace = true
pulldown-cmark.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
toml.workspace = true
[lints]
workspace = true
[features]
search = ["dep:elasticlunr-rs"]

View File

@@ -0,0 +1,13 @@
# mdbook-html
[![Documentation](https://img.shields.io/docsrs/mdbook-html)](https://docs.rs/mdbook-html)
[![crates.io](https://img.shields.io/crates/v/mdbook-html.svg)](https://crates.io/crates/mdbook-html)
[![Changelog](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md)
This is the HTML renderer for [mdBook](https://rust-lang.github.io/mdBook/). This is intended for internal use only. It is automatically included by [`mdbook-driver`](https://crates.io/crates/mdbook-driver) to render books to HTML.
> This crate is maintained by the mdBook team, primarily for use by mdBook and not intended for external use (except as a transitive dependency). This crate may make major changes to its APIs or be deprecated without warning.
## License
[Mozilla Public License, version 2.0](https://github.com/rust-lang/mdBook/blob/master/LICENSE)

View File

@@ -13,7 +13,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
.hljs-comment,
.hljs-quote {
color: #5c6773;
font-style: italic;
}
.hljs-variable,

View File

@@ -3,7 +3,7 @@
html {
scrollbar-color: var(--scrollbar) var(--bg);
}
#searchresults a,
#mdbook-searchresults a,
.content a:link,
a:visited,
a > .hljs {
@@ -11,10 +11,10 @@ a > .hljs {
}
/*
body-container is necessary because mobile browsers don't seem to like
mdbook-body-container is necessary because mobile browsers don't seem to like
overflow-x on the body tag when there is a <meta name="viewport"> tag.
*/
#body-container {
#mdbook-body-container {
/*
This is used when the sidebar pushes the body content off the side of
the screen on small screens. Without it, dragging on mobile Safari
@@ -25,12 +25,12 @@ a > .hljs {
/* Menu Bar */
#menu-bar,
#menu-bar-hover-placeholder {
#mdbook-menu-bar,
#mdbook-menu-bar-hover-placeholder {
z-index: 101;
margin: auto calc(0px - var(--page-padding));
}
#menu-bar {
#mdbook-menu-bar {
position: relative;
display: flex;
flex-wrap: wrap;
@@ -39,24 +39,24 @@ a > .hljs {
border-block-end-width: 1px;
border-block-end-style: solid;
}
#menu-bar.sticky,
.js #menu-bar-hover-placeholder:hover + #menu-bar,
.js #menu-bar:hover,
.js.sidebar-visible #menu-bar {
#mdbook-menu-bar.sticky,
#mdbook-menu-bar-hover-placeholder:hover + #mdbook-menu-bar,
#mdbook-menu-bar:hover,
html.sidebar-visible #mdbook-menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0 !important;
}
#menu-bar-hover-placeholder {
#mdbook-menu-bar-hover-placeholder {
position: sticky;
position: -webkit-sticky;
top: 0;
height: var(--menu-bar-height);
}
#menu-bar.bordered {
#mdbook-menu-bar.bordered {
border-block-end-color: var(--table-border-color);
}
#menu-bar i, #menu-bar .icon-button {
#mdbook-menu-bar .fa-svg, #mdbook-menu-bar .icon-button {
position: relative;
padding: 0 8px;
z-index: 10;
@@ -65,7 +65,7 @@ a > .hljs {
transition: color 0.5s;
}
@media only screen and (max-width: 420px) {
#menu-bar i, #menu-bar .icon-button {
#mdbook-menu-bar .fa-svg, #mdbook-menu-bar .icon-button {
padding: 0 5px;
}
}
@@ -76,7 +76,7 @@ a > .hljs {
padding: 0;
color: inherit;
}
.icon-button i {
.icon-button .fa-svg {
margin: 0;
}
@@ -91,7 +91,7 @@ a > .hljs {
display: flex;
margin: 0 5px;
}
.no-js .left-buttons button {
html:not(.js) .left-buttons button {
display: none;
}
@@ -107,7 +107,7 @@ a > .hljs {
overflow: hidden;
text-overflow: ellipsis;
}
.js .menu-title {
.menu-title {
cursor: pointer;
}
@@ -118,14 +118,14 @@ a > .hljs {
.mobile-nav-chapters,
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
.menu-bar a .fa-svg {
color: var(--icons);
}
.menu-bar i:hover,
.menu-bar .fa-svg:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
.mobile-nav-chapters .fa-svg:hover {
color: var(--icons-hover);
}
@@ -186,10 +186,6 @@ a > .hljs {
left: var(--page-padding);
}
/* Use the correct buttons for RTL layouts*/
[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";}
[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; }
@media only screen and (max-width: 1080px) {
.nav-wide-wrapper { display: none; }
.nav-wrapper { display: block; }
@@ -197,8 +193,8 @@ a > .hljs {
/* sidebar-visible */
@media only screen and (max-width: 1380px) {
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
}
/* Inline code */
@@ -244,14 +240,11 @@ pre > .buttons :hover {
border-color: var(--icons-hover);
background-color: var(--theme-hover);
}
pre > .buttons i {
margin-inline-start: 8px;
}
pre > .buttons button {
cursor: inherit;
margin: 0px 5px;
padding: 3px 5px;
font-size: 14px;
padding: 2px 3px 0px 4px;
font-size: 23px;
border-style: solid;
border-width: 1px;
@@ -262,6 +255,27 @@ pre > .buttons button {
transition-property: color,border-color,background-color;
color: var(--icons);
}
pre > .buttons button.clip-button {
padding: 2px 4px 0px 6px;
}
pre > .buttons button.clip-button::before {
/* clipboard image from octicons (https://github.com/primer/octicons/tree/v2.0.0) MIT license
*/
content: url('data:image/svg+xml,<svg width="21" height="20" viewBox="0 0 24 25" \
xmlns="http://www.w3.org/2000/svg" aria-label="Copy to clipboard">\
<path d="M18 20h2v3c0 1-1 2-2 2H2c-.998 0-2-1-2-2V5c0-.911.755-1.667 1.667-1.667h5A3.323 3.323 0 \
0110 0a3.323 3.323 0 013.333 3.333h5C19.245 3.333 20 4.09 20 5v8.333h-2V9H2v14h16v-3zM3 \
7h14c0-.911-.793-1.667-1.75-1.667H13.5c-.957 0-1.75-.755-1.75-1.666C11.75 2.755 10.957 2 10 \
2s-1.75.755-1.75 1.667c0 .911-.793 1.666-1.75 1.666H4.75C3.793 5.333 3 6.09 3 7z"/>\
<path d="M4 19h6v2H4zM12 11H4v2h8zM4 17h4v-2H4zM15 15v-3l-4.5 4.5L15 21v-3l8.027-.032L23 15z"/>\
</svg>');
filter: var(--copy-button-filter);
}
pre > .buttons button.clip-button:hover::before {
filter: var(--copy-button-filter-hover);
}
@media (pointer: coarse) {
pre > .buttons button {
/* On mobile, make it easier to tap buttons. */
@@ -293,7 +307,7 @@ pre > .result {
/* Search */
#searchresults a {
#mdbook-searchresults a {
text-decoration: none;
}
@@ -323,9 +337,48 @@ mark.fade-out {
max-width: var(--content-max-width);
}
#searchbar {
#mdbook-searchbar-outer.searching #mdbook-searchbar {
padding-right: 30px;
}
#mdbook-searchbar-outer .spinner-wrapper {
display: none;
}
#mdbook-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);
}
#fa-spin {
animation: rotating 2s linear infinite;
display: inline-block;
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#mdbook-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;
@@ -336,8 +389,8 @@ mark.fade-out {
background-color: var(--searchbar-bg);
color: var(--searchbar-fg);
}
#searchbar:focus,
#searchbar.active {
#mdbook-searchbar:focus,
#mdbook-searchbar.active {
box-shadow: 0 0 3px var(--searchbar-shadow-color);
}
@@ -358,19 +411,19 @@ mark.fade-out {
border-block-end: 1px dashed var(--searchresults-border-color);
}
ul#searchresults {
ul#mdbook-searchresults {
list-style: none;
padding-inline-start: 20px;
}
ul#searchresults li {
ul#mdbook-searchresults li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
ul#searchresults li.focus {
ul#mdbook-searchresults li.focus {
background-color: var(--searchresults-li-bg);
}
ul#searchresults span.teaser {
ul#mdbook-searchresults span.teaser {
display: block;
clear: both;
margin-block-start: 5px;
@@ -379,7 +432,7 @@ ul#searchresults span.teaser {
margin-inline-end: 0;
font-size: 0.8em;
}
ul#searchresults span.teaser em {
ul#mdbook-searchresults span.teaser em {
font-weight: bold;
font-style: normal;
}
@@ -399,6 +452,25 @@ ul#searchresults span.teaser em {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.sidebar-iframe-inner {
--padding: 10px;
background-color: var(--sidebar-bg);
padding: var(--padding);
margin: 0;
font-size: 1.4rem;
color: var(--sidebar-fg);
min-height: calc(100vh - var(--padding) * 2);
}
.sidebar-iframe-outer {
border: none;
height: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
[dir=rtl] .sidebar { left: unset; right: 0; }
.sidebar-resizing {
-moz-user-select: none;
@@ -406,8 +478,7 @@ ul#searchresults span.teaser em {
-ms-user-select: none;
user-select: none;
}
.no-js .sidebar,
.js:not(.sidebar-resizing) .sidebar {
html:not(.sidebar-resizing) .sidebar {
transition: transform 0.3s; /* Animation: slide away */
}
.sidebar code {
@@ -435,9 +506,24 @@ ul#searchresults span.teaser em {
.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 {
@@ -449,11 +535,10 @@ ul#searchresults span.teaser em {
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
}
/* sidebar-hidden */
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
#mdbook-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 {
[dir=rtl] #mdbook-sidebar-toggle-anchor:not(:checked) ~ .sidebar {
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
}
.sidebar::-webkit-scrollbar {
@@ -464,18 +549,18 @@ ul#searchresults span.teaser em {
}
/* sidebar-visible */
#sidebar-toggle-anchor:checked ~ .page-wrapper {
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
}
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
[dir=rtl] #mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
}
@media only screen and (min-width: 620px) {
#sidebar-toggle-anchor:checked ~ .page-wrapper {
#mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: none;
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
}
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
[dir=rtl] #mdbook-sidebar-toggle-anchor:checked ~ .page-wrapper {
transform: none;
}
}
@@ -486,17 +571,18 @@ ul#searchresults span.teaser em {
line-height: 2.2em;
}
.chapter ol {
width: 100%;
}
.chapter li {
display: flex;
color: var(--sidebar-non-existant);
}
/* This is a span wrapping the chapter link and the fold chevron. */
.chapter-link-wrapper {
/* Used to position the chevron to the right, allowing the text to wrap before it. */
display: flex;
}
.chapter li a {
display: block;
padding: 0;
/* Remove underlines. */
text-decoration: none;
color: var(--sidebar-fg);
}
@@ -509,21 +595,22 @@ ul#searchresults span.teaser em {
color: var(--sidebar-active);
}
.chapter li > a.toggle {
/* This is the toggle chevron. */
.chapter-fold-toggle {
cursor: pointer;
display: block;
/* Positions the chevron to the side. */
margin-inline-start: auto;
padding: 0 10px;
user-select: none;
opacity: 0.68;
}
.chapter li > a.toggle div {
.chapter-fold-toggle div {
transition: transform 0.5s;
}
/* collapse the section */
.chapter li:not(.expanded) + li > ol {
.chapter li:not(.expanded) > ol {
display: none;
}
@@ -532,10 +619,26 @@ ul#searchresults span.teaser em {
margin-block-start: 0.6em;
}
.chapter li.expanded > a.toggle div {
/* When expanded, rotate the chevron to point down. */
.chapter li.expanded > span > .chapter-fold-toggle div {
transform: rotate(90deg);
}
.chapter a.current-header {
color: var(--sidebar-active);
}
.on-this-page {
margin-left: 22px;
border-inline-start: 4px solid var(--sidebar-header-border-color);
padding-left: 8px;
}
.on-this-page > ol {
padding-left: 0;
}
/* Horizontal line in chapter list. */
.spacer {
width: 100%;
height: 3px;
@@ -545,6 +648,7 @@ ul#searchresults span.teaser em {
background-color: var(--sidebar-spacer);
}
/* On touch devices, add more vertical spacing to make it easier to tap links. */
@media (-moz-touch-enabled: 1), (pointer: coarse) {
.chapter li a { padding: 5px 0; }
.spacer { margin: 10px 0; }
@@ -602,3 +706,46 @@ ul#searchresults span.teaser em {
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

@@ -0,0 +1,408 @@
/* Base styles and content styles */
:root {
/* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%;
color-scheme: var(--color-scheme);
}
html {
font-family: "Open Sans", sans-serif;
color: var(--fg);
background-color: var(--bg);
text-size-adjust: none;
-webkit-text-size-adjust: none;
}
body {
margin: 0;
font-size: 1.6rem;
overflow-x: hidden;
}
code {
font-family: var(--mono-font) !important;
font-size: var(--code-font-size);
direction: ltr !important;
}
/* make long words/inline code not x overflow */
main {
overflow-wrap: break-word;
}
/* make wide tables scroll if they overflow */
.table-wrapper {
overflow-x: auto;
}
/* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset;
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none !important; }
h2, h3 { margin-block-start: 2.5em; }
h4, h5 { margin-block-start: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-block-start: 1em;
}
h1:target::before,
h2:target::before,
h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before,
dt:target::before {
display: inline-block;
content: "»";
margin-inline-start: -30px;
width: 30px;
}
/* This is broken on Safari as of version 14, but is fixed
in Safari Technology Preview 117 which I think will be Safari 14.2.
https://bugs.webkit.org/show_bug.cgi?id=218076
*/
:target {
/* Safari does not support logical properties */
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #mdbook-menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
background-color: var(--bg);
}
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]: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 */
}
.content {
overflow-y: auto;
padding: 0 5px 50px 5px;
}
.content main {
margin-inline-start: auto;
margin-inline-end: auto;
max-width: var(--content-max-width);
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img, .content video { max-width: 100%; }
.content .header:link,
.content .header:visited {
color: var(--fg);
}
.content .header:link,
.content .header:visited:hover {
text-decoration: none;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px var(--table-border-color) solid;
}
table thead {
background: var(--table-header-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: 3px 20px;
}
table thead tr {
border: 1px var(--table-header-bg) solid;
}
/* Alternate background colors for rows */
table tbody tr:nth-child(2n) {
background: var(--table-alternate-bg);
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: var(--fg);
background-color: var(--quote-bg);
border-block-start: .1em solid var(--quote-border);
border-block-end: .1em solid var(--quote-border);
}
/* TODO: Remove .warning in a future version of mdbook, it is replaced by
blockquote tags. */
.warning {
margin: 20px;
padding: 0 20px;
border-inline-start: 2px solid var(--warning-border);
}
.warning:before {
position: absolute;
width: 3rem;
height: 3rem;
margin-inline-start: calc(-1.5rem - 21px);
content: "ⓘ";
text-align: center;
background-color: var(--bg);
color: var(--warning-border);
font-weight: bold;
font-size: 2rem;
}
blockquote .warning:before {
background-color: var(--quote-bg);
}
kbd {
background-color: var(--table-border-color);
border-radius: 4px;
border: solid 1px var(--theme-popup-border);
box-shadow: inset 0 -1px 0 var(--theme-hover);
display: inline-block;
font-size: var(--code-font-size);
font-family: var(--mono-font);
line-height: 10px;
padding: 4px 5px;
vertical-align: middle;
}
sup {
/* Set the line-height for superscript and footnote references so that there
isn't an awkward space appearing above lines that contain the footnote.
See https://github.com/rust-lang/mdBook/pull/2443#discussion_r1813773583
for an explanation.
*/
line-height: 0;
}
.footnote-definition {
font-size: 0.9em;
}
/* The default spacing for a list is a little too large. */
.footnote-definition ul,
.footnote-definition ol {
padding-left: 20px;
}
.footnote-definition > li {
/* Required to position the ::before target */
position: relative;
}
.footnote-definition > li:target {
scroll-margin-top: 50vh;
}
.footnote-reference:target {
scroll-margin-top: 50vh;
}
/* Draws a border around the footnote (including the marker) when it is selected.
TODO: If there are multiple linkbacks, highlight which one you just came
from so you know which one to click.
*/
.footnote-definition > li:target::before {
border: 2px solid var(--footnote-highlight);
border-radius: 6px;
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -32px;
pointer-events: none;
content: "";
}
/* Pulses the footnote reference so you can quickly see where you left off reading.
This could use some improvement.
*/
@media not (prefers-reduced-motion) {
.footnote-reference:target {
animation: fn-highlight 0.8s;
border-radius: 2px;
}
@keyframes fn-highlight {
from {
background-color: var(--footnote-highlight);
}
}
}
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}
.result-no-output {
font-style: italic;
}
.fa-svg svg {
width: 1em;
height: 1em;
fill: currentColor;
margin-bottom: -0.1em;
}
dt {
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.1em;
}
/* This uses a CSS counter to add numbers to definitions, but only if there is
more than one definition. */
dl, dt {
counter-reset: dd-counter;
}
/* When there is more than one definition, increment the counter. The first
selector selects the first definition, and the second one selects definitions
2 and beyond.*/
dd:has(+ dd), dd + dd {
counter-increment: dd-counter;
/* Use flex display to help with positioning the numbers when there is a p
tag inside the definition. */
display: flex;
align-items: flex-start;
}
/* Shows the counter for definitions. The first selector selects the first
definition, and the second one selections definitions 2 and beyond.*/
dd:has(+ dd)::before, dd + dd::before {
content: counter(dd-counter) ". ";
font-weight: 600;
display: inline-block;
margin-right: 0.5em;
}
dd > p {
/* For loose definitions that have a p tag inside, don't add a bunch of
space before the definition. */
margin-top: 0;
}
/* Remove some excess space from the bottom. */
.blockquote-tag p:last-child {
margin-bottom: 2px;
}
.blockquote-tag {
/* Add some padding to make the vertical bar a little taller than the text.*/
padding: 2px 0px 2px 20px;
/* Add a solid color bar on the left side. */
border-inline-start-style: solid;
border-inline-start-width: 4px;
/* Disable the background color from normal blockquotes . */
background-color: inherit;
/* Disable border blocks from blockquotes. */
border-block-start: none;
border-block-end: none;
}
.blockquote-tag-title svg {
fill: currentColor;
/* Add space between the icon and the title. */
margin-right: 8px;
}
.blockquote-tag-note {
border-inline-start-color: var(--blockquote-note-color);
}
.blockquote-tag-tip {
border-inline-start-color: var(--blockquote-tip-color);
}
.blockquote-tag-important {
border-inline-start-color: var(--blockquote-important-color);
}
.blockquote-tag-warning {
border-inline-start-color: var(--blockquote-warning-color);
}
.blockquote-tag-caution {
border-inline-start-color: var(--blockquote-caution-color);
}
.blockquote-tag-note .blockquote-tag-title {
color: var(--blockquote-note-color);
}
.blockquote-tag-tip .blockquote-tag-title {
color: var(--blockquote-tip-color);
}
.blockquote-tag-important .blockquote-tag-title {
color: var(--blockquote-important-color);
}
.blockquote-tag-warning .blockquote-tag-title {
color: var(--blockquote-warning-color);
}
.blockquote-tag-caution .blockquote-tag-title {
color: var(--blockquote-caution-color);
}
.blockquote-tag-title {
/* Slightly increase the weight for more emphasis. */
font-weight: 600;
/* Vertically center the icon with the text. */
display: flex;
align-items: center;
/* Remove default large margins for a more compact display. */
margin: 2px 0 8px 0;
}
.blockquote-tag-title .fa-svg {
fill: currentColor;
/* Add some space between the icon and the text. */
margin-right: 8px;
}

View File

@@ -16,6 +16,7 @@
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-attr,
.hljs-tag,
.hljs-name,
.hljs-regexp,

View File

@@ -1,18 +1,18 @@
#sidebar,
#menu-bar,
#mdbook-sidebar,
#mdbook-menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none;
}
#page-wrapper.page-wrapper {
#mdbook-page-wrapper.page-wrapper {
transform: none !important;
margin-inline-start: 0px;
overflow-y: initial;
}
#content {
#mdbook-content {
max-width: none;
margin: 0;
padding: 0;

View File

@@ -11,6 +11,7 @@
/* Tomorrow Red */
.hljs-variable,
.hljs-attribute,
.hljs-attr,
.hljs-tag,
.hljs-regexp,
.ruby .hljs-constant,
@@ -54,6 +55,7 @@
/* Tomorrow Aqua */
.hljs-title,
.hljs-section,
.css .hljs-hexcolor {
color: #8abeb7;
}

View File

@@ -2,14 +2,16 @@
/* 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;
--content-max-width: 750px;
--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 */
--code-font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
--searchbar-margin-block-start: 5px;
}
/* Themes */
@@ -56,6 +58,23 @@
--search-mark-bg: #e3b171;
--color-scheme: dark;
/* Same as `--icons` */
--copy-button-filter: invert(45%) sepia(6%) saturate(621%) hue-rotate(198deg) brightness(99%) contrast(85%);
/* Same as `--sidebar-active` */
--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);
--blockquote-note-color: #74b9ff;
--blockquote-tip-color: #09ca09;
--blockquote-important-color: #d3abff;
--blockquote-warning-color: #f0b72f;
--blockquote-caution-color: #f21424;
--sidebar-header-border-color: #c18639;
}
.coal {
@@ -100,9 +119,26 @@
--search-mark-bg: #355c7d;
--color-scheme: dark;
/* Same as `--icons` */
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
/* Same as `--sidebar-active` */
--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);
--blockquote-note-color: #4493f8;
--blockquote-tip-color: #08ae08;
--blockquote-important-color: #ab7df8;
--blockquote-warning-color: #d29922;
--blockquote-caution-color: #d91b29;
--sidebar-header-border-color: #3473ad;
}
.light {
.light, html:not(.js) {
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%);
@@ -144,6 +180,23 @@
--search-mark-bg: #a2cff5;
--color-scheme: light;
/* Same as `--icons` */
--copy-button-filter: invert(45.49%);
/* Same as `--sidebar-active` */
--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);
--blockquote-note-color: #0969da;
--blockquote-tip-color: #008000;
--blockquote-important-color: #8250df;
--blockquote-warning-color: #9a6700;
--blockquote-caution-color: #b52731;
--sidebar-header-border-color: #6e6edb;
}
.navy {
@@ -188,6 +241,23 @@
--search-mark-bg: #a2cff5;
--color-scheme: dark;
/* Same as `--icons` */
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
/* Same as `--sidebar-active` */
--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);
--blockquote-note-color: #4493f8;
--blockquote-tip-color: #09ca09;
--blockquote-important-color: #ab7df8;
--blockquote-warning-color: #d29922;
--blockquote-caution-color: #f21424;
--sidebar-header-border-color: #2f6ab5;
}
.rust {
@@ -231,11 +301,26 @@
--searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67;
--color-scheme: light;
/* Same as `--icons` */
--copy-button-filter: invert(51%) sepia(10%) saturate(393%) hue-rotate(198deg) brightness(86%) contrast(87%);
/* Same as `--sidebar-active` */
--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);
--blockquote-note-color: #023b95;
--blockquote-tip-color: #007700;
--blockquote-important-color: #8250df;
--blockquote-warning-color: #603700;
--blockquote-caution-color: #aa1721;
--sidebar-header-border-color: #8c391f;
}
@media (prefers-color-scheme: dark) {
.light.no-js {
html:not(.js) {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
@@ -275,5 +360,24 @@
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
--color-scheme: dark;
/* Same as `--icons` */
--copy-button-filter: invert(26%) sepia(8%) saturate(575%) hue-rotate(169deg) brightness(87%) contrast(82%);
/* Same as `--sidebar-active` */
--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);
--blockquote-note-color: #4493f8;
--blockquote-tip-color: #08ae08;
--blockquote-important-color: #ab7df8;
--blockquote-warning-color: #d29922;
--blockquote-caution-color: #d91b29;
--sidebar-header-border-color: #3473ad;
}
}

View File

@@ -7,7 +7,7 @@
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'),
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-300.woff2" }}') format('woff2');
}
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -16,7 +16,7 @@
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-300italic.woff2" }}') format('woff2');
}
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -25,7 +25,7 @@
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'),
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-regular.woff2" }}') format('woff2');
}
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -34,7 +34,7 @@
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'),
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-italic.woff2" }}') format('woff2');
}
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -43,7 +43,7 @@
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-600.woff2" }}') format('woff2');
}
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -52,7 +52,7 @@
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-600italic.woff2" }}') format('woff2');
}
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -61,7 +61,7 @@
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'),
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-700.woff2" }}') format('woff2');
}
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -70,7 +70,7 @@
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-700italic.woff2" }}') format('woff2');
}
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -79,7 +79,7 @@
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-800.woff2" }}') format('woff2');
}
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@@ -88,7 +88,7 @@
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
url('{{ resource "fonts/open-sans-v17-all-charsets-800italic.woff2" }}') format('woff2');
}
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
@@ -96,5 +96,5 @@
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 500;
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
src: url('{{ resource "fonts/source-code-pro-v11-all-charsets-500.woff2" }}') format('woff2');
}

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,843 @@
'use strict';
/* global default_theme, default_dark_theme, default_light_theme, hljs, ClipboardJS */
// Fix back button cache problem
window.onunload = function() { };
// Global variable, shared between modules
function playground_text(playground, hidden = true) {
const code_block = playground.querySelector('code');
if (window.ace && code_block.classList.contains('editable')) {
const editor = window.ace.edit(code_block);
return editor.getValue();
} else if (hidden) {
return code_block.textContent;
} else {
return code_block.innerText;
}
}
(function codeSnippets() {
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
]);
}
const playgrounds = Array.from(document.querySelectorAll('.playground'));
if (playgrounds.length > 0) {
fetch_with_timeout('https://play.rust-lang.org/meta/crates', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
const playground_crates = response.crates.map(item => item['id']);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playground_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playground_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
const code_block = playground_block.querySelector('code');
if (code_block.classList.contains('editable')) {
const editor = window.ace.edit(code_block);
editor.addEventListener('change', () => {
update_play_button(playground_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
name: 'run',
bindKey: {
win: 'Ctrl-Enter',
mac: 'Ctrl-Enter',
},
exec: _editor => run_rust_code(playground_block),
});
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on https://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
const play_button = pre_block.querySelector('.play-button');
// skip if code is `no_run`
if (pre_block.querySelector('code').classList.contains('no_run')) {
play_button.classList.add('hidden');
return;
}
// get list of `extern crate`'s from snippet
const txt = playground_text(pre_block);
const re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
const snippet_crates = [];
let item;
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
const all_available = snippet_crates.every(function(elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove('hidden');
play_button.hidden = false;
} else {
play_button.classList.add('hidden');
}
}
function run_rust_code(code_block) {
let result_block = code_block.querySelector('.result');
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
code_block.append(result_block);
}
const text = playground_text(code_block);
const classes = code_block.querySelector('code').classList;
let edition = '2015';
classes.forEach(className => {
if (className.startsWith('edition')) {
edition = className.slice(7);
}
});
const params = {
version: 'stable',
optimize: '0',
code: text,
edition: edition,
};
if (text.indexOf('#![feature') !== -1) {
params.version = 'nightly';
}
result_block.innerText = 'Running...';
fetch_with_timeout('https://play.rust-lang.org/evaluate.json', {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params),
})
.then(response => response.json())
.then(response => {
if (response.result.trim() === '') {
result_block.innerText = 'No output';
result_block.classList.add('result-no-output');
} else {
result_block.innerText = response.result;
result_block.classList.remove('result-no-output');
}
})
.catch(error => result_block.innerText = 'Playground Communication: ' + error.message);
}
// Syntax highlighting Configuration
hljs.configure({
tabReplace: ' ', // 4 spaces
languages: [], // Languages used for auto-detection
});
const code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function(node) {
return !node.parentElement.classList.contains('header');
});
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
code_nodes
.filter(function(node) {
return node.classList.contains('editable');
})
.forEach(function(block) {
block.classList.remove('language-rust');
});
code_nodes
.filter(function(node) {
return !node.classList.contains('editable');
})
.forEach(function(block) {
hljs.highlightBlock(block);
});
} else {
code_nodes.forEach(function(block) {
hljs.highlightBlock(block);
});
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
code_nodes.forEach(function(block) {
block.classList.add('hljs');
});
Array.from(document.querySelectorAll('code.hljs')).forEach(function(block) {
const lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines.length) {
return;
}
block.classList.add('hide-boring');
const buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = '<button title="Show hidden lines" \
aria-label="Show hidden lines"></button>';
buttons.firstChild.innerHTML = document.getElementById('fa-eye').innerHTML;
// add expand button
const pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
buttons.firstChild.addEventListener('click', function(e) {
if (this.title === 'Show hidden lines') {
this.innerHTML = document.getElementById('fa-eye-slash').innerHTML;
this.title = 'Hide lines';
this.setAttribute('aria-label', e.target.title);
block.classList.remove('hide-boring');
} else if (this.title === 'Hide lines') {
this.innerHTML = document.getElementById('fa-eye').innerHTML;
this.title = 'Show hidden lines';
this.setAttribute('aria-label', e.target.title);
block.classList.add('hide-boring');
}
});
});
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function(block) {
const pre_block = block.parentNode;
if (!pre_block.classList.contains('playground')) {
let buttons = pre_block.querySelector('.buttons');
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
const clipButton = document.createElement('button');
clipButton.className = 'clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class="tooltiptext"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
});
}
// Process playground code blocks
Array.from(document.querySelectorAll('.playground')).forEach(function(pre_block) {
// Add play button
let buttons = pre_block.querySelector('.buttons');
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
const runCodeButton = document.createElement('button');
runCodeButton.className = 'play-button';
runCodeButton.hidden = true;
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
runCodeButton.innerHTML = document.getElementById('fa-play').innerHTML;
buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', () => {
run_rust_code(pre_block);
});
if (window.playground_copyable) {
const copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
const code_block = pre_block.querySelector('code');
if (window.ace && code_block.classList.contains('editable')) {
const undoChangesButton = document.createElement('button');
undoChangesButton.className = 'reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
undoChangesButton.innerHTML +=
document.getElementById('fa-clock-rotate-left').innerHTML;
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function() {
const editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
}
});
})();
(function themes() {
const html = document.querySelector('html');
const themeToggleButton = document.getElementById('mdbook-theme-toggle');
const themePopup = document.getElementById('mdbook-theme-list');
const themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
const themeIds = [];
themePopup.querySelectorAll('button.theme').forEach(function(el) {
themeIds.push(el.id);
});
const stylesheets = {
ayuHighlight: document.querySelector('#mdbook-ayu-highlight-css'),
tomorrowNight: document.querySelector('#mdbook-tomorrow-night-css'),
highlight: document.querySelector('#mdbook-highlight-css'),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector('button#mdbook-theme-' + get_theme()).focus();
}
function updateThemeSelected() {
themePopup.querySelectorAll('.theme-selected').forEach(function(el) {
el.classList.remove('theme-selected');
});
const selected = get_saved_theme() ?? 'default_theme';
let element = themePopup.querySelector('button#mdbook-theme-' + selected);
if (element === null) {
// Fall back in case there is no "Default" item.
element = themePopup.querySelector('button#mdbook-theme-' + get_theme());
}
element.classList.add('theme-selected');
}
function hideThemes() {
themePopup.style.display = 'none';
themeToggleButton.setAttribute('aria-expanded', false);
themeToggleButton.focus();
}
function get_saved_theme() {
let theme = null;
try {
theme = localStorage.getItem('mdbook-theme');
} catch {
// ignore error.
}
return theme;
}
function delete_saved_theme() {
localStorage.removeItem('mdbook-theme');
}
function get_theme() {
const theme = get_saved_theme();
if (theme === null || theme === undefined || !themeIds.includes('mdbook-theme-' + theme)) {
if (typeof default_dark_theme === 'undefined') {
// A customized index.hbs might not define this, so fall back to
// old behavior of determining the default on page load.
return default_theme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? default_dark_theme
: default_light_theme;
} else {
return theme;
}
}
let previousTheme = default_theme;
function set_theme(theme, store = true) {
let ace_theme;
if (theme === 'coal' || theme === 'navy') {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = false;
stylesheets.highlight.disabled = true;
ace_theme = 'ace/theme/tomorrow_night';
} else if (theme === 'ayu') {
stylesheets.ayuHighlight.disabled = false;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = true;
ace_theme = 'ace/theme/tomorrow_night';
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = 'ace/theme/dawn';
}
setTimeout(function() {
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function(editor) {
editor.setTheme(ace_theme);
});
}
if (store) {
try {
localStorage.setItem('mdbook-theme', theme);
} catch {
// ignore error.
}
}
html.classList.remove(previousTheme);
html.classList.add(theme);
previousTheme = theme;
updateThemeSelected();
}
const query = window.matchMedia('(prefers-color-scheme: dark)');
query.onchange = function() {
set_theme(get_theme(), false);
};
// Set theme.
set_theme(get_theme(), false);
themeToggleButton.addEventListener('click', function() {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
showThemes();
}
});
themePopup.addEventListener('click', function(e) {
let theme;
if (e.target.className === 'theme') {
theme = e.target.id;
} else if (e.target.parentElement.className === 'theme') {
theme = e.target.parentElement.id;
} else {
return;
}
theme = theme.replace(/^mdbook-theme-/, '');
if (theme === 'default_theme' || theme === null) {
delete_saved_theme();
set_theme(get_theme(), false);
} else {
set_theme(theme);
}
});
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget &&
!themeToggleButton.contains(e.relatedTarget) &&
!themePopup.contains(e.relatedTarget)
) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS:
// https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' &&
!themeToggleButton.contains(e.target) &&
!themePopup.contains(e.target)
) {
hideThemes();
}
});
document.addEventListener('keydown', function(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
if (!themePopup.contains(e.target)) {
return;
}
let li;
switch (e.key) {
case 'Escape':
e.preventDefault();
hideThemes();
break;
case 'ArrowUp':
e.preventDefault();
li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
li = document.activeElement.parentElement;
if (li && li.nextElementSibling) {
li.nextElementSibling.querySelector('button').focus();
}
break;
case 'Home':
e.preventDefault();
themePopup.querySelector('li:first-child button').focus();
break;
case 'End':
e.preventDefault();
themePopup.querySelector('li:last-child button').focus();
break;
}
});
})();
(function sidebar() {
const sidebar = document.getElementById('mdbook-sidebar');
const sidebarLinks = document.querySelectorAll('#mdbook-sidebar a');
const sidebarToggleButton = document.getElementById('mdbook-sidebar-toggle');
const sidebarResizeHandle = document.getElementById('mdbook-sidebar-resize-handle');
const sidebarCheckbox = document.getElementById('mdbook-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() {
document.documentElement.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function(link) {
link.setAttribute('tabIndex', 0);
});
sidebarToggleButton.setAttribute('aria-expanded', true);
sidebar.setAttribute('aria-hidden', false);
try {
localStorage.setItem('mdbook-sidebar', 'visible');
} catch {
// Ignore error.
}
}
function hideSidebar() {
document.documentElement.classList.remove('sidebar-visible');
Array.from(sidebarLinks).forEach(function(link) {
link.setAttribute('tabIndex', -1);
});
sidebarToggleButton.setAttribute('aria-expanded', false);
sidebar.setAttribute('aria-hidden', true);
try {
localStorage.setItem('mdbook-sidebar', 'hidden');
} catch {
// Ignore error.
}
}
// Toggle sidebar
sidebarCheckbox.addEventListener('change', function sidebarToggle() {
if (sidebarCheckbox.checked) {
const current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
}
showSidebar();
} else {
hideSidebar();
}
});
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize() {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
document.documentElement.classList.add('sidebar-resizing');
}
function resize(e) {
let pos = e.clientX - sidebar.offsetLeft;
if (pos < 20) {
hideSidebar();
} else {
if (!document.documentElement.classList.contains('sidebar-visible')) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize() {
document.documentElement.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function(e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now(),
};
}, { passive: true });
document.addEventListener('touchmove', function(e) {
if (!firstContact) {
return;
}
const curX = e.touches[0].clientX;
const xDiff = curX - firstContact.x,
tDiff = Date.now() - firstContact.time;
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) {
showSidebar();
} else if (xDiff < 0 && curX < 300) {
hideSidebar();
}
firstContact = null;
}
}, { passive: true });
})();
(function chapterNavigation() {
document.addEventListener('keydown', function(e) {
if (e.altKey || e.ctrlKey || e.metaKey) {
return;
}
if (window.search && window.search.hasFocus()) {
return;
}
const html = document.querySelector('html');
function next() {
const nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
}
function prev() {
const previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
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();
if (html.dir === 'rtl') {
prev();
} else {
next();
}
break;
case 'ArrowLeft':
e.preventDefault();
if (html.dir === 'rtl') {
next();
} else {
prev();
}
break;
}
});
})();
(function clipboard() {
const clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = '';
elem.className = 'clip-button';
}
function showTooltip(elem, msg) {
elem.firstChild.innerText = msg;
elem.className = 'clip-button tooltipped';
}
const clipboardSnippets = new ClipboardJS('.clip-button', {
text: function(trigger) {
hideTooltip(trigger);
const playground = trigger.closest('pre');
return playground_text(playground, false);
},
});
Array.from(clipButtons).forEach(function(clipButton) {
clipButton.addEventListener('mouseout', function(e) {
hideTooltip(e.currentTarget);
});
});
clipboardSnippets.on('success', function(e) {
e.clearSelection();
showTooltip(e.trigger, 'Copied!');
});
clipboardSnippets.on('error', function(e) {
showTooltip(e.trigger, 'Clipboard error!');
});
})();
(function scrollToTop() {
const menuTitle = document.querySelector('.menu-title');
menuTitle.addEventListener('click', function() {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
(function controllMenu() {
const menu = document.getElementById('mdbook-menu-bar');
(function controllPosition() {
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 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);
menu.classList.remove('sticky');
let stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function() {
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
// `null` means that it doesn't need to be updated
let nextSticky = null;
let nextTop = null;
const scrollDown = scrollTop > prevScrollTop;
const menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {
nextTop = prevScrollTop;
}
} else {
if (menuPosAbsoluteY > 0) {
nextSticky = true;
} else if (menuPosAbsoluteY < minMenuY) {
nextTop = prevScrollTop + minMenuY;
}
}
if (nextSticky === true && stickyCache === false) {
menu.classList.add('sticky');
stickyCache = true;
} else if (nextSticky === false && stickyCache === true) {
menu.classList.remove('sticky');
stickyCache = false;
}
if (nextTop !== null) {
menu.style.top = nextTop + 'px';
topCache = nextTop;
}
prevScrollTop = scrollTop;
}, { passive: true });
})();
(function controllBorder() {
function updateBorder() {
if (menu.offsetTop === 0) {
menu.classList.remove('bordered');
} else {
menu.classList.add('bordered');
}
}
updateBorder();
document.addEventListener('scroll', updateBorder, { passive: true });
})();
})();

View File

@@ -0,0 +1,555 @@
'use strict';
/* global Mark, elasticlunr, path_to_root */
window.search = window.search || {};
(function search() {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
// event handlers while the user is typing their search.
if (!Mark || !elasticlunr) {
return;
}
// eslint-disable-next-line max-len
// IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search, pos) {
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
};
}
const search_wrap = document.getElementById('mdbook-search-wrapper'),
searchbar_outer = document.getElementById('mdbook-searchbar-outer'),
searchbar = document.getElementById('mdbook-searchbar'),
searchresults = document.getElementById('mdbook-searchresults'),
searchresults_outer = document.getElementById('mdbook-searchresults-outer'),
searchresults_header = document.getElementById('mdbook-searchresults-header'),
searchicon = document.getElementById('mdbook-search-toggle'),
content = document.getElementById('mdbook-content'),
// 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';
let current_searchterm = '',
doc_urls = [],
search_options = {
bool: 'AND',
expand: true,
fields: {
title: {boost: 1},
body: {boost: 1},
breadcrumbs: {boost: 0},
},
},
searchindex = null,
results_options = {
teaser_word_count: 30,
limit_results: 30,
},
teaser_count = 0;
function hasFocus() {
return searchbar === document.activeElement;
}
function removeChildren(elem) {
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
}
// Helper to parse a url into its building blocks.
function parseURL(url) {
const a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':', ''),
host: a.hostname,
port: a.port,
params: (function() {
const ret = {};
const seg = a.search.replace(/^\?/, '').split('&');
for (const part of seg) {
if (!part) {
continue;
}
const s = part.split('=');
ret[s[0]] = s[1];
}
return ret;
})(),
file: (a.pathname.match(/\/([^/?#]+)$/i) || ['', ''])[1],
hash: a.hash.replace('#', ''),
path: a.pathname.replace(/^([^/])/, '/$1'),
};
}
// Helper to recreate a url string from its building blocks.
function renderURL(urlobject) {
let url = urlobject.protocol + '://' + urlobject.host;
if (urlobject.port !== '') {
url += ':' + urlobject.port;
}
url += urlobject.path;
let joiner = '?';
for (const prop in urlobject.params) {
if (Object.prototype.hasOwnProperty.call(urlobject.params, prop)) {
url += joiner + prop + '=' + urlobject.params[prop];
joiner = '&';
}
}
if (urlobject.hash !== '') {
url += '#' + urlobject.hash;
}
return url;
}
// Helper to escape html special chars for displaying the teasers
const escapeHTML = (function() {
const MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
'\'': '&#39;',
};
const repl = function(c) {
return MAP[c];
};
return function(s) {
return s.replace(/[&<>'"]/g, repl);
};
})();
function formatSearchMetric(count, searchterm) {
if (count === 1) {
return count + ' search result for \'' + searchterm + '\':';
} else if (count === 0) {
return 'No search results for \'' + searchterm + '\'.';
} else {
return count + ' search results for \'' + searchterm + '\':';
}
}
function formatSearchResult(result, searchterms) {
const teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
const url = doc_urls[result.ref].split('#');
if (url.length === 1) { // no anchor found
url.push('');
}
// encodeURIComponent escapes all chars that could allow an XSS except
// for '. Due to that we also manually replace ' with its url-encoded
// representation (%27).
const encoded_search = encodeURIComponent(searchterms.join(' ')).replace(/'/g, '%27');
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + encoded_search
+ '#' + url[1] + '" aria-details="mdbook-teaser_' + teaser_count + '">'
+ result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="mdbook-teaser_' + teaser_count
+ '" aria-label="Search Result Teaser">' + teaser + '</span>';
}
function makeTeaser(body, searchterms) {
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <em>.
const stemmed_searchterms = searchterms.map(function(w) {
return elasticlunr.stemmer(w.toLowerCase());
});
const searchterm_weight = 40;
const weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
const sentences = body.toLowerCase().split('. ');
let index = 0;
let value = 0;
let searchterm_found = false;
for (const sentenceindex in sentences) {
const words = sentences[sentenceindex].split(' ');
value = 8;
for (const wordindex in words) {
const word = words[wordindex];
if (word.length > 0) {
for (const searchtermindex in stemmed_searchterms) {
if (elasticlunr.stemmer(word).startsWith(
stemmed_searchterms[searchtermindex])
) {
value = searchterm_weight;
searchterm_found = true;
}
}
weighted.push([word, value, index]);
value = 2;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
}
index += 1; // because we split at a two-char boundary '. '
}
if (weighted.length === 0) {
return body;
}
const window_weight = [];
const window_size = Math.min(weighted.length, results_options.teaser_word_count);
let cur_sum = 0;
for (let wordindex = 0; wordindex < window_size; wordindex++) {
cur_sum += weighted[wordindex][1];
}
window_weight.push(cur_sum);
for (let wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
cur_sum -= weighted[wordindex][1];
cur_sum += weighted[wordindex + window_size][1];
window_weight.push(cur_sum);
}
let max_sum_window_index = 0;
if (searchterm_found) {
let max_sum = 0;
// backwards
for (let i = window_weight.length - 1; i >= 0; i--) {
if (window_weight[i] > max_sum) {
max_sum = window_weight[i];
max_sum_window_index = i;
}
}
} else {
max_sum_window_index = 0;
}
// add <em/> around searchterms
const teaser_split = [];
index = weighted[max_sum_window_index][2];
for (let i = max_sum_window_index; i < max_sum_window_index + window_size; i++) {
const word = weighted[i];
if (index < word[2]) {
// missing text from index to start of `word`
teaser_split.push(body.substring(index, word[2]));
index = word[2];
}
if (word[1] === searchterm_weight) {
teaser_split.push('<em>');
}
index = word[2] + word[0].length;
teaser_split.push(body.substring(word[2], index));
if (word[1] === searchterm_weight) {
teaser_split.push('</em>');
}
}
return teaser_split.join('');
}
function init(config) {
results_options = config.results_options;
search_options = config.search_options;
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();
}, false);
searchbar.addEventListener('keyup', () => {
searchbarKeyUpHandler();
}, false);
document.addEventListener('keydown', e => {
globalKeyHandler(e);
}, false);
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = () => {
doSearchOrMarkFromUrl();
};
// Suppress "submit" events so the page doesn't reload when the user presses Enter
document.addEventListener('submit', e => {
e.preventDefault();
}, false);
// 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');
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
searchicon.appendChild(tmp);
tmp.focus();
tmp.remove();
}
// On reload or browser history backwards/forwards events, parse the url and do search or mark
function doSearchOrMarkFromUrl() {
// Check current URL for search request
const url = parseURL(window.location.href);
if (Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM)
&& url.params[URL_SEARCH_PARAM] !== '') {
showSearch(true);
searchbar.value = decodeURIComponent(
(url.params[URL_SEARCH_PARAM] + '').replace(/\+/g, '%20'));
searchbarKeyUpHandler(); // -> doSearch()
} else {
showSearch(false);
}
if (Object.prototype.hasOwnProperty.call(url.params, URL_MARK_PARAM)) {
const words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
marker.mark(words, {
exclude: mark_exclude,
});
const markers = document.querySelectorAll('mark');
const hide = () => {
for (let i = 0; i < markers.length; i++) {
markers[i].classList.add('fade-out');
window.setTimeout(() => {
marker.unmark();
}, 300);
}
};
for (let i = 0; i < markers.length; i++) {
markers[i].addEventListener('click', hide);
}
}
}
// Eventhandler for keyevents on `document`
function globalKeyHandler(e) {
if (e.altKey ||
e.ctrlKey ||
e.metaKey ||
e.shiftKey ||
e.target.type === 'textarea' ||
e.target.type === 'text' ||
!hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)
) {
return;
}
if (e.key === 'Escape') {
e.preventDefault();
searchbar.classList.remove('active');
setSearchUrlParameters('',
searchbar.value.trim() !== '' ? 'push' : 'replace');
if (hasFocus()) {
unfocusSearchbar();
}
showSearch(false);
marker.unmark();
} else if (!hasFocus() && (e.key === 's' || e.key === '/')) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else if (hasFocus() && (e.key === 'ArrowDown'
|| e.key === 'Enter')) {
e.preventDefault();
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.key === 'ArrowDown') {
const next = focused.nextElementSibling;
if (next) {
focused.classList.remove('focus');
next.classList.add('focus');
}
} else if (e.key === 'ArrowUp') {
focused.classList.remove('focus');
const prev = focused.previousElementSibling;
if (prev) {
prev.classList.add('focus');
} else {
searchbar.select();
}
} 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" }}',
'mdbook-search-index');
search_wrap.classList.remove('hidden');
searchicon.setAttribute('aria-expanded', 'true');
} else {
search_wrap.classList.add('hidden');
searchicon.setAttribute('aria-expanded', 'false');
const results = searchresults.children;
for (let i = 0; i < results.length; i++) {
results[i].classList.remove('focus');
}
}
}
function showResults(yes) {
if (yes) {
searchresults_outer.classList.remove('hidden');
} else {
searchresults_outer.classList.add('hidden');
}
}
// Eventhandler for search icon
function searchIconClickHandler() {
if (search_wrap.classList.contains('hidden')) {
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else {
showSearch(false);
}
}
// Eventhandler for keyevents while the searchbar is focused
function searchbarKeyUpHandler() {
const searchterm = searchbar.value.trim();
if (searchterm !== '') {
searchbar.classList.add('active');
doSearch(searchterm);
} else {
searchbar.classList.remove('active');
showResults(false);
removeChildren(searchresults);
}
setSearchUrlParameters(searchterm, 'push_if_new_search_else_replace');
// Remove marks
marker.unmark();
}
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and
// `#heading-anchor`. `action` can be one of "push", "replace",
// "push_if_new_search_else_replace" and replaces or pushes a new browser history item.
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
function setSearchUrlParameters(searchterm, action) {
const url = parseURL(window.location.href);
const first_search = !Object.prototype.hasOwnProperty.call(url.params, URL_SEARCH_PARAM);
if (searchterm !== '' || action === 'push_if_new_search_else_replace') {
url.params[URL_SEARCH_PARAM] = searchterm;
delete url.params[URL_MARK_PARAM];
url.hash = '';
} else {
delete url.params[URL_MARK_PARAM];
delete url.params[URL_SEARCH_PARAM];
}
// A new search will also add a new history item, so the user can go back
// to the page prior to searching. A updated search term will only replace
// the url.
if (action === 'push' || action === 'push_if_new_search_else_replace' && first_search ) {
history.pushState({}, document.title, renderURL(url));
} else if (action === 'replace' ||
action === 'push_if_new_search_else_replace' &&
!first_search
) {
history.replaceState({}, document.title, renderURL(url));
}
}
function doSearch(searchterm) {
// Don't search the same twice
if (current_searchterm === searchterm) {
return;
}
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);
// Display search metrics
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
// Clear and insert results
const searchterms = searchterm.split(' ');
removeChildren(searchresults);
for (let i = 0; i < resultcount ; i++) {
const resultElem = document.createElement('li');
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
searchresults.appendChild(resultElem);
}
// Display results
showResults(true);
searchbar_outer.classList.remove('searching');
}
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View File

@@ -0,0 +1,367 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="{{ default_theme }} sidebar-visible" dir="{{ text_direction }}">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>{{ title }}</title>
{{#if is_print }}
<meta name="robots" content="noindex">
{{/if}}
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
{{#if favicon_svg}}
<link rel="icon" href="{{ resource "favicon.svg" }}">
{{/if}}
{{#if favicon_png}}
<link rel="shortcut icon" href="{{ resource "favicon.png" }}">
{{/if}}
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
{{#if print_enable}}
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
{{/if}}
<!-- Fonts -->
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" id="mdbook-highlight-css" href="{{ resource "highlight.css" }}">
<link rel="stylesheet" id="mdbook-tomorrow-night-css" href="{{ resource "tomorrow-night.css" }}">
<link rel="stylesheet" id="mdbook-ayu-highlight-css" href="{{ resource "ayu-highlight.css" }}">
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ resource this }}">
{{/each}}
{{#if mathjax_support}}
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
<!-- Provide site root and default themes to javascript -->
<script>
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="mdbook-body-container">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
let theme = localStorage.getItem('mdbook-theme');
let sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
let theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
const html = document.documentElement;
html.classList.remove('{{ default_theme }}')
html.classList.add(theme);
html.classList.add("js");
</script>
<input type="checkbox" id="mdbook-sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
let sidebar = null;
const sidebar_toggle = document.getElementById("mdbook-sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
sidebar_toggle.checked = false;
}
if (sidebar === 'visible') {
sidebar_toggle.checked = true;
} else {
html.classList.remove('sidebar-visible');
}
</script>
<nav id="mdbook-sidebar" class="sidebar" aria-label="Table of contents">
<!-- populated by js -->
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
<noscript>
<iframe class="sidebar-iframe-outer" src="{{ path_to_root }}toc.html"></iframe>
</noscript>
<div id="mdbook-sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<div id="mdbook-page-wrapper" class="page-wrapper">
<div class="page">
{{> header}}
<div id="mdbook-menu-bar-hover-placeholder"></div>
<div id="mdbook-menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="mdbook-sidebar-toggle" class="icon-button" for="mdbook-sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="mdbook-sidebar">
{{fa "solid" "bars"}}
</label>
<button id="mdbook-theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="mdbook-theme-list">
{{fa "solid" "paintbrush"}}
</button>
<ul id="mdbook-theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-default_theme">Auto</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="mdbook-theme-ayu">Ayu</button></li>
</ul>
{{#if search_enabled}}
<button id="mdbook-search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="mdbook-searchbar">
{{fa "solid" "magnifying-glass"}}
</button>
{{/if}}
</div>
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
{{#if print_enable}}
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
{{fa "solid" "print" "print-button"}}
</a>
{{/if}}
{{#if git_repository_url}}
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
{{fa git_repository_icon_class git_repository_icon}}
</a>
{{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit" rel="edit">
{{fa "solid" "pencil" "git-edit-button"}}
</a>
{{/if}}
</div>
</div>
{{#if search_enabled}}
<div id="mdbook-search-wrapper" class="hidden">
<form id="mdbook-searchbar-outer" class="searchbar-outer">
<div class="search-wrapper">
<input type="search" id="mdbook-searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="mdbook-searchresults-outer" aria-describedby="searchresults-header">
<div class="spinner-wrapper">
{{fa "solid" "spinner" "fa-spin"}}
</div>
</div>
</form>
<div id="mdbook-searchresults-outer" class="searchresults-outer hidden">
<div id="mdbook-searchresults-header" class="searchresults-header"></div>
<ul id="mdbook-searchresults">
</ul>
</div>
</div>
{{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('mdbook-sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('mdbook-sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#mdbook-sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="mdbook-content" class="content">
<main>
{{{ content }}}
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
{{#if previous}}
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
{{#if (eq ../text_direction "rtl")}}
{{fa "solid" "angle-right"}}
{{else}}
{{fa "solid" "angle-left"}}
{{/if}}
</a>
{{/if}}
{{#if next}}
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
{{#if (eq ../text_direction "rtl")}}
{{fa "solid" "angle-left"}}
{{else}}
{{fa "solid" "angle-right"}}
{{/if}}
</a>
{{/if}}
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
{{#if previous}}
<a rel="prev" href="{{ path_to_root }}{{previous.link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
{{#if (eq ../text_direction "rtl")}}
{{fa "solid" "angle-right"}}
{{else}}
{{fa "solid" "angle-left"}}
{{/if}}
</a>
{{/if}}
{{#if next}}
<a rel="next prefetch" href="{{ path_to_root }}{{next.link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
{{#if (eq text_direction "rtl")}}
{{fa "solid" "angle-left"}}
{{else}}
{{fa "solid" "angle-right"}}
{{/if}}
</a>
{{/if}}
</nav>
</div>
<template id=fa-eye>{{fa "solid" "eye"}}</template>
<template id=fa-eye-slash>{{fa "solid" "eye-slash"}}</template>
<template id=fa-copy>{{fa "regular" "copy"}}</template>
<template id=fa-play>{{fa "solid" "play"}}</template>
<template id=fa-clock-rotate-left>{{fa "solid" "clock-rotate-left"}}</template>
{{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) -->
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload();
}
};
window.onbeforeunload = function() {
socket.close();
}
</script>
{{/if}}
{{#if playground_line_numbers}}
<script>
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playground_copyable}}
<script>
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ resource "ace.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}}
{{#if search_js}}
<script src="{{ resource "elasticlunr.min.js" }}"></script>
<script src="{{ resource "mark.min.js" }}"></script>
<script src="{{ resource "searcher.js" }}"></script>
{{/if}}
<script src="{{ resource "clipboard.min.js" }}"></script>
<script src="{{ resource "highlight.js" }}"></script>
<script src="{{ resource "book.js" }}"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script src="{{ resource this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script>
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
});
});
</script>
{{else}}
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
{{/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

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; URL={{url}}">
<link rel="canonical" href="{{url}}">
</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

@@ -0,0 +1,40 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
<head>
<!-- sidebar iframe generated using mdBook
This is a frame, and not included directly in the page, to control the total size of the
book. The TOC contains an entry for each page, so if each page includes a copy of the TOC,
the total size of the page becomes O(n**2).
The frame is only used as a fallback when JS is turned off. When it's on, the sidebar is
instead added to the main page by `toc.js` instead. The JavaScript mode is better
because, when running in a `file:///` URL, the iframed page would not be Same-Origin as
the rest of the page, so the sidebar and the main page theme would fall out of sync.
-->
<meta charset="UTF-8">
<meta name="robots" content="noindex">
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ resource "css/variables.css" }}">
<link rel="stylesheet" href="{{ resource "css/general.css" }}">
<link rel="stylesheet" href="{{ resource "css/chrome.css" }}">
{{#if print_enable}}
<link rel="stylesheet" href="{{ resource "css/print.css" }}" media="print">
{{/if}}
<!-- Fonts -->
<link rel="stylesheet" href="{{ resource "fonts/fonts.css" }}">
<!-- Custom theme stylesheets -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ resource this }}">
{{/each}}
</head>
<body class="sidebar-iframe-inner">
{{#toc}}{{/toc}}
</body>
</html>

View File

@@ -0,0 +1,448 @@
// Populate the sidebar
//
// This is a script, and not included directly in the page, to control the total size of the book.
// The TOC contains an entry for each page, so if each page includes a copy of the TOC,
// the total size of the page becomes O(n**2).
class MDBookSidebarScrollbox extends HTMLElement {
constructor() {
super();
}
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].split('?')[0];
if (current_page.endsWith('/')) {
current_page += 'index.html';
}
const links = Array.prototype.slice.call(this.querySelectorAll('a'));
const l = links.length;
for (let i = 0; i < l; ++i) {
const link = links[i];
const href = link.getAttribute('href');
if (href && !href.startsWith('#') && !/^(?:[a-z+]+:)?\/\//.test(href)) {
link.href = path_to_root + href;
}
// The 'index' page is supposed to alias the first chapter in the book.
if (link.href === current_page
|| i === 0
&& path_to_root === ''
&& current_page.endsWith('/index.html')) {
link.classList.add('active');
let parent = link.parentElement;
while (parent) {
if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) {
parent.classList.add('expanded');
}
parent = parent.parentElement;
}
}
}
// Track and set sidebar scroll position
this.addEventListener('click', e => {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', this.scrollTop);
}
}, { passive: true });
const sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
this.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via
// 'next/previous chapter' buttons
const activeSection = document.querySelector('#mdbook-sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
// Toggle buttons
const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle');
function toggleSection(ev) {
ev.currentTarget.parentElement.parentElement.classList.toggle('expanded');
}
Array.from(sidebarAnchorToggles).forEach(el => {
el.addEventListener('click', toggleSection);
});
}
}
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
{{#if sidebar_header_nav}}
// ---------------------------------------------------------------------------
// Support for dynamically adding headers to the sidebar.
(function() {
// This is used to detect which direction the page has scrolled since the
// last scroll event.
let lastKnownScrollPosition = 0;
// This is the threshold in px from the top of the screen where it will
// consider a header the "current" header when scrolling down.
const defaultDownThreshold = 150;
// Same as defaultDownThreshold, except when scrolling up.
const defaultUpThreshold = 300;
// The threshold is a virtual horizontal line on the screen where it
// considers the "current" header to be above the line. The threshold is
// modified dynamically to handle headers that are near the bottom of the
// screen, and to slightly offset the behavior when scrolling up vs down.
let threshold = defaultDownThreshold;
// This is used to disable updates while scrolling. This is needed when
// clicking the header in the sidebar, which triggers a scroll event. It
// is somewhat finicky to detect when the scroll has finished, so this
// uses a relatively dumb system of disabling scroll updates for a short
// time after the click.
let disableScroll = false;
// Array of header elements on the page.
let headers;
// Array of li elements that are initially collapsed headers in the sidebar.
// I'm not sure why eslint seems to have a false positive here.
// eslint-disable-next-line prefer-const
let headerToggles = [];
// This is a debugging tool for the threshold which you can enable in the console.
let thresholdDebug = false;
// Updates the threshold based on the scroll position.
function updateThreshold() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// The number of pixels below the viewport, at most documentHeight.
// This is used to push the threshold down to the bottom of the page
// as the user scrolls towards the bottom.
const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight));
// The number of pixels above the viewport, at least defaultDownThreshold.
// Similar to pixelsBelow, this is used to push the threshold back towards
// the top when reaching the top of the page.
const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop);
// How much the threshold should be offset once it gets close to the
// bottom of the page.
const bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold);
let adjustedBottomAdd = bottomAdd;
// Adjusts bottomAdd for a small document. The calculation above
// assumes the document is at least twice the windowheight in size. If
// it is less than that, then bottomAdd needs to be shrunk
// proportional to the difference in size.
if (documentHeight < windowHeight * 2) {
const maxPixelsBelow = documentHeight - windowHeight;
const t = 1 - pixelsBelow / Math.max(1, maxPixelsBelow);
const clamp = Math.max(0, Math.min(1, t));
adjustedBottomAdd *= clamp;
}
let scrollingDown = true;
if (scrollTop < lastKnownScrollPosition) {
scrollingDown = false;
}
if (scrollingDown) {
// When scrolling down, move the threshold up towards the default
// downwards threshold position. If near the bottom of the page,
// adjustedBottomAdd will offset the threshold towards the bottom
// of the page.
const amountScrolledDown = scrollTop - lastKnownScrollPosition;
const adjustedDefault = defaultDownThreshold + adjustedBottomAdd;
threshold = Math.max(adjustedDefault, threshold - amountScrolledDown);
} else {
// When scrolling up, move the threshold down towards the default
// upwards threshold position. If near the bottom of the page,
// quickly transition the threshold back up where it normally
// belongs.
const amountScrolledUp = lastKnownScrollPosition - scrollTop;
const adjustedDefault = defaultUpThreshold - pixelsAbove
+ Math.max(0, adjustedBottomAdd - defaultDownThreshold);
threshold = Math.min(adjustedDefault, threshold + amountScrolledUp);
}
if (documentHeight <= windowHeight) {
threshold = 0;
}
if (thresholdDebug) {
const id = 'mdbook-threshold-debug-data';
let data = document.getElementById(id);
if (data === null) {
data = document.createElement('div');
data.id = id;
data.style.cssText = `
position: fixed;
top: 50px;
right: 10px;
background-color: 0xeeeeee;
z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(data);
}
data.innerHTML = `
<table>
<tr><td>documentHeight</td><td>${documentHeight.toFixed(1)}</td></tr>
<tr><td>windowHeight</td><td>${windowHeight.toFixed(1)}</td></tr>
<tr><td>scrollTop</td><td>${scrollTop.toFixed(1)}</td></tr>
<tr><td>pixelsAbove</td><td>${pixelsAbove.toFixed(1)}</td></tr>
<tr><td>pixelsBelow</td><td>${pixelsBelow.toFixed(1)}</td></tr>
<tr><td>bottomAdd</td><td>${bottomAdd.toFixed(1)}</td></tr>
<tr><td>adjustedBottomAdd</td><td>${adjustedBottomAdd.toFixed(1)}</td></tr>
<tr><td>scrollingDown</td><td>${scrollingDown}</td></tr>
<tr><td>threshold</td><td>${threshold.toFixed(1)}</td></tr>
</table>
`;
drawDebugLine();
}
lastKnownScrollPosition = scrollTop;
}
function drawDebugLine() {
if (!document.body) {
return;
}
const id = 'mdbook-threshold-debug-line';
const existingLine = document.getElementById(id);
if (existingLine) {
existingLine.remove();
}
const line = document.createElement('div');
line.id = id;
line.style.cssText = `
position: fixed;
top: ${threshold}px;
left: 0;
width: 100vw;
height: 2px;
background-color: red;
z-index: 9999;
pointer-events: none;
`;
document.body.appendChild(line);
}
function mdbookEnableThresholdDebug() {
thresholdDebug = true;
updateThreshold();
drawDebugLine();
}
window.mdbookEnableThresholdDebug = mdbookEnableThresholdDebug;
// Updates which headers in the sidebar should be expanded. If the current
// header is inside a collapsed group, then it, and all its parents should
// be expanded.
function updateHeaderExpanded(currentA) {
// Add expanded to all header-item li ancestors.
let current = currentA.parentElement;
while (current) {
if (current.tagName === 'LI' && current.classList.contains('header-item')) {
current.classList.add('expanded');
}
current = current.parentElement;
}
}
// Updates which header is marked as the "current" header in the sidebar.
// This is done with a virtual Y threshold, where headers at or below
// that line will be considered the current one.
function updateCurrentHeader() {
if (!headers || !headers.length) {
return;
}
// Reset the classes, which will be rebuilt below.
const els = document.getElementsByClassName('current-header');
for (const el of els) {
el.classList.remove('current-header');
}
for (const toggle of headerToggles) {
toggle.classList.remove('expanded');
}
// Find the last header that is above the threshold.
let lastHeader = null;
for (const header of headers) {
const rect = header.getBoundingClientRect();
if (rect.top <= threshold) {
lastHeader = header;
} else {
break;
}
}
if (lastHeader === null) {
lastHeader = headers[0];
const rect = lastHeader.getBoundingClientRect();
const windowHeight = window.innerHeight;
if (rect.top >= windowHeight) {
return;
}
}
// Get the anchor in the summary.
const href = '#' + lastHeader.id;
const a = [...document.querySelectorAll('.header-in-summary')]
.find(element => element.getAttribute('href') === href);
if (!a) {
return;
}
a.classList.add('current-header');
updateHeaderExpanded(a);
}
// Updates which header is "current" based on the threshold line.
function reloadCurrentHeader() {
if (disableScroll) {
return;
}
updateThreshold();
updateCurrentHeader();
}
// When clicking on a header in the sidebar, this adjusts the threshold so
// that it is located next to the header. This is so that header becomes
// "current".
function headerThresholdClick(event) {
// See disableScroll description why this is done.
disableScroll = true;
setTimeout(() => {
disableScroll = false;
}, 100);
// requestAnimationFrame is used to delay the update of the "current"
// header until after the scroll is done, and the header is in the new
// position.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Closest is needed because if it has child elements like <code>.
const a = event.target.closest('a');
const href = a.getAttribute('href');
const targetId = href.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
threshold = targetElement.getBoundingClientRect().bottom;
updateCurrentHeader();
}
});
});
}
// Takes the nodes from the given head and copies them over to the
// destination, along with some filtering.
function filterHeader(source, dest) {
const clone = source.cloneNode(true);
clone.querySelectorAll('mark').forEach(mark => {
mark.replaceWith(...mark.childNodes);
});
dest.append(...clone.childNodes);
}
// Scans page for headers and adds them to the sidebar.
document.addEventListener('DOMContentLoaded', function() {
const activeSection = document.querySelector('#mdbook-sidebar .active');
if (activeSection === null) {
return;
}
const main = document.getElementsByTagName('main')[0];
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
if (headers.length === 0) {
return;
}
// Build a tree of headers in the sidebar.
const stack = [];
const firstLevel = parseInt(headers[0].tagName.charAt(1));
for (let i = 1; i < firstLevel; i++) {
const ol = document.createElement('ol');
ol.classList.add('section');
if (stack.length > 0) {
stack[stack.length - 1].ol.appendChild(ol);
}
stack.push({level: i + 1, ol: ol});
}
// The level where it will start folding deeply nested headers.
const foldLevel = 3;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const level = parseInt(header.tagName.charAt(1));
const currentLevel = stack[stack.length - 1].level;
if (level > currentLevel) {
// Begin nesting to this level.
for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) {
const ol = document.createElement('ol');
ol.classList.add('section');
const last = stack[stack.length - 1];
const lastChild = last.ol.lastChild;
// Handle the case where jumping more than one nesting
// level, which doesn't have a list item to place this new
// list inside of.
if (lastChild) {
lastChild.appendChild(ol);
} else {
last.ol.appendChild(ol);
}
stack.push({level: nextLevel, ol: ol});
}
} else if (level < currentLevel) {
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
stack.pop();
}
}
const li = document.createElement('li');
li.classList.add('header-item');
li.classList.add('expanded');
if (level < foldLevel) {
li.classList.add('expanded');
}
const span = document.createElement('span');
span.classList.add('chapter-link-wrapper');
const a = document.createElement('a');
span.appendChild(a);
a.href = '#' + header.id;
a.classList.add('header-in-summary');
filterHeader(header.children[0], a);
a.addEventListener('click', headerThresholdClick);
const nextHeader = headers[i + 1];
if (nextHeader !== undefined) {
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
if (nextLevel > level && level >= foldLevel) {
const toggle = document.createElement('a');
toggle.classList.add('chapter-fold-toggle');
toggle.classList.add('header-toggle');
toggle.addEventListener('click', () => {
li.classList.toggle('expanded');
});
const toggleDiv = document.createElement('div');
toggleDiv.textContent = '❱';
toggle.appendChild(toggleDiv);
span.appendChild(toggle);
headerToggles.push(li);
}
}
li.appendChild(span);
const currentParent = stack[stack.length - 1];
currentParent.ol.appendChild(li);
}
const onThisPage = document.createElement('div');
onThisPage.classList.add('on-this-page');
onThisPage.append(stack[0].ol);
const activeItemSpan = activeSection.parentElement;
activeItemSpan.after(onThisPage);
});
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
})();
{{/if}}

View File

@@ -0,0 +1,26 @@
use pulldown_cmark::BlockQuoteKind;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_NOTE: &str = r#"<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_TIP: &str = r#"<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_IMPORTANT: &str = r#"<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_WARNING: &str = r#"<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_CAUTION: &str = r#"<path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
pub(crate) fn select_tag(kind: BlockQuoteKind) -> (&'static str, &'static str, &'static str) {
match kind {
BlockQuoteKind::Note => ("note", ICON_NOTE, "Note"),
BlockQuoteKind::Tip => ("tip", ICON_TIP, "Tip"),
BlockQuoteKind::Important => ("important", ICON_IMPORTANT, "Important"),
BlockQuoteKind::Warning => ("warning", ICON_WARNING, "Warning"),
BlockQuoteKind::Caution => ("caution", ICON_CAUTION, "Caution"),
}
}

View File

@@ -0,0 +1,193 @@
//! Support for hiding code lines.
use crate::html::{Element, Node};
use ego_tree::{NodeId, Tree};
use html5ever::tendril::StrTendril;
use mdbook_core::static_regex;
use std::collections::HashMap;
/// Wraps hidden lines in a `<span>` for the given code block.
pub(crate) fn hide_lines(
tree: &mut Tree<Node>,
code_id: NodeId,
hidelines: &HashMap<String, String>,
) {
let mut node = tree.get_mut(code_id).unwrap();
let el = node.value().as_element().unwrap();
let classes: Vec<_> = el.attr("class").unwrap_or_default().split(' ').collect();
let language = classes
.iter()
.filter_map(|cls| cls.strip_prefix("language-"))
.next()
.unwrap_or_default()
.to_string();
let hideline_info = classes
.iter()
.filter_map(|cls| cls.strip_prefix("hidelines="))
.map(|prefix| prefix.to_string())
.next();
if let Some(mut child) = node.first_child()
&& let Node::Text(text) = child.value()
{
if language == "rust" {
let new_nodes = hide_lines_rust(text);
child.detach();
let root = tree.extend_tree(new_nodes);
let root_id = root.id();
let mut node = tree.get_mut(code_id).unwrap();
node.reparent_from_id_append(root_id);
} else {
// Use the prefix from the code block, else the prefix from config.
let hidelines_prefix = hideline_info
.as_deref()
.or_else(|| hidelines.get(&language).map(|p| p.as_str()));
if let Some(prefix) = hidelines_prefix {
let new_nodes = hide_lines_with_prefix(text, prefix);
child.detach();
let root = tree.extend_tree(new_nodes);
let root_id = root.id();
let mut node = tree.get_mut(code_id).unwrap();
node.reparent_from_id_append(root_id);
}
}
}
}
/// Wraps hidden lines in a `<span>` specifically for Rust code blocks.
fn hide_lines_rust(text: &StrTendril) -> Tree<Node> {
static_regex!(BORING_LINES_REGEX, r"^(\s*)#(.?)(.*)$");
let mut tree = Tree::new(Node::Fragment);
let mut root = tree.root_mut();
let mut lines = text.lines().peekable();
while let Some(line) = lines.next() {
// Don't include newline on the last line.
let newline = if lines.peek().is_none() { "" } else { "\n" };
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
if &caps[2] == "#" {
root.append(Node::Text(
format!("{}{}{}{newline}", &caps[1], &caps[2], &caps[3]).into(),
));
continue;
} else if matches!(&caps[2], "" | " ") {
let mut span = Element::new("span");
span.insert_attr("class", "boring".into());
let mut span = root.append(Node::Element(span));
span.append(Node::Text(
format!("{}{}{newline}", &caps[1], &caps[3]).into(),
));
continue;
}
}
root.append(Node::Text(format!("{line}{newline}").into()));
}
tree
}
/// Wraps hidden lines in a `<span>` tag for lines starting with the given prefix.
fn hide_lines_with_prefix(content: &str, prefix: &str) -> Tree<Node> {
let mut tree = Tree::new(Node::Fragment);
let mut root = tree.root_mut();
for line in content.lines() {
if line.trim_start().starts_with(prefix) {
let pos = line.find(prefix).unwrap();
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
let mut span = Element::new("span");
span.insert_attr("class", "boring".into());
let mut span = root.append(Node::Element(span));
span.append(Node::Text(format!("{ws}{rest}\n").into()));
} else {
root.append(Node::Text(format!("{line}\n").into()));
}
}
tree
}
/// If this code text is missing an `fn main`, the wrap it with `fn main` in a
/// fashion similar to rustdoc, with the wrapper hidden.
pub(crate) fn wrap_rust_main(text: &str) -> Option<String> {
if !text.contains("fn main") && !text.contains("quick_main!") {
let (attrs, code) = partition_rust_source(text);
let newline = if code.is_empty() || code.ends_with('\n') {
""
} else {
"\n"
};
Some(format!(
"# #![allow(unused)]\n{attrs}# fn main() {{\n{code}{newline}# }}"
))
} else {
None
}
}
/// Splits Rust inner attributes from the given source string.
///
/// Returns `(inner_attrs, rest_of_code)`.
fn partition_rust_source(s: &str) -> (&str, &str) {
static_regex!(
HEADER_RE,
r"^(?mx)
(
(?:
^[ \t]*\#!\[.* (?:\r?\n)?
|
^\s* (?:\r?\n)?
)*
)"
);
let split_idx = match HEADER_RE.captures(s) {
Some(caps) => {
let attributes = &caps[1];
if attributes.trim().is_empty() {
// Don't include pure whitespace as an attribute. The
// whitespace in the regex is intended to handle multiple
// attributes *separated* by potential whitespace.
0
} else {
attributes.len()
}
}
None => 0,
};
s.split_at(split_idx)
}
#[test]
fn it_partitions_rust_source() {
assert_eq!(partition_rust_source(""), ("", ""));
assert_eq!(partition_rust_source("let x = 1;"), ("", "let x = 1;"));
assert_eq!(
partition_rust_source("fn main()\n{ let x = 1; }\n"),
("", "fn main()\n{ let x = 1; }\n")
);
assert_eq!(
partition_rust_source("#![allow(foo)]"),
("#![allow(foo)]", "")
);
assert_eq!(
partition_rust_source("#![allow(foo)]\n"),
("#![allow(foo)]\n", "")
);
assert_eq!(
partition_rust_source("#![allow(foo)]\nlet x = 1;"),
("#![allow(foo)]\n", "let x = 1;")
);
assert_eq!(
partition_rust_source(
"\n\
#![allow(foo)]\n\
\n\
#![allow(bar)]\n\
\n\
let x = 1;"
),
("\n#![allow(foo)]\n\n#![allow(bar)]\n\n", "let x = 1;")
);
assert_eq!(
partition_rust_source(" // Example"),
("", " // Example")
);
}

View File

@@ -0,0 +1,108 @@
//! HTML rendering support.
//!
//! This module's primary entry point is [`render_markdown`] which will take
//! markdown text and render it to HTML. In summary, the general procedure of
//! that function is:
//!
//! 1. Use [`pulldown_cmark`] to parse the markdown and generate events.
//! 2. [`tree`] converts those events to a tree data structure.
//! 1. Parse HTML inside the markdown using [`tokenizer`].
//! 2. Apply various transformations to the tree data structure, such as adding header links.
//! 3. Serialize the tree to HTML in [`serialize()`].
use ego_tree::Tree;
use mdbook_core::book::{Book, Chapter};
use mdbook_core::config::{HtmlConfig, RustEdition};
use mdbook_markdown::{MarkdownOptions, new_cmark_parser};
use std::path::{Path, PathBuf};
mod admonitions;
mod hide_lines;
mod print;
mod serialize;
#[cfg(test)]
mod tests;
mod tokenizer;
mod tree;
pub(crate) use hide_lines::{hide_lines, wrap_rust_main};
pub(crate) use print::render_print_page;
pub(crate) use serialize::serialize;
pub(crate) use tree::{Element, Node};
/// Options for converting a single chapter's markdown to HTML.
pub(crate) struct HtmlRenderOptions<'a> {
/// Options for parsing markdown.
pub markdown_options: MarkdownOptions,
/// The chapter's location, relative to the `SUMMARY.md` file.
pub path: &'a Path,
/// The default Rust edition, used to set the proper class on the code blocks.
pub edition: Option<RustEdition>,
/// The [`HtmlConfig`], whose options affect how the HTML is generated.
pub config: &'a HtmlConfig,
}
impl<'a> HtmlRenderOptions<'a> {
/// Creates a new [`HtmlRenderOptions`].
pub(crate) fn new(
path: &'a Path,
config: &'a HtmlConfig,
edition: Option<RustEdition>,
) -> HtmlRenderOptions<'a> {
let mut markdown_options = MarkdownOptions::default();
markdown_options.smart_punctuation = config.smart_punctuation;
markdown_options.definition_lists = config.definition_lists;
markdown_options.admonitions = config.admonitions;
HtmlRenderOptions {
markdown_options,
path,
edition,
config,
}
}
}
/// Renders markdown to HTML.
pub(crate) fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String {
let tree = build_tree(text, options);
let mut output = String::new();
serialize::serialize(&tree, &mut output);
output
}
/// Renders markdown to a [`Tree`].
fn build_tree(text: &str, options: &HtmlRenderOptions<'_>) -> Tree<Node> {
let events = new_cmark_parser(text, &options.markdown_options);
tree::MarkdownTreeBuilder::build(options, events)
}
/// The parsed chapter, and some information about the chapter.
pub(crate) struct ChapterTree<'book> {
pub(crate) chapter: &'book Chapter,
/// The path to the chapter relative to the root with the `.html` extension.
pub(crate) html_path: PathBuf,
/// The chapter tree.
pub(crate) tree: Tree<Node>,
}
/// Creates all of the [`ChapterTree`]s for the book.
pub(crate) fn build_trees<'book>(
book: &'book Book,
html_config: &HtmlConfig,
edition: Option<RustEdition>,
) -> Vec<ChapterTree<'book>> {
book.chapters()
.map(|ch| {
let path = ch.path.as_ref().unwrap();
let html_path = ch.path.as_ref().unwrap().with_extension("html");
let options = HtmlRenderOptions::new(path, html_config, edition);
let tree = build_tree(&ch.content, &options);
ChapterTree {
chapter: ch,
html_path,
tree,
}
})
.collect()
}

View File

@@ -0,0 +1,214 @@
//! Support for generating the print page.
//!
//! The print page takes all the individual chapters (as `Tree<Node>`
//! elements) and modifies the chapters so that they work on a consolidated
//! print page, and then serializes it all as one HTML page.
use super::Node;
use crate::html::{ChapterTree, Element, serialize};
use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id};
use mdbook_core::static_regex;
use std::collections::{HashMap, HashSet};
use std::path::{Component, PathBuf};
/// Takes all the chapter trees, modifies them to be suitable to render for
/// the print page, and returns an string of all the chapters rendered to a
/// single HTML page.
pub(crate) fn render_print_page(mut chapter_trees: Vec<ChapterTree<'_>>) -> String {
let (id_remap, mut id_counter) = make_ids_unique(&mut chapter_trees);
let path_to_root_id = make_root_id_map(&mut chapter_trees, &mut id_counter);
rewrite_links(&mut chapter_trees, &id_remap, &path_to_root_id);
let mut print_content = String::new();
for ChapterTree { tree, .. } in chapter_trees {
if !print_content.is_empty() {
// Add page break between chapters
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
// Add both two CSS properties because of the compatibility issue
print_content
.push_str(r#"<div style="break-before: page; page-break-before: always;"></div>"#);
}
serialize(&tree, &mut print_content);
}
print_content
}
/// Make all IDs unique, and create a map from old to new IDs.
///
/// The first map is a map of the chapter path to the IDs that were rewritten
/// in that chapter (old ID to new ID).
///
/// The second map is a map of every ID seen to the number of times it has
/// been seen. This is used to generate unique IDs.
fn make_ids_unique(
chapter_trees: &mut [ChapterTree<'_>],
) -> (HashMap<PathBuf, HashMap<String, String>>, HashSet<String>) {
let mut id_remap = HashMap::new();
let mut id_counter = HashSet::new();
for ChapterTree {
html_path, tree, ..
} in chapter_trees
{
for value in tree.values_mut() {
if let Node::Element(el) = value
&& let Some(id) = el.attr("id")
{
let new_id = unique_id(id, &mut id_counter);
if new_id != id {
let id = id.to_string();
el.insert_attr("id", new_id.clone().into());
let map: &mut HashMap<_, _> = id_remap.entry(html_path.clone()).or_default();
map.insert(id, new_id);
}
}
}
}
(id_remap, id_counter)
}
/// Generates a map of a chapter path to the ID of the top of the chapter.
///
/// If a chapter is missing an `h1` tag, then one is synthesized so that the
/// print output has something to link to.
fn make_root_id_map(
chapter_trees: &mut [ChapterTree<'_>],
id_counter: &mut HashSet<String>,
) -> HashMap<PathBuf, String> {
let mut path_to_root_id = HashMap::new();
for ChapterTree {
chapter,
html_path,
tree,
..
} in chapter_trees
{
let mut h1_found = false;
for value in tree.values_mut() {
if let Node::Element(el) = value {
if el.name() == "h1" {
if let Some(id) = el.attr("id") {
h1_found = true;
path_to_root_id.insert(html_path.clone(), id.to_string());
}
break;
} else if matches!(el.name(), "h2" | "h3" | "h4" | "h5" | "h6") {
// h1 not found.
break;
}
}
}
if !h1_found {
// Synthesize a root id to be able to link to the start of the page.
// TODO: This might want to be a warning? Chapters generally
// should start with an h1.
let mut h1 = Element::new("h1");
let id = id_from_content(&chapter.name);
let id = unique_id(&id, id_counter);
h1.insert_attr("id", id.clone().into());
let mut root = tree.root_mut();
let mut h1 = root.prepend(Node::Element(h1));
let mut a = Element::new("a");
a.insert_attr("href", format!("#{id}").into());
a.insert_attr("class", "header".into());
let mut a = h1.append(Node::Element(a));
a.append(Node::Text(chapter.name.clone().into()));
path_to_root_id.insert(html_path.clone(), id);
}
}
path_to_root_id
}
/// Rewrite links so that they point to IDs on the print page.
fn rewrite_links(
chapter_trees: &mut [ChapterTree<'_>],
id_remap: &HashMap<PathBuf, HashMap<String, String>>,
path_to_root_id: &HashMap<PathBuf, String>,
) {
static_regex!(
LINK,
r"(?x)
(?P<scheme>^[a-z][a-z0-9+.-]*:)?
(?P<path>[^\#]+)?
(?:\#(?P<anchor>.*))?"
);
// Rewrite path links to go to the appropriate place.
for ChapterTree {
html_path, tree, ..
} in chapter_trees
{
let base = html_path.parent().expect("path can't be empty");
for value in tree.values_mut() {
let Node::Element(el) = value else {
continue;
};
if !matches!(el.name(), "a" | "img") {
continue;
}
for attr in ["href", "src", "xlink:href"] {
let Some(dest) = el.attr(attr) else {
continue;
};
let Some(caps) = LINK.captures(&dest) else {
continue;
};
if caps.name("scheme").is_some() {
continue;
}
// The lookup_key is the key to look up in the remap table.
let mut lookup_key = html_path.clone();
if let Some(href_path) = caps.name("path")
&& let href_path = href_path.as_str()
&& !href_path.is_empty()
{
lookup_key.pop();
lookup_key.push(href_path);
let normalized = normalize_path(&lookup_key);
// If this points outside of the book, don't modify it.
let is_outside = matches!(
normalized.components().next(),
Some(Component::ParentDir | Component::RootDir)
);
if is_outside || !href_path.ends_with(".html") {
// Make the link relative to the print page location.
let mut rel_path = normalize_path(&base.join(href_path)).to_url_path();
if let Some(anchor) = caps.name("anchor") {
rel_path.push('#');
rel_path.push_str(anchor.as_str());
}
el.insert_attr(attr, rel_path.into());
continue;
}
}
let lookup_key = normalize_path(&lookup_key);
let anchor = caps.name("anchor");
let id = match anchor {
Some(anchor_id) => {
let anchor_id = anchor_id.as_str().to_string();
match id_remap.get(&lookup_key) {
Some(id_map) => match id_map.get(&anchor_id) {
Some(new_id) => new_id.clone(),
None => anchor_id,
},
None => {
// Assume the anchor goes to some non-remapped
// ID that already exists.
anchor_id
}
}
}
None => match path_to_root_id.get(&lookup_key) {
Some(id) => id.to_string(),
None => continue,
},
};
el.insert_attr(attr, format!("#{id}").into());
}
}
}
}

View File

@@ -0,0 +1,112 @@
//! Serializes the [`Node`] tree to an HTML string.
use super::tree::is_void_element;
use super::tree::{Element, Node};
use ego_tree::{Tree, iter::Edge};
use html5ever::{local_name, ns};
use mdbook_core::utils::{escape_html, escape_html_attribute};
use std::ops::Deref;
/// Serializes the given tree of [`Node`] elements to an HTML string.
pub(crate) fn serialize(tree: &Tree<Node>, output: &mut String) {
for edge in tree.root().traverse() {
match edge {
Edge::Open(node) => match node.value() {
Node::Element(el) => serialize_start(el, output),
Node::Text(text) => {
output.push_str(&escape_html(text));
}
Node::Comment(comment) => {
output.push_str("<!--");
output.push_str(comment);
output.push_str("-->");
}
Node::Fragment => {}
Node::RawData(html) => {
output.push_str(html);
}
},
Edge::Close(node) => {
if let Node::Element(el) = node.value() {
serialize_end(el, output);
}
}
}
}
}
/// Returns true if this HTML element wants a newline to keep the emitted
/// output more readable.
fn wants_pretty_html_newline(name: &str) -> bool {
matches!(name, |"blockquote"| "dd"
| "div"
| "dl"
| "dt"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "hr"
| "li"
| "ol"
| "p"
| "pre"
| "table"
| "tbody"
| "thead"
| "tr"
| "ul")
}
/// Emit the start tag of an element.
fn serialize_start(el: &Element, output: &mut String) {
let el_name = el.name();
if wants_pretty_html_newline(el_name) {
if !output.is_empty() {
if !output.ends_with('\n') {
output.push('\n');
}
}
}
output.push('<');
output.push_str(el_name);
for (attr_name, value) in &el.attrs {
output.push(' ');
match attr_name.ns {
ns!() => (),
ns!(xml) => output.push_str("xml:"),
ns!(xmlns) => {
if el.name.local != local_name!("xmlns") {
output.push_str("xmlns:");
}
}
ns!(xlink) => output.push_str("xlink:"),
_ => (), // TODO what should it do here?
}
output.push_str(attr_name.local.deref());
output.push_str("=\"");
output.push_str(&escape_html_attribute(&value));
output.push('"');
}
if el.self_closing {
output.push_str(" /");
}
output.push('>');
}
/// Emit the end tag of an element.
fn serialize_end(el: &Element, output: &mut String) {
// Void elements do not have an end tag.
if el.self_closing || is_void_element(el.name()) {
return;
}
let name = el.name();
output.push_str("</");
output.push_str(name);
output.push('>');
if wants_pretty_html_newline(name) {
output.push('\n');
}
}

View File

@@ -0,0 +1,53 @@
use crate::html::tokenizer::parse_html;
use html5ever::tokenizer::{Tag, TagKind, Token};
// Basic tokenizer behavior of a script.
#[test]
fn parse_html_script() {
let script = r#"
if (3 < 5 > 10)
{
alert("The sky is falling!");
}
"#;
let t = format!("<script>{script}</script>");
let ts = parse_html(&t);
eprintln!("{ts:#?}",);
let mut output = String::new();
let mut in_script = false;
for t in ts {
match t {
Token::ParseError(e) => panic!("{e:?}"),
Token::CharacterTokens(s) => {
if in_script {
output.push_str(&s)
}
}
Token::TagToken(Tag {
kind: TagKind::StartTag,
..
}) => in_script = true,
Token::TagToken(Tag {
kind: TagKind::EndTag,
..
}) => in_script = false,
_ => {}
}
}
assert_eq!(output, script);
}
// What happens if a script doesn't end.
#[test]
fn parse_html_script_unclosed() {
let t = r#"<script>
// Test
"#;
let ts = parse_html(t);
eprintln!("{ts:#?}",);
for t in ts {
if let Token::ParseError(e) = t {
panic!("{e:?}",);
}
}
}

View File

@@ -0,0 +1,83 @@
//! Support for parsing HTML.
//!
//! The primary entry point is [`parse_html`] which uses [`html5ever`] to
//! tokenize the input.
use html5ever::TokenizerResult;
use html5ever::tendril::ByteTendril;
use html5ever::tokenizer::states::RawKind;
use html5ever::tokenizer::{
BufferQueue, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer, TokenizerOpts,
};
use std::cell::RefCell;
/// Collector for HTML tokens.
#[derive(Default)]
struct TokenCollector {
/// Parsed HTML tokens.
tokens: RefCell<Vec<Token>>,
}
impl TokenSink for TokenCollector {
type Handle = ();
fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<()> {
match &token {
Token::DoctypeToken(_) => {}
Token::TagToken(tag) => {
let tag_name = tag.name.as_bytes();
// TODO: This could probably use special support for SVG and MathML.
if tag_name == b"script" {
match tag.kind {
TagKind::StartTag => {
self.tokens.borrow_mut().push(token);
return TokenSinkResult::RawData(RawKind::ScriptData);
}
TagKind::EndTag => {}
}
}
if tag_name == b"style" {
match tag.kind {
TagKind::StartTag => {
self.tokens.borrow_mut().push(token);
return TokenSinkResult::RawData(RawKind::Rawtext);
}
TagKind::EndTag => {}
}
}
self.tokens.borrow_mut().push(token);
}
Token::CommentToken(_) => {
self.tokens.borrow_mut().push(token);
}
Token::CharacterTokens(_) => {
self.tokens.borrow_mut().push(token);
}
Token::NullCharacterToken => {}
Token::EOFToken => {}
Token::ParseError(_) => {
self.tokens.borrow_mut().push(token);
}
}
TokenSinkResult::Continue
}
}
/// Parse HTML into tokens.
pub(crate) fn parse_html(html: &str) -> Vec<Token> {
let tendril: ByteTendril = html.as_bytes().into();
let mut queue = BufferQueue::default();
queue.push_back(tendril.try_reinterpret().unwrap());
let collector = TokenCollector::default();
let tok = Tokenizer::new(collector, TokenizerOpts::default());
let result = tok.feed(&mut queue);
assert_eq!(result, TokenizerResult::Done);
assert!(
queue.is_empty(),
"queue wasn't empty: {:?}",
queue.pop_front()
);
tok.end();
tok.sink.tokens.take()
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,696 @@
use super::helpers;
use super::static_files::StaticFiles;
use crate::html::ChapterTree;
use crate::html::{build_trees, render_markdown, serialize};
use crate::theme::Theme;
use crate::utils::ToUrlPath;
use anyhow::{Context, Result, bail};
use handlebars::Handlebars;
use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::{BookConfig, Config, HtmlConfig};
use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer};
use serde_json::json;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use tracing::error;
use tracing::{debug, info, trace, warn};
/// The HTML renderer for mdBook.
#[derive(Default)]
#[non_exhaustive]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
/// Returns a new instance of [`HtmlHandlebars`].
pub fn new() -> Self {
HtmlHandlebars
}
fn render_chapter(
&self,
chapter_tree: &ChapterTree<'_>,
prev_ch: Option<&Chapter>,
next_ch: Option<&Chapter>,
mut ctx: RenderChapterContext<'_>,
) -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state
let ch = chapter_tree.chapter;
let path = ch.path.as_ref().unwrap();
// "print.html" is used for the print page.
if path == Path::new("print.md") {
bail!("{} is reserved for internal use", path.display());
};
if let Some(ref edit_url_template) = ctx.html_config.edit_url_template {
let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned()
+ "/"
+ ch.source_path
.clone()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
let edit_url = edit_url_template.replace("{path}", &full_path);
ctx.data
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
}
let mut content = String::new();
serialize(&chapter_tree.tree, &mut content);
let ctx_path = path
.to_str()
.with_context(|| "Could not convert path to str")?;
let filepath = Path::new(&ctx_path).with_extension("html");
let book_title = ctx
.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let title = if let Some(title) = ctx.chapter_titles.get(path) {
title.clone()
} else if book_title.is_empty() {
ch.name.clone()
} else {
ch.name.clone() + " - " + book_title
};
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data
.insert("path_to_root".to_owned(), json!(fs::path_to_root(path)));
if let Some(ref section) = ch.number {
ctx.data
.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)?),
);
}
let mut nav = |name: &str, ch: Option<&Chapter>| {
let Some(ch) = ch else { return };
let path = ch
.path
.as_ref()
.unwrap()
.with_extension("html")
.to_url_path();
let obj = json!( {
"title": ch.name,
"link": path,
});
ctx.data.insert(name.to_string(), obj);
};
nav("previous", prev_ch);
nav("next", next_ch);
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
// Write to file
let out_path = ctx.destination.join(filepath);
fs::write(&out_path, rendered)?;
if prev_ch.is_none() {
ctx.data.insert("path".to_owned(), json!("index.md"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
ctx.data.insert("is_index".to_owned(), json!(true));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
debug!("Creating index.html from {}", ctx_path);
fs::write(ctx.destination.join("index.html"), rendered_index)?;
}
Ok(())
}
fn render_404(
&self,
ctx: &RenderContext,
html_config: &HtmlConfig,
src_dir: &Path,
handlebars: &mut Handlebars<'_>,
data: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<()> {
let content_404 = if let Some(ref filename) = html_config.input_404 {
let path = src_dir.join(filename);
fs::read_to_string(&path).with_context(|| "failed to read the 404 input file")?
} else {
// 404 input not explicitly configured try the default file 404.md
let default_404_location = src_dir.join("404.md");
if default_404_location.exists() {
fs::read_to_string(&default_404_location)
.with_context(|| "failed to read the 404 input file")?
} else {
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
navigation bar or search to continue."
.to_string()
}
};
let options = crate::html::HtmlRenderOptions::new(
Path::new("404.md"),
html_config,
ctx.config.rust.edition,
);
let html_content_404 = render_markdown(&content_404, &options);
let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url {
site_url
} else {
debug!(
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
this to ensure the 404 page work correctly, especially if your site is hosted in a \
subdirectory on the HTTP server."
);
"/"
};
data_404.insert("base_url".to_owned(), json!(base_url));
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
data_404.insert("path".to_owned(), json!("404.md"));
data_404.insert("content".to_owned(), json!(html_content_404));
let mut title = String::from("Page not found");
if let Some(book_title) = &ctx.config.book.title {
title.push_str(" - ");
title.push_str(book_title);
}
data_404.insert("title".to_owned(), json!(title));
let rendered = handlebars.render("index", &data_404)?;
let output_file = ctx.destination.join(html_config.get_404_output_file());
fs::write(output_file, rendered)?;
debug!("Creating 404.html ✓");
Ok(())
}
fn render_print_page(
&self,
ctx: &RenderContext,
handlebars: &Handlebars<'_>,
data: &mut serde_json::Map<String, serde_json::Value>,
chapter_trees: Vec<ChapterTree<'_>>,
) -> Result<String> {
let print_content = crate::html::render_print_page(chapter_trees);
if let Some(ref title) = ctx.config.book.title {
data.insert("title".to_owned(), json!(title));
} else {
// Make sure that the Print chapter does not display the title from
// the last rendered chapter by removing it from its context
data.remove("title");
}
data.insert("is_print".to_owned(), json!(true));
data.insert("path".to_owned(), json!("print.md"));
data.insert("content".to_owned(), json!(print_content));
data.insert(
"path_to_root".to_owned(),
json!(fs::path_to_root(Path::new("print.md"))),
);
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
Ok(rendered)
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
handlebars.register_helper(
"toc",
Box::new(helpers::toc::RenderToc {
no_section_label: html_config.no_section_label,
}),
);
handlebars.register_helper("fa", Box::new(helpers::fontawesome::fa_helper));
}
fn emit_redirects(
&self,
root: &Path,
handlebars: &Handlebars<'_>,
redirects: &HashMap<String, String>,
) -> Result<()> {
if redirects.is_empty() {
return Ok(());
}
debug!("Emitting redirects");
let redirects = combine_fragment_redirects(redirects);
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);
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."
);
}
debug!("Redirecting \"{}\"\"{}\"", original, dest);
self.emit_redirect(handlebars, &filename, &dest, &fragment_map)?;
}
Ok(())
}
fn emit_redirect(
&self,
handlebars: &Handlebars<'_>,
original: &Path,
destination: &str,
fragment_map: &BTreeMap<String, String>,
) -> Result<()> {
if let Some(parent) = original.parent() {
fs::create_dir_all(parent)?
}
let js_map = serde_json::to_string(fragment_map)?;
let ctx = json!({
"fragment_map": js_map,
"url": destination,
});
let rendered = handlebars.render("redirect", &ctx).with_context(|| {
format!(
"Unable to create a redirect file at `{}`",
original.display()
)
})?;
fs::write(original, rendered)?;
Ok(())
}
}
impl Renderer for HtmlHandlebars {
fn name(&self) -> &str {
"html"
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
let book_config = &ctx.config.book;
let html_config = ctx.config.html_config().unwrap_or_default();
let src_dir = ctx.root.join(&ctx.config.book.src);
let destination = &ctx.destination;
let book = &ctx.book;
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
if destination.exists() {
fs::remove_dir_content(destination)
.with_context(|| "Unable to remove stale HTML output")?;
}
trace!("render");
let mut handlebars = Handlebars::new();
let theme_dir = match html_config.theme {
Some(ref theme) => {
let dir = ctx.root.join(theme);
if !dir.is_dir() {
bail!("theme dir {} does not exist", dir.display());
}
dir
}
None => ctx.root.join("theme"),
};
let theme = Theme::new(theme_dir);
debug!("Register the index handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
debug!("Register the head handlebars template");
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
debug!("Register the redirect handlebars template");
handlebars
.register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
debug!("Register the header handlebars template");
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
debug!("Register the toc handlebars template");
handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?;
handlebars
.register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?;
debug!("Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars, &html_config);
let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?;
let chapter_trees = build_trees(book, &html_config, ctx.config.rust.edition);
fs::create_dir_all(destination)
.with_context(|| "Unexpected error when constructing destination path")?;
let mut static_files = StaticFiles::new(&theme, &html_config, &ctx.root)?;
// Render search index
#[cfg(feature = "search")]
{
let default = mdbook_core::config::Search::default();
let search = html_config.search.as_ref().unwrap_or(&default);
if search.enable {
super::search::create_files(&search, &mut static_files, &chapter_trees)?;
}
}
debug!("Render toc js");
{
let rendered_toc = handlebars.render("toc_js", &data)?;
static_files.add_builtin("toc.js", rendered_toc.as_bytes());
debug!("Creating toc.js ✓");
}
if html_config.hash_files {
static_files.hash_files()?;
}
debug!("Copy static files");
let resource_helper = static_files
.write_files(&destination)
.with_context(|| "Unable to copy across static files")?;
handlebars.register_helper("resource", Box::new(resource_helper));
debug!("Render toc html");
{
data.insert("is_toc_html".to_owned(), json!(true));
data.insert("path".to_owned(), json!("toc.html"));
let rendered_toc = handlebars.render("toc_html", &data)?;
fs::write(destination.join("toc.html"), rendered_toc)?;
debug!("Creating toc.html ✓");
data.remove("path");
data.remove("is_toc_html");
}
fs::write(
destination.join(".nojekyll"),
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
)?;
if let Some(cname) = &html_config.cname {
fs::write(destination.join("CNAME"), format!("{cname}\n"))?;
}
for (i, chapter_tree) in chapter_trees.iter().enumerate() {
let previous = (i != 0).then(|| chapter_trees[i - 1].chapter);
let next = (i != chapter_trees.len() - 1).then(|| chapter_trees[i + 1].chapter);
let ctx = RenderChapterContext {
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
book_config: book_config.clone(),
html_config: html_config.clone(),
chapter_titles: &ctx.chapter_titles,
};
self.render_chapter(chapter_tree, previous, next, ctx)?;
}
// Render 404 page
if html_config.input_404 != Some("".to_string()) {
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
}
// Render the print version.
if html_config.print.enable {
let print_rendered =
self.render_print_page(ctx, &handlebars, &mut data, chapter_trees)?;
fs::write(destination.join("print.html"), print_rendered)?;
debug!("Creating print.html ✓");
}
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
.context("Unable to emit redirects")?;
// Copy all remaining files, avoid a recursive copy from/to the book build dir
fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
info!("HTML book written to `{}`", destination.display());
Ok(())
}
}
fn make_data(
root: &Path,
book: &Book,
config: &Config,
html_config: &HtmlConfig,
theme: &Theme,
) -> Result<serde_json::Map<String, serde_json::Value>> {
trace!("make_data");
let mut data = serde_json::Map::new();
data.insert(
"language".to_owned(),
json!(config.book.language.clone().unwrap_or_default()),
);
data.insert(
"text_direction".to_owned(),
json!(config.book.realized_text_direction()),
);
data.insert(
"book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()),
);
data.insert(
"description".to_owned(),
json!(config.book.description.clone().unwrap_or_default()),
);
if theme.favicon_png.is_some() {
data.insert("favicon_png".to_owned(), json!("favicon.png"));
}
if theme.favicon_svg.is_some() {
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
}
if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint {
data.insert(
"live_reload_endpoint".to_owned(),
json!(live_reload_endpoint),
);
}
let default_theme = match html_config.default_theme {
Some(ref theme) => theme.to_lowercase(),
None => "light".to_string(),
};
data.insert("default_theme".to_owned(), json!(default_theme));
let preferred_dark_theme = match html_config.preferred_dark_theme {
Some(ref theme) => theme.to_lowercase(),
None => "navy".to_string(),
};
data.insert(
"preferred_dark_theme".to_owned(),
json!(preferred_dark_theme),
);
if html_config.mathjax_support {
data.insert("mathjax_support".to_owned(), json!(true));
}
// Add check to see if there is an additional style
if !html_config.additional_css.is_empty() {
let mut css = Vec::new();
for style in &html_config.additional_css {
match style.strip_prefix(root) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
}
}
data.insert("additional_css".to_owned(), json!(css));
}
// Add check to see if there is an additional script
if !html_config.additional_js.is_empty() {
let mut js = Vec::new();
for script in &html_config.additional_js {
match script.strip_prefix(root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
}
}
data.insert("additional_js".to_owned(), json!(js));
}
if html_config.playground.editable && html_config.playground.copy_js {
data.insert("playground_js".to_owned(), json!(true));
if html_config.playground.line_numbers {
data.insert("playground_line_numbers".to_owned(), json!(true));
}
}
if html_config.playground.copyable {
data.insert("playground_copyable".to_owned(), json!(true));
}
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
data.insert(
"sidebar_header_nav".to_owned(),
json!(html_config.sidebar_header_nav),
);
let search = html_config.search.clone();
if cfg!(feature = "search") {
let search = search.unwrap_or_default();
data.insert("search_enabled".to_owned(), json!(search.enable));
data.insert(
"search_js".to_owned(),
json!(search.enable && search.copy_js),
);
} else if search.is_some() {
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
warn!(
"please reinstall with `cargo install mdbook --force --features search`to use the \
search feature"
)
}
if let Some(ref git_repository_url) = html_config.git_repository_url {
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
}
let git_repository_icon = match html_config.git_repository_icon {
Some(ref git_repository_icon) => git_repository_icon,
None => "fab-github",
};
let git_repository_icon_class = match git_repository_icon.split('-').next() {
Some("fa") => "regular",
Some("fas") => "solid",
Some("fab") => "brands",
_ => "regular",
};
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
data.insert(
"git_repository_icon_class".to_owned(),
json!(git_repository_icon_class),
);
let mut chapters = vec![];
for item in book.iter() {
// Create the data to inject in the template
let mut chapter = BTreeMap::new();
match *item {
BookItem::PartTitle(ref title) => {
chapter.insert("part".to_owned(), json!(title));
}
BookItem::Chapter(ref ch) => {
if let Some(ref section) = ch.number {
chapter.insert("section".to_owned(), json!(section.to_string()));
}
chapter.insert(
"has_sub_items".to_owned(),
json!((!ch.sub_items.is_empty()).to_string()),
);
chapter.insert("name".to_owned(), json!(ch.name));
if let Some(ref path) = ch.path {
let p = path
.to_str()
.with_context(|| "Could not convert path to str")?;
chapter.insert("path".to_owned(), json!(p));
}
}
BookItem::Separator => {
chapter.insert("spacer".to_owned(), json!("_spacer_"));
}
}
chapters.push(chapter);
}
data.insert("chapters".to_owned(), json!(chapters));
debug!("[*]: JSON constructed");
Ok(data)
}
struct RenderChapterContext<'a> {
handlebars: &'a Handlebars<'a>,
destination: PathBuf,
data: serde_json::Map<String, serde_json::Value>,
book_config: BookConfig,
html_config: HtmlConfig,
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()) {
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.to_url_path());
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)
}

View File

@@ -0,0 +1,53 @@
use font_awesome_as_a_crate as fa;
use handlebars::{
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
};
use std::str::FromStr;
use tracing::trace;
pub(crate) fn fa_helper(
h: &Helper<'_>,
_r: &Handlebars<'_>,
_ctx: &Context,
_rc: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("fa_helper (handlebars helper)");
let type_ = h
.param(0)
.and_then(|v| v.value().as_str())
.and_then(|v| fa::Type::from_str(v).ok())
.ok_or_else(|| {
RenderErrorReason::Other(
"Param 0 with String type is required for fontawesome helper.".to_owned(),
)
})?;
let name = h.param(1).and_then(|v| v.value().as_str()).ok_or_else(|| {
RenderErrorReason::Other(
"Param 1 with String type is required for fontawesome helper.".to_owned(),
)
})?;
trace!("fa_helper: {} {}", type_, name);
let name = name
.strip_prefix("fa-")
.or_else(|| name.strip_prefix("fab-"))
.or_else(|| name.strip_prefix("fas-"))
.unwrap_or(name);
if let Some(id) = h.param(2).and_then(|v| v.value().as_str()) {
out.write(&format!("<span class=fa-svg id=\"{}\">", id))?;
} else {
out.write("<span class=fa-svg>")?;
}
out.write(
fa::svg(type_, name)
.map_err(|_| RenderErrorReason::Other(format!("Missing font {}", name)))?,
)?;
out.write("</span>")?;
Ok(())
}

View File

@@ -0,0 +1,3 @@
pub(crate) mod fontawesome;
pub(crate) mod resources;
pub(crate) mod toc;

View File

@@ -0,0 +1,45 @@
use std::collections::HashMap;
use mdbook_core::utils;
use handlebars::{
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
};
// Handlebars helper to find filenames with hashes in them
#[derive(Clone)]
pub(crate) struct ResourceHelper {
pub hash_map: HashMap<String, String>,
}
impl HelperDef for ResourceHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'rc>,
_r: &'reg Handlebars<'_>,
ctx: &'rc Context,
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
RenderErrorReason::Other(
"Param 0 with String type is required for resource helper.".to_owned(),
)
})?;
let base_path = rc
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| {
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
})?
.replace("\"", "");
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))?;
Ok(())
}
}

View File

@@ -1,16 +1,14 @@
use std::path::Path;
use std::{cmp::Ordering, collections::BTreeMap};
use crate::utils;
use crate::utils::bracket_escape;
use crate::utils::ToUrlPath;
use handlebars::{
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
};
use mdbook_core::utils::escape_html_attribute;
use std::path::Path;
use std::{cmp::Ordering, collections::BTreeMap};
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
pub struct RenderToc {
pub(crate) struct RenderToc {
pub no_section_label: bool,
}
@@ -32,21 +30,6 @@ impl HelperDef for RenderToc {
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
})
})?;
let current_path = rc
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| {
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
})?
.replace('\"', "");
let current_section = rc
.evaluate(ctx, "@root/section")?
.as_json()
.as_str()
.map(str::to_owned)
.unwrap_or_default();
let fold_enable = rc
.evaluate(ctx, "@root/fold_enable")?
@@ -64,53 +47,55 @@ impl HelperDef for RenderToc {
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
})?;
// If true, then this is the iframe and we need target="_parent"
let is_toc_html = rc
.evaluate(ctx, "@root/is_toc_html")?
.as_json()
.as_bool()
.unwrap_or(false);
out.write("<ol class=\"chapter\">")?;
let mut current_level = 1;
// The "index" page, which has this attribute set, is supposed to alias the first chapter in
// the book, i.e. the first link. There seems to be no easy way to determine which chapter
// the "index" is aliasing from within the renderer, so this is used instead to force the
// first link to be active. See further below.
let mut is_first_chapter = ctx.data().get("is_index").is_some();
let mut first = true;
for item in chapters {
let (section, level) = if let Some(s) = item.get("section") {
(s.as_str(), s.matches('.').count())
} else {
("", 1)
};
let level = item
.get("section")
.map(|s| s.matches('.').count())
.unwrap_or(1);
let is_expanded =
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
// Expand if folding is disabled, or if the section is an
// ancestor or the current section itself.
true
} else {
// Levels that are larger than this would be folded.
level - 1 < fold_level as usize
};
// Expand if folding is disabled, or if levels that are larger than this would not
// be folded.
let is_expanded = !fold_enable || level - 1 < (fold_level as usize);
match level.cmp(&current_level) {
Ordering::Greater => {
while level > current_level {
out.write("<li>")?;
out.write("<ol class=\"section\">")?;
current_level += 1;
}
write_li_open_tag(out, is_expanded, false)?;
// There is an assumption that when descending, it can
// only go one level down at a time. This should be
// enforced by the nature of markdown lists and the
// summary parser.
assert_eq!(level, current_level + 1);
current_level += 1;
out.write("<ol class=\"section\">")?;
write_li_open_tag(out, is_expanded)?;
}
Ordering::Less => {
while level < current_level {
out.write("</ol>")?;
out.write("</li>")?;
out.write("</ol>")?;
current_level -= 1;
}
write_li_open_tag(out, is_expanded, false)?;
write_li_open_tag(out, is_expanded)?;
}
Ordering::Equal => {
write_li_open_tag(out, is_expanded, !item.contains_key("section"))?;
if !first {
out.write("</li>")?;
}
write_li_open_tag(out, is_expanded)?;
}
}
first = false;
// Spacer
if item.contains_key("spacer") {
@@ -121,41 +106,33 @@ impl HelperDef for RenderToc {
// Part title
if let Some(title) = item.get("part") {
out.write("<li class=\"part-title\">")?;
out.write(&bracket_escape(title))?;
out.write(&escape_html_attribute(title))?;
out.write("</li>")?;
continue;
}
out.write("<span class=\"chapter-link-wrapper\">")?;
// 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)
.with_extension("html")
.to_str()
.unwrap()
// Hack for windows who tends to use `\` as separator instead of `/`
.replace('\\', "/");
let tmp = Path::new(path).with_extension("html").to_url_path();
// Add link
out.write(&utils::fs::path_to_root(&current_path))?;
out.write(&tmp)?;
out.write("\"")?;
if path == &current_path || is_first_chapter {
is_first_chapter = false;
out.write(" class=\"active\"")?;
}
out.write(">")?;
path_exists = true;
out.write(if is_toc_html {
"\" target=\"_parent\">"
} else {
"\">"
})?;
true
}
_ => {
out.write("<div>")?;
path_exists = false;
out.write("<span>")?;
false
}
}
};
if !self.no_section_label {
// Section does not necessarily exist
@@ -167,47 +144,41 @@ impl HelperDef for RenderToc {
}
if let Some(name) = item.get("name") {
out.write(&bracket_escape(name))?
out.write(&escape_html_attribute(name))?;
}
if path_exists {
out.write("</a>")?;
} else {
out.write("</div>")?;
out.write("</span>")?;
}
// Render expand/collapse toggle
if let Some(flag) = item.get("has_sub_items") {
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
if fold_enable && has_sub_items {
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
// The <div> here is to manage rotating the element when
// the chapter title is long and word-wraps.
out.write("<a class=\"chapter-fold-toggle\"><div>❱</div></a>")?;
}
}
out.write("</li>")?;
out.write("</span>")?;
}
while current_level > 1 {
out.write("</ol>")?;
while current_level > 0 {
out.write("</li>")?;
out.write("</ol>")?;
current_level -= 1;
}
out.write("</ol>")?;
Ok(())
}
}
fn write_li_open_tag(
out: &mut dyn Output,
is_expanded: bool,
is_affix: bool,
) -> Result<(), std::io::Error> {
fn write_li_open_tag(out: &mut dyn Output, is_expanded: bool) -> Result<(), std::io::Error> {
let mut li = String::from("<li class=\"chapter-item ");
if is_expanded {
li.push_str("expanded ");
}
if is_affix {
li.push_str("affix ");
}
li.push_str("\">");
out.write(&li)
}

View File

@@ -1,9 +1,7 @@
#![allow(missing_docs)] // FIXME: Document this
pub use self::hbs_renderer::HtmlHandlebars;
mod hbs_renderer;
mod helpers;
#[cfg(feature = "search")]
mod search;
mod static_files;
pub use self::hbs_renderer::HtmlHandlebars;

View File

@@ -0,0 +1,445 @@
use super::static_files::StaticFiles;
use crate::html::{ChapterTree, Node};
use crate::theme::searcher;
use crate::utils::ToUrlPath;
use anyhow::{Result, bail};
use ego_tree::iter::Edge;
use elasticlunr::{Index, IndexBuilder};
use mdbook_core::book::Chapter;
use mdbook_core::config::{Search, SearchChapterSettings};
use mdbook_core::static_regex;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
fn tokenize(text: &str) -> Vec<String> {
text.split(|c: char| c.is_whitespace() || c == '-')
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_lowercase())
.filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX)
.collect()
}
/// Creates all files required for search.
pub(super) fn create_files(
search_config: &Search,
static_files: &mut StaticFiles,
chapter_trees: &[ChapterTree<'_>],
) -> Result<()> {
let mut index = IndexBuilder::new()
.add_field_with_tokenizer("title", Box::new(&tokenize))
.add_field_with_tokenizer("body", Box::new(&tokenize))
.add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
.build();
// These are links to all of the headings in all of the chapters.
let mut doc_urls = Vec::new();
let chapter_configs = sort_search_config(&search_config.chapter);
validate_chapter_config(&chapter_configs, chapter_trees)?;
for ct in chapter_trees {
let path = settings_path(ct.chapter);
let chapter_settings = get_chapter_settings(&chapter_configs, path);
if !chapter_settings.enable.unwrap_or(true) {
continue;
}
index_chapter(&mut index, search_config, &mut doc_urls, ct)?;
}
let index = write_to_json(index, search_config, doc_urls)?;
debug!("Writing search index ✓");
if index.len() > 10_000_000 {
warn!("search index is very large ({} bytes)", index.len());
}
if search_config.copy_js {
static_files.add_builtin(
"searchindex.js",
// 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);
static_files.add_builtin("elasticlunr.min.js", searcher::ELASTICLUNR_JS);
debug!("Copying search files ✓");
}
Ok(())
}
/// Uses the given arguments to construct a search document, then inserts it to the given index.
fn add_doc(
index: &mut Index,
doc_urls: &mut Vec<String>,
anchor_base: &str,
heading_id: &str,
items: &[&str],
) {
let mut url = anchor_base.to_string();
if !heading_id.is_empty() {
url.push('#');
url.push_str(heading_id);
}
let doc_ref = doc_urls.len().to_string();
doc_urls.push(url);
let items = items.iter().map(|&x| collapse_whitespace(x.trim()));
index.add_doc(&doc_ref, items);
}
/// Adds the chapter to the search index.
fn index_chapter(
index: &mut Index,
search_config: &Search,
doc_urls: &mut Vec<String>,
chapter_tree: &ChapterTree<'_>,
) -> Result<()> {
let anchor_base = chapter_tree.html_path.to_url_path();
let mut in_heading = false;
let max_section_depth = search_config.heading_split_level;
let mut section_id = None;
let mut heading = String::new();
let mut body = String::new();
let mut breadcrumbs = chapter_tree.chapter.parent_names.clone();
breadcrumbs.push(chapter_tree.chapter.name.clone());
let mut traverse = chapter_tree.tree.root().traverse();
while let Some(edge) = traverse.next() {
match edge {
Edge::Open(node) => match node.value() {
Node::Element(el) => {
if let Some(level) = el.heading_level()
&& level <= max_section_depth
&& let Some(heading_id) = el.attr("id")
{
if !heading.is_empty() {
// Section finished, the next heading is following now
// Write the data to the index, and clear it for the next section
add_doc(
index,
doc_urls,
&anchor_base,
section_id.unwrap(),
&[&heading, &body, &breadcrumbs.join(" » ")],
);
heading.clear();
body.clear();
breadcrumbs.pop();
}
section_id = Some(heading_id);
in_heading = true;
} else if matches!(el.name(), "script" | "style") {
// Skip this node.
while let Some(edge) = traverse.next() {
if let Edge::Close(close) = edge
&& close == node
{
break;
}
}
// Insert spaces where HTML output would usually separate text
// to ensure words don't get merged together
} else if in_heading {
heading.push(' ');
} else {
body.push(' ');
}
}
Node::Text(text) => {
if in_heading {
heading.push_str(text);
} else {
body.push_str(text);
}
}
Node::Comment(_) => {}
Node::Fragment => {}
Node::RawData(_) => {}
},
Edge::Close(node) => match node.value() {
Node::Element(el) => {
if let Some(level) = el.heading_level()
&& level <= max_section_depth
{
in_heading = false;
breadcrumbs.push(heading.clone());
}
}
_ => {}
},
}
}
if !body.is_empty() || !heading.is_empty() {
// Make sure the last section is added to the index
let title = if heading.is_empty() {
if let Some(chapter) = breadcrumbs.first() {
chapter
} else {
""
}
} else {
&heading
};
add_doc(
index,
doc_urls,
&anchor_base,
section_id.unwrap_or_default(),
&[title, &body, &breadcrumbs.join(" » ")],
);
}
Ok(())
}
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
use std::collections::BTreeMap;
#[derive(Serialize)]
struct ResultsOptions {
limit_results: u32,
teaser_word_count: u32,
}
#[derive(Serialize)]
struct SearchindexJson {
/// The options used for displaying search results
results_options: ResultsOptions,
/// The searchoptions for elasticlunr.js
search_options: SearchOptions,
/// Used to lookup a document's URL from an integer document ref.
doc_urls: Vec<String>,
/// The index for elasticlunr.js
index: elasticlunr::Index,
}
let mut fields = BTreeMap::new();
let mut opt = SearchOptionsField::default();
let mut insert_boost = |key: &str, boost| {
opt.boost = Some(boost);
fields.insert(key.into(), opt);
};
insert_boost("title", search_config.boost_title);
insert_boost("body", search_config.boost_paragraph);
insert_boost("breadcrumbs", search_config.boost_hierarchy);
let search_options = SearchOptions {
bool: if search_config.use_boolean_and {
SearchBool::And
} else {
SearchBool::Or
},
expand: search_config.expand,
fields,
};
let results_options = ResultsOptions {
limit_results: search_config.limit_results,
teaser_word_count: search_config.teaser_word_count,
};
let json_contents = SearchindexJson {
results_options,
search_options,
doc_urls,
index,
};
// By converting to serde_json::Value as an intermediary, we use a
// BTreeMap internally and can force a stable ordering of map keys.
let json_contents = serde_json::to_value(&json_contents)?;
let json_contents = serde_json::to_string(&json_contents)?;
Ok(json_contents)
}
fn settings_path(ch: &Chapter) -> &Path {
ch.source_path
.as_deref()
.unwrap_or_else(|| ch.path.as_deref().unwrap())
}
fn validate_chapter_config(
chapter_configs: &[(PathBuf, SearchChapterSettings)],
chapter_trees: &[ChapterTree<'_>],
) -> Result<()> {
for (path, _) in chapter_configs {
let found = chapter_trees
.iter()
.any(|ct| settings_path(ct.chapter).starts_with(path));
if !found {
bail!(
"[output.html.search.chapter] key `{}` does not match any chapter paths",
path.display()
);
}
}
Ok(())
}
fn sort_search_config(
map: &HashMap<String, SearchChapterSettings>,
) -> Vec<(PathBuf, SearchChapterSettings)> {
let mut settings: Vec<_> = map
.iter()
.map(|(key, value)| (PathBuf::from(key), value.clone()))
.collect();
// Note: This is case-sensitive, and assumes the author uses the same case
// as the actual filename.
settings.sort_by(|a, b| a.0.cmp(&b.0));
settings
}
fn get_chapter_settings(
chapter_configs: &[(PathBuf, SearchChapterSettings)],
source_path: &Path,
) -> SearchChapterSettings {
let mut result = SearchChapterSettings::default();
for (path, config) in chapter_configs {
if source_path.starts_with(path) {
result.enable = config.enable.or(result.enable);
}
}
result
}
/// Replaces multiple consecutive whitespace characters with a single space character.
fn collapse_whitespace(text: &str) -> Cow<'_, str> {
static_regex!(WS, r"\s\s+");
WS.replace_all(text, " ")
}
#[test]
fn chapter_settings_priority() {
let cfg = r#"
[output.html.search.chapter]
"cli/watch.md" = { enable = true }
"cli" = { enable = false }
"cli/inner/foo.md" = { enable = false }
"cli/inner" = { enable = true }
"foo" = {} # Just to make sure empty table is allowed.
"#;
let cfg: mdbook_core::config::Config = toml::from_str(cfg).unwrap();
let html = cfg.html_config().unwrap();
let chapter_configs = sort_search_config(&html.search.unwrap().chapter);
for (path, enable) in [
("foo.md", None),
("cli/watch.md", Some(true)),
("cli/index.md", Some(false)),
("cli/inner/index.md", Some(true)),
("cli/inner/foo.md", Some(false)),
] {
let mut settings = SearchChapterSettings::default();
settings.enable = enable;
assert_eq!(
get_chapter_settings(&chapter_configs, Path::new(path)),
settings
);
}
}
#[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

@@ -0,0 +1,320 @@
//! Support for writing static files.
use super::helpers::resources::ResourceHelper;
use crate::theme::{self, Theme, playground_editor};
use anyhow::{Context, Result};
use mdbook_core::config::HtmlConfig;
use mdbook_core::static_regex;
use mdbook_core::utils::fs;
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tracing::debug;
/// Map static files to their final names and contents.
///
/// It performs [fingerprinting], if you call the `hash_files` method.
/// If hash-files is turned off, then the files will not be renamed.
/// It also writes files to their final destination, when `write_files` is called,
/// and interprets the `{{ resource }}` directives to allow assets to name each other.
///
/// [fingerprinting]: https://guides.rubyonrails.org/asset_pipeline.html#fingerprinting-versioning-with-digest-based-urls
pub(super) struct StaticFiles {
static_files: Vec<StaticFile>,
hash_map: HashMap<String, String>,
}
enum StaticFile {
Builtin {
data: Vec<u8>,
filename: String,
},
Additional {
input_location: PathBuf,
filename: String,
},
}
impl StaticFiles {
pub(super) fn new(theme: &Theme, html_config: &HtmlConfig, root: &Path) -> Result<StaticFiles> {
let static_files = Vec::new();
let mut this = StaticFiles {
hash_map: HashMap::new(),
static_files,
};
this.add_builtin("book.js", &theme.js);
this.add_builtin("css/general.css", &theme.general_css);
this.add_builtin("css/chrome.css", &theme.chrome_css);
if html_config.print.enable {
this.add_builtin("css/print.css", &theme.print_css);
}
this.add_builtin("css/variables.css", &theme.variables_css);
if let Some(contents) = &theme.favicon_png {
this.add_builtin("favicon.png", contents);
}
if let Some(contents) = &theme.favicon_svg {
this.add_builtin("favicon.svg", contents);
}
this.add_builtin("highlight.css", &theme.highlight_css);
this.add_builtin("tomorrow-night.css", &theme.tomorrow_night_css);
this.add_builtin("ayu-highlight.css", &theme.ayu_highlight_css);
this.add_builtin("highlight.js", &theme.highlight_js);
this.add_builtin("clipboard.min.js", &theme.clipboard_js);
if theme.fonts_css.is_none() {
this.add_builtin("fonts/fonts.css", theme::fonts::CSS);
for (file_name, contents) in theme::fonts::LICENSES.iter() {
this.add_builtin(file_name, contents);
}
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
this.add_builtin(file_name, contents);
}
this.add_builtin(
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1,
);
} else if let Some(fonts_css) = &theme.fonts_css {
if !fonts_css.is_empty() {
this.add_builtin("fonts/fonts.css", fonts_css);
}
}
let playground_config = &html_config.playground;
// Ace is a very large dependency, so only load it when requested
if playground_config.editable && playground_config.copy_js {
// Load the editor
this.add_builtin("editor.js", playground_editor::JS);
this.add_builtin("ace.js", playground_editor::ACE_JS);
this.add_builtin("mode-rust.js", playground_editor::MODE_RUST_JS);
this.add_builtin("theme-dawn.js", playground_editor::THEME_DAWN_JS);
this.add_builtin(
"theme-tomorrow_night.js",
playground_editor::THEME_TOMORROW_NIGHT_JS,
);
}
let custom_files = html_config
.additional_css
.iter()
.chain(html_config.additional_js.iter());
for custom_file in custom_files {
let input_location = root.join(custom_file);
this.static_files.push(StaticFile::Additional {
input_location,
filename: custom_file
.to_str()
.with_context(|| "resource file names must be valid utf8")?
.to_owned(),
});
}
for input_location in theme.font_files.iter().cloned() {
let filename = Path::new("fonts")
.join(input_location.file_name().unwrap())
.to_str()
.with_context(|| "resource file names must be valid utf8")?
.to_owned();
this.static_files.push(StaticFile::Additional {
input_location,
filename,
});
}
Ok(this)
}
pub(super) fn add_builtin(&mut self, filename: &str, data: &[u8]) {
self.static_files.push(StaticFile::Builtin {
filename: filename.to_owned(),
data: data.to_owned(),
});
}
/// Updates this [`StaticFiles`] to hash the contents for determining the
/// filename for each resource.
pub(super) fn hash_files(&mut self) -> Result<()> {
use sha2::{Digest, Sha256};
use std::io::Read;
for static_file in &mut self.static_files {
match static_file {
&mut StaticFile::Builtin {
ref mut filename,
ref data,
} => {
let mut parts = filename.splitn(2, '.');
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
if let Some((name, suffix)) = parts {
if name != "" && suffix != "" && suffix != "txt" {
let hex = hex::encode(&Sha256::digest(data)[..4]);
let new_filename = format!("{}-{}.{}", name, hex, suffix);
self.hash_map.insert(filename.clone(), new_filename.clone());
*filename = new_filename;
}
}
}
&mut StaticFile::Additional {
ref mut filename,
ref input_location,
} => {
let mut parts = filename.splitn(2, '.');
let parts = parts.next().and_then(|p| Some((p, parts.next()?)));
if let Some((name, suffix)) = parts {
if name != "" && suffix != "" {
let mut digest = Sha256::new();
let mut input_file =
std::fs::File::open(input_location).with_context(|| {
format!("failed to open `{filename}` for hashing")
})?;
let mut buf = vec![0; 1024];
loop {
let amt = input_file
.read(&mut buf)
.with_context(|| "read static file for hashing")?;
if amt == 0 {
break;
};
digest.update(&buf[..amt]);
}
let hex = hex::encode(&digest.finalize()[..4]);
let new_filename = format!("{}-{}.{}", name, hex, suffix);
self.hash_map.insert(filename.clone(), new_filename.clone());
*filename = new_filename;
}
}
}
}
}
Ok(())
}
pub(super) fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
use regex::bytes::Captures;
// The `{{ resource "name" }}` directive in static resources look like
// handlebars syntax, even if they technically aren't.
static_regex!(RESOURCE, bytes, r#"\{\{ resource "([^"]+)" \}\}"#);
fn replace_all<'a>(
hash_map: &HashMap<String, String>,
data: &'a [u8],
filename: &str,
) -> Cow<'a, [u8]> {
RESOURCE.replace_all(data, move |captures: &Captures<'_>| {
let name = captures
.get(1)
.expect("capture 1 in resource regex")
.as_bytes();
let name = std::str::from_utf8(name).expect("resource name with invalid utf8");
let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name);
let path_to_root = fs::path_to_root(filename);
format!("{}{}", path_to_root, resource_filename)
.as_bytes()
.to_owned()
})
}
for static_file in &self.static_files {
match static_file {
StaticFile::Builtin { filename, data } => {
debug!("Writing builtin -> {}", filename);
let data = if filename.ends_with(".css") || filename.ends_with(".js") {
replace_all(&self.hash_map, data, filename)
} else {
Cow::Borrowed(&data[..])
};
let path = destination.join(filename);
fs::write(path, &data)?;
}
StaticFile::Additional {
input_location,
filename,
} => {
let output_location = destination.join(filename);
debug!(
"Copying {} -> {}",
input_location.display(),
output_location.display()
);
if let Some(parent) = output_location.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Unable to create {}", parent.display()))?;
}
if filename.ends_with(".css") || filename.ends_with(".js") {
let data = fs::read_to_string(input_location)?;
let data = replace_all(&self.hash_map, data.as_bytes(), filename);
let path = destination.join(filename);
fs::write(path, &data)?;
} else {
std::fs::copy(input_location, &output_location).with_context(|| {
format!(
"Unable to copy {} to {}",
input_location.display(),
output_location.display()
)
})?;
}
}
}
}
let hash_map = self.hash_map;
Ok(ResourceHelper { hash_map })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme::Theme;
use mdbook_core::config::HtmlConfig;
use mdbook_core::utils::fs;
use tempfile::TempDir;
#[test]
fn test_write_directive() {
let theme = Theme {
index: Vec::new(),
head: Vec::new(),
redirect: Vec::new(),
header: Vec::new(),
chrome_css: Vec::new(),
general_css: Vec::new(),
print_css: Vec::new(),
variables_css: Vec::new(),
favicon_png: Some(Vec::new()),
favicon_svg: Some(Vec::new()),
js: Vec::new(),
highlight_css: Vec::new(),
tomorrow_night_css: Vec::new(),
ayu_highlight_css: Vec::new(),
highlight_js: Vec::new(),
clipboard_js: Vec::new(),
toc_js: Vec::new(),
toc_html: Vec::new(),
fonts_css: None,
font_files: Vec::new(),
};
let temp_dir = TempDir::with_prefix("mdbook-").unwrap();
let reference_js = Path::new("static-files-test-case-reference.js");
let mut html_config = HtmlConfig::default();
html_config.additional_js.push(reference_js.to_owned());
fs::write(
temp_dir.path().join(reference_js),
br#"{{ resource "book.js" }}"#,
)
.unwrap();
let mut static_files = StaticFiles::new(&theme, &html_config, temp_dir.path()).unwrap();
static_files.hash_files().unwrap();
static_files.write_files(temp_dir.path()).unwrap();
// custom JS winds up referencing book.js
let reference_js_content = fs::read_to_string(
temp_dir
.path()
.join("static-files-test-case-reference-635c9cdc.js"),
)
.unwrap();
assert_eq!("book-e3b0c442.js", reference_js_content);
// book.js winds up empty
let book_js_content = fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
assert_eq!("", book_js_content);
}
}

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