Compare commits

...

162 Commits

Author SHA1 Message Date
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
Gabor Szabo
a7ca2e169f [test] error Couldn't open SUMMARY.md in load_book 2025-03-09 14:40:00 +02: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
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
278 changed files with 4684 additions and 10683 deletions

95
.eslintrc.json Normal file
View File

@@ -0,0 +1,95 @@
{
"env": {
"browser": true,
"node": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"module": "readonly",
"require": "readonly"
},
"parserOptions": {
"ecmaVersion": 2021,
"requireConfigFile": false,
"sourceType": "module"
},
"ignorePatterns": ["**min.js", "**/highlight.js", "**/playground_editor/*"],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"brace-style": [
"error",
"1tbs",
{ "allowSingleLine": false }
],
"curly": "error",
"no-trailing-spaces": "error",
"no-multi-spaces": "error",
"keyword-spacing": [
"error",
{ "before": true, "after": true }
],
"comma-spacing": [
"error",
{ "before": false, "after": true }
],
"arrow-spacing": [
"error",
{ "before": true, "after": true }
],
"key-spacing": [
"error",
{ "beforeColon": false, "afterColon": true, "mode": "strict" }
],
"func-call-spacing": ["error", "never"],
"space-infix-ops": "error",
"space-before-function-paren": ["error", "never"],
"space-before-blocks": "error",
"no-console": [
"error",
{ "allow": ["warn", "error"] }
],
"comma-dangle": ["error", "always-multiline"],
"comma-style": ["error", "last"],
"max-len": ["error", { "code": 100, "tabWidth": 2 }],
"eol-last": ["error", "always"],
"no-extra-parens": "error",
"arrow-parens": ["error", "as-needed"],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"prefer-const": ["error"],
"no-var": "error",
"eqeqeq": "error"
},
"overrides": [
{
"files": [
"tests/**/*.js"
],
"env": {
"jest": true,
"node": true
}
}
]
}

2
.gitattributes vendored
View File

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

View File

@@ -48,7 +48,7 @@ jobs:
run: rustup update stable --no-self-update && rustup default stable
- name: Build book
run: cargo run -- build guide
- name: Deploy to GitHub
- name: Deploy the User Guide to GitHub Pages using the gh-pages branch
env:
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
run: |

View File

@@ -3,9 +3,6 @@ on:
pull_request:
merge_group:
env:
BROWSER_UI_TEST_VERSION: '0.19.0'
jobs:
test:
runs-on: ${{ matrix.os }}
@@ -43,7 +40,7 @@ jobs:
- name: msrv
os: ubuntu-22.04
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
rust: 1.77.0
rust: 1.81.0
target: x86_64-unknown-linux-gnu
name: ${{ matrix.name }}
steps:
@@ -85,10 +82,34 @@ jobs:
with:
node-version: 20
- name: Install browser-ui-test
run: npm install browser-ui-test@"${BROWSER_UI_TEST_VERSION}"
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@v4
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- run: rustup component add clippy
- run: cargo clippy --workspace --all-targets --no-deps -- -D warnings
docs:
name: Check API docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu
- name: Ensure intradoc links are valid
run: cargo doc --workspace --document-private-items --no-deps
env:
RUSTDOCFLAGS: -D warnings
# The success job is here to consolidate the total success/failure state of
# all other jobs. This job is then included in the GitHub branch protection
# rule which prevents merges unless all other jobs are passing. This makes
@@ -100,6 +121,10 @@ jobs:
needs:
- test
- rustfmt
- aarch64-cross-builds
- gui
- clippy
- docs
runs-on: ubuntu-latest
steps:
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'

1
.gitignore vendored
View File

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

View File

@@ -1,5 +1,55 @@
# Changelog
## 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)

View File

@@ -144,9 +144,16 @@ GUI tests are checked with the GUI testsuite. To run it, you need to install `np
cargo test --test gui
```
The first time, it'll fail and ask you to install the `browser-ui-test` package. Install it then re-run the tests.
If you want to only run some tests, you can filter them by passing (part of) their name:
If you want to disable the headless mode, use the `DISABLE_HEADLESS_TEST=1` environment variable:
```
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
@@ -156,6 +163,21 @@ The GUI tests are in the directory `tests/gui` in text files with the `.goml` ex
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
The following are instructions for updating [highlight.js](https://highlightjs.org/).

793
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,15 @@
[workspace]
members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
[workspace.lints.clippy]
all = { level = "allow", priority = -2 }
correctness = { level = "warn", priority = -1 }
complexity = { level = "warn", priority = -1 }
needless-lifetimes = "allow" # Remove once 1.87 is stable, https://github.com/rust-lang/rust-clippy/issues/13514
[package]
name = "mdbook"
version = "0.4.47"
version = "0.4.49"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
@@ -17,7 +23,7 @@ license = "MPL-2.0"
readme = "README.md"
repository = "https://github.com/rust-lang/mdBook"
description = "Creates a book from markdown files"
rust-version = "1.77" # Keep in sync with installation.md and .github/workflows/main.yml
rust-version = "1.81" # Keep in sync with installation.md and .github/workflows/main.yml
[dependencies]
anyhow = "1.0.71"
@@ -50,7 +56,7 @@ walkdir = { version = "2.3.3", optional = true }
# Serve feature
futures-util = { version = "0.3.28", optional = true }
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"], optional = true }
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
# Search feature
@@ -58,10 +64,9 @@ elasticlunr-rs = { version = "3.0.2", optional = true }
ammonia = { version = "4.0.0", optional = true }
[dev-dependencies]
assert_cmd = "2.0.11"
predicates = "3.0.3"
select = "0.6.0"
semver = "1.0.17"
snapbox = { version = "0.6.21", features = ["diff", "dir", "term-svg", "regex", "json"] }
pretty_assertions = "1.3.0"
walkdir = "2.3.3"
@@ -91,3 +96,6 @@ test = false
name = "gui"
path = "tests/gui/runner.rs"
crate-type = ["bin"]
[lints]
workspace = true

View File

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

View File

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

View File

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

View File

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

10
package.json Normal file
View File

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

View File

@@ -647,4 +647,19 @@ And here is some \
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#"Couldn't open SUMMARY.md in {:?} directory"#,
temp_dir.path()
);
assert_eq!(error_message, expected);
}
}

View File

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

View File

@@ -3,6 +3,7 @@ use log::{debug, trace, warn};
use memchr::Memchr;
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
@@ -245,6 +246,11 @@ impl<'a> SummaryParser<'a> {
.parse_affix(false)
.with_context(|| "There was an error parsing the suffix chapters")?;
let mut files = HashSet::new();
for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] {
Self::check_for_duplicates(&part, &mut files)?;
}
Ok(Summary {
title,
prefix_chapters,
@@ -253,6 +259,28 @@ impl<'a> SummaryParser<'a> {
})
}
/// Recursively check for duplicate files in the summary items.
fn check_for_duplicates<'b>(
items: &'b [SummaryItem],
files: &mut HashSet<&'b PathBuf>,
) -> Result<()> {
for item in items {
if let SummaryItem::Link(link) = item {
if let Some(location) = &link.location {
if !files.insert(location) {
bail!(anyhow::anyhow!(
"Duplicate file in SUMMARY.md: {:?}",
location
));
}
}
// Recursively check nested items
Self::check_for_duplicates(&link.nested_items, files)?;
}
}
Ok(())
}
/// Parse the affix chapters.
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
@@ -1127,4 +1155,83 @@ mod tests {
let got = parser.parse_affix(false).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn duplicate_entries_1() {
let src = r#"
# Summary
- [A](./a.md)
- [A](./a.md)
"#;
let res = parse_summary(src);
assert!(res.is_err());
let error_message = res.err().unwrap().to_string();
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
}
#[test]
fn duplicate_entries_2() {
let src = r#"
# Summary
- [A](./a.md)
- [A](./a.md)
"#;
let res = parse_summary(src);
assert!(res.is_err());
let error_message = res.err().unwrap().to_string();
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
}
#[test]
fn duplicate_entries_3() {
let src = r#"
# Summary
- [A](./a.md)
- [B](./b.md)
- [A](./a.md)
"#;
let res = parse_summary(src);
assert!(res.is_err());
let error_message = res.err().unwrap().to_string();
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
}
#[test]
fn duplicate_entries_4() {
let src = r#"
# Summary
[A](./a.md)
- [B](./b.md)
- [A](./a.md)
"#;
let res = parse_summary(src);
assert!(res.is_err());
let error_message = res.err().unwrap().to_string();
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
}
#[test]
fn duplicate_entries_5() {
let src = r#"
# Summary
[A](./a.md)
# hi
- [B](./b.md)
# bye
---
[A](./a.md)
"#;
let res = parse_summary(src);
assert!(res.is_err());
let error_message = res.err().unwrap().to_string();
assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
}
}

View File

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

View File

@@ -200,18 +200,53 @@ sup {
line-height: 0;
}
:not(.footnote-definition) + .footnote-definition {
margin-block-start: 2em;
}
.footnote-definition:not(:has(+ .footnote-definition)) {
margin-block-end: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
}
.footnote-definition p {
display: inline;
/* 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 {

View File

@@ -2,7 +2,8 @@
/* Globals */
:root {
--sidebar-width: 300px;
--sidebar-target-width: 300px;
--sidebar-width: min(var(--sidebar-target-width), 80vw);
--sidebar-resize-indicator-width: 8px;
--sidebar-resize-indicator-space: 2px;
--page-padding: 15px;
@@ -61,6 +62,8 @@
--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;
}
.coal {
@@ -110,6 +113,8 @@
--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;
}
.light, html:not(.js) {
@@ -159,6 +164,8 @@
--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;
}
.navy {
@@ -208,6 +215,8 @@
--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;
}
.rust {
@@ -255,6 +264,8 @@
--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;
}
@media (prefers-color-scheme: dark) {

View File

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 434 KiB

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

@@ -1,14 +1,16 @@
"use strict";
'use strict';
/* global default_theme, default_dark_theme, default_light_theme, hljs, ClipboardJS */
// Fix back button cache problem
window.onunload = function () { };
window.onunload = function() { };
// Global variable, shared between modules
function playground_text(playground, hidden = true) {
let code_block = playground.querySelector("code");
const code_block = playground.querySelector('code');
if (window.ace && code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
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;
@@ -21,25 +23,25 @@ function playground_text(playground, hidden = true) {
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
]);
}
var playgrounds = Array.from(document.querySelectorAll(".playground"));
const playgrounds = Array.from(document.querySelectorAll('.playground'));
if (playgrounds.length > 0) {
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
fetch_with_timeout('https://play.rust-lang.org/meta/crates', {
headers: {
'Content-Type': "application/json",
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
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) {
@@ -48,20 +50,20 @@ function playground_text(playground, hidden = true) {
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playground_block.querySelector("code");
if (code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
editor.addEventListener("change", function (e) {
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",
name: 'run',
bindKey: {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
win: 'Ctrl-Enter',
mac: 'Ctrl-Enter',
},
exec: _editor => run_rust_code(playground_block)
exec: _editor => run_rust_code(playground_block),
});
}
}
@@ -70,37 +72,38 @@ function playground_text(playground, hidden = true) {
// 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) {
var play_button = pre_block.querySelector(".play-button");
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");
if (pre_block.querySelector('code').classList.contains('no_run')) {
play_button.classList.add('hidden');
return;
}
// get list of `extern crate`'s from snippet
var txt = playground_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
var item;
const txt = playground_text(pre_block);
const re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
const snippet_crates = [];
let item;
// eslint-disable-next-line no-cond-assign
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
var all_available = snippet_crates.every(function (elem) {
const all_available = snippet_crates.every(function(elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove("hidden");
play_button.classList.remove('hidden');
} else {
play_button.classList.add("hidden");
play_button.classList.add('hidden');
}
}
function run_rust_code(code_block) {
var result_block = code_block.querySelector(".result");
let result_block = code_block.querySelector('.result');
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
@@ -108,93 +111,110 @@ function playground_text(playground, hidden = true) {
code_block.append(result_block);
}
let text = playground_text(code_block);
let classes = code_block.querySelector('code').classList;
let edition = "2015";
const text = playground_text(code_block);
const classes = code_block.querySelector('code').classList;
let edition = '2015';
classes.forEach(className => {
if (className.startsWith("edition")) {
if (className.startsWith('edition')) {
edition = className.slice(7);
}
});
var params = {
version: "stable",
optimize: "0",
const params = {
version: 'stable',
optimize: '0',
code: text,
edition: edition
edition: edition,
};
if (text.indexOf("#![feature") !== -1) {
params.version = "nightly";
if (text.indexOf('#![feature') !== -1) {
params.version = 'nightly';
}
result_block.innerText = "Running...";
result_block.innerText = 'Running...';
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
fetch_with_timeout('https://play.rust-lang.org/evaluate.json', {
headers: {
'Content-Type': "application/json",
'Content-Type': 'application/json',
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
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);
.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
languages: [], // Languages used for auto-detection
});
let code_nodes = Array
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"); });
.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'); });
.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); });
.filter(function(node) {
return !node.classList.contains('editable');
})
.forEach(function(block) {
hljs.highlightBlock(block);
});
} else {
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
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'); });
code_nodes.forEach(function(block) {
block.classList.add('hljs');
});
Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
Array.from(document.querySelectorAll('code.hljs')).forEach(function(block) {
var lines = Array.from(block.querySelectorAll('.boring'));
const lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines.length) { return; }
block.classList.add("hide-boring");
if (!lines.length) {
return;
}
block.classList.add('hide-boring');
var buttons = document.createElement('div');
const buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
buttons.innerHTML = '<button class="fa fa-eye" title="Show hidden lines" \
aria-label="Show hidden lines"></button>';
// add expand button
var pre_block = block.parentNode;
const pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
pre_block.querySelector('.buttons').addEventListener('click', function(e) {
if (e.target.classList.contains('fa-eye')) {
e.target.classList.remove('fa-eye');
e.target.classList.add('fa-eye-slash');
@@ -214,21 +234,21 @@ function playground_text(playground, hidden = true) {
});
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
Array.from(document.querySelectorAll('pre code')).forEach(function(block) {
const pre_block = block.parentNode;
if (!pre_block.classList.contains('playground')) {
var buttons = pre_block.querySelector(".buttons");
let buttons = pre_block.querySelector('.buttons');
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var clipButton = document.createElement('button');
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>';
clipButton.innerHTML = '<i class="tooltiptext"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
@@ -236,28 +256,28 @@ function playground_text(playground, hidden = true) {
}
// Process playground code blocks
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
Array.from(document.querySelectorAll('.playground')).forEach(function(pre_block) {
// Add play button
var buttons = pre_block.querySelector(".buttons");
let buttons = pre_block.querySelector('.buttons');
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var runCodeButton = document.createElement('button');
const runCodeButton = document.createElement('button');
runCodeButton.className = 'fa fa-play play-button';
runCodeButton.hidden = true;
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', function (e) {
runCodeButton.addEventListener('click', () => {
run_rust_code(pre_block);
});
if (window.playground_copyable) {
var copyCodeClipboardButton = document.createElement('button');
const copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
@@ -266,17 +286,17 @@ function playground_text(playground, hidden = true) {
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
let code_block = pre_block.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
var undoChangesButton = document.createElement('button');
const code_block = pre_block.querySelector('code');
if (window.ace && code_block.classList.contains('editable')) {
const undoChangesButton = document.createElement('button');
undoChangesButton.className = 'fa fa-history reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
undoChangesButton.addEventListener('click', function() {
const editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
@@ -285,31 +305,37 @@ function playground_text(playground, hidden = true) {
})();
(function themes() {
var html = document.querySelector('html');
var themeToggleButton = document.getElementById('theme-toggle');
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var themeIds = [];
themePopup.querySelectorAll('button.theme').forEach(function (el) {
const html = document.querySelector('html');
const themeToggleButton = document.getElementById('theme-toggle');
const themePopup = document.getElementById('theme-list');
const themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
const themeIds = [];
themePopup.querySelectorAll('button.theme').forEach(function(el) {
themeIds.push(el.id);
});
var stylesheets = {
ayuHighlight: document.querySelector("#ayu-highlight-css"),
tomorrowNight: document.querySelector("#tomorrow-night-css"),
highlight: document.querySelector("#highlight-css"),
const stylesheets = {
ayuHighlight: document.querySelector('#ayu-highlight-css'),
tomorrowNight: document.querySelector('#tomorrow-night-css'),
highlight: document.querySelector('#highlight-css'),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector("button#" + get_theme()).focus();
themePopup.querySelector('button#' + get_theme()).focus();
}
function updateThemeSelected() {
themePopup.querySelectorAll('.theme-selected').forEach(function (el) {
themePopup.querySelectorAll('.theme-selected').forEach(function(el) {
el.classList.remove('theme-selected');
});
themePopup.querySelector("button#" + get_theme()).classList.add('theme-selected');
const selected = get_saved_theme() ?? 'default_theme';
let element = themePopup.querySelector('button#' + selected);
if (element === null) {
// Fall back in case there is no "Default" item.
element = themePopup.querySelector('button#' + get_theme());
}
element.classList.add('theme-selected');
}
function hideThemes() {
@@ -318,64 +344,91 @@ function playground_text(playground, hidden = true) {
themeToggleButton.focus();
}
function get_saved_theme() {
let theme = null;
try {
theme = localStorage.getItem('mdbook-theme');
} catch (e) {
// ignore error.
}
return theme;
}
function delete_saved_theme() {
localStorage.removeItem('mdbook-theme');
}
function get_theme() {
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
const theme = get_saved_theme();
if (theme === null || theme === undefined || !themeIds.includes(theme)) {
return default_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') {
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') {
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";
ace_theme = 'ace/theme/tomorrow_night';
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = "ace/theme/dawn";
ace_theme = 'ace/theme/dawn';
}
setTimeout(function () {
setTimeout(function() {
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function (editor) {
window.editors.forEach(function(editor) {
editor.setTheme(ace_theme);
});
}
var previousTheme = get_theme();
if (store) {
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
try {
localStorage.setItem('mdbook-theme', theme);
} catch (e) {
// ignore error.
}
}
html.classList.remove(previousTheme);
html.classList.add(theme);
previousTheme = theme;
updateThemeSelected();
}
// Set theme
var theme = get_theme();
const query = window.matchMedia('(prefers-color-scheme: dark)');
query.onchange = function() {
set_theme(get_theme(), false);
};
set_theme(theme, false);
// Set theme.
set_theme(get_theme(), false);
themeToggleButton.addEventListener('click', function () {
themeToggleButton.addEventListener('click', function() {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
@@ -383,105 +436,130 @@ function playground_text(playground, hidden = true) {
}
});
themePopup.addEventListener('click', function (e) {
var theme;
if (e.target.className === "theme") {
themePopup.addEventListener('click', function(e) {
let theme;
if (e.target.className === 'theme') {
theme = e.target.id;
} else if (e.target.parentElement.className === "theme") {
} else if (e.target.parentElement.className === 'theme') {
theme = e.target.parentElement.id;
} else {
return;
}
set_theme(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)) {
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
// 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)) {
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; }
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();
var li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
var 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;
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() {
var body = document.querySelector("body");
var sidebar = document.getElementById("sidebar");
var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle");
var sidebarToggleAnchor = document.getElementById("sidebar-toggle-anchor");
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
var firstContact = null;
const body = document.querySelector('body');
const sidebar = document.getElementById('sidebar');
const sidebarLinks = document.querySelectorAll('#sidebar a');
const sidebarToggleButton = document.getElementById('sidebar-toggle');
const sidebarToggleAnchor = document.getElementById('sidebar-toggle-anchor');
const sidebarResizeHandle = document.getElementById('sidebar-resize-handle');
let firstContact = null;
function showSidebar() {
body.classList.remove('sidebar-hidden')
body.classList.remove('sidebar-hidden');
body.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function (link) {
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 (e) { }
try {
localStorage.setItem('mdbook-sidebar', 'visible');
} catch (e) {
// Ignore error.
}
}
function hideSidebar() {
body.classList.remove('sidebar-visible')
body.classList.remove('sidebar-visible');
body.classList.add('sidebar-hidden');
Array.from(sidebarLinks).forEach(function (link) {
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 (e) { }
try {
localStorage.setItem('mdbook-sidebar', 'hidden');
} catch (e) {
// Ignore error.
}
}
// Toggle sidebar
sidebarToggleAnchor.addEventListener('change', function sidebarToggle() {
if (sidebarToggleAnchor.checked) {
var current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
const current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-target-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px');
document.documentElement.style.setProperty('--sidebar-target-width', '150px');
}
showSidebar();
} else {
@@ -491,50 +569,52 @@ function playground_text(playground, hidden = true) {
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize(e) {
function initResize() {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
body.classList.add('sidebar-resizing');
}
function resize(e) {
var pos = (e.clientX - sidebar.offsetLeft);
let pos = e.clientX - sidebar.offsetLeft;
if (pos < 20) {
hideSidebar();
} else {
if (body.classList.contains("sidebar-hidden")) {
if (body.classList.contains('sidebar-hidden')) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
document.documentElement.style.setProperty('--sidebar-target-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize(e) {
function stopResize() {
body.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function (e) {
document.addEventListener('touchstart', function(e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now()
time: Date.now(),
};
}, { passive: true });
document.addEventListener('touchmove', function (e) {
if (!firstContact)
document.addEventListener('touchmove', function(e) {
if (!firstContact) {
return;
}
var curX = e.touches[0].clientX;
var xDiff = curX - firstContact.x,
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))
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300)) {
showSidebar();
else if (xDiff < 0 && curX < 300)
} else if (xDiff < 0 && curX < 300) {
hideSidebar();
}
firstContact = null;
}
@@ -542,49 +622,53 @@ function playground_text(playground, hidden = true) {
})();
(function chapterNavigation() {
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; }
var html = document.querySelector('html');
document.addEventListener('keydown', function(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
if (window.search && window.search.hasFocus()) {
return;
}
const html = document.querySelector('html');
function next() {
var nextButton = document.querySelector('.nav-chapters.next');
const nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
}
function prev() {
var previousButton = document.querySelector('.nav-chapters.previous');
const previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
}
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;
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() {
var clipButtons = document.querySelectorAll('.clip-button');
const clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = "";
elem.firstChild.innerText = '';
elem.className = 'clip-button';
}
@@ -593,58 +677,58 @@ function playground_text(playground, hidden = true) {
elem.className = 'clip-button tooltipped';
}
var clipboardSnippets = new ClipboardJS('.clip-button', {
text: function (trigger) {
const clipboardSnippets = new ClipboardJS('.clip-button', {
text: function(trigger) {
hideTooltip(trigger);
let playground = trigger.closest("pre");
const playground = trigger.closest('pre');
return playground_text(playground, false);
}
},
});
Array.from(clipButtons).forEach(function (clipButton) {
clipButton.addEventListener('mouseout', function (e) {
Array.from(clipButtons).forEach(function(clipButton) {
clipButton.addEventListener('mouseout', function(e) {
hideTooltip(e.currentTarget);
});
});
clipboardSnippets.on('success', function (e) {
clipboardSnippets.on('success', function(e) {
e.clearSelection();
showTooltip(e.trigger, "Copied!");
showTooltip(e.trigger, 'Copied!');
});
clipboardSnippets.on('error', function (e) {
showTooltip(e.trigger, "Clipboard error!");
clipboardSnippets.on('error', function(e) {
showTooltip(e.trigger, 'Clipboard error!');
});
})();
(function scrollToTop () {
var menuTitle = document.querySelector('.menu-title');
(function scrollToTop() {
const menuTitle = document.querySelector('.menu-title');
menuTitle.addEventListener('click', function () {
menuTitle.addEventListener('click', function() {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
(function controllMenu() {
var menu = document.getElementById('menu-bar');
const menu = document.getElementById('menu-bar');
(function controllPosition() {
var scrollTop = document.scrollingElement.scrollTop;
var prevScrollTop = scrollTop;
var minMenuY = -menu.clientHeight - 50;
let scrollTop = document.scrollingElement.scrollTop;
let prevScrollTop = scrollTop;
const minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
var topCache = menu.style.top.slice(0, -2);
let topCache = menu.style.top.slice(0, -2);
menu.classList.remove('sticky');
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function () {
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
var nextSticky = null;
var nextTop = null;
var scrollDown = scrollTop > prevScrollTop;
var menuPosAbsoluteY = topCache - scrollTop;
let nextSticky = null;
let nextTop = null;
const scrollDown = scrollTop > prevScrollTop;
const menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {

View File

@@ -13,32 +13,31 @@ use std::path::{Path, PathBuf};
use crate::errors::*;
use log::warn;
pub static INDEX: &[u8] = include_bytes!("index.hbs");
pub static HEAD: &[u8] = include_bytes!("head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("header.hbs");
pub static TOC_JS: &[u8] = include_bytes!("toc.js.hbs");
pub static TOC_HTML: &[u8] = include_bytes!("toc.html.hbs");
pub static INDEX: &[u8] = include_bytes!("templates/index.hbs");
pub static HEAD: &[u8] = include_bytes!("templates/head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("templates/redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("templates/header.hbs");
pub static TOC_JS: &[u8] = include_bytes!("templates/toc.js.hbs");
pub static TOC_HTML: &[u8] = include_bytes!("templates/toc.html.hbs");
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
pub static JS: &[u8] = include_bytes!("book.js");
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &[u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
pub static FAVICON_PNG: &[u8] = include_bytes!("images/favicon.png");
pub static FAVICON_SVG: &[u8] = include_bytes!("images/favicon.svg");
pub static JS: &[u8] = include_bytes!("js/book.js");
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("js/highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("css/tomorrow-night.css");
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("css/highlight.css");
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("css/ayu-highlight.css");
pub static CLIPBOARD_JS: &[u8] = include_bytes!("js/clipboard.min.js");
pub static FONT_AWESOME: &[u8] = include_bytes!("css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &[u8] = include_bytes!("fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("fonts/FontAwesome.otf");
/// The `Theme` struct should be used instead of the static variables because
/// the `new()` method will look if the user has a theme directory in their

View File

@@ -1,6 +1,9 @@
"use strict";
'use strict';
/* global Mark, elasticlunr, path_to_root */
window.search = window.search || {};
(function search(search) {
(function search() {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
@@ -10,43 +13,27 @@ window.search = window.search || {};
return;
}
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
// 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;
};
}
var search_wrap = document.getElementById('search-wrapper'),
const search_wrap = document.getElementById('search-wrapper'),
searchbar = document.getElementById('searchbar'),
searchbar_outer = document.getElementById('searchbar-outer'),
searchresults = document.getElementById('searchresults'),
searchresults_outer = document.getElementById('searchresults-outer'),
searchresults_header = document.getElementById('searchresults-header'),
searchicon = document.getElementById('search-toggle'),
content = document.getElementById('content'),
searchindex = null,
doc_urls = [],
results_options = {
teaser_word_count: 30,
limit_results: 30,
},
search_options = {
bool: "AND",
expand: true,
fields: {
title: {boost: 1},
body: {boost: 1},
breadcrumbs: {boost: 0}
}
},
mark_exclude = [],
// SVG text elements don't render if inside a <mark> tag.
mark_exclude = ['text'],
marker = new Mark(content),
current_searchterm = "",
URL_SEARCH_PARAM = 'search',
URL_MARK_PARAM = 'highlight',
teaser_count = 0,
SEARCH_HOTKEY_KEYCODE = 83,
ESCAPE_KEYCODE = 27,
@@ -54,6 +41,24 @@ window.search = window.search || {};
UP_KEYCODE = 38,
SELECT_KEYCODE = 13;
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;
}
@@ -66,96 +71,99 @@ window.search = window.search || {};
// Helper to parse a url into its building blocks.
function parseURL(url) {
var a = document.createElement('a');
const a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':',''),
protocol: a.protocol.replace(':', ''),
host: a.hostname,
port: a.port,
params: (function(){
var ret = {};
var seg = a.search.replace(/^\?/,'').split('&');
var len = seg.length, i = 0, s;
for (;i<len;i++) {
if (!seg[i]) { continue; }
s = seg[i].split('=');
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')
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) {
var url = urlobject.protocol + "://" + urlobject.host;
if (urlobject.port != "") {
url += ":" + urlobject.port;
let url = urlobject.protocol + '://' + urlobject.host;
if (urlobject.port !== '') {
url += ':' + urlobject.port;
}
url += urlobject.path;
var joiner = "?";
for(var prop in urlobject.params) {
if(urlobject.params.hasOwnProperty(prop)) {
url += joiner + prop + "=" + urlobject.params[prop];
joiner = "&";
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;
if (urlobject.hash !== '') {
url += '#' + urlobject.hash;
}
return url;
}
// Helper to escape html special chars for displaying the teasers
var escapeHTML = (function() {
var MAP = {
const escapeHTML = (function() {
const MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
'\'': '&#39;',
};
const repl = function(c) {
return MAP[c];
};
var 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 + "'.";
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 + "':";
return count + ' search results for \'' + searchterm + '\':';
}
}
function formatSearchResult(result, searchterms) {
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
const teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
var url = doc_urls[result.ref].split("#");
if (url.length == 1) { // no anchor found
url.push("");
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).
var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
const encoded_search = encodeURIComponent(searchterms.join(' ')).replace(/'/g, '%27');
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
+ teaser + '</span>';
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + encoded_search
+ '#' + url[1] + '" aria-details="teaser_' + teaser_count + '">'
+ result.doc.breadcrumbs + '</a>' + '<span class="teaser" id="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:
@@ -166,88 +174,90 @@ window.search = window.search || {};
// 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>.
var stemmed_searchterms = searchterms.map(function(w) {
const stemmed_searchterms = searchterms.map(function(w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var searchterm_weight = 40;
var weighted = []; // contains elements of ["word", weight, index_in_document]
const searchterm_weight = 40;
const weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split('. ');
var index = 0;
var value = 0;
var searchterm_found = false;
for (var sentenceindex in sentences) {
var words = sentences[sentenceindex].split(' ');
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 (var wordindex in words) {
var word = words[wordindex];
for (const wordindex in words) {
const word = words[wordindex];
if (word.length > 0) {
for (var searchtermindex in stemmed_searchterms) {
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
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) {
if (weighted.length === 0) {
return body;
}
var window_weight = [];
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
const window_weight = [];
const window_size = Math.min(weighted.length, results_options.teaser_word_count);
var cur_sum = 0;
for (var wordindex = 0; wordindex < window_size; wordindex++) {
let cur_sum = 0;
for (let wordindex = 0; wordindex < window_size; wordindex++) {
cur_sum += weighted[wordindex][1];
};
}
window_weight.push(cur_sum);
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
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) {
var max_sum = 0;
var max_sum_window_index = 0;
let max_sum = 0;
// backwards
for (var i = window_weight.length - 1; i >= 0; i--) {
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
var teaser_split = [];
var index = weighted[max_sum_window_index][2];
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
var word = weighted[i];
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>")
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>")
if (word[1] === searchterm_weight) {
teaser_split.push('</em>');
}
};
}
return teaser_split.join('');
}
@@ -255,74 +265,98 @@ window.search = window.search || {};
function init(config) {
results_options = config.results_options;
search_options = config.search_options;
searchbar_outer = config.searchbar_outer;
doc_urls = config.doc_urls;
searchindex = elasticlunr.Index.load(config.index);
// Set up events
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
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 = function(e) { doSearchOrMarkFromUrl(); };
window.onpopstate = () => {
doSearchOrMarkFromUrl();
};
// Suppress "submit" events so the page doesn't reload when the user presses Enter
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
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;
}
function unfocusSearchbar() {
// hacky, but just focusing a div only works once
var tmp = document.createElement('input');
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
var url = parseURL(window.location.href);
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
&& url.params[URL_SEARCH_PARAM] != "") {
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'));
(url.params[URL_SEARCH_PARAM] + '').replace(/\+/g, '%20'));
searchbarKeyUpHandler(); // -> doSearch()
} else {
showSearch(false);
}
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
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
exclude: mark_exclude,
});
var markers = document.querySelectorAll("mark");
function hide() {
for (var i = 0; i < markers.length; i++) {
markers[i].classList.add("fade-out");
window.setTimeout(function(e) { marker.unmark(); }, 300);
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 (var i = 0; i < markers.length; i++) {
};
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.altKey ||
e.ctrlKey ||
e.metaKey ||
e.shiftKey ||
e.target.type === 'textarea' ||
e.target.type === 'text' ||
!hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)
) {
return;
}
if (e.keyCode === ESCAPE_KEYCODE) {
e.preventDefault();
searchbar.classList.remove("active");
setSearchUrlParameters("",
(searchbar.value.trim() !== "") ? "push" : "replace");
searchbar.classList.remove('active');
setSearchUrlParameters('',
searchbar.value.trim() !== '' ? 'push' : 'replace');
if (hasFocus()) {
unfocusSearchbar();
}
@@ -336,25 +370,27 @@ window.search = window.search || {};
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
e.preventDefault();
unfocusSearchbar();
searchresults.firstElementChild.classList.add("focus");
searchresults.firstElementChild.classList.add('focus');
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|| e.keyCode === UP_KEYCODE
|| e.keyCode === SELECT_KEYCODE)) {
// not `:focus` because browser does annoying scrolling
var focused = searchresults.querySelector("li.focus");
if (!focused) return;
const focused = searchresults.querySelector('li.focus');
if (!focused) {
return;
}
e.preventDefault();
if (e.keyCode === DOWN_KEYCODE) {
var next = focused.nextElementSibling;
const next = focused.nextElementSibling;
if (next) {
focused.classList.remove("focus");
next.classList.add("focus");
focused.classList.remove('focus');
next.classList.add('focus');
}
} else if (e.keyCode === UP_KEYCODE) {
focused.classList.remove("focus");
var prev = focused.previousElementSibling;
focused.classList.remove('focus');
const prev = focused.previousElementSibling;
if (prev) {
prev.classList.add("focus");
prev.classList.add('focus');
} else {
searchbar.select();
}
@@ -363,7 +399,7 @@ window.search = window.search || {};
}
}
}
function showSearch(yes) {
if (yes) {
search_wrap.classList.remove('hidden');
@@ -371,9 +407,9 @@ window.search = window.search || {};
} else {
search_wrap.classList.add('hidden');
searchicon.setAttribute('aria-expanded', 'false');
var results = searchresults.children;
for (var i = 0; i < results.length; i++) {
results[i].classList.remove("focus");
const results = searchresults.children;
for (let i = 0; i < results.length; i++) {
results[i].classList.remove('focus');
}
}
}
@@ -396,36 +432,37 @@ window.search = window.search || {};
showSearch(false);
}
}
// Eventhandler for keyevents while the searchbar is focused
function searchbarKeyUpHandler() {
var searchterm = searchbar.value.trim();
if (searchterm != "") {
searchbar.classList.add("active");
const searchterm = searchbar.value.trim();
if (searchterm !== '') {
searchbar.classList.add('active');
doSearch(searchterm);
} else {
searchbar.classList.remove("active");
searchbar.classList.remove('active');
showResults(false);
removeChildren(searchresults);
}
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
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.
// 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) {
var url = parseURL(window.location.href);
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
if (searchterm != "" || action == "push_if_new_search_else_replace") {
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 = "";
url.hash = '';
} else {
delete url.params[URL_MARK_PARAM];
delete url.params[URL_SEARCH_PARAM];
@@ -433,33 +470,40 @@ window.search = window.search || {};
// 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) ) {
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) ) {
} 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; }
else { current_searchterm = searchterm; }
if (current_searchterm === searchterm) {
return;
} else {
current_searchterm = searchterm;
}
if (searchindex == null) { return; }
if (searchindex === null) {
return;
}
// Do the actual search
var results = searchindex.search(searchterm, search_options);
var resultcount = Math.min(results.length, results_options.limit_results);
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
var searchterms = searchterm.split(' ');
const searchterms = searchterm.split(' ');
removeChildren(searchresults);
for(var i = 0; i < resultcount ; i++){
var resultElem = document.createElement('li');
for (let i = 0; i < resultcount ; i++) {
const resultElem = document.createElement('li');
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
searchresults.appendChild(resultElem);
}
@@ -468,16 +512,17 @@ window.search = window.search || {};
showResults(true);
}
fetch(path_to_root + '{{ resource "searchindex.json" }}')
.then(response => response.json())
.then(json => init(json))
.catch(error => { // Try to load searchindex.js if fetch failed
var script = document.createElement('script');
script.src = path_to_root + '{{ resource "searchindex.js" }}';
script.onload = () => init(window.search);
document.head.appendChild(script);
});
function loadScript(url, id) {
const script = document.createElement('script');
script.src = url;
script.id = id;
script.onload = () => init(window.search);
script.onerror = error => {
console.error(`Failed to load \`${url}\`: ${error}`);
};
document.head.append(script);
}
loadScript(path_to_root + '{{ resource "searchindex.js" }}', 'search-index');
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View File

@@ -53,10 +53,11 @@
<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 to javascript -->
<!-- Provide site root and default themes to javascript -->
<script>
var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
const path_to_root = "{{ path_to_root }}";
const default_light_theme = "{{ default_theme }}";
const default_dark_theme = "{{ preferred_dark_theme }}";
</script>
<!-- Start loading toc.js asap -->
<script src="{{ resource "toc.js" }}"></script>
@@ -66,8 +67,8 @@
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
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));
@@ -81,7 +82,8 @@
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
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;
@@ -94,8 +96,8 @@
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
let sidebar = null;
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
@@ -132,6 +134,7 @@
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
@@ -251,7 +254,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script>
var localAddrs = ["localhost", "127.0.0.1", ""];
const localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
// inspecting the book locally...

View File

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

View File

@@ -87,6 +87,7 @@ pub mod book;
pub mod config;
pub mod preprocess;
pub mod renderer;
#[path = "front-end/mod.rs"]
pub mod theme;
pub mod utils;

View File

@@ -148,7 +148,6 @@ enum RangeOrAnchor {
}
// A range of lines specified with some include directive.
#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
#[derive(PartialEq, Debug, Clone)]
enum LineRange {
Range(Range<usize>),

View File

@@ -20,10 +20,12 @@ use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use serde_json::json;
/// The HTML renderer for mdBook.
#[derive(Default)]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
/// Returns a new instance of [`HtmlHandlebars`].
pub fn new() -> Self {
HtmlHandlebars
}
@@ -207,7 +209,6 @@ impl HtmlHandlebars {
Ok(())
}
#[allow(clippy::let_and_return)]
fn post_process(
&self,
rendered: String,
@@ -696,7 +697,7 @@ fn build_header_links(html: &str) -> String {
.into_owned()
}
/// Insert a sinle link into a header, making sure each link gets its own
/// Insert a single link into a header, making sure each link gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn insert_link_into_header(
level: usize,

View File

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

View File

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

View File

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

View File

@@ -60,14 +60,19 @@ pub fn create_files(
let index = write_to_json(index, search_config, doc_urls)?;
debug!("Writing search index ✓");
if index.len() > 10_000_000 {
warn!("searchindex.json is very large ({} bytes)", index.len());
warn!("search index is very large ({} bytes)", index.len());
}
if search_config.copy_js {
static_files.add_builtin("searchindex.json", index.as_bytes());
static_files.add_builtin(
"searchindex.js",
format!("Object.assign(window.search, {});", index).as_bytes(),
// To reduce the size of the generated JSON by preventing all `"` characters to be
// escaped, we instead surround the string with much less common `'` character.
format!(
"window.search = 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);

View File

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

View File

@@ -1,4 +1,4 @@
#![allow(missing_docs)] // FIXME: Document this
//! Various helpers and utilities.
pub mod fs;
mod string;
@@ -194,6 +194,7 @@ pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
render_markdown_with_path(text, smart_punctuation, None)
}
/// Creates a new pulldown-cmark parser of the given text.
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
let mut opts = Options::empty();
opts.insert(Options::ENABLE_TABLES);
@@ -207,23 +208,180 @@ pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
Parser::new_ext(text, opts)
}
/// Renders markdown to HTML.
///
/// `path` should only be set if this is being generated for the consolidated
/// print page. It should point to the page being rendered relative to the
/// root of the book.
pub fn render_markdown_with_path(
text: &str,
smart_punctuation: bool,
path: Option<&Path>,
) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let p = new_cmark_parser(text, smart_punctuation);
let events = p
let mut body = String::with_capacity(text.len() * 3 / 2);
// Based on
// https://github.com/pulldown-cmark/pulldown-cmark/blob/master/pulldown-cmark/examples/footnote-rewrite.rs
// This handling of footnotes is a two-pass process. This is done to
// support linkbacks, little arrows that allow you to jump back to the
// footnote reference. The first pass collects the footnote definitions.
// The second pass modifies those definitions to include the linkbacks,
// and inserts the definitions back into the `events` list.
// This is a map of name -> (number, count)
// `name` is the name of the footnote.
// `number` is the footnote number displayed in the output.
// `count` is the number of references to this footnote (used for multiple
// linkbacks, and checking for unused footnotes).
let mut footnote_numbers = HashMap::new();
// This is a map of name -> Vec<Event>
// `name` is the name of the footnote.
// The events list is the list of events needed to build the footnote definition.
let mut footnote_defs = HashMap::new();
// The following are used when currently processing a footnote definition.
//
// This is the name of the footnote (escaped).
let mut in_footnote_name = String::new();
// This is the list of events to build the footnote definition.
let mut in_footnote = Vec::new();
let events = new_cmark_parser(text, smart_punctuation)
.map(clean_codeblock_headers)
.map(|event| adjust_links(event, path))
.flat_map(|event| {
let (a, b) = wrap_tables(event);
a.into_iter().chain(b)
})
// Footnote rewriting must go last to ensure inner definition contents
// are processed (since they get pulled out of the initial stream).
.filter_map(|event| {
match event {
Event::Start(Tag::FootnoteDefinition(name)) => {
if !in_footnote.is_empty() {
log::warn!("internal bug: nested footnote not expected in {path:?}");
}
in_footnote_name = special_escape(&name);
None
}
Event::End(TagEnd::FootnoteDefinition) => {
let def_events = std::mem::take(&mut in_footnote);
let name = std::mem::take(&mut in_footnote_name);
if footnote_defs.contains_key(&name) {
log::warn!(
"footnote `{name}` in {} defined multiple times - \
not updating to new definition",
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
);
} else {
footnote_defs.insert(name, def_events);
}
None
}
Event::FootnoteReference(name) => {
let name = special_escape(&name);
let len = footnote_numbers.len() + 1;
let (n, count) = footnote_numbers.entry(name.clone()).or_insert((len, 0));
*count += 1;
let html = Event::Html(
format!(
"<sup class=\"footnote-reference\" id=\"fr-{name}-{count}\">\
<a href=\"#footnote-{name}\">{n}</a>\
</sup>"
)
.into(),
);
if in_footnote_name.is_empty() {
Some(html)
} else {
// While inside a footnote, we need to accumulate.
in_footnote.push(html);
None
}
}
// While inside a footnote, accumulate all events into a local.
_ if !in_footnote_name.is_empty() => {
in_footnote.push(event);
None
}
_ => Some(event),
}
});
html::push_html(&mut s, events);
s
html::push_html(&mut body, events);
if !footnote_defs.is_empty() {
add_footnote_defs(
&mut body,
path,
footnote_defs.into_iter().collect(),
&footnote_numbers,
);
}
body
}
/// Adds all footnote definitions into `body`.
fn add_footnote_defs(
body: &mut String,
path: Option<&Path>,
mut defs: Vec<(String, Vec<Event<'_>>)>,
numbers: &HashMap<String, (usize, u32)>,
) {
// Remove unused.
defs.retain(|(name, _)| {
if !numbers.contains_key(name) {
log::warn!(
"footnote `{name}` in `{}` is defined but not referenced",
path.map_or_else(|| Cow::from("<unknown>"), |p| p.to_string_lossy())
);
false
} else {
true
}
});
defs.sort_by_cached_key(|(name, _)| numbers[name].0);
body.push_str(
"<hr>\n\
<ol class=\"footnote-definition\">",
);
// Insert the backrefs to the definition, and put the definitions in the output.
for (name, mut fn_events) in defs {
let count = numbers[&name].1;
fn_events.insert(
0,
Event::Html(format!("<li id=\"footnote-{name}\">").into()),
);
// Generate the linkbacks.
for usage in 1..=count {
let nth = if usage == 1 {
String::new()
} else {
usage.to_string()
};
let backlink =
Event::Html(format!(" <a href=\"#fr-{name}-{usage}\">↩{nth}</a>").into());
if matches!(fn_events.last(), Some(Event::End(TagEnd::Paragraph))) {
// Put the linkback at the end of the last paragraph instead
// of on a line by itself.
fn_events.insert(fn_events.len() - 1, backlink);
} else {
// Not a clear place to put it in this circumstance, so put it
// at the end.
fn_events.push(backlink);
}
}
fn_events.push(Event::Html("</li>\n".into()));
html::push_html(body, fn_events.into_iter());
}
body.push_str("</ol>");
}
/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
@@ -267,13 +425,14 @@ pub fn log_backtrace(e: &Error) {
pub(crate) fn special_escape(mut s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
let needs_escape: &[char] = &['<', '>', '\'', '\\', '&'];
let needs_escape: &[char] = &['<', '>', '\'', '"', '\\', '&'];
while let Some(next) = s.find(needs_escape) {
escaped.push_str(&s[..next]);
match s.as_bytes()[next] {
b'<' => escaped.push_str("&lt;"),
b'>' => escaped.push_str("&gt;"),
b'\'' => escaped.push_str("&#39;"),
b'"' => escaped.push_str("&quot;"),
b'\\' => escaped.push_str("&#92;"),
b'&' => escaped.push_str("&amp;"),
_ => unreachable!(),

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
use crate::cli::cmd::mdbook_cmd;
use crate::dummy_book::DummyBook;
use mdbook::config::Config;
/// Run `mdbook init` with `--force` to skip the confirmation prompts
#[test]
fn base_mdbook_init_can_skip_confirmation_prompts() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.args(["init", "--force"]).current_dir(temp.path());
cmd.assert()
.success()
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
assert_eq!(config.book.title, None);
assert!(!temp.path().join(".gitignore").exists());
}
/// Run `mdbook init` with `--title` without git config.
///
/// Regression test for https://github.com/rust-lang/mdBook/issues/2485
#[test]
fn no_git_config_with_title() {
let temp = DummyBook::new().build().unwrap();
// doesn't exist before
assert!(!temp.path().join("book").exists());
let mut cmd = mdbook_cmd();
cmd.args(["init", "--title", "Example title"])
.current_dir(temp.path())
.env("GIT_CONFIG_GLOBAL", "")
.env("GIT_CONFIG_NOSYSTEM", "1");
cmd.assert()
.success()
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
assert_eq!(config.book.title.as_deref(), Some("Example title"));
}

View File

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

View File

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

View File

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

View File

@@ -1,56 +0,0 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook::MDBook;
fn example() -> CmdPreprocessor {
CmdPreprocessor::new(
"nop-preprocessor".to_string(),
"cargo run --example nop-preprocessor --".to_string(),
)
}
#[test]
fn example_supports_whatever() {
let cmd = example();
let got = cmd.supports_renderer("whatever");
assert_eq!(got, true);
}
#[test]
fn example_doesnt_support_not_supported() {
let cmd = example();
let got = cmd.supports_renderer("not-supported");
assert_eq!(got, false);
}
#[test]
fn ask_the_preprocessor_to_blow_up() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.config
.set("preprocessor.nop-preprocessor.blow-up", true)
.unwrap();
let got = md.build();
assert!(got.is_err());
}
#[test]
fn process_the_dummy_book() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.build().unwrap();
}

View File

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

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
# Markdown tests
Tests for some markdown output.
## Tables
| foo | bar |
| --- | --- |
| baz | bim |
## Footnotes
Footnote example[^1], or with a word[^word].
[^1]: This is a footnote.
[^word]: A longer footnote.
With multiple lines.
Third line.
## Strikethrough
~~strikethrough example~~
## Tasklisks
- [X] Apples
- [X] Broccoli
- [ ] Carrots

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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