Repository: rust-lang/mdBook Branch: master Commit: 30e0e1d1021d Files: 601 Total size: 1.5 MB Directory structure: gitextract_yv8ug7wf/ ├── .cargo/ │ └── config.toml ├── .git-blame-ignore-revs ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── renovate.json5 │ └── workflows/ │ ├── deploy.yml │ ├── main.yml │ └── update-dependencies.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── ci/ │ ├── install-rust.sh │ ├── make-release-asset.sh │ ├── publish-guide.sh │ └── update-dependencies.sh ├── crates/ │ ├── mdbook-compare/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── mdbook-core/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ ├── book/ │ │ │ └── tests.rs │ │ ├── book.rs │ │ ├── config.rs │ │ ├── lib.rs │ │ └── utils/ │ │ ├── fs.rs │ │ ├── html.rs │ │ ├── mod.rs │ │ └── toml_ext.rs │ ├── mdbook-driver/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ ├── builtin_preprocessors/ │ │ │ ├── cmd.rs │ │ │ ├── index.rs │ │ │ ├── links/ │ │ │ │ └── take_lines.rs │ │ │ ├── links.rs │ │ │ └── mod.rs │ │ ├── builtin_renderers/ │ │ │ ├── markdown_renderer.rs │ │ │ └── mod.rs │ │ ├── init.rs │ │ ├── lib.rs │ │ ├── load.rs │ │ ├── mdbook/ │ │ │ └── tests.rs │ │ └── mdbook.rs │ ├── mdbook-html/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── front-end/ │ │ │ ├── css/ │ │ │ │ ├── ayu-highlight.css │ │ │ │ ├── chrome.css │ │ │ │ ├── general.css │ │ │ │ ├── highlight.css │ │ │ │ ├── print.css │ │ │ │ ├── tomorrow-night.css │ │ │ │ └── variables.css │ │ │ ├── fonts/ │ │ │ │ ├── OPEN-SANS-LICENSE.txt │ │ │ │ ├── SOURCE-CODE-PRO-LICENSE.txt │ │ │ │ └── fonts.css │ │ │ ├── js/ │ │ │ │ ├── book.js │ │ │ │ └── highlight.js │ │ │ ├── playground_editor/ │ │ │ │ ├── ace.js │ │ │ │ ├── editor.js │ │ │ │ ├── mode-rust.js │ │ │ │ ├── theme-dawn.js │ │ │ │ └── theme-tomorrow_night.js │ │ │ ├── searcher/ │ │ │ │ └── searcher.js │ │ │ └── templates/ │ │ │ ├── head.hbs │ │ │ ├── header.hbs │ │ │ ├── index.hbs │ │ │ ├── redirect.hbs │ │ │ ├── toc.html.hbs │ │ │ └── toc.js.hbs │ │ └── src/ │ │ ├── html/ │ │ │ ├── admonitions.rs │ │ │ ├── hide_lines.rs │ │ │ ├── mod.rs │ │ │ ├── print.rs │ │ │ ├── serialize.rs │ │ │ ├── tests.rs │ │ │ ├── tokenizer.rs │ │ │ └── tree.rs │ │ ├── html_handlebars/ │ │ │ ├── hbs_renderer.rs │ │ │ ├── helpers/ │ │ │ │ ├── fontawesome.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── resources.rs │ │ │ │ └── toc.rs │ │ │ ├── mod.rs │ │ │ ├── search.rs │ │ │ └── static_files.rs │ │ ├── lib.rs │ │ ├── theme/ │ │ │ ├── fonts.rs │ │ │ ├── mod.rs │ │ │ ├── playground_editor.rs │ │ │ └── searcher.rs │ │ └── utils.rs │ ├── mdbook-markdown/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── lib.rs │ ├── mdbook-preprocessor/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── lib.rs │ ├── mdbook-renderer/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── lib.rs │ ├── mdbook-summary/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── lib.rs │ └── xtask/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── changelog.rs │ └── main.rs ├── eslint.config.mjs ├── examples/ │ ├── nop-preprocessor.rs │ └── remove-emphasis/ │ ├── .gitignore │ ├── book.toml │ ├── mdbook-remove-emphasis/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── src/ │ │ ├── SUMMARY.md │ │ └── chapter_1.md │ └── test.rs ├── guide/ │ ├── book.toml │ ├── guide-helper/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── lib.rs │ │ └── main.rs │ └── src/ │ ├── 404.md │ ├── README.md │ ├── SUMMARY.md │ ├── cli/ │ │ ├── README.md │ │ ├── arg-watcher.md │ │ ├── build.md │ │ ├── clean.md │ │ ├── completions.md │ │ ├── init.md │ │ ├── serve.md │ │ ├── test.md │ │ └── watch.md │ ├── continuous-integration.md │ ├── for_developers/ │ │ ├── README.md │ │ ├── backends.md │ │ ├── mdbook-wordcount/ │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ └── main.rs │ │ └── preprocessors.md │ ├── format/ │ │ ├── README.md │ │ ├── configuration/ │ │ │ ├── README.md │ │ │ ├── environment-variables.md │ │ │ ├── general.md │ │ │ ├── preprocessors.md │ │ │ └── renderers.md │ │ ├── example.rs │ │ ├── markdown.md │ │ ├── mathjax.md │ │ ├── mdbook.md │ │ ├── summary.md │ │ └── theme/ │ │ ├── README.md │ │ ├── editor.md │ │ ├── index-hbs.md │ │ └── syntax-highlighting.md │ ├── guide/ │ │ ├── README.md │ │ ├── creating.md │ │ ├── installation.md │ │ └── reading.md │ └── misc/ │ └── contributors.md ├── rustfmt.toml ├── src/ │ ├── cmd/ │ │ ├── build.rs │ │ ├── clean.rs │ │ ├── command_prelude.rs │ │ ├── init.rs │ │ ├── mod.rs │ │ ├── serve.rs │ │ ├── test.rs │ │ ├── watch/ │ │ │ ├── native.rs │ │ │ └── poller.rs │ │ └── watch.rs │ └── main.rs ├── tests/ │ ├── gui/ │ │ ├── books/ │ │ │ ├── all-summary/ │ │ │ │ ├── README.md │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ ├── intro.md │ │ │ │ ├── part-1/ │ │ │ │ │ └── chapter-1.md │ │ │ │ ├── part-2/ │ │ │ │ │ └── chapter-1.md │ │ │ │ ├── prefix-1.md │ │ │ │ ├── prefix-2.md │ │ │ │ ├── suffix-1.md │ │ │ │ └── suffix-2.md │ │ │ ├── basic/ │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ └── chapter_1.md │ │ │ ├── heading-nav/ │ │ │ │ ├── README.md │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ ├── collapsed.md │ │ │ │ ├── current-to-bottom.md │ │ │ │ ├── empty.md │ │ │ │ ├── filtered-headings.md │ │ │ │ ├── large-intro.md │ │ │ │ ├── markup.md │ │ │ │ ├── normal-intro.md │ │ │ │ └── unusual-heading-levels.md │ │ │ ├── heading-nav-folded/ │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ ├── intro.md │ │ │ │ ├── next-main.md │ │ │ │ └── sub/ │ │ │ │ ├── index.md │ │ │ │ ├── inner/ │ │ │ │ │ └── index.md │ │ │ │ └── second.md │ │ │ ├── highlighting/ │ │ │ │ ├── README.md │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ └── languages.md │ │ │ ├── redirect/ │ │ │ │ ├── README.md │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ ├── chapter_1.md │ │ │ │ ├── new-chapter.md │ │ │ │ └── other-chapter.md │ │ │ ├── search/ │ │ │ │ ├── README.md │ │ │ │ ├── book.toml │ │ │ │ └── src/ │ │ │ │ ├── SUMMARY.md │ │ │ │ ├── chapter_1.md │ │ │ │ └── inner/ │ │ │ │ └── chapter_2.md │ │ │ └── sidebar-scroll/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── chapter_1.md │ │ │ ├── chapter_10.md │ │ │ ├── chapter_100.md │ │ │ ├── chapter_11.md │ │ │ ├── chapter_12.md │ │ │ ├── chapter_13.md │ │ │ ├── chapter_14.md │ │ │ ├── chapter_15.md │ │ │ ├── chapter_16.md │ │ │ ├── chapter_17.md │ │ │ ├── chapter_18.md │ │ │ ├── chapter_19.md │ │ │ ├── chapter_2.md │ │ │ ├── chapter_20.md │ │ │ ├── chapter_21.md │ │ │ ├── chapter_22.md │ │ │ ├── chapter_23.md │ │ │ ├── chapter_24.md │ │ │ ├── chapter_25.md │ │ │ ├── chapter_26.md │ │ │ ├── chapter_27.md │ │ │ ├── chapter_28.md │ │ │ ├── chapter_29.md │ │ │ ├── chapter_3.md │ │ │ ├── chapter_30.md │ │ │ ├── chapter_31.md │ │ │ ├── chapter_32.md │ │ │ ├── chapter_33.md │ │ │ ├── chapter_34.md │ │ │ ├── chapter_35.md │ │ │ ├── chapter_36.md │ │ │ ├── chapter_37.md │ │ │ ├── chapter_38.md │ │ │ ├── chapter_39.md │ │ │ ├── chapter_4.md │ │ │ ├── chapter_40.md │ │ │ ├── chapter_41.md │ │ │ ├── chapter_42.md │ │ │ ├── chapter_43.md │ │ │ ├── chapter_44.md │ │ │ ├── chapter_45.md │ │ │ ├── chapter_46.md │ │ │ ├── chapter_47.md │ │ │ ├── chapter_48.md │ │ │ ├── chapter_49.md │ │ │ ├── chapter_5.md │ │ │ ├── chapter_50.md │ │ │ ├── chapter_51.md │ │ │ ├── chapter_52.md │ │ │ ├── chapter_53.md │ │ │ ├── chapter_54.md │ │ │ ├── chapter_55.md │ │ │ ├── chapter_56.md │ │ │ ├── chapter_57.md │ │ │ ├── chapter_58.md │ │ │ ├── chapter_59.md │ │ │ ├── chapter_6.md │ │ │ ├── chapter_60.md │ │ │ ├── chapter_61.md │ │ │ ├── chapter_62.md │ │ │ ├── chapter_63.md │ │ │ ├── chapter_64.md │ │ │ ├── chapter_65.md │ │ │ ├── chapter_66.md │ │ │ ├── chapter_67.md │ │ │ ├── chapter_68.md │ │ │ ├── chapter_69.md │ │ │ ├── chapter_7.md │ │ │ ├── chapter_70.md │ │ │ ├── chapter_71.md │ │ │ ├── chapter_72.md │ │ │ ├── chapter_73.md │ │ │ ├── chapter_74.md │ │ │ ├── chapter_75.md │ │ │ ├── chapter_76.md │ │ │ ├── chapter_77.md │ │ │ ├── chapter_78.md │ │ │ ├── chapter_79.md │ │ │ ├── chapter_8.md │ │ │ ├── chapter_80.md │ │ │ ├── chapter_81.md │ │ │ ├── chapter_82.md │ │ │ ├── chapter_83.md │ │ │ ├── chapter_84.md │ │ │ ├── chapter_85.md │ │ │ ├── chapter_86.md │ │ │ ├── chapter_87.md │ │ │ ├── chapter_88.md │ │ │ ├── chapter_89.md │ │ │ ├── chapter_9.md │ │ │ ├── chapter_90.md │ │ │ ├── chapter_91.md │ │ │ ├── chapter_92.md │ │ │ ├── chapter_93.md │ │ │ ├── chapter_94.md │ │ │ ├── chapter_95.md │ │ │ ├── chapter_96.md │ │ │ ├── chapter_97.md │ │ │ ├── chapter_98.md │ │ │ └── chapter_99.md │ │ ├── heading-nav-collapsed.goml │ │ ├── heading-nav-current-to-bottom.goml │ │ ├── heading-nav-empty.goml │ │ ├── heading-nav-filter.goml │ │ ├── heading-nav-folded.goml │ │ ├── heading-nav-large-intro.goml │ │ ├── heading-nav-markup.goml │ │ ├── heading-nav-normal-intro.goml │ │ ├── heading-nav-unusual-levels.goml │ │ ├── help.goml │ │ ├── highlighting.goml │ │ ├── move-between-pages.goml │ │ ├── redirect.goml │ │ ├── runner.rs │ │ ├── search.goml │ │ ├── sidebar-active.goml │ │ ├── sidebar-nojs.goml │ │ ├── sidebar-scroll.goml │ │ ├── sidebar.goml │ │ └── theme.goml │ └── testsuite/ │ ├── README.md │ ├── book_test.rs │ ├── build/ │ │ ├── basic_build/ │ │ │ ├── README.md │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ ├── create_missing/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── missing_file/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ └── no_reserved_filename/ │ │ ├── book.toml │ │ └── src/ │ │ ├── SUMMARY.md │ │ └── print.md │ ├── build.rs │ ├── cli.rs │ ├── config/ │ │ └── empty/ │ │ ├── book.toml │ │ └── src/ │ │ ├── SUMMARY.md │ │ └── chapter_1.md │ ├── config.rs │ ├── includes/ │ │ └── all_includes/ │ │ ├── book.toml │ │ └── src/ │ │ ├── SUMMARY.md │ │ ├── anchors.md │ │ ├── example.rs │ │ ├── includes.md │ │ ├── nested-test-with-anchors.rs │ │ ├── partially-included-test-with-anchors.rs │ │ ├── partially-included-test.rs │ │ ├── playground.md │ │ ├── recursive.md │ │ ├── relative/ │ │ │ └── includes.md │ │ ├── rustdoc.md │ │ └── sample.md │ ├── includes.rs │ ├── index/ │ │ └── basic_readme/ │ │ ├── book.toml │ │ └── src/ │ │ ├── README.md │ │ ├── SUMMARY.md │ │ ├── first/ │ │ │ └── README │ │ └── second/ │ │ └── Readme.md │ ├── index.rs │ ├── init/ │ │ └── init_from_summary/ │ │ └── src/ │ │ └── SUMMARY.md │ ├── init.rs │ ├── main.rs │ ├── markdown/ │ │ ├── admonitions/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── admonitions.html │ │ │ ├── expected_disabled/ │ │ │ │ └── admonitions.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── admonitions.md │ │ ├── basic_markdown/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ ├── blank.html │ │ │ │ ├── blockquotes.html │ │ │ │ ├── code-blocks.html │ │ │ │ ├── html.html │ │ │ │ ├── images.html │ │ │ │ ├── inlines.html │ │ │ │ ├── links.html │ │ │ │ ├── lists.html │ │ │ │ └── svg.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── blank.md │ │ │ ├── blockquotes.md │ │ │ ├── code-blocks.md │ │ │ ├── html.md │ │ │ ├── images.md │ │ │ ├── inlines.md │ │ │ ├── links.md │ │ │ ├── lists.md │ │ │ └── svg.md │ │ ├── custom_header_attributes/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── custom_header_attributes.md │ │ ├── definition_lists/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ ├── definition_lists.html │ │ │ │ └── html_definition_lists.html │ │ │ ├── expected_disabled/ │ │ │ │ ├── definition_lists.html │ │ │ │ └── html_definition_lists.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── definition_lists.md │ │ │ └── html_definition_lists.md │ │ ├── footnotes/ │ │ │ ├── expected/ │ │ │ │ └── footnotes.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── footnotes.md │ │ ├── smart_punctuation/ │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── smart_punctuation.md │ │ ├── strikethrough/ │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── strikethrough.md │ │ ├── tables/ │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── tables.md │ │ └── tasklists/ │ │ └── src/ │ │ ├── SUMMARY.md │ │ └── tasklists.md │ ├── markdown.rs │ ├── playground/ │ │ ├── disabled_playground/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── disabled-rust-playground.md │ │ └── playground_on_rust_code/ │ │ ├── book.toml │ │ └── src/ │ │ ├── SUMMARY.md │ │ └── rust-playground.md │ ├── playground.rs │ ├── preprocessor/ │ │ ├── extension_compatibility/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── chapter_1.md │ │ │ ├── part/ │ │ │ │ ├── chapter.md │ │ │ │ └── sub-chapter.md │ │ │ ├── prefix.md │ │ │ └── suffix.md │ │ ├── failing_preprocessor/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── missing_optional_not_fatal/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── missing_preprocessor/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ └── nop_preprocessor/ │ │ ├── book.toml │ │ └── src/ │ │ └── SUMMARY.md │ ├── preprocessor.rs │ ├── print/ │ │ ├── chapter_no_h1/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── print.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── chapter_1.md │ │ │ ├── chapter_2.md │ │ │ └── h2-instead.md │ │ ├── duplicate_ids/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── print.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── chapter_1.md │ │ │ └── chapter_2.md │ │ ├── noindex/ │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── index.md │ │ └── relative_links/ │ │ ├── book.toml │ │ ├── expected/ │ │ │ └── print.html │ │ └── src/ │ │ ├── SUMMARY.md │ │ ├── first/ │ │ │ ├── index.md │ │ │ └── nested.md │ │ └── second/ │ │ └── nested.md │ ├── print.rs │ ├── redirects/ │ │ ├── redirect_existing_page/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ ├── redirect_removed_with_fragments_only/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ └── redirects_are_emitted_correctly/ │ │ ├── book.toml │ │ ├── expected/ │ │ │ ├── nested/ │ │ │ │ └── page.html │ │ │ └── overview.html │ │ └── src/ │ │ ├── SUMMARY.md │ │ ├── chapter_1.md │ │ └── chapter_2.md │ ├── redirects.rs │ ├── renderer/ │ │ ├── backends_receive_render_context_via_stdin/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ ├── missing_optional_not_fatal/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── missing_renderer/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ └── renderer_with_arguments/ │ │ ├── book.toml │ │ └── src/ │ │ └── SUMMARY.md │ ├── renderer.rs │ ├── rendering/ │ │ ├── code_blocks_fenced_with_indent/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── code-blocks-fenced-with-indent.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── code-blocks-fenced-with-indent.md │ │ ├── default_rust_edition/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── default-rust-edition.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── default-rust-edition.md │ │ ├── edit_url_template/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── edit_url_template_explicit_src/ │ │ │ ├── book.toml │ │ │ └── src2/ │ │ │ └── SUMMARY.md │ │ ├── editable_rust_block/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── editable-rust.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── editable-rust.md │ │ ├── first_chapter_is_copied_as_index_even_if_not_first_elem/ │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── fontawesome/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── fa.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── fa.md │ │ ├── fontawesome_error/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ ├── header_links/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── header_links.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── header_links.md │ │ ├── hidelines/ │ │ │ ├── book.toml │ │ │ ├── expected/ │ │ │ │ └── hide-lines.html │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── hide-lines.md │ │ └── html_blocks/ │ │ ├── book.toml │ │ ├── expected/ │ │ │ ├── comment-in-list.html │ │ │ └── script-in-block.html │ │ └── src/ │ │ ├── SUMMARY.md │ │ ├── comment-in-list.md │ │ └── script-in-block.md │ ├── rendering.rs │ ├── search/ │ │ ├── chapter_settings_validation_error/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ └── SUMMARY.md │ │ ├── disable_search_chapter/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── first/ │ │ │ │ ├── disable_me.md │ │ │ │ └── keep_me.md │ │ │ ├── second/ │ │ │ │ └── nested.md │ │ │ └── second.md │ │ └── reasonable_search_index/ │ │ ├── expected_index.js │ │ └── src/ │ │ ├── SUMMARY.md │ │ ├── first/ │ │ │ ├── duplicate-headers.md │ │ │ ├── heading-attributes.md │ │ │ ├── includes.md │ │ │ ├── index.md │ │ │ ├── no-headers.md │ │ │ └── unicode.md │ │ └── intro.md │ ├── search.rs │ ├── test/ │ │ ├── failing_tests/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── failing.md │ │ │ ├── failing_include.md │ │ │ └── test1.rs │ │ └── passing_tests/ │ │ ├── book.toml │ │ └── src/ │ │ ├── SUMMARY.md │ │ ├── passing1.md │ │ ├── passing2.md │ │ ├── test1.rs │ │ ├── test2.rs │ │ └── test3.rs │ ├── test.rs │ ├── theme/ │ │ ├── custom_fonts_css/ │ │ │ ├── book.toml │ │ │ ├── src/ │ │ │ │ └── SUMMARY.md │ │ │ └── theme/ │ │ │ └── fonts/ │ │ │ └── fonts.css │ │ ├── empty_fonts_css/ │ │ │ ├── book.toml │ │ │ ├── src/ │ │ │ │ └── SUMMARY.md │ │ │ └── theme/ │ │ │ └── fonts/ │ │ │ └── fonts.css │ │ ├── empty_theme/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ ├── fonts_css/ │ │ │ ├── src/ │ │ │ │ └── SUMMARY.md │ │ │ └── theme/ │ │ │ └── fonts/ │ │ │ └── fonts.css │ │ ├── missing_theme/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── SUMMARY.md │ │ │ └── chapter_1.md │ │ └── override_index/ │ │ ├── src/ │ │ │ └── SUMMARY.md │ │ └── theme/ │ │ └── index.hbs │ ├── theme.rs │ ├── toc/ │ │ ├── basic_toc/ │ │ │ ├── book.toml │ │ │ └── src/ │ │ │ ├── README.md │ │ │ ├── SUMMARY.md │ │ │ ├── deep/ │ │ │ │ ├── a/ │ │ │ │ │ ├── b/ │ │ │ │ │ │ └── index.md │ │ │ │ │ └── index.md │ │ │ │ └── index.md │ │ │ ├── nested/ │ │ │ │ ├── index.md │ │ │ │ └── two.md │ │ │ ├── prefix1.md │ │ │ ├── prefix2.md │ │ │ ├── suffix1.md │ │ │ └── suffix2.md │ │ └── summary_with_markdown_formatting/ │ │ └── src/ │ │ └── SUMMARY.md │ └── toc.rs └── triagebot.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [alias] xtask = "run --manifest-path=crates/xtask/Cargo.toml --" ================================================ FILE: .git-blame-ignore-revs ================================================ # Use `git config blame.ignorerevsfile .git-blame-ignore-revs` to make `git blame` ignore the following commits. # rustfmt ad0794a0bd692e4f2ff23b85e361889620e93f51 # rustfmt and use_try_shorthand 75bbd55128083897d40c3f5265cc5b1f10314ddb # rustfmt 382fc4139b96bde3c4b8875b499c720eabc89c6a # rustfmt 154e0fb3080c6ffc225b0d47b5d835e589789892 # rustfmt 5835da243244bfc5c95c6c6db96f453da4bb5740 # rustfmt fd9d27e082f5e9eea50e4fa9fa3a22060d02c66b # rustfmt 1d69ccae4854f13552d452d0bffef95cbff70364 # rustfmt 3688f73052454bf510a5acc85cf55aae450c6e46 # rustfmt 742dbbc91700dce1b7d910bca6b3e10a5ae46b86 # rustfmt 1.38 b88839cc25a6fd1c782101e94318959e8079bb20 # rustfmt 1.40 2f59943c04f0aa204a9238d6a699ba9cc06c88d9 # Rustfmt for 2024 c7b67e363bb9ce3383636ee615e8e761bf185b33 ================================================ FILE: .gitattributes ================================================ [attr]rust text eol=lf whitespace=tab-in-indent,trailing-space,tabwidth=4 * text=auto eol=lf *.rs rust *.woff binary *.ttf binary *.otf binary *.png binary *.eot binary *.woff2 binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug Report description: Create a report to help us improve labels: ["C-bug"] body: - type: markdown attributes: value: Thanks for filing a 🐛 bug report 😄! - type: textarea id: problem attributes: label: Problem description: > Please provide a clear and concise description of what the bug is, including what currently happens and what you expected to happen. validations: required: true - type: textarea id: steps attributes: label: Steps description: Please list the steps to reproduce the bug. placeholder: | 1. 2. 3. - type: textarea id: possible-solutions attributes: label: Possible Solution(s) description: > Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change. - type: textarea id: notes attributes: label: Notes description: Provide any additional notes that might be helpful. - type: textarea id: version attributes: label: Version description: > Please paste the output of running `mdbook --version` or which version of the library you are using. render: text ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Enhancement description: Suggest an idea for enhancing mdBook labels: ["C-enhancement"] body: - type: markdown attributes: value: | Thanks for filing a 🙋 feature request 😄! - type: textarea id: problem attributes: label: Problem description: > Please provide a clear description of your use case and the problem this feature request is trying to solve. validations: required: true - type: textarea id: solution attributes: label: Proposed Solution description: > Please provide a clear and concise description of what you want to happen. - type: textarea id: notes attributes: label: Notes description: Provide any additional context or information that might be helpful. ================================================ FILE: .github/ISSUE_TEMPLATE/question.yml ================================================ name: Question description: Have a question on how to use mdBook? labels: ["C-question"] body: - type: markdown attributes: value: | Got a question on how to do something with mdBook? - type: textarea id: question attributes: label: Question description: > Enter your question here. Please try to provide as much detail as possible. validations: required: true - type: textarea id: version attributes: label: Version description: > Please paste the output of running `mdbook --version` or which version of the library you are using. render: text ================================================ FILE: .github/renovate.json5 ================================================ { schedule: ['before 5am on the first day of the month'], // Raise from default of 2 to reduce trickle. prHourlyLimit: 6, dependencyDashboard: true, // Creates PRs if this renovate config file needs updating. configMigration: true, ignorePaths: [ 'guide/src/for_developers/mdbook-wordcount/', ], customManagers: [ // Custom manager to extract the version of cargo-semver-checks from the workflow. { customType: 'regex', managerFilePatterns: [ '/^.github.workflows.main.yml$/', ], matchStrings: [ 'cargo-semver-checks.releases.download.v(?\\d+\\.\\d+(\\.\\d+)?)', ], depNameTemplate: 'cargo-semver-checks', packageNameTemplate: 'obi1kenobi/cargo-semver-checks', datasourceTemplate: 'github-releases', }, ], packageRules: [ // The next two rules disable compatible dependency updates. I wasn't // able to get Renovate to be able to update Cargo.toml for compatible // updates only, update all transitive dependencies, and do that all // in a single PR. Instead, the `update-dependencies.sh` will handle // that. { matchManagers: ['cargo'], matchUpdateTypes: ['patch'], enabled: false, }, { matchManagers: ['cargo'], matchCurrentVersion: '>=1.0.0', matchUpdateTypes: ['minor'], enabled: false, }, // Allow minor updates for pre-1.0 dependencies (semver-breaking) { matchManagers: ['cargo'], matchCurrentVersion: '<1.0.0', matchUpdateTypes: ['minor'], }, // Allow major updates for stable dependencies (semver-breaking) { matchManagers: ['cargo'], matchCurrentVersion: '>=1.0.0', matchUpdateTypes: ['major'], }, // Update cargo-semver-checks when a new version is available. { commitMessageTopic: 'cargo-semver-checks', matchManagers: [ 'custom.regex', ], matchDepNames: [ 'cargo-semver-checks', ], extractVersion: '^v(?\\d+\\.\\d+\\.\\d+)', schedule: [ '* * * * *', ], internalChecksFilter: 'strict', }, ] } ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy on: release: types: [created] defaults: run: shell: bash permissions: contents: write jobs: release: runs-on: ${{ matrix.os }} strategy: matrix: include: - target: aarch64-unknown-linux-musl os: ubuntu-22.04 - target: x86_64-unknown-linux-gnu os: ubuntu-22.04 - target: x86_64-unknown-linux-musl os: ubuntu-22.04 - target: x86_64-apple-darwin os: macos-latest - target: aarch64-apple-darwin os: macos-latest - target: x86_64-pc-windows-msvc os: windows-latest name: Deploy ${{ matrix.target }} steps: - uses: actions/checkout@v5 - name: Install Rust run: ci/install-rust.sh stable ${{ matrix.target }} - name: Build asset run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }} - name: Update release with new asset env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET pages: name: GitHub Pages runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install Rust (rustup) run: rustup update stable --no-self-update && rustup default stable - name: Deploy the User Guide to GitHub Pages using the gh-pages branch run: ci/publish-guide.sh publish: name: Publish to crates.io runs-on: ubuntu-latest permissions: # Required for OIDC token exchange id-token: write environment: publish steps: - uses: actions/checkout@v5 - name: Install Rust (rustup) run: rustup update stable --no-self-update && rustup default stable - name: Authenticate with crates.io id: auth uses: rust-lang/crates-io-auth-action@v1 - name: Publish env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} run: cargo publish --workspace --no-verify ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: pull_request: merge_group: jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: include: - name: stable linux os: ubuntu-latest rust: stable target: x86_64-unknown-linux-gnu - name: beta linux os: ubuntu-latest rust: beta target: x86_64-unknown-linux-gnu - name: nightly linux os: ubuntu-latest rust: nightly target: x86_64-unknown-linux-gnu - name: stable x86_64-unknown-linux-musl os: ubuntu-22.04 rust: stable target: x86_64-unknown-linux-musl - name: stable x86_64 macos os: macos-latest rust: stable target: x86_64-apple-darwin - name: stable aarch64 macos os: macos-latest rust: stable target: aarch64-apple-darwin - name: stable windows-msvc os: windows-latest rust: stable target: x86_64-pc-windows-msvc - name: msrv os: ubuntu-22.04 # sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml rust: 1.88.0 target: x86_64-unknown-linux-gnu name: ${{ matrix.name }} steps: - uses: actions/checkout@v5 - name: Install Rust run: bash ci/install-rust.sh ${{ matrix.rust }} ${{ matrix.target }} - name: Build and run tests run: cargo test --workspace --locked --target ${{ matrix.target }} - name: Test no default run: cargo test --workspace --no-default-features --target ${{ matrix.target }} aarch64-cross-builds: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v5 - name: Install Rust run: bash ci/install-rust.sh stable aarch64-unknown-linux-musl - name: Build run: cargo build --locked --target aarch64-unknown-linux-musl rustfmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install Rust run: rustup update stable && rustup default stable && rustup component add rustfmt - run: cargo fmt --check gui: name: GUI tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install Rust run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu - name: Install npm uses: actions/setup-node@v6 with: node-version: 24 - name: Install browser-ui-test run: npm install - name: Run eslint run: npm run lint - name: Build and run tests (+ GUI) run: cargo test --locked --target x86_64-unknown-linux-gnu --test gui # Ensure there are no clippy warnings clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install Rust run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu - run: rustup component add clippy - run: cargo clippy --workspace --all-targets --no-deps -- -D warnings docs: name: Check API docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Install Rust run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu - name: Ensure intradoc links are valid run: cargo doc --workspace --document-private-items --no-deps env: RUSTDOCFLAGS: -D warnings check-version-bump: name: Check version bump runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: rustup update stable && rustup default stable - name: Install cargo-semver-checks run: | mkdir installed-bins curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.47.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \ | tar -xz --directory=./installed-bins echo `pwd`/installed-bins >> $GITHUB_PATH - run: cargo semver-checks --workspace # The success job is here to consolidate the total success/failure state of # all other jobs. This job is then included in the GitHub branch protection # rule which prevents merges unless all other jobs are passing. This makes # it easier to manage the list of jobs via this yml file and to prevent # accidentally adding new jobs without also updating the branch protections. success: name: Success gate if: always() needs: - test - rustfmt - aarch64-cross-builds - gui - clippy - docs - check-version-bump runs-on: ubuntu-latest steps: - run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' - name: Done run: exit 0 ================================================ FILE: .github/workflows/update-dependencies.yml ================================================ name: Update dependencies on: schedule: - cron: '0 0 1 * *' workflow_dispatch: jobs: update: name: Update dependencies runs-on: ubuntu-latest if: github.repository == 'rust-lang/mdBook' steps: - uses: actions/checkout@v5 - name: Install Rust run: bash ci/install-rust.sh stable x86_64-unknown-linux-gnu - name: Install cargo-edit run: cargo install cargo-edit --locked - name: Update dependencies run: ci/update-dependencies.sh env: GH_TOKEN: ${{ github.token }} ================================================ FILE: .gitignore ================================================ target # MacOS temp file .DS_Store book-test guide/book .vscode tests/dummy_book/book/ tests/gui/books/*/book/ tests/testsuite/*/*/book/ # Ignore Jetbrains specific files. .idea/ # Ignore Vim temporary and swap files. *.sw? *~ # GUI tests node_modules package-lock.json package.json ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## mdBook 0.5.2 [v0.5.1...v0.5.2](https://github.com/rust-lang/mdBook/compare/v0.5.1...v0.5.2) ### Changed - Updated Rust crate html5ever to 0.36.0. [#2970](https://github.com/rust-lang/mdBook/pull/2970) - Updated cargo dependencies. [#2969](https://github.com/rust-lang/mdBook/pull/2969) ### Fixed - Fixed repeated error message when HTML config is invalid in `mdbook serve`. [#2983](https://github.com/rust-lang/mdBook/pull/2983) - Fixed sidebar scroll position when heading nav is involved. [#2982](https://github.com/rust-lang/mdBook/pull/2982) - Fixed color for rustdoc error messages. [#2981](https://github.com/rust-lang/mdBook/pull/2981) - Fixed usage of custom preprocessors with `MDBook::test`. [#2980](https://github.com/rust-lang/mdBook/pull/2980) ## mdBook 0.5.1 [v0.5.0...v0.5.1](https://github.com/rust-lang/mdBook/compare/v0.5.0...v0.5.1) ### Changed - Changed the scrollbar background to be transparent. [#2932](https://github.com/rust-lang/mdBook/pull/2932) - Ignore invalid top-level environment variable config keys. This allows setting things like `MDBOOK_VERSION` to not cause an error. [#2952](https://github.com/rust-lang/mdBook/pull/2952) ### Fixed - Fixed the sidebar heading nav to have the correct nesting levels. [#2953](https://github.com/rust-lang/mdBook/pull/2953) - Various Font Awesome fixes and improvements. [#2951](https://github.com/rust-lang/mdBook/pull/2951) ## mdBook 0.5.0 [v0.4.52...v0.5.0](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0) The 0.5.0 release is the next major release of mdBook, containing over 130 PRs since 0.4.52! The primary focus for this release has been an evolution of the Rust APIs to make it easier to maintain, to evolve in a backwards-compatible fashion, to clean up some things that have accumulated over time, and to significantly improve the performance and compile-times. This release also includes many new features described below. We have prepared a [0.5 Migration Guide](#05-migration-guide) to help existing authors switch from 0.4. The final 0.5.0 release only contains the following changes since [0.5.0-beta.2](#mdbook-050-beta2): - Added error handling to environment config handling. This checks that environment variables starting with `MDBOOK_` are correctly specified instead of silently ignoring. This also fixed being able to replace entire top-level tables like `MDBOOK_OUTPUT`. [#2942](https://github.com/rust-lang/mdBook/pull/2942) ## 0.5 Migration Guide The 0.5 release contains several breaking changes from the 0.4 release. Preprocessors and renderers will need to be migrated to continue to work with this release. After updating your configuration, it is recommended to carefully compare and review how your book renders to ensure everything is working correctly. If you have overridden any of the theme files, you will likely need to update them to match the current version. See the entries below for [mdBook 0.5.0-alpha.1](#mdbook-050-alpha1), [mdBook 0.5.0-beta.1](#mdbook-050-beta1), and [mdBook 0.5.0-beta.2](#mdbook-050-beta2) for a more complete list of changes and fixes. The following is a summary of the changes that may require your attention when updating to 0.5: ### Major additions - Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it. [#2822](https://github.com/rust-lang/mdBook/pull/2822) - Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#definition-lists) for more. [#2847](https://github.com/rust-lang/mdBook/pull/2847) - Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it. See [docs](https://rust-lang.github.io/mdBook/format/markdown.html#admonitions) for more. [#2851](https://github.com/rust-lang/mdBook/pull/2851) - Links on the print page now link to elements on the print page instead of linking out to the individual chapters. [#2844](https://github.com/rust-lang/mdBook/pull/2844) ### Config changes - Unknown fields in config are now an error. [#2787](https://github.com/rust-lang/mdBook/pull/2787) [#2801](https://github.com/rust-lang/mdBook/pull/2801) - Removed `curly-quotes`, use `output.html.smart-punctuation` instead. [#2788](https://github.com/rust-lang/mdBook/pull/2788) - Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file. [#2790](https://github.com/rust-lang/mdBook/pull/2790) - If the `command` path for a renderer or preprocessor is relative, it is now always relative to the book root. [#2792](https://github.com/rust-lang/mdBook/pull/2792) [#2796](https://github.com/rust-lang/mdBook/pull/2796) - Added the `optional` field for preprocessors. The default is `false`, so this also means it is an error by default if the preprocessor is missing. [#2797](https://github.com/rust-lang/mdBook/pull/2797) - `output.html.smart-punctuation` is now `true` by default. [#2810](https://github.com/rust-lang/mdBook/pull/2810) - `output.html.hash-files` is now `true` by default. [#2820](https://github.com/rust-lang/mdBook/pull/2820) - Removed support for google-analytics. Use a theme extension (like `head.hbs`) if you need to continue to support this. [#2776](https://github.com/rust-lang/mdBook/pull/2776) - Removed the `book.multilingual` field. This was never used. [#2775](https://github.com/rust-lang/mdBook/pull/2775) - Removed the very old legacy config support. Warnings have been displayed in previous versions on how to migrate. [#2783](https://github.com/rust-lang/mdBook/pull/2783) - Top-level config values set from the environment like `MDBOOK_BOOK` now *replace* the contents of the top-level table instead of merging into it. [#2942](https://github.com/rust-lang/mdBook/pull/2942) - Invalid environment variables are now rejected. Previously unknown keys like `MDBOOK_FOO` would be ignored, or keys or invalid values inside objects like the `[book]` table would be ignored. [#2942](https://github.com/rust-lang/mdBook/pull/2942) ### Theme changes - Replaced the `{{#previous}}` and `{{#next}}` handlebars helpers with simple objects that contain the previous and next values. [#2794](https://github.com/rust-lang/mdBook/pull/2794) - Removed the `{{theme_option}}` handlebars helper. It has not been used for a while. [#2795](https://github.com/rust-lang/mdBook/pull/2795) ### Rendering changes - Updated to a newer version of `pulldown-cmark`. This brings a large number of fixes to markdown processing. [#2401](https://github.com/rust-lang/mdBook/pull/2401) - The font-awesome font is no longer loaded as a font. Instead, the corresponding SVG is embedded in the output for the corresponding `` tags. Additionally, a handlebars helper has been added for the `hbs` files. This also updates the version from 4.7.0 to 6.2.0, which means some of the icon names and styles have changed. Most of the free icons are in the "solid" set. See the [free icon set](https://fontawesome.com/v6/search) for the available icons. [#1330](https://github.com/rust-lang/mdBook/pull/1330) - Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs. [#2808](https://github.com/rust-lang/mdBook/pull/2808) - There is a new internal HTML rendering pipeline. This is primarily intended to give mdBook more flexibility in generating its HTML output. This resulted in some small changes to the HTML structure. HTML parsing may now be more strict than before. [#2844](https://github.com/rust-lang/mdBook/pull/2844) - Links on the print page now link to elements on the print page instead of linking out to the individual chapters. [#2844](https://github.com/rust-lang/mdBook/pull/2844) - Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it. [#2847](https://github.com/rust-lang/mdBook/pull/2847) - Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it. [#2851](https://github.com/rust-lang/mdBook/pull/2851) - Header ID generation has some minor changes to bring the ID generation closer to other tools and sites: - IDs now use Unicode lowercase instead of ASCII lowercase. [#2922](https://github.com/rust-lang/mdBook/pull/2922) - Headers that start or end with HTML characters like `<`, `&`, or `>` now replace those characters in the link ID with `-` instead of being stripped. [#2844](https://github.com/rust-lang/mdBook/pull/2844) - Headers are no longer modified if the tag is manually written HTML. [#2913](https://github.com/rust-lang/mdBook/pull/2913) ### CLI changes - Removed the `--dest-dir` option to `mdbook test`. It was unused since `mdbook test` does not generate output. [#2805](https://github.com/rust-lang/mdBook/pull/2805) - Changed CLI `--dest-dir` to be relative to the current directory, not the book root. [#2806](https://github.com/rust-lang/mdBook/pull/2806) ### Rust API - The Rust API has been split into several crates ([#2766](https://github.com/rust-lang/mdBook/pull/2766)). In summary, the different crates are: - `mdbook` — The CLI binary. - [`mdbook-driver`](https://docs.rs/mdbook-driver/latest/mdbook_driver/) — The high-level library for running mdBook, primarily through the `MDBook` type. If you are driving mdBook programmatically, this is the crate you want. - [`mdbook-preprocessor`](https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/) — Support for implementing preprocessors. If you have a preprocessor, then this is the crate you should depend on. - [`mdbook-renderer`](https://docs.rs/mdbook-renderer/latest/mdbook_renderer/) — Support for implementing renderers. If you have a custom renderer, this is the crate you should depend on. - [`mdbook-markdown`](https://docs.rs/mdbook-markdown/latest/mdbook_markdown/) — The Markdown renderer. If you are processing markdown, this is the crate you should depend on. This is essentially a thin wrapper around `pulldown-cmark`, and re-exports that crate so that you can ensure the version stays in sync with mdBook. - [`mdbook-summary`](https://docs.rs/mdbook-summary/latest/mdbook_summary/) — The `SUMMARY.md` parser. - [`mdbook-html`](https://docs.rs/mdbook-html/latest/mdbook_html/) — The HTML renderer. - [`mdbook-core`](https://docs.rs/mdbook-core/latest/mdbook_core/) — An internal library that is used by the other crates for shared types. You should not depend on this crate directly since types from this crate are re-exported from the other crates as appropriate. - Changes to `Config`: - [`Config::get`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.get) is now generic over the return value, using `serde` to deserialize the value. It also returns a `Result` to handle deserialization errors. [#2773](https://github.com/rust-lang/mdBook/pull/2773) - [`Config::set`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.set) now validates that the config keys and values are valid. [#2942](https://github.com/rust-lang/mdBook/pull/2942) - [`Config::update_from_env`](https://docs.rs/mdbook-core/latest/mdbook_core/config/struct.Config.html#method.update_from_env) now returns a `Result` to indicate any errors. [#2942](https://github.com/rust-lang/mdBook/pull/2942) - Removed `Config::get_deserialized`. Use `Config::get` instead. - Removed `Config::get_deserialized_opt`. Use `Config::get` instead. - Removed `Config::get_mut`. Use `Config::set` instead. - Removed deprecated `Config::get_deserialized_opt`. Use `Config::get` instead. - Removed `Config::get_renderer`. Use `Config::get` instead. - Removed `Config::get_preprocessor`. Use `Config::get` instead. - Public types have been switch to use the `#[non_exhaustive]` attribute to help allow them to change in a backwards-compatible way. [#2779](https://github.com/rust-lang/mdBook/pull/2779) [#2823](https://github.com/rust-lang/mdBook/pull/2823) - Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded. This allows the caller to replace an entry. [#2802](https://github.com/rust-lang/mdBook/pull/2802) - Added `MarkdownOptions` struct to specify settings for markdown rendering for `mdbook_markdown::new_cmark_parser`. [#2809](https://github.cocm/rust-lang/mdBook/pull/2809) - Renamed `Book::sections` to `Book::items`. [#2813](https://github.com/rust-lang/mdBook/pull/2813) - `mdbook::book::load_book` is now private. Instead, use one of the `MDBook` load functions like `MDBook::load_with_config`. - Removed `HtmlConfig::smart_punctuation` method, use the field of the same name. - `CmdPreprocessor::parse_input` moved to `mdbook_preprocessor::parse_input`. - `Preprocessor::supports_renderer` now returns a `Result` instead of `bool` to be able to handle errors. - Most of the types from the `theme` module are now private. The `Theme` struct is still exposed for working with themes. - Various functions in the `utils::fs` module have been removed, renamed, or reworked. - Most of the functions in the `utils` module have been moved, removed, or made private. ## mdBook 0.5.0-beta.2 [v0.5.0-beta.1...v0.5.0-beta.2](https://github.com/rust-lang/mdBook/compare/v0.5.0-beta.1...v0.5.0-beta.2) ### Added - Added a warning when a Font Awesome icon is missing. [#2915](https://github.com/rust-lang/mdBook/pull/2915) - Added some trace logging for event processing. [#2911](https://github.com/rust-lang/mdBook/pull/2911) - Added `Config::contains_key`. [#2910](https://github.com/rust-lang/mdBook/pull/2910) ### Changed - Heading IDs are now lowercase. [#2922](https://github.com/rust-lang/mdBook/pull/2922) - Updated cargo dependencies. [#2916](https://github.com/rust-lang/mdBook/pull/2916) - Removed italics for in quotes/comments in code blocks with the `ayu` theme. [#2904](https://github.com/rust-lang/mdBook/pull/2904) - Exposed "search" feature from mdbook-driver. [#2907](https://github.com/rust-lang/mdBook/pull/2907) ### Fixed - Fixed rust fenced code blocks with an indent. [#2905](https://github.com/rust-lang/mdBook/pull/2905) - Headers and `dt` tags are no longer modified if the tag is manually written HTML. [#2913](https://github.com/rust-lang/mdBook/pull/2913) - Fixed print page links for internal links to non-chapters. [#2914](https://github.com/rust-lang/mdBook/pull/2914) - Better handling for unbalanced HTML tags. [#2924](https://github.com/rust-lang/mdBook/pull/2924) - Handle unclosed HTML tags inside a markdown element. [#2927](https://github.com/rust-lang/mdBook/pull/2927) - Fixed missing font-awesome icons in the guide. [#2926](https://github.com/rust-lang/mdBook/pull/2926) - Hide the sidebar resize indicator when JS isn't available. [#2923](https://github.com/rust-lang/mdBook/pull/2923) ## mdBook 0.5.0-beta.1 [v0.5.0-alpha.1...v0.5.0-beta.1](https://github.com/rust-lang/mdBook/compare/v0.5.0-alpha.1...v0.5.0-beta.1) ### Changed - Reworked the look of the header navigation. [#2898](https://github.com/rust-lang/mdBook/pull/2898) - Update cargo dependencies. [#2896](https://github.com/rust-lang/mdBook/pull/2896) - Improved the heading nav debug. [#2892](https://github.com/rust-lang/mdBook/pull/2892) ### Fixed - Fixed error message for config.get deserialization error. [#2902](https://github.com/rust-lang/mdBook/pull/2902) - Filter `` tags from sidebar heading nav. [#2899](https://github.com/rust-lang/mdBook/pull/2899) - Avoid divide-by-zero in heading nav computation [#2891](https://github.com/rust-lang/mdBook/pull/2891) - Fixed heading nav with folded chapters. [#2893](https://github.com/rust-lang/mdBook/pull/2893) ## mdBook 0.5.0-alpha.1 [v0.4.52...v0.5.0-alpha.1](https://github.com/rust-lang/mdBook/compare/v0.4.52...v0.5.0-alpha.1) ### Added - The location of the generated HTML book is now displayed on the console. [#2729](https://github.com/rust-lang/mdBook/pull/2729) - ❗ Added the `optional` field for preprocessors. The default is `false`, so this also changes it so that it is an error if the preprocessor is missing. [#2797](https://github.com/rust-lang/mdBook/pull/2797) - ❗ Added `MarkdownOptions` struct to specify settings for markdown rendering. [#2809](https://github.cocm/rust-lang/mdBook/pull/2809) - Added sidebar heading navigation. This includes the `output.html.sidebar-header-nav` option to disable it. [#2822](https://github.com/rust-lang/mdBook/pull/2822) - Added the mdbook version to the guide. [#2826](https://github.com/rust-lang/mdBook/pull/2826) - Added `Book::chapters` and `Book::for_each_chapter_mut` to more conveniently iterate over chapters (instead of all items). [#2838](https://github.com/rust-lang/mdBook/pull/2838) - ❗ Added support for definition lists. These are enabled by default, with the option `output.html.definition-lists` to disable it. [#2847](https://github.com/rust-lang/mdBook/pull/2847) - ❗ Added support for admonitions. These are enabled by default, with the option `output.html.admonitions` to disable it. [#2851](https://github.com/rust-lang/mdBook/pull/2851) ### Changed - ❗ The `mdbook` crate has been split into multiple crates. [#2766](https://github.com/rust-lang/mdBook/pull/2766) - The minimum Rust version has been updated to 1.88. [#2844](https://github.com/rust-lang/mdBook/pull/2844) - ❗ `pulldown-cmark` has been upgraded to 0.13.0, bringing a large number of fixes to markdown processing. [#2401](https://github.com/rust-lang/mdBook/pull/2401) - ❗ Switched public types to `non_exhaustive` to help allow them to change in a backwards-compatible way. [#2779](https://github.com/rust-lang/mdBook/pull/2779) [#2823](https://github.com/rust-lang/mdBook/pull/2823) - ❗ Unknown fields in config are now an error. [#2787](https://github.com/rust-lang/mdBook/pull/2787) [#2801](https://github.com/rust-lang/mdBook/pull/2801) - ❗ Changed `id_from_content` to be private. [#2791](https://github.com/rust-lang/mdBook/pull/2791) - ❗ Changed preprocessor `command` to use paths relative to the book root. [#2796](https://github.com/rust-lang/mdBook/pull/2796) - ❗ Replaced the `{{#previous}}` and `{{#next}}` handelbars navigation helpers with objects. [#2794](https://github.com/rust-lang/mdBook/pull/2794) - ❗ Use embedded SVG instead of fonts for icons, font-awesome 6.2. [#1330](https://github.com/rust-lang/mdBook/pull/1330) - The `book.src` field is no longer serialized if it is the default of "src". [#2800](https://github.com/rust-lang/mdBook/pull/2800) - ❗ Changed `MDBook` `with_renderer`/`with_preprocessor` to overwrite the entry if an extension of the same name is already loaded. [#2802](https://github.com/rust-lang/mdBook/pull/2802) - ❗ Changed CLI `--dest-dir` to be relative to the current directory, not the book root. [#2806](https://github.com/rust-lang/mdBook/pull/2806) - ❗ Changed all internal HTML IDs to have an `mdbook-` prefix. This helps avoid namespace conflicts with header IDs. [#2808](https://github.com/rust-lang/mdBook/pull/2808) - ❗ `output.html.smart-punctuation` is now `true` by default. [#2810](https://github.com/rust-lang/mdBook/pull/2810) - ❗ Renamed `Book::sections` to `Book::items`. [#2813](https://github.com/rust-lang/mdBook/pull/2813) - ❗ `output.html.hash-files` is now `true` by default. [#2820](https://github.com/rust-lang/mdBook/pull/2820) - Switched from `log` to `tracing`. [#2829](https://github.com/rust-lang/mdBook/pull/2829) - ❗ Rewrote the HTML rendering pipeline. [#2844](https://github.com/rust-lang/mdBook/pull/2844) - ❗ Links on the print page now link to elements on the print page instead of linking out to the individual chapters. [#2844](https://github.com/rust-lang/mdBook/pull/2844) - ❗ Moved theme copy to the Theme type and reduced visibility. [#2857](https://github.com/rust-lang/mdBook/pull/2857) - ❗ Cleaned up some fs-related utilities. [#2856](https://github.com/rust-lang/mdBook/pull/2856) - ❗ Moved `get_404_output_file` to `HtmlConfig`. [#2855](https://github.com/rust-lang/mdBook/pull/2855) - ❗ Moved `take_lines` functions to `mdbook-driver` and made private. [#2854](https://github.com/rust-lang/mdBook/pull/2854) - Updated dependencies. [#2793](https://github.com/rust-lang/mdBook/pull/2793) [#2869](https://github.com/rust-lang/mdBook/pull/2869) ### Removed - ❗ Removed `toml` as a public dependency. [#2773](https://github.com/rust-lang/mdBook/pull/2773) - ❗ Removed the `book.multilingual` field. This was never used. [#2775](https://github.com/rust-lang/mdBook/pull/2775) - ❗ Removed support for google-analytics. [#2776](https://github.com/rust-lang/mdBook/pull/2776) - ❗ Removed the very old legacy config support. [#2783](https://github.com/rust-lang/mdBook/pull/2783) - ❗ Removed `curly-quotes`, use `output.html.smart-punctuation` instead. [#2788](https://github.com/rust-lang/mdBook/pull/2788) - Removed old warning about `book.json`. [#2789](https://github.com/rust-lang/mdBook/pull/2789) - ❗ Removed `output.html.copy-fonts`. The default fonts are now always copied unless you override the `theme/fonts/fonts.css` file. [#2790](https://github.com/rust-lang/mdBook/pull/2790) - ❗ Removed legacy relative renderer command paths. Relative renderer command paths now must always be relative to the book root. [#2792](https://github.com/rust-lang/mdBook/pull/2792) - ❗ Removed the `{{theme_option}}` handlebars helper. It has not been used for a while. [#2795](https://github.com/rust-lang/mdBook/pull/2795) - ❗ Removed the `--dest-dir` option to `mdbook test`. [#2805](https://github.com/rust-lang/mdBook/pull/2805) ### Fixed - Fixed handling of multiple footnotes in a row. [#2807](https://github.com/rust-lang/mdBook/pull/2807) - Fixed ID collisions when the numeric suffix gets used. [#2846](https://github.com/rust-lang/mdBook/pull/2846) - Fixed missing css vars for no-js dark mode. [#2850](https://github.com/rust-lang/mdBook/pull/2850) ## mdBook 0.4.52 [v0.4.51...v0.4.52](https://github.com/rust-lang/mdBook/compare/v0.4.51...v0.4.52) **Note:** If you have a custom `index.hbs` theme file, it is recommended that you update it to the latest version to pick up the fixes in this release. ### Added - Added the ability to redirect `#` HTML fragments using the existing `output.html.redirect` table. [#2747](https://github.com/rust-lang/mdBook/pull/2747) - Added the `rel="edit"` attribute to the edit page button. [#2702](https://github.com/rust-lang/mdBook/pull/2702) ### Changed - The search index is now only loaded when the search input is opened instead of always being loaded. [#2553](https://github.com/rust-lang/mdBook/pull/2553) [#2735](https://github.com/rust-lang/mdBook/pull/2735) - The `mdbook serve` command has switched its underlying server library from warp to axum. [#2748](https://github.com/rust-lang/mdBook/pull/2748) - Updated dependencies. [#2752](https://github.com/rust-lang/mdBook/pull/2752) ### Fixed - The sidebar is now set to `display:none` when it is hidden in order to prevent the browser's search from thinking the sidebar's text is visible. [#2725](https://github.com/rust-lang/mdBook/pull/2725) - Fixed search index URL not updating correctly when `hash-files` is enabled. [#2742](https://github.com/rust-lang/mdBook/pull/2742) [#2746](https://github.com/rust-lang/mdBook/pull/2746) - Fixed several sidebar animation bugs, particularly when manually resizing. [#2750](https://github.com/rust-lang/mdBook/pull/2750) ## mdBook 0.4.51 [v0.4.50...v0.4.51](https://github.com/rust-lang/mdBook/compare/v0.4.50...v0.4.51) ### Fixed - Fixed regression that broke the `S` search hotkey. [#2713](https://github.com/rust-lang/mdBook/pull/2713) ## mdBook 0.4.50 [v0.4.49...v0.4.50](https://github.com/rust-lang/mdBook/compare/v0.4.49...v0.4.50) ### Added - Added a keyboard shortcut help popup when pressing `?`. [#2608](https://github.com/rust-lang/mdBook/pull/2608) ### Changed - Changed the look of the sidebar resize handle to match the new rustdoc format. [#2691](https://github.com/rust-lang/mdBook/pull/2691) - `/` can now be used to open the search bar. [#2698](https://github.com/rust-lang/mdBook/pull/2698) - Pressing enter from the search bar will navigate to the first entry. [#2698](https://github.com/rust-lang/mdBook/pull/2698) - Updated `opener` to drop some dependencies. [#2709](https://github.com/rust-lang/mdBook/pull/2709) - Updated dependencies, MSRV raised to 1.82. [#2711](https://github.com/rust-lang/mdBook/pull/2711) ### Fixed - Fixed uncaught exception when pressing down when there are no search results. [#2698](https://github.com/rust-lang/mdBook/pull/2698) - Fixed syntax highlighting of Rust code in the ACE editor. [#2710](https://github.com/rust-lang/mdBook/pull/2710) ## mdBook 0.4.49 [v0.4.48...v0.4.49](https://github.com/rust-lang/mdBook/compare/v0.4.48...v0.4.49) ### Added - Added a warning on unused fields in the root of `book.toml`. [#2622](https://github.com/rust-lang/mdBook/pull/2622) ### Changed - Updated dependencies. [#2650](https://github.com/rust-lang/mdBook/pull/2650) [#2688](https://github.com/rust-lang/mdBook/pull/2688) - Updated minimum Rust version to 1.81. [#2688](https://github.com/rust-lang/mdBook/pull/2688) - The unused `book.multilingual` field is no longer serialized, or shown in `mdbook init`. [#2689](https://github.com/rust-lang/mdBook/pull/2689) - Speed up search index loading by using `JSON.parse` instead of parsing JavaScript. [#2633](https://github.com/rust-lang/mdBook/pull/2633) ### Fixed - Search highlighting will not try to highlight in SVG `` elements because it breaks the element. [#2668](https://github.com/rust-lang/mdBook/pull/2668) - Fixed scrolling of the sidebar when a search highlight term is in the URL. [#2675](https://github.com/rust-lang/mdBook/pull/2675) - Fixed issues when multiple footnote definitions use the same ID. Now, only one definition is used, and a warning is displayed. [#2681](https://github.com/rust-lang/mdBook/pull/2681) - The sidebar is now restricted to 80% of the viewport width to make it possible to collapse it when the viewport is very narrow. [#2679](https://github.com/rust-lang/mdBook/pull/2679) ## mdBook 0.4.48 [v0.4.47...v0.4.48](https://github.com/rust-lang/mdBook/compare/v0.4.47...v0.4.48) ### Added - Footnotes now have back-reference links. These links bring the reader back to the original location. As part of this change, footnotes are now only rendered at the bottom of the page. This also includes some styling updates and fixes for footnote rendering. [#2626](https://github.com/rust-lang/mdBook/pull/2626) - Added an "Auto" theme selection option which will default to the system-preferred mode. This will also automatically switch when the system changes the preferred mode. [#2576](https://github.com/rust-lang/mdBook/pull/2576) ### Changed - The `searchindex.json` file has been removed; only the `searchindex.js` file will be generated. [#2552](https://github.com/rust-lang/mdBook/pull/2552) - Updated Javascript code to use eslint. [#2554](https://github.com/rust-lang/mdBook/pull/2554) - An error is generated if there are duplicate files in `SUMMARY.md`. [#2613](https://github.com/rust-lang/mdBook/pull/2613) ## mdBook 0.4.47 [v0.4.46...v0.4.47](https://github.com/rust-lang/mdBook/compare/v0.4.46...v0.4.47) ### Fixed - Fixed search not showing up in sub-directories. [#2586](https://github.com/rust-lang/mdBook/pull/2586) ## mdBook 0.4.46 [v0.4.45...v0.4.46](https://github.com/rust-lang/mdBook/compare/v0.4.45...v0.4.46) ### Changed - The `output.html.hash-files` config option has been added to add hashes to static filenames to bust any caches when a book is updated. `{{resource}}` template tags have been added so that links can be properly generated to those files. [#1368](https://github.com/rust-lang/mdBook/pull/1368) ### Fixed - Playground links for Rust 2024 now set the edition correctly. [#2557](https://github.com/rust-lang/mdBook/pull/2557) ## mdBook 0.4.45 [v0.4.44...v0.4.45](https://github.com/rust-lang/mdBook/compare/v0.4.44...v0.4.45) ### Changed - Added context to error message when rustdoc is not found. [#2545](https://github.com/rust-lang/mdBook/pull/2545) - Slightly changed the styling rules around margins of footnotes. [#2524](https://github.com/rust-lang/mdBook/pull/2524) ### Fixed - Fixed an issue where it would panic if a source_path is not set. [#2550](https://github.com/rust-lang/mdBook/pull/2550) ## mdBook 0.4.44 [v0.4.43...v0.4.44](https://github.com/rust-lang/mdBook/compare/v0.4.43...v0.4.44) ### Added - Added pre-built aarch64-apple-darwin binaries to the releases. [#2500](https://github.com/rust-lang/mdBook/pull/2500) - `mdbook clean` now shows a summary of what it did. [#2458](https://github.com/rust-lang/mdBook/pull/2458) - Added the `output.html.search.chapter` config setting to disable search indexing of individual chapters. [#2533](https://github.com/rust-lang/mdBook/pull/2533) ### Fixed - Fixed auto-scrolling the side-bar when loading a page with a `#` fragment URL. [#2517](https://github.com/rust-lang/mdBook/pull/2517) - Fixed display of sidebar when javascript is disabled. [#2529](https://github.com/rust-lang/mdBook/pull/2529) - Fixed the sidebar visibility getting out of sync with the button. [#2532](https://github.com/rust-lang/mdBook/pull/2532) ### Changed - ❗ Rust code block hidden lines now follow the same logic as rustdoc. This requires a space after the `#` symbol. [#2530](https://github.com/rust-lang/mdBook/pull/2530) - ❗ Updated the Linux pre-built binaries which requires a newer version of glibc (2.34). [#2523](https://github.com/rust-lang/mdBook/pull/2523) - Updated dependencies [#2538](https://github.com/rust-lang/mdBook/pull/2538) [#2539](https://github.com/rust-lang/mdBook/pull/2539) ## mdBook 0.4.43 [v0.4.42...v0.4.43](https://github.com/rust-lang/mdBook/compare/v0.4.42...v0.4.43) ### Fixed - Fixed setting the title in `mdbook init` when no git user is configured. [#2486](https://github.com/rust-lang/mdBook/pull/2486) ### Changed - The Rust 2024 edition no longer needs `-Zunstable-options`. [#2495](https://github.com/rust-lang/mdBook/pull/2495) ## mdBook 0.4.42 [v0.4.41...v0.4.42](https://github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42) ### Fixed - Fixed chapter list folding. [#2473](https://github.com/rust-lang/mdBook/pull/2473) ## mdBook 0.4.41 [v0.4.40...v0.4.41](https://github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41) **Note:** If you have a custom `index.hbs` theme file, you will need to update it to the latest version. ### Added - Added preliminary support for Rust 2024 edition. [#2398](https://github.com/rust-lang/mdBook/pull/2398) - Added a full example of the remove-emphasis preprocessor. [#2464](https://github.com/rust-lang/mdBook/pull/2464) ### Changed - Adjusted styling of clipboard/play icons. [#2421](https://github.com/rust-lang/mdBook/pull/2421) - Updated to handlebars v6. [#2416](https://github.com/rust-lang/mdBook/pull/2416) - Attr and section rules now have specific code highlighting. [#2448](https://github.com/rust-lang/mdBook/pull/2448) - The sidebar is now loaded from a common file, significantly reducing the book size when there are many chapters. [#2414](https://github.com/rust-lang/mdBook/pull/2414) - Updated dependencies. [#2470](https://github.com/rust-lang/mdBook/pull/2470) ### Fixed - Improved theme support when JavaScript is disabled. [#2454](https://github.com/rust-lang/mdBook/pull/2454) - Fixed broken themes when localStorage has an invalid theme id. [#2463](https://github.com/rust-lang/mdBook/pull/2463) - Adjusted the line-height of superscripts (and footnotes) to avoid adding extra space between lines. [#2465](https://github.com/rust-lang/mdBook/pull/2465) ## mdBook 0.4.40 [v0.4.39...v0.4.40](https://github.com/rust-lang/mdBook/compare/v0.4.39...v0.4.40) ### Fixed - Reverted the update to pulldown-cmark which broke the semver API. [#2388](https://github.com/rust-lang/mdBook/pull/2388) ## mdBook 0.4.39 [v0.4.38...v0.4.39](https://github.com/rust-lang/mdBook/compare/v0.4.38...v0.4.39) ### Fixed - Fixed the automatic deploy broken in the previous release. [#2383](https://github.com/rust-lang/mdBook/pull/2383) ## mdBook 0.4.38 [v0.4.37...v0.4.38](https://github.com/rust-lang/mdBook/compare/v0.4.37...v0.4.38) ### Added - Added `nix` to the default set of languages supported for syntax highlighting. [#2262](https://github.com/rust-lang/mdBook/pull/2262) ### Changed - The `output.html.curly-quotes` option has been renamed to `output.html.smart-punctuation` to better reflect what it does. The old option `curly-quotes` is kept for compatibility, but may be removed in the future. [#2327](https://github.com/rust-lang/mdBook/pull/2327) - The file-watcher used in `mdbook serve` and `mdbook watch` now uses a poll-based watcher instead of the native operating system notifications. This should fix issues on various systems and environments, and more accurately detect when files change. The native watcher can still be used with the `--watcher native` CLI option. [#2325](https://github.com/rust-lang/mdBook/pull/2325) - `mdbook test` output now includes color, and shows relative paths to the source. [#2259](https://github.com/rust-lang/mdBook/pull/2259) - Updated dependencies, MSRV raised to 1.74 [#2350](https://github.com/rust-lang/mdBook/pull/2350) [#2351](https://github.com/rust-lang/mdBook/pull/2351) [#2378](https://github.com/rust-lang/mdBook/pull/2378) [#2381](https://github.com/rust-lang/mdBook/pull/2381) ### Fixed - Reduced memory allocation when copying files. [#2355](https://github.com/rust-lang/mdBook/pull/2355) - Fixed the horizontal divider in `SUMMARY.md` from being indented into the previous nested section. [#2364](https://github.com/rust-lang/mdBook/pull/2364) - Removed unnecessary `@import` in the CSS. [#2260](https://github.com/rust-lang/mdBook/pull/2260) ## mdBook 0.4.37 [v0.4.36...v0.4.37](https://github.com/rust-lang/mdBook/compare/v0.4.36...v0.4.37) ### Changed - ❗️ Updated the markdown parser. This brings in many changes to more closely follow the CommonMark spec. This may cause some small rendering changes. It is recommended to compare the output of the old and new version to check for changes. See for more information. [#2308](https://github.com/rust-lang/mdBook/pull/2308) - The warning about the legacy `src/theme` directory has been removed. [#2263](https://github.com/rust-lang/mdBook/pull/2263) - Updated dependencies. MSRV raised to 1.71.0. [#2283](https://github.com/rust-lang/mdBook/pull/2283) [#2293](https://github.com/rust-lang/mdBook/pull/2293) [#2297](https://github.com/rust-lang/mdBook/pull/2297) [#2310](https://github.com/rust-lang/mdBook/pull/2310) [#2309](https://github.com/rust-lang/mdBook/pull/2309) - Some internal performance/memory improvements. [#2273](https://github.com/rust-lang/mdBook/pull/2273) [#2290](https://github.com/rust-lang/mdBook/pull/2290) - Made the `pathdiff` dependency optional based on the `watch` feature. [#2291](https://github.com/rust-lang/mdBook/pull/2291) ### Fixed - The `s` shortcut key handler should not trigger when focus is in an HTML form. [#2311](https://github.com/rust-lang/mdBook/pull/2311) ## mdBook 0.4.36 [v0.4.35...v0.4.36](https://github.com/rust-lang/mdBook/compare/v0.4.35...v0.4.36) ### Added - Added Nim to the default highlighted languages. [#2232](https://github.com/rust-lang/mdBook/pull/2232) - Added a small indicator for the sidebar resize handle. [#2209](https://github.com/rust-lang/mdBook/pull/2209) ### Changed - Updated dependencies. MSRV raised to 1.70.0. [#2173](https://github.com/rust-lang/mdBook/pull/2173) [#2250](https://github.com/rust-lang/mdBook/pull/2250) [#2252](https://github.com/rust-lang/mdBook/pull/2252) ### Fixed - Fixed blank column in print page when the sidebar was visible. [#2235](https://github.com/rust-lang/mdBook/pull/2235) - Fixed indentation of code blocks when Javascript is disabled. [#2162](https://github.com/rust-lang/mdBook/pull/2162) - Fixed a panic when `mdbook serve` or `mdbook watch` were given certain kinds of paths. [#2229](https://github.com/rust-lang/mdBook/pull/2229) ## mdBook 0.4.35 [v0.4.34...v0.4.35](https://github.com/rust-lang/mdBook/compare/v0.4.34...v0.4.35) ### Added - Added the `book.text-direction` setting for explicit support for right-to-left languages. [#1641](https://github.com/rust-lang/mdBook/pull/1641) - Added `rel=prefetch` to the "next" links to potentially improve browser performance. [#2168](https://github.com/rust-lang/mdBook/pull/2168) - Added a `.warning` CSS class which is styled for displaying warning blocks. [#2187](https://github.com/rust-lang/mdBook/pull/2187) ### Changed - Better support of the sidebar when JavaScript is disabled. [#2175](https://github.com/rust-lang/mdBook/pull/2175) ## mdBook 0.4.34 [v0.4.33...v0.4.34](https://github.com/rust-lang/mdBook/compare/v0.4.33...v0.4.34) ### Fixed - Fixed file change watcher failing on macOS with a large number of files. [#2157](https://github.com/rust-lang/mdBook/pull/2157) ## mdBook 0.4.33 [v0.4.32...v0.4.33](https://github.com/rust-lang/mdBook/compare/v0.4.32...v0.4.33) ### Added - The `color-scheme` CSS property is now set based on the light/dark theme, which applies some slight color differences in browser elements like scroll bars on some browsers. [#2134](https://github.com/rust-lang/mdBook/pull/2134) ### Fixed - Fixed watching of extra-watch-dirs when not running in the book root directory. [#2146](https://github.com/rust-lang/mdBook/pull/2146) - Reverted the dependency update to the `toml` crate (again!). This was an unintentional breaking change in 0.4.32. [#2021](https://github.com/rust-lang/mdBook/pull/2021) - Changed macOS change notifications to use the kqueue implementation which should fix some issues with repeated rebuilds when a file changed. [#2152](https://github.com/rust-lang/mdBook/pull/2152) - Don't set a background color in the print page for code blocks in a header. [#2150](https://github.com/rust-lang/mdBook/pull/2150) ## mdBook 0.4.32 [v0.4.31...v0.4.32](https://github.com/rust-lang/mdBook/compare/v0.4.31...v0.4.32) ### Fixed - Fixed theme-color meta tag not syncing with the theme. [#2118](https://github.com/rust-lang/mdBook/pull/2118) ### Changed - Updated all dependencies. [#2121](https://github.com/rust-lang/mdBook/pull/2121) [#2122](https://github.com/rust-lang/mdBook/pull/2122) [#2123](https://github.com/rust-lang/mdBook/pull/2123) [#2124](https://github.com/rust-lang/mdBook/pull/2124) [#2125](https://github.com/rust-lang/mdBook/pull/2125) [#2126](https://github.com/rust-lang/mdBook/pull/2126) ## mdBook 0.4.31 [v0.4.30...v0.4.31](https://github.com/rust-lang/mdBook/compare/v0.4.30...v0.4.31) ### Fixed - Fixed menu border render flash during page navigation. [#2101](https://github.com/rust-lang/mdBook/pull/2101) - Fixed flicker setting sidebar scroll position. [#2104](https://github.com/rust-lang/mdBook/pull/2104) - Fixed compile error with proc-macro2 on latest Rust nightly. [#2109](https://github.com/rust-lang/mdBook/pull/2109) ## mdBook 0.4.30 [v0.4.29...v0.4.30](https://github.com/rust-lang/mdBook/compare/v0.4.29...v0.4.30) ### Added - Added support for heading attributes. Attributes are specified in curly braces just after the heading text. An HTML ID can be specified with `#` and classes with `.`. For example: `## My heading {#custom-id .class1 .class2}` [#2013](https://github.com/rust-lang/mdBook/pull/2013) - Added support for hidden code lines for languages other than Rust. The `output.html.code.hidelines` table allows you to define the prefix character that will be used to hide code lines based on the language. [#2093](https://github.com/rust-lang/mdBook/pull/2093) ### Fixed - Fixed a few minor markdown rendering issues. [#2092](https://github.com/rust-lang/mdBook/pull/2092) ## mdBook 0.4.29 [v0.4.28...v0.4.29](https://github.com/rust-lang/mdBook/compare/v0.4.28...v0.4.29) ### Changed - Built-in fonts are no longer copied when `fonts/fonts.css` is overridden in the theme directory. Additionally, the warning about `copy-fonts` has been removed if `fonts/fonts.css` is specified. [#2080](https://github.com/rust-lang/mdBook/pull/2080) - `mdbook init --force` now skips all interactive prompts as intended. [#2057](https://github.com/rust-lang/mdBook/pull/2057) - Updated dependencies [#2063](https://github.com/rust-lang/mdBook/pull/2063) [#2086](https://github.com/rust-lang/mdBook/pull/2086) [#2082](https://github.com/rust-lang/mdBook/pull/2082) [#2084](https://github.com/rust-lang/mdBook/pull/2084) [#2085](https://github.com/rust-lang/mdBook/pull/2085) ### Fixed - Switched from the `gitignore` library to `ignore`. This should bring some improvements with gitignore handling. [#2076](https://github.com/rust-lang/mdBook/pull/2076) ## mdBook 0.4.28 [v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28) ### Changed - The sidebar is now shown on wide screens when localstorage is disabled. [#2017](https://github.com/rust-lang/mdBook/pull/2017) - Preprocessors are now run with `mdbook test`. [#1986](https://github.com/rust-lang/mdBook/pull/1986) ### Fixed - Fixed regression in 0.4.26 that prevented the title bar from scrolling properly on smaller screens. [#2039](https://github.com/rust-lang/mdBook/pull/2039) ## mdBook 0.4.27 [v0.4.26...v0.4.27](https://github.com/rust-lang/mdBook/compare/v0.4.26...v0.4.27) ### Changed - Reverted the dependency update to the `toml` crate. This was an unintentional breaking change in 0.4.26. [#2021](https://github.com/rust-lang/mdBook/pull/2021) ## mdBook 0.4.26 [v0.4.25...v0.4.26](https://github.com/rust-lang/mdBook/compare/v0.4.25...v0.4.26) **The 0.4.26 release has been yanked due to an unintentional breaking change.** ### Changed - Removed custom scrollbars for webkit browsers [#1961](https://github.com/rust-lang/mdBook/pull/1961) - Updated some dependencies [#1998](https://github.com/rust-lang/mdBook/pull/1998) [#2009](https://github.com/rust-lang/mdBook/pull/2009) [#2011](https://github.com/rust-lang/mdBook/pull/2011) - Fonts are now part of the theme. The `output.html.copy-fonts` option has been deprecated. To define custom fonts, be sure to define `theme/fonts.css`. [#1987](https://github.com/rust-lang/mdBook/pull/1987) ### Fixed - Fixed overflow viewport issue with mobile Safari [#1994](https://github.com/rust-lang/mdBook/pull/1994) ## mdBook 0.4.25 [e14d381...1ba74a3](https://github.com/rust-lang/mdBook/compare/e14d381...1ba74a3) ### Fixed - Fixed a regression where `mdbook test -L deps path-to-book` would not work. [#1959](https://github.com/rust-lang/mdBook/pull/1959) ## mdBook 0.4.24 [eb77083...8767ebf](https://github.com/rust-lang/mdBook/compare/eb77083...8767ebf) ### Fixed - The precompiled linux-gnu mdbook binary available on [GitHub Releases](https://github.com/rust-lang/mdBook/releases) inadvertently switched to a newer version of glibc. This release goes back to an older version that should be more compatible on older versions of Linux. [#1955](https://github.com/rust-lang/mdBook/pull/1955) ## mdBook 0.4.23 [678b469...68a75da](https://github.com/rust-lang/mdBook/compare/678b469...68a75da) ### Changed - Updated all dependencies [#1951](https://github.com/rust-lang/mdBook/pull/1951) [#1952](https://github.com/rust-lang/mdBook/pull/1952) [#1844](https://github.com/rust-lang/mdBook/pull/1844) - Updated minimum Rust version to 1.60. [#1951](https://github.com/rust-lang/mdBook/pull/1951) ### Fixed - Fixed a regression where playground code was missing hidden lines, preventing it from compiling correctly. [#1950](https://github.com/rust-lang/mdBook/pull/1950) ## mdBook 0.4.22 [40c06f5...4844f72](https://github.com/rust-lang/mdBook/compare/40c06f5...4844f72) ### Added - Added a `--chapter` option to `mdbook test` to specify a specific chapter to test. [#1741](https://github.com/rust-lang/mdBook/pull/1741) - Added CSS styling for `` tags. [#1906](https://github.com/rust-lang/mdBook/pull/1906) - Added pre-compiled binaries for `x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl` (see [Releases](https://github.com/rust-lang/mdBook/releases)). [#1862](https://github.com/rust-lang/mdBook/pull/1862) - Added `build.extra-watch-dirs` which is an array of additional directories to watch for changes when running `mdbook serve`. [#1884](https://github.com/rust-lang/mdBook/pull/1884) ### Changed - Removed the `type="text/javascript"` attribute from ` {{/if}}

Keyboard shortcuts

Press or to navigate between chapters

{{#if search_enabled}}

Press S or / to search in the book

{{/if}}

Press ? to show this help

Press Esc to hide this help

{{> header}}
{{#if search_enabled}} {{/if}}
{{#if live_reload_endpoint}} {{/if}} {{#if playground_line_numbers}} {{/if}} {{#if playground_copyable}} {{/if}} {{#if playground_js}} {{/if}} {{#if search_js}} {{/if}} {{#each additional_js}} {{/each}} {{#if is_print}} {{#if mathjax_support}} {{else}} {{/if}} {{/if}} {{#if fragment_map}} {{/if}}
================================================ FILE: crates/mdbook-html/front-end/templates/redirect.hbs ================================================ Redirecting...

Redirecting to... {{url}}.

================================================ FILE: crates/mdbook-html/front-end/templates/toc.html.hbs ================================================ {{#if base_url}} {{/if}} {{> head}} {{#if print_enable}} {{/if}} {{#each additional_css}} {{/each}} {{#toc}}{{/toc}} ================================================ FILE: crates/mdbook-html/front-end/templates/toc.js.hbs ================================================ // Populate the sidebar // // This is a script, and not included directly in the page, to control the total size of the book. // The TOC contains an entry for each page, so if each page includes a copy of the TOC, // the total size of the page becomes O(n**2). class MDBookSidebarScrollbox extends HTMLElement { constructor() { super(); } connectedCallback() { this.innerHTML = '{{#toc}}{{/toc}}'; // Set the current, active page, and reveal it if it's hidden let current_page = document.location.href.toString().split('#')[0].split('?')[0]; if (current_page.endsWith('/')) { current_page += 'index.html'; } const links = Array.prototype.slice.call(this.querySelectorAll('a')); const l = links.length; for (let i = 0; i < l; ++i) { const link = links[i]; const href = link.getAttribute('href'); if (href && !href.startsWith('#') && !/^(?:[a-z+]+:)?\/\//.test(href)) { link.href = path_to_root + href; } // The 'index' page is supposed to alias the first chapter in the book. if (link.href === current_page || i === 0 && path_to_root === '' && current_page.endsWith('/index.html')) { link.classList.add('active'); let parent = link.parentElement; while (parent) { if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) { parent.classList.add('expanded'); } parent = parent.parentElement; } } } // Track and set sidebar scroll position this.addEventListener('click', e => { if (e.target.tagName === 'A') { const clientRect = e.target.getBoundingClientRect(); const sidebarRect = this.getBoundingClientRect(); sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top); } }, { passive: true }); const sidebarScrollOffset = sessionStorage.getItem('sidebar-scroll-offset'); sessionStorage.removeItem('sidebar-scroll-offset'); if (sidebarScrollOffset !== null) { // preserve sidebar scroll position when navigating via links within sidebar const activeSection = this.querySelector('.active'); if (activeSection) { const clientRect = activeSection.getBoundingClientRect(); const sidebarRect = this.getBoundingClientRect(); const currentOffset = clientRect.top - sidebarRect.top; this.scrollTop += currentOffset - parseFloat(sidebarScrollOffset); } } else { // scroll sidebar to current active section when navigating via // 'next/previous chapter' buttons const activeSection = document.querySelector('#mdbook-sidebar .active'); if (activeSection) { activeSection.scrollIntoView({ block: 'center' }); } } // Toggle buttons const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle'); function toggleSection(ev) { ev.currentTarget.parentElement.parentElement.classList.toggle('expanded'); } Array.from(sidebarAnchorToggles).forEach(el => { el.addEventListener('click', toggleSection); }); } } window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox); {{#if sidebar_header_nav}} // --------------------------------------------------------------------------- // Support for dynamically adding headers to the sidebar. (function() { // This is used to detect which direction the page has scrolled since the // last scroll event. let lastKnownScrollPosition = 0; // This is the threshold in px from the top of the screen where it will // consider a header the "current" header when scrolling down. const defaultDownThreshold = 150; // Same as defaultDownThreshold, except when scrolling up. const defaultUpThreshold = 300; // The threshold is a virtual horizontal line on the screen where it // considers the "current" header to be above the line. The threshold is // modified dynamically to handle headers that are near the bottom of the // screen, and to slightly offset the behavior when scrolling up vs down. let threshold = defaultDownThreshold; // This is used to disable updates while scrolling. This is needed when // clicking the header in the sidebar, which triggers a scroll event. It // is somewhat finicky to detect when the scroll has finished, so this // uses a relatively dumb system of disabling scroll updates for a short // time after the click. let disableScroll = false; // Array of header elements on the page. let headers; // Array of li elements that are initially collapsed headers in the sidebar. // I'm not sure why eslint seems to have a false positive here. // eslint-disable-next-line prefer-const let headerToggles = []; // This is a debugging tool for the threshold which you can enable in the console. let thresholdDebug = false; // Updates the threshold based on the scroll position. function updateThreshold() { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; // The number of pixels below the viewport, at most documentHeight. // This is used to push the threshold down to the bottom of the page // as the user scrolls towards the bottom. const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight)); // The number of pixels above the viewport, at least defaultDownThreshold. // Similar to pixelsBelow, this is used to push the threshold back towards // the top when reaching the top of the page. const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop); // How much the threshold should be offset once it gets close to the // bottom of the page. const bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold); let adjustedBottomAdd = bottomAdd; // Adjusts bottomAdd for a small document. The calculation above // assumes the document is at least twice the windowheight in size. If // it is less than that, then bottomAdd needs to be shrunk // proportional to the difference in size. if (documentHeight < windowHeight * 2) { const maxPixelsBelow = documentHeight - windowHeight; const t = 1 - pixelsBelow / Math.max(1, maxPixelsBelow); const clamp = Math.max(0, Math.min(1, t)); adjustedBottomAdd *= clamp; } let scrollingDown = true; if (scrollTop < lastKnownScrollPosition) { scrollingDown = false; } if (scrollingDown) { // When scrolling down, move the threshold up towards the default // downwards threshold position. If near the bottom of the page, // adjustedBottomAdd will offset the threshold towards the bottom // of the page. const amountScrolledDown = scrollTop - lastKnownScrollPosition; const adjustedDefault = defaultDownThreshold + adjustedBottomAdd; threshold = Math.max(adjustedDefault, threshold - amountScrolledDown); } else { // When scrolling up, move the threshold down towards the default // upwards threshold position. If near the bottom of the page, // quickly transition the threshold back up where it normally // belongs. const amountScrolledUp = lastKnownScrollPosition - scrollTop; const adjustedDefault = defaultUpThreshold - pixelsAbove + Math.max(0, adjustedBottomAdd - defaultDownThreshold); threshold = Math.min(adjustedDefault, threshold + amountScrolledUp); } if (documentHeight <= windowHeight) { threshold = 0; } if (thresholdDebug) { const id = 'mdbook-threshold-debug-data'; let data = document.getElementById(id); if (data === null) { data = document.createElement('div'); data.id = id; data.style.cssText = ` position: fixed; top: 50px; right: 10px; background-color: 0xeeeeee; z-index: 9999; pointer-events: none; `; document.body.appendChild(data); } data.innerHTML = `
documentHeight${documentHeight.toFixed(1)}
windowHeight${windowHeight.toFixed(1)}
scrollTop${scrollTop.toFixed(1)}
pixelsAbove${pixelsAbove.toFixed(1)}
pixelsBelow${pixelsBelow.toFixed(1)}
bottomAdd${bottomAdd.toFixed(1)}
adjustedBottomAdd${adjustedBottomAdd.toFixed(1)}
scrollingDown${scrollingDown}
threshold${threshold.toFixed(1)}
`; drawDebugLine(); } lastKnownScrollPosition = scrollTop; } function drawDebugLine() { if (!document.body) { return; } const id = 'mdbook-threshold-debug-line'; const existingLine = document.getElementById(id); if (existingLine) { existingLine.remove(); } const line = document.createElement('div'); line.id = id; line.style.cssText = ` position: fixed; top: ${threshold}px; left: 0; width: 100vw; height: 2px; background-color: red; z-index: 9999; pointer-events: none; `; document.body.appendChild(line); } function mdbookEnableThresholdDebug() { thresholdDebug = true; updateThreshold(); drawDebugLine(); } window.mdbookEnableThresholdDebug = mdbookEnableThresholdDebug; // Updates which headers in the sidebar should be expanded. If the current // header is inside a collapsed group, then it, and all its parents should // be expanded. function updateHeaderExpanded(currentA) { // Add expanded to all header-item li ancestors. let current = currentA.parentElement; while (current) { if (current.tagName === 'LI' && current.classList.contains('header-item')) { current.classList.add('expanded'); } current = current.parentElement; } } // Updates which header is marked as the "current" header in the sidebar. // This is done with a virtual Y threshold, where headers at or below // that line will be considered the current one. function updateCurrentHeader() { if (!headers || !headers.length) { return; } // Reset the classes, which will be rebuilt below. const els = document.getElementsByClassName('current-header'); for (const el of els) { el.classList.remove('current-header'); } for (const toggle of headerToggles) { toggle.classList.remove('expanded'); } // Find the last header that is above the threshold. let lastHeader = null; for (const header of headers) { const rect = header.getBoundingClientRect(); if (rect.top <= threshold) { lastHeader = header; } else { break; } } if (lastHeader === null) { lastHeader = headers[0]; const rect = lastHeader.getBoundingClientRect(); const windowHeight = window.innerHeight; if (rect.top >= windowHeight) { return; } } // Get the anchor in the summary. const href = '#' + lastHeader.id; const a = [...document.querySelectorAll('.header-in-summary')] .find(element => element.getAttribute('href') === href); if (!a) { return; } a.classList.add('current-header'); updateHeaderExpanded(a); } // Updates which header is "current" based on the threshold line. function reloadCurrentHeader() { if (disableScroll) { return; } updateThreshold(); updateCurrentHeader(); } // When clicking on a header in the sidebar, this adjusts the threshold so // that it is located next to the header. This is so that header becomes // "current". function headerThresholdClick(event) { // See disableScroll description why this is done. disableScroll = true; setTimeout(() => { disableScroll = false; }, 100); // requestAnimationFrame is used to delay the update of the "current" // header until after the scroll is done, and the header is in the new // position. requestAnimationFrame(() => { requestAnimationFrame(() => { // Closest is needed because if it has child elements like . const a = event.target.closest('a'); const href = a.getAttribute('href'); const targetId = href.substring(1); const targetElement = document.getElementById(targetId); if (targetElement) { threshold = targetElement.getBoundingClientRect().bottom; updateCurrentHeader(); } }); }); } // Takes the nodes from the given head and copies them over to the // destination, along with some filtering. function filterHeader(source, dest) { const clone = source.cloneNode(true); clone.querySelectorAll('mark').forEach(mark => { mark.replaceWith(...mark.childNodes); }); dest.append(...clone.childNodes); } // Scans page for headers and adds them to the sidebar. document.addEventListener('DOMContentLoaded', function() { const activeSection = document.querySelector('#mdbook-sidebar .active'); if (activeSection === null) { return; } const main = document.getElementsByTagName('main')[0]; headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6')) .filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A'); if (headers.length === 0) { return; } // Build a tree of headers in the sidebar. const stack = []; const firstLevel = parseInt(headers[0].tagName.charAt(1)); for (let i = 1; i < firstLevel; i++) { const ol = document.createElement('ol'); ol.classList.add('section'); if (stack.length > 0) { stack[stack.length - 1].ol.appendChild(ol); } stack.push({level: i + 1, ol: ol}); } // The level where it will start folding deeply nested headers. const foldLevel = 3; for (let i = 0; i < headers.length; i++) { const header = headers[i]; const level = parseInt(header.tagName.charAt(1)); const currentLevel = stack[stack.length - 1].level; if (level > currentLevel) { // Begin nesting to this level. for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) { const ol = document.createElement('ol'); ol.classList.add('section'); const last = stack[stack.length - 1]; const lastChild = last.ol.lastChild; // Handle the case where jumping more than one nesting // level, which doesn't have a list item to place this new // list inside of. if (lastChild) { lastChild.appendChild(ol); } else { last.ol.appendChild(ol); } stack.push({level: nextLevel, ol: ol}); } } else if (level < currentLevel) { while (stack.length > 1 && stack[stack.length - 1].level > level) { stack.pop(); } } const li = document.createElement('li'); li.classList.add('header-item'); li.classList.add('expanded'); if (level < foldLevel) { li.classList.add('expanded'); } const span = document.createElement('span'); span.classList.add('chapter-link-wrapper'); const a = document.createElement('a'); span.appendChild(a); a.href = '#' + header.id; a.classList.add('header-in-summary'); filterHeader(header.children[0], a); a.addEventListener('click', headerThresholdClick); const nextHeader = headers[i + 1]; if (nextHeader !== undefined) { const nextLevel = parseInt(nextHeader.tagName.charAt(1)); if (nextLevel > level && level >= foldLevel) { const toggle = document.createElement('a'); toggle.classList.add('chapter-fold-toggle'); toggle.classList.add('header-toggle'); toggle.addEventListener('click', () => { li.classList.toggle('expanded'); }); const toggleDiv = document.createElement('div'); toggleDiv.textContent = '❱'; toggle.appendChild(toggleDiv); span.appendChild(toggle); headerToggles.push(li); } } li.appendChild(span); const currentParent = stack[stack.length - 1]; currentParent.ol.appendChild(li); } const onThisPage = document.createElement('div'); onThisPage.classList.add('on-this-page'); onThisPage.append(stack[0].ol); const activeItemSpan = activeSection.parentElement; activeItemSpan.after(onThisPage); }); document.addEventListener('DOMContentLoaded', reloadCurrentHeader); document.addEventListener('scroll', reloadCurrentHeader, { passive: true }); })(); {{/if}} ================================================ FILE: crates/mdbook-html/src/html/admonitions.rs ================================================ use pulldown_cmark::BlockQuoteKind; // This icon is from GitHub, MIT License, see https://github.com/primer/octicons const ICON_NOTE: &str = r#""#; // This icon is from GitHub, MIT License, see https://github.com/primer/octicons const ICON_TIP: &str = r#""#; // This icon is from GitHub, MIT License, see https://github.com/primer/octicons const ICON_IMPORTANT: &str = r#""#; // This icon is from GitHub, MIT License, see https://github.com/primer/octicons const ICON_WARNING: &str = r#""#; // This icon is from GitHub, MIT License, see https://github.com/primer/octicons const ICON_CAUTION: &str = r#""#; pub(crate) fn select_tag(kind: BlockQuoteKind) -> (&'static str, &'static str, &'static str) { match kind { BlockQuoteKind::Note => ("note", ICON_NOTE, "Note"), BlockQuoteKind::Tip => ("tip", ICON_TIP, "Tip"), BlockQuoteKind::Important => ("important", ICON_IMPORTANT, "Important"), BlockQuoteKind::Warning => ("warning", ICON_WARNING, "Warning"), BlockQuoteKind::Caution => ("caution", ICON_CAUTION, "Caution"), } } ================================================ FILE: crates/mdbook-html/src/html/hide_lines.rs ================================================ //! Support for hiding code lines. use crate::html::{Element, Node}; use ego_tree::{NodeId, Tree}; use html5ever::tendril::StrTendril; use mdbook_core::static_regex; use std::collections::HashMap; /// Wraps hidden lines in a `` for the given code block. pub(crate) fn hide_lines( tree: &mut Tree, code_id: NodeId, hidelines: &HashMap, ) { let mut node = tree.get_mut(code_id).unwrap(); let el = node.value().as_element().unwrap(); let classes: Vec<_> = el.attr("class").unwrap_or_default().split(' ').collect(); let language = classes .iter() .filter_map(|cls| cls.strip_prefix("language-")) .next() .unwrap_or_default() .to_string(); let hideline_info = classes .iter() .filter_map(|cls| cls.strip_prefix("hidelines=")) .map(|prefix| prefix.to_string()) .next(); if let Some(mut child) = node.first_child() && let Node::Text(text) = child.value() { if language == "rust" { let new_nodes = hide_lines_rust(text); child.detach(); let root = tree.extend_tree(new_nodes); let root_id = root.id(); let mut node = tree.get_mut(code_id).unwrap(); node.reparent_from_id_append(root_id); } else { // Use the prefix from the code block, else the prefix from config. let hidelines_prefix = hideline_info .as_deref() .or_else(|| hidelines.get(&language).map(|p| p.as_str())); if let Some(prefix) = hidelines_prefix { let new_nodes = hide_lines_with_prefix(text, prefix); child.detach(); let root = tree.extend_tree(new_nodes); let root_id = root.id(); let mut node = tree.get_mut(code_id).unwrap(); node.reparent_from_id_append(root_id); } } } } /// Wraps hidden lines in a `` specifically for Rust code blocks. fn hide_lines_rust(text: &StrTendril) -> Tree { static_regex!(BORING_LINES_REGEX, r"^(\s*)#(.?)(.*)$"); let mut tree = Tree::new(Node::Fragment); let mut root = tree.root_mut(); let mut lines = text.lines().peekable(); while let Some(line) = lines.next() { // Don't include newline on the last line. let newline = if lines.peek().is_none() { "" } else { "\n" }; if let Some(caps) = BORING_LINES_REGEX.captures(line) { if &caps[2] == "#" { root.append(Node::Text( format!("{}{}{}{newline}", &caps[1], &caps[2], &caps[3]).into(), )); continue; } else if matches!(&caps[2], "" | " ") { let mut span = Element::new("span"); span.insert_attr("class", "boring".into()); let mut span = root.append(Node::Element(span)); span.append(Node::Text( format!("{}{}{newline}", &caps[1], &caps[3]).into(), )); continue; } } root.append(Node::Text(format!("{line}{newline}").into())); } tree } /// Wraps hidden lines in a `` tag for lines starting with the given prefix. fn hide_lines_with_prefix(content: &str, prefix: &str) -> Tree { let mut tree = Tree::new(Node::Fragment); let mut root = tree.root_mut(); for line in content.lines() { if line.trim_start().starts_with(prefix) { let pos = line.find(prefix).unwrap(); let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]); let mut span = Element::new("span"); span.insert_attr("class", "boring".into()); let mut span = root.append(Node::Element(span)); span.append(Node::Text(format!("{ws}{rest}\n").into())); } else { root.append(Node::Text(format!("{line}\n").into())); } } tree } /// If this code text is missing an `fn main`, the wrap it with `fn main` in a /// fashion similar to rustdoc, with the wrapper hidden. pub(crate) fn wrap_rust_main(text: &str) -> Option { if !text.contains("fn main") && !text.contains("quick_main!") { let (attrs, code) = partition_rust_source(text); let newline = if code.is_empty() || code.ends_with('\n') { "" } else { "\n" }; Some(format!( "# #![allow(unused)]\n{attrs}# fn main() {{\n{code}{newline}# }}" )) } else { None } } /// Splits Rust inner attributes from the given source string. /// /// Returns `(inner_attrs, rest_of_code)`. fn partition_rust_source(s: &str) -> (&str, &str) { static_regex!( HEADER_RE, r"^(?mx) ( (?: ^[ \t]*\#!\[.* (?:\r?\n)? | ^\s* (?:\r?\n)? )* )" ); let split_idx = match HEADER_RE.captures(s) { Some(caps) => { let attributes = &caps[1]; if attributes.trim().is_empty() { // Don't include pure whitespace as an attribute. The // whitespace in the regex is intended to handle multiple // attributes *separated* by potential whitespace. 0 } else { attributes.len() } } None => 0, }; s.split_at(split_idx) } #[test] fn it_partitions_rust_source() { assert_eq!(partition_rust_source(""), ("", "")); assert_eq!(partition_rust_source("let x = 1;"), ("", "let x = 1;")); assert_eq!( partition_rust_source("fn main()\n{ let x = 1; }\n"), ("", "fn main()\n{ let x = 1; }\n") ); assert_eq!( partition_rust_source("#![allow(foo)]"), ("#![allow(foo)]", "") ); assert_eq!( partition_rust_source("#![allow(foo)]\n"), ("#![allow(foo)]\n", "") ); assert_eq!( partition_rust_source("#![allow(foo)]\nlet x = 1;"), ("#![allow(foo)]\n", "let x = 1;") ); assert_eq!( partition_rust_source( "\n\ #![allow(foo)]\n\ \n\ #![allow(bar)]\n\ \n\ let x = 1;" ), ("\n#![allow(foo)]\n\n#![allow(bar)]\n\n", "let x = 1;") ); assert_eq!( partition_rust_source(" // Example"), ("", " // Example") ); } ================================================ FILE: crates/mdbook-html/src/html/mod.rs ================================================ //! HTML rendering support. //! //! This module's primary entry point is [`render_markdown`] which will take //! markdown text and render it to HTML. In summary, the general procedure of //! that function is: //! //! 1. Use [`pulldown_cmark`] to parse the markdown and generate events. //! 2. [`tree`] converts those events to a tree data structure. //! 1. Parse HTML inside the markdown using [`tokenizer`]. //! 2. Apply various transformations to the tree data structure, such as adding header links. //! 3. Serialize the tree to HTML in [`serialize()`]. use ego_tree::Tree; use mdbook_core::book::{Book, Chapter}; use mdbook_core::config::{HtmlConfig, RustEdition}; use mdbook_markdown::{MarkdownOptions, new_cmark_parser}; use std::path::{Path, PathBuf}; mod admonitions; mod hide_lines; mod print; mod serialize; #[cfg(test)] mod tests; mod tokenizer; mod tree; pub(crate) use hide_lines::{hide_lines, wrap_rust_main}; pub(crate) use print::render_print_page; pub(crate) use serialize::serialize; pub(crate) use tree::{Element, Node}; /// Options for converting a single chapter's markdown to HTML. pub(crate) struct HtmlRenderOptions<'a> { /// Options for parsing markdown. pub markdown_options: MarkdownOptions, /// The chapter's location, relative to the `SUMMARY.md` file. pub path: &'a Path, /// The default Rust edition, used to set the proper class on the code blocks. pub edition: Option, /// The [`HtmlConfig`], whose options affect how the HTML is generated. pub config: &'a HtmlConfig, } impl<'a> HtmlRenderOptions<'a> { /// Creates a new [`HtmlRenderOptions`]. pub(crate) fn new( path: &'a Path, config: &'a HtmlConfig, edition: Option, ) -> HtmlRenderOptions<'a> { let mut markdown_options = MarkdownOptions::default(); markdown_options.smart_punctuation = config.smart_punctuation; markdown_options.definition_lists = config.definition_lists; markdown_options.admonitions = config.admonitions; HtmlRenderOptions { markdown_options, path, edition, config, } } } /// Renders markdown to HTML. pub(crate) fn render_markdown(text: &str, options: &HtmlRenderOptions<'_>) -> String { let tree = build_tree(text, options); let mut output = String::new(); serialize::serialize(&tree, &mut output); output } /// Renders markdown to a [`Tree`]. fn build_tree(text: &str, options: &HtmlRenderOptions<'_>) -> Tree { let events = new_cmark_parser(text, &options.markdown_options); tree::MarkdownTreeBuilder::build(options, events) } /// The parsed chapter, and some information about the chapter. pub(crate) struct ChapterTree<'book> { pub(crate) chapter: &'book Chapter, /// The path to the chapter relative to the root with the `.html` extension. pub(crate) html_path: PathBuf, /// The chapter tree. pub(crate) tree: Tree, } /// Creates all of the [`ChapterTree`]s for the book. pub(crate) fn build_trees<'book>( book: &'book Book, html_config: &HtmlConfig, edition: Option, ) -> Vec> { book.chapters() .map(|ch| { let path = ch.path.as_ref().unwrap(); let html_path = ch.path.as_ref().unwrap().with_extension("html"); let options = HtmlRenderOptions::new(path, html_config, edition); let tree = build_tree(&ch.content, &options); ChapterTree { chapter: ch, html_path, tree, } }) .collect() } ================================================ FILE: crates/mdbook-html/src/html/print.rs ================================================ //! Support for generating the print page. //! //! The print page takes all the individual chapters (as `Tree` //! elements) and modifies the chapters so that they work on a consolidated //! print page, and then serializes it all as one HTML page. use super::Node; use crate::html::{ChapterTree, Element, serialize}; use crate::utils::{ToUrlPath, id_from_content, normalize_path, unique_id}; use mdbook_core::static_regex; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; /// Takes all the chapter trees, modifies them to be suitable to render for /// the print page, and returns an string of all the chapters rendered to a /// single HTML page. pub(crate) fn render_print_page(mut chapter_trees: Vec>) -> String { let (id_remap, mut id_counter) = make_ids_unique(&mut chapter_trees); let path_to_root_id = make_root_id_map(&mut chapter_trees, &mut id_counter); rewrite_links(&mut chapter_trees, &id_remap, &path_to_root_id); let mut print_content = String::new(); for ChapterTree { tree, .. } in chapter_trees { if !print_content.is_empty() { // Add page break between chapters // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before // Add both two CSS properties because of the compatibility issue print_content .push_str(r#"
"#); } serialize(&tree, &mut print_content); } print_content } /// Make all IDs unique, and create a map from old to new IDs. /// /// The first map is a map of the chapter path to the IDs that were rewritten /// in that chapter (old ID to new ID). /// /// The second map is a map of every ID seen to the number of times it has /// been seen. This is used to generate unique IDs. fn make_ids_unique( chapter_trees: &mut [ChapterTree<'_>], ) -> (HashMap>, HashSet) { let mut id_remap = HashMap::new(); let mut id_counter = HashSet::new(); for ChapterTree { html_path, tree, .. } in chapter_trees { for value in tree.values_mut() { if let Node::Element(el) = value && let Some(id) = el.attr("id") { let new_id = unique_id(id, &mut id_counter); if new_id != id { let id = id.to_string(); el.insert_attr("id", new_id.clone().into()); let map: &mut HashMap<_, _> = id_remap.entry(html_path.clone()).or_default(); map.insert(id, new_id); } } } } (id_remap, id_counter) } /// Generates a map of a chapter path to the ID of the top of the chapter. /// /// If a chapter is missing an `h1` tag, then one is synthesized so that the /// print output has something to link to. fn make_root_id_map( chapter_trees: &mut [ChapterTree<'_>], id_counter: &mut HashSet, ) -> HashMap { let mut path_to_root_id = HashMap::new(); for ChapterTree { chapter, html_path, tree, .. } in chapter_trees { let mut h1_found = false; for value in tree.values_mut() { if let Node::Element(el) = value { if el.name() == "h1" { if let Some(id) = el.attr("id") { h1_found = true; path_to_root_id.insert(html_path.clone(), id.to_string()); } break; } else if matches!(el.name(), "h2" | "h3" | "h4" | "h5" | "h6") { // h1 not found. break; } } } if !h1_found { // Synthesize a root id to be able to link to the start of the page. // TODO: This might want to be a warning? Chapters generally // should start with an h1. let mut h1 = Element::new("h1"); let id = id_from_content(&chapter.name); let id = unique_id(&id, id_counter); h1.insert_attr("id", id.clone().into()); let mut root = tree.root_mut(); let mut h1 = root.prepend(Node::Element(h1)); let mut a = Element::new("a"); a.insert_attr("href", format!("#{id}").into()); a.insert_attr("class", "header".into()); let mut a = h1.append(Node::Element(a)); a.append(Node::Text(chapter.name.clone().into())); path_to_root_id.insert(html_path.clone(), id); } } path_to_root_id } /// Rewrite links so that they point to IDs on the print page. fn rewrite_links( chapter_trees: &mut [ChapterTree<'_>], id_remap: &HashMap>, path_to_root_id: &HashMap, ) { static_regex!( LINK, r"(?x) (?P^[a-z][a-z0-9+.-]*:)? (?P[^\#]+)? (?:\#(?P.*))?" ); // Rewrite path links to go to the appropriate place. for ChapterTree { html_path, tree, .. } in chapter_trees { let base = html_path.parent().expect("path can't be empty"); for value in tree.values_mut() { let Node::Element(el) = value else { continue; }; if !matches!(el.name(), "a" | "img") { continue; } for attr in ["href", "src", "xlink:href"] { let Some(dest) = el.attr(attr) else { continue; }; let Some(caps) = LINK.captures(&dest) else { continue; }; if caps.name("scheme").is_some() { continue; } // The lookup_key is the key to look up in the remap table. let mut lookup_key = html_path.clone(); if let Some(href_path) = caps.name("path") && let href_path = href_path.as_str() && !href_path.is_empty() { lookup_key.pop(); lookup_key.push(href_path); lookup_key = normalize_path(&lookup_key); let is_a_chapter = path_to_root_id.contains_key(&lookup_key); if !is_a_chapter { // Make the link relative to the print page location. let mut rel_path = normalize_path(&base.join(href_path)).to_url_path(); if let Some(anchor) = caps.name("anchor") { rel_path.push('#'); rel_path.push_str(anchor.as_str()); } el.insert_attr(attr, rel_path.into()); continue; } } let id = match caps.name("anchor") { Some(anchor_id) => { let anchor_id = anchor_id.as_str().to_string(); match id_remap.get(&lookup_key) { Some(id_map) => match id_map.get(&anchor_id) { Some(new_id) => new_id.clone(), None => anchor_id, }, None => { // Assume the anchor goes to some non-remapped // ID that already exists. anchor_id } } } None => match path_to_root_id.get(&lookup_key) { Some(id) => id.to_string(), None => { // This should be guaranteed that either the // chapter itself is in the map (for anchor-only // links), or the is_a_chapter check above. panic!( "internal error: expected `{lookup_key:?}` to be in \ root map (chapter path is `{html_path:?}`)" ); } }, }; el.insert_attr(attr, format!("#{id}").into()); } } } } ================================================ FILE: crates/mdbook-html/src/html/serialize.rs ================================================ //! Serializes the [`Node`] tree to an HTML string. use super::tree::is_void_element; use super::tree::{Element, Node}; use ego_tree::{Tree, iter::Edge}; use html5ever::{local_name, ns}; use mdbook_core::utils::{escape_html, escape_html_attribute}; use std::ops::Deref; /// Serializes the given tree of [`Node`] elements to an HTML string. pub(crate) fn serialize(tree: &Tree, output: &mut String) { for edge in tree.root().traverse() { match edge { Edge::Open(node) => match node.value() { Node::Element(el) => serialize_start(el, output), Node::Text(text) => { output.push_str(&escape_html(text)); } Node::Comment(comment) => { output.push_str(""); } Node::Fragment => {} Node::RawData(html) => { output.push_str(html); } }, Edge::Close(node) => { if let Node::Element(el) = node.value() { serialize_end(el, output); } } } } } /// Returns true if this HTML element wants a newline to keep the emitted /// output more readable. fn wants_pretty_html_newline(name: &str) -> bool { matches!(name, |"blockquote"| "dd" | "div" | "dl" | "dt" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "hr" | "li" | "ol" | "p" | "pre" | "table" | "tbody" | "thead" | "tr" | "ul") } /// Emit the start tag of an element. fn serialize_start(el: &Element, output: &mut String) { let el_name = el.name(); if wants_pretty_html_newline(el_name) { if !output.is_empty() { if !output.ends_with('\n') { output.push('\n'); } } } output.push('<'); output.push_str(el_name); for (attr_name, value) in &el.attrs { output.push(' '); match attr_name.ns { ns!() => (), ns!(xml) => output.push_str("xml:"), ns!(xmlns) => { if el.name.local != local_name!("xmlns") { output.push_str("xmlns:"); } } ns!(xlink) => output.push_str("xlink:"), _ => (), // TODO what should it do here? } output.push_str(attr_name.local.deref()); output.push_str("=\""); output.push_str(&escape_html_attribute(&value)); output.push('"'); } if el.self_closing { output.push_str(" /"); } output.push('>'); } /// Emit the end tag of an element. fn serialize_end(el: &Element, output: &mut String) { // Void elements do not have an end tag. if el.self_closing || is_void_element(el.name()) { return; } let name = el.name(); output.push_str("'); if wants_pretty_html_newline(name) { output.push('\n'); } } ================================================ FILE: crates/mdbook-html/src/html/tests.rs ================================================ use crate::html::tokenizer::parse_html; use html5ever::tokenizer::{Tag, TagKind, Token}; // Basic tokenizer behavior of a script. #[test] fn parse_html_script() { let script = r#" if (3 < 5 > 10) { alert("The sky is falling!"); } "#; let t = format!(""); let ts = parse_html(&t); eprintln!("{ts:#?}",); let mut output = String::new(); let mut in_script = false; for t in ts { match t { Token::ParseError(e) => panic!("{e:?}"), Token::CharacterTokens(s) => { if in_script { output.push_str(&s) } } Token::TagToken(Tag { kind: TagKind::StartTag, .. }) => in_script = true, Token::TagToken(Tag { kind: TagKind::EndTag, .. }) => in_script = false, _ => {} } } assert_eq!(output, script); } // What happens if a script doesn't end. #[test] fn parse_html_script_unclosed() { let t = r#" ``` ### resource The path to a static file. It implicitly includes `path_to_root`, and accounts for files that are renamed with a hash in their filename. ```handlebars ``` ### fa mdBook includes a copy of [Font Awesome Free's](https://fontawesome.com) MIT-licensed SVG files. It accepts three positional arguments: 1. Type: one of "solid", "regular", and "brands" (light and duotone are not currently supported) 2. Icon: anything chosen from the [free icon set](https://fontawesome.com/v6/search) 3. ID (optional): if included, an HTML ID attribute will be added to the icon's wrapping `` tag For example, this handlebars syntax will become this HTML: ```handlebars {{fa "solid" "print" "print-button"}} ``` ```html ``` ================================================ FILE: guide/src/format/theme/syntax-highlighting.md ================================================ # Syntax highlighting mdBook uses [Highlight.js](https://highlightjs.org) with a custom theme for syntax highlighting. Automatic language detection has been turned off, so you will probably want to specify the programming language you use like this: ~~~markdown ```rust fn main() { // Some code } ``` ~~~ ## Supported languages These languages are supported by default, but you can add more by supplying your own `highlight.js` file: - apache - armasm - bash - c - coffeescript - cpp - csharp - css - d - diff - go - handlebars - haskell - http - ini - java - javascript - json - julia - kotlin - less - lua - makefile - markdown - nginx - nim - nix - objectivec - perl - php - plaintext - properties - python - r - ruby - rust - scala - scss - shell - sql - swift - typescript - x86asm - xml - yaml ## Custom theme Like the rest of the theme, the files used for syntax highlighting can be overridden with your own. - ***highlight.js*** normally you shouldn't have to overwrite this file, unless you want to use a more recent version. - ***highlight.css*** theme used by highlight.js for syntax highlighting. If you want to use another theme for `highlight.js` download it from their website, or make it yourself, rename it to `highlight.css` and put it in the `theme` folder of your book. Now your theme will be used instead of the default theme. ## Improve default theme If you think the default theme doesn't look quite right for a specific language, or could be improved, feel free to [submit a new issue](https://github.com/rust-lang/mdBook/issues) explaining what you have in mind and I will take a look at it. You could also create a pull-request with the proposed improvements. Overall the theme should be light and sober, without too many flashy colors. ================================================ FILE: guide/src/guide/README.md ================================================ # User guide This user guide provides an introduction to basic concepts of using mdBook. - [Installation](installation.md) - [Reading Books](reading.md) - [Creating a Book](creating.md) ================================================ FILE: guide/src/guide/creating.md ================================================ # Creating a book Once you have the `mdbook` CLI tool installed, you can use it to create and render a book. ## Initializing a book The `mdbook init` command will create a new directory containing an empty book for you to get started. Give it the name of the directory that you want to create: ```sh mdbook init my-first-book ``` It will ask a few questions before generating the book. After answering the questions, you can change the current directory into the new book: ```sh cd my-first-book ``` There are several ways to render a book, but one of the easiest methods is to use the `serve` command, which will build your book and start a local webserver: ```sh mdbook serve --open ``` The `--open` option will open your default web browser to view your new book. You can leave the server running even while you edit the content of the book, and `mdbook` will automatically rebuild the output *and* automatically refresh your web browser. Check out the [CLI Guide](../cli/index.html) for more information about other `mdbook` commands and CLI options. ## Anatomy of a book A book is built from several files which define the settings and layout of the book. ### `book.toml` In the root of your book, there is a `book.toml` file which contains settings for describing how to build your book. This is written in the [TOML markup language](https://toml.io/). The default settings are usually good enough to get you started. When you are interested in exploring more features and options that mdBook provides, check out the [Configuration chapter](../format/configuration/index.html) for more details. A very basic `book.toml` can be as simple as this: ```toml [book] title = "My First Book" ``` ### `SUMMARY.md` The next major part of a book is the summary file located at `src/SUMMARY.md`. This file contains a list of all the chapters in the book. Before a chapter can be viewed, it must be added to this list. Here's a basic summary file with a few chapters: ```md # Summary [Introduction](README.md) - [My First Chapter](my-first-chapter.md) - [Nested example](nested/README.md) - [Sub-chapter](nested/sub-chapter.md) ``` Try opening up `src/SUMMARY.md` in your editor and adding a few chapters. If any of the chapter files do not exist, `mdbook` will automatically create them for you. For more details on other formatting options for the summary file, check out the [Summary chapter](../format/summary.md). ### Source files The content of your book is all contained in the `src` directory. Each chapter is a separate Markdown file. Typically, each chapter starts with a level 1 heading with the title of the chapter. ```md # My First Chapter Fill out your content here. ``` The precise layout of the files is up to you. The organization of the files will correspond to the HTML files generated, so keep in mind that the file layout is part of the URL of each chapter. While the `mdbook serve` command is running, you can open any of the chapter files and start editing them. Each time you save the file, `mdbook` will rebuild the book and refresh your web browser. Check out the [Markdown chapter](../format/markdown.md) for more information on formatting the content of your chapters. All other files in the `src` directory will be included in the output. So if you have images or other static files, just include them somewhere in the `src` directory. ## Publishing a book Once you've written your book, you may want to host it somewhere for others to view. The first step is to build the output of the book. This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located: ```sh mdbook build ``` This will generate a directory named `book` which contains the HTML content of your book. You can then place this directory on any web server to host it. For more information about publishing and deploying, check out the [Continuous Integration chapter](../continuous-integration.md) for more. ================================================ FILE: guide/src/guide/installation.md ================================================ # Installation There are multiple ways to install the mdBook CLI tool. Choose any one of the methods below that best suit your needs. If you are installing mdBook for automatic deployment, check out the [continuous integration] chapter for more examples on how to install. [continuous integration]: ../continuous-integration.md ## Pre-compiled binaries Executable binaries are available for download on the [GitHub Releases page][releases]. Download the binary for your platform (Windows, macOS, or Linux) and extract the archive. The archive contains an `mdbook` executable which you can run to build your books. To make it easier to run, put the path to the binary into your `PATH`. [releases]: https://github.com/rust-lang/mdBook/releases ## Build from source using Rust 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.88. Once you have installed Rust, the following command can be used to build and install mdBook: ```sh cargo install mdbook ``` This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). You can run `cargo install mdbook` again whenever you want to update to a new version. That command will check if there is a newer version, and re-install mdBook if a newer version is found. To uninstall, run the command `cargo uninstall mdbook`. [Rust installation page]: https://www.rust-lang.org/tools/install [crates.io]: https://crates.io/ ### Installing the latest master version The version published to crates.io will ever so slightly be behind the version hosted on GitHub. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***! ```sh cargo install --git https://github.com/rust-lang/mdBook.git mdbook ``` Again, make sure to add the Cargo bin directory to your `PATH`. ## Modifying and contributing If you are interested in making modifications to mdBook itself, check out the [Contributing Guide] for more information. [Contributing Guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md ================================================ FILE: guide/src/guide/reading.md ================================================ # Reading books This chapter gives an introduction on how to interact with a book produced by mdBook. This assumes you are reading an HTML book. The options and formatting will be different for other output formats such as PDF. A book is organized into *chapters*. Each chapter is a separate page. Chapters can be nested into a hierarchy of sub-chapters. Typically, each chapter will be organized into a series of *headings* to subdivide a chapter. ## Navigation There are several methods for navigating through the chapters of a book. The **sidebar** on the left provides a list of all chapters. Clicking on any of the chapter titles will load that page. The sidebar may not automatically appear if the window is too narrow, particularly on mobile displays. In that situation, the menu icon (three horizontal bars) at the top-left of the page can be pressed to open and close the sidebar. The **arrow buttons** at the bottom of the page can be used to navigate to the previous or the next chapter. The **left and right arrow keys** on the keyboard can be used to navigate to the previous or the next chapter. ## Top menu bar The menu bar at the top of the page provides some icons for interacting with the book. The icons displayed will depend on the settings of how the book was generated. | Icon | Description | |------|-------------| | | Opens and closes the chapter listing sidebar. | | | Opens a picker to choose a different color theme. | | | Opens a search bar for searching within the book. | | | Instructs the web browser to print the entire book. | | | Opens a link to the website that hosts the source code of the book. | | | Opens a page to directly edit the source of the page you are currently reading. | Tapping the menu bar will scroll the page to the top. ## Search Each book has a built-in search system. Pressing the search icon in the menu bar, or pressing the / or S key on the keyboard will open an input box for entering search terms. Typing some terms will show matching chapters and sections in real time. Clicking any of the results will jump to that section. The up and down arrow keys can be used to navigate the results, and enter will open the highlighted section. After loading a search result, the matching search terms will be highlighted in the text. Clicking a highlighted word or pressing the Escape key will remove the highlighting. ## Code blocks mdBook books are often used for programming projects, and thus support highlighting code blocks and samples. Code blocks may contain several different icons for interacting with them: | Icon | Description | |------|-------------| | | Copies the code block into your local clipboard, to allow pasting into another application. | | | For Rust code examples, this will execute the sample code and display the compiler output just below the example (see [playground]). | | | For Rust code examples, this will toggle visibility of "hidden" lines. Sometimes, larger examples will hide lines which are not particularly relevant to what is being illustrated (see [hiding code lines]). | | | For [editable code examples][editor], this will undo any changes you have made. | Here's an example: ```rust println!("Hello, World!"); ``` [editor]: ../format/theme/editor.md [playground]: ../format/mdbook.md#rust-playground [hiding code lines]: ../format/mdbook.md#hiding-code-lines ================================================ FILE: guide/src/misc/contributors.md ================================================ # Contributors Here is a list of the contributors who have helped improving mdBook. Big shout-out to them! - [mdinger](https://github.com/mdinger) - Kevin ([kbknapp](https://github.com/kbknapp)) - Steve Klabnik ([steveklabnik](https://github.com/steveklabnik)) - Adam Solove ([asolove](https://github.com/asolove)) - Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen)) - [funnkill](https://github.com/funkill) - Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang)) - [Michael-F-Bryan](https://github.com/Michael-F-Bryan) - Chris Spiegel ([cspiegel](https://github.com/cspiegel)) - [projektir](https://github.com/projektir) - [Phaiax](https://github.com/Phaiax) - Matt Ickstadt ([mattico](https://github.com/mattico)) - Weihang Lo ([weihanglo](https://github.com/weihanglo)) - Avision Ho ([avisionh](https://github.com/avisionh)) - Vivek Akupatni ([apatniv](https://github.com/apatniv)) - Eric Huss ([ehuss](https://github.com/ehuss)) - Josh Rotenberg ([joshrotenberg](https://github.com/joshrotenberg)) If you feel you're missing from this list, feel free to add yourself in a PR. ================================================ FILE: rustfmt.toml ================================================ style_edition = "2024" ================================================ FILE: src/cmd/build.rs ================================================ use super::command_prelude::*; use crate::{get_book_dir, open}; use anyhow::Result; use mdbook_driver::MDBook; use tracing::error; // Create clap subcommand arguments pub fn make_subcommand() -> Command { Command::new("build") .about("Builds a book from its markdown files") .arg_dest_dir() .arg_root_dir() .arg_open() } // Build command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let mut book = MDBook::load(book_dir)?; set_dest_dir(args, &mut book); book.build()?; if args.get_flag("open") { // FIXME: What's the right behaviour if we don't use the HTML renderer? let path = book.build_dir_for("html").join("index.html"); if !path.exists() { error!("No chapter available to open"); std::process::exit(1) } open(path); } Ok(()) } ================================================ FILE: src/cmd/clean.rs ================================================ use super::command_prelude::*; use crate::get_book_dir; use anyhow::Context; use anyhow::Result; use mdbook_driver::MDBook; use std::mem::take; use std::path::PathBuf; use std::{fmt, fs}; // Create clap subcommand arguments pub fn make_subcommand() -> Command { Command::new("clean") .about("Deletes a built book") .arg_dest_dir() .arg_root_dir() } // Clean command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let book = MDBook::load(book_dir)?; let dir_to_remove = match args.get_one::("dest-dir") { Some(dest_dir) => std::env::current_dir() .expect("current dir should be valid") .join(dest_dir), None => book.root.join(&book.config.build.build_dir), }; let removed = Clean::new(&dir_to_remove)?; println!("{removed}"); Ok(()) } /// Formats a number of bytes into a human readable SI-prefixed size. /// Returns a tuple of `(quantity, units)`. pub fn human_readable_bytes(bytes: u64) -> (f32, &'static str) { static UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; let bytes = bytes as f32; let i = ((bytes.log2() / 10.0) as usize).min(UNITS.len() - 1); (bytes / 1024_f32.powi(i as i32), UNITS[i]) } #[derive(Debug)] pub struct Clean { num_files_removed: u64, num_dirs_removed: u64, total_bytes_removed: u64, } impl Clean { fn new(dir: &PathBuf) -> Result { let mut files = vec![dir.clone()]; let mut children = Vec::new(); let mut num_files_removed = 0; let mut num_dirs_removed = 0; let mut total_bytes_removed = 0; if dir.exists() { while !files.is_empty() { for file in files { if let Ok(meta) = file.metadata() { // Note: This can over-count bytes removed for hard-linked // files. It also under-counts since it only counts the exact // byte sizes and not the block sizes. total_bytes_removed += meta.len(); } if file.is_file() { num_files_removed += 1; } else if file.is_dir() { num_dirs_removed += 1; for entry in fs::read_dir(file)? { children.push(entry?.path()); } } } files = take(&mut children); } fs::remove_dir_all(&dir).with_context(|| "Unable to remove the build directory")?; } Ok(Clean { num_files_removed, num_dirs_removed, total_bytes_removed, }) } } impl fmt::Display for Clean { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Removed ")?; match (self.num_files_removed, self.num_dirs_removed) { (0, 0) => write!(f, "0 files")?, (0, 1) => write!(f, "1 directory")?, (0, 2..) => write!(f, "{} directories", self.num_dirs_removed)?, (1, _) => write!(f, "1 file")?, (2.., _) => write!(f, "{} files", self.num_files_removed)?, } if self.total_bytes_removed == 0 { Ok(()) } else { // Don't show a fractional number of bytes. if self.total_bytes_removed < 1024 { write!(f, ", {}B total", self.total_bytes_removed) } else { let (bytes, unit) = human_readable_bytes(self.total_bytes_removed); write!(f, ", {bytes:.2}{unit} total") } } } } ================================================ FILE: src/cmd/command_prelude.rs ================================================ //! Helpers for building the command-line arguments for commands. pub use clap::{Arg, ArgMatches, Command, arg}; use mdbook_driver::MDBook; use std::path::PathBuf; pub trait CommandExt: Sized { fn _arg(self, arg: Arg) -> Self; fn arg_dest_dir(self) -> Self { self._arg( Arg::new("dest-dir") .short('d') .long("dest-dir") .value_name("dest-dir") .value_parser(clap::value_parser!(PathBuf)) .help( "Output directory for the book\n\ Relative paths are interpreted relative to the current directory.\n\ If omitted, mdBook uses build.build-dir from book.toml \ or defaults to `./book`.", ), ) } fn arg_root_dir(self) -> Self { self._arg( Arg::new("dir") .help( "Root directory for the book\n\ (Defaults to the current directory when omitted)", ) .value_parser(clap::value_parser!(PathBuf)), ) } fn arg_open(self) -> Self { self._arg(arg!(-o --open "Opens the compiled book in a web browser")) } #[cfg(any(feature = "watch", feature = "serve"))] fn arg_watcher(self) -> Self { #[cfg(feature = "watch")] return self._arg( Arg::new("watcher") .long("watcher") .value_parser(["poll", "native"]) .default_value("poll") .help("The filesystem watching technique"), ); #[cfg(not(feature = "watch"))] return self; } } impl CommandExt for Command { fn _arg(self, arg: Arg) -> Self { self.arg(arg) } } pub fn set_dest_dir(args: &ArgMatches, book: &mut MDBook) { if let Some(dest_dir) = args.get_one::("dest-dir") { let build_dir = std::env::current_dir() .expect("current dir should be valid") .join(dest_dir); book.config.build.build_dir = build_dir; } } ================================================ FILE: src/cmd/init.rs ================================================ use crate::get_book_dir; use anyhow::Result; use clap::{ArgMatches, Command as ClapCommand, arg}; use mdbook_core::config; use mdbook_driver::MDBook; use std::io; use std::io::Write; use std::process::Command; use tracing::debug; // Create clap subcommand arguments pub fn make_subcommand() -> ClapCommand { ClapCommand::new("init") .about("Creates the boilerplate structure and files for a new book") .arg( arg!([dir] "Directory to create the book in\n\ (Defaults to the current directory when omitted)" ) .value_parser(clap::value_parser!(std::path::PathBuf)), ) .arg(arg!(--theme "Copies the default theme into your source folder")) .arg(arg!(--force "Skips confirmation prompts")) .arg(arg!(--title "Sets the book title")) .arg( arg!(--ignore <ignore> "Creates a VCS ignore file (i.e. .gitignore)") .value_parser(["none", "git"]), ) } // Init command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let mut builder = MDBook::init(&book_dir); let mut config = config::Config::default(); // If flag `--theme` is present, copy theme to src if args.get_flag("theme") { let theme_dir = book_dir.join("theme"); println!(); println!("Copying the default theme to {}", theme_dir.display()); // Skip this if `--force` is present if !args.get_flag("force") && theme_dir.exists() { println!("This could potentially overwrite files already present in that directory."); print!("\nAre you sure you want to continue? (y/n) "); // Read answer from user and exit if it's not 'yes' if confirm() { builder.copy_theme(true); } } else { builder.copy_theme(true); } } if let Some(ignore) = args.get_one::<String>("ignore").map(|s| s.as_str()) { match ignore { "git" => builder.create_gitignore(true), _ => builder.create_gitignore(false), }; } else if !args.get_flag("force") { println!("\nDo you want a .gitignore to be created? (y/n)"); if confirm() { builder.create_gitignore(true); } } config.book.title = if args.contains_id("title") { args.get_one::<String>("title").map(String::from) } else if args.get_flag("force") { None } else { request_book_title() }; if let Some(author) = get_author_name() { debug!("Obtained user name from gitconfig: {:?}", author); config.book.authors.push(author); } builder.with_config(config); builder.build()?; println!("\nAll done, no errors..."); Ok(()) } /// Obtains author name from git config file by running the `git config` command. fn get_author_name() -> Option<String> { let output = Command::new("git") .args(["config", "--get", "user.name"]) .output() .ok()?; if output.status.success() { Some(String::from_utf8_lossy(&output.stdout).trim().to_owned()) } else { None } } /// Request book title from user and return if provided. fn request_book_title() -> Option<String> { println!("What title would you like to give the book? "); io::stdout().flush().unwrap(); let mut resp = String::new(); io::stdin().read_line(&mut resp).unwrap(); let resp = resp.trim(); if resp.is_empty() { None } else { Some(resp.into()) } } // Simple function for user confirmation fn confirm() -> bool { io::stdout().flush().unwrap(); let mut s = String::new(); io::stdin().read_line(&mut s).ok(); matches!(s.trim(), "Y" | "y" | "yes" | "Yes") } ================================================ FILE: src/cmd/mod.rs ================================================ //! Subcommand modules for the `mdbook` binary. pub mod build; pub mod clean; pub mod command_prelude; pub mod init; #[cfg(feature = "serve")] pub mod serve; pub mod test; #[cfg(feature = "watch")] pub mod watch; ================================================ FILE: src/cmd/serve.rs ================================================ use super::command_prelude::*; #[cfg(feature = "watch")] use super::watch; use crate::{get_book_dir, open}; use anyhow::Result; use axum::Router; use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::routing::get; use clap::builder::NonEmptyStringValueParser; use futures_util::StreamExt; use futures_util::sink::SinkExt; use mdbook_driver::MDBook; use std::net::{SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use tokio::sync::broadcast; use tower_http::services::{ServeDir, ServeFile}; use tracing::{error, info, trace}; /// The HTTP endpoint for the websocket used to trigger reloads when a file changes. const LIVE_RELOAD_ENDPOINT: &str = "__livereload"; // Create clap subcommand arguments pub fn make_subcommand() -> Command { Command::new("serve") .about("Serves a book at http://localhost:3000, and rebuilds it on changes") .arg_dest_dir() .arg_root_dir() .arg( Arg::new("hostname") .short('n') .long("hostname") .num_args(1) .default_value("localhost") .value_parser(NonEmptyStringValueParser::new()) .help("Hostname to listen on for HTTP connections"), ) .arg( Arg::new("port") .short('p') .long("port") .num_args(1) .default_value("3000") .value_parser(NonEmptyStringValueParser::new()) .help("Port to use for HTTP connections"), ) .arg_open() .arg_watcher() } // Serve command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let mut book = MDBook::load(&book_dir)?; let port = args.get_one::<String>("port").unwrap(); let hostname = args.get_one::<String>("hostname").unwrap(); let open_browser = args.get_flag("open"); let address = format!("{hostname}:{port}"); let update_config = |book: &mut MDBook| { book.config .set("output.html.live-reload-endpoint", LIVE_RELOAD_ENDPOINT) .expect("live-reload-endpoint update failed"); set_dest_dir(args, book); // Override site-url for local serving of the 404 file book.config.set("output.html.site-url", "/").unwrap(); }; update_config(&mut book); book.build()?; let sockaddr: SocketAddr = address .to_socket_addrs()? .next() .ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?; let build_dir = book.build_dir_for("html"); let html_config = book.config.html_config().unwrap_or_default(); let file_404 = html_config.get_404_output_file(); // A channel used to broadcast to any websockets to reload when a file changes. let (tx, _rx) = tokio::sync::broadcast::channel::<Message>(100); let reload_tx = tx.clone(); let thread_handle = std::thread::spawn(move || { serve(build_dir, sockaddr, reload_tx, &file_404); }); let serving_url = format!("http://{address}"); info!("Serving on: {}", serving_url); if open_browser { open(serving_url); } #[cfg(feature = "watch")] { let watcher = watch::WatcherKind::from_str(args.get_one::<String>("watcher").unwrap()); watch::rebuild_on_change(watcher, &book_dir, &update_config, &move || { let _ = tx.send(Message::text("reload")); }); } let _ = thread_handle.join(); Ok(()) } #[tokio::main] async fn serve( build_dir: PathBuf, address: SocketAddr, reload_tx: broadcast::Sender<Message>, file_404: &str, ) { let reload_tx_clone = reload_tx.clone(); // WebSocket handler for live reload let websocket_handler = move |ws: WebSocketUpgrade| async move { let reload_tx = reload_tx_clone.clone(); ws.on_upgrade(move |socket| websocket_connection(socket, reload_tx)) }; let app = Router::new() .route(&format!("/{LIVE_RELOAD_ENDPOINT}"), get(websocket_handler)) .fallback_service( ServeDir::new(&build_dir).not_found_service(ServeFile::new(build_dir.join(file_404))), ); std::panic::set_hook(Box::new(move |panic_info| { // exit if serve panics error!("Unable to serve: {}", panic_info); std::process::exit(1); })); let listener = tokio::net::TcpListener::bind(&address) .await .unwrap_or_else(|e| panic!("Unable to bind to {address}: {e}")); axum::serve(listener, app).await.unwrap(); } async fn websocket_connection(ws: WebSocket, reload_tx: broadcast::Sender<Message>) { let (mut user_ws_tx, _user_ws_rx) = ws.split(); let mut rx = reload_tx.subscribe(); trace!("websocket got connection"); if let Ok(m) = rx.recv().await { trace!("notify of reload"); let _ = user_ws_tx.send(m).await; } } ================================================ FILE: src/cmd/test.rs ================================================ use super::command_prelude::*; use crate::get_book_dir; use anyhow::Result; use clap::ArgAction; use clap::builder::NonEmptyStringValueParser; use mdbook_driver::MDBook; // Create clap subcommand arguments pub fn make_subcommand() -> Command { Command::new("test") .about("Tests that a book's Rust code samples compile") .arg_root_dir() .arg( Arg::new("chapter") .short('c') .long("chapter") .value_name("chapter"), ) .arg( Arg::new("library-path") .short('L') .long("library-path") .value_name("dir") .value_delimiter(',') .value_parser(NonEmptyStringValueParser::new()) .action(ArgAction::Append) .help( "A comma-separated list of directories to add to the crate \ search path when building tests", ), ) } // test command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let library_paths: Vec<&str> = args .get_many("library-path") .map(|it| it.map(String::as_str).collect()) .unwrap_or_default(); let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str()); let book_dir = get_book_dir(args); let mut book = MDBook::load(book_dir)?; match chapter { Some(_) => book.test_chapter(library_paths, chapter), None => book.test(library_paths), }?; Ok(()) } ================================================ FILE: src/cmd/watch/native.rs ================================================ //! A filesystem watcher using native operating system facilities. use ignore::gitignore::Gitignore; use mdbook_driver::MDBook; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; use std::thread::sleep; use std::time::Duration; use tracing::{error, info, warn}; pub fn rebuild_on_change( book_dir: &Path, update_config: &dyn Fn(&mut MDBook), post_build: &dyn Fn(), ) { use notify::RecursiveMode::*; let mut book = MDBook::load(book_dir).unwrap_or_else(|e| { error!("failed to load book: {e}"); std::process::exit(1); }); // Create a channel to receive the events. let (tx, rx) = channel(); let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) { Ok(d) => d, Err(e) => { error!("Error while trying to watch the files:\n\n\t{:?}", e); std::process::exit(1) } }; let watcher = debouncer.watcher(); // Add the source directory to the watcher if let Err(e) = watcher.watch(&book.source_dir(), Recursive) { error!("Error while watching {:?}:\n {:?}", book.source_dir(), e); std::process::exit(1); }; let _ = watcher.watch(&book.theme_dir(), Recursive); // Add the book.toml file to the watcher if it exists let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive); for dir in &book.config.build.extra_watch_dirs { let path = book.root.join(dir); let canonical_path = path.canonicalize().unwrap_or_else(|e| { error!("Error while watching extra directory {path:?}:\n {e}"); std::process::exit(1); }); if let Err(e) = watcher.watch(&canonical_path, Recursive) { error!( "Error while watching extra directory {:?}:\n {:?}", canonical_path, e ); std::process::exit(1); } } info!("Listening for changes..."); loop { let first_event = rx.recv().unwrap(); sleep(Duration::from_millis(50)); let other_events = rx.try_iter(); let all_events = std::iter::once(first_event).chain(other_events); let paths: Vec<_> = all_events .filter_map(|event| match event { Ok(events) => Some(events), Err(error) => { warn!("error while watching for changes: {error}"); None } }) .flatten() .map(|event| event.path) .collect(); // If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally // ignored by gitignore. So we handle this case by including such files into the watched paths list. let any_external_paths = paths.iter().filter(|p| !p.starts_with(&book.root)).cloned(); let mut paths = remove_ignored_files(&book.root, &paths[..]); paths.extend(any_external_paths); if !paths.is_empty() { info!("Files changed: {paths:?}"); match MDBook::load(book_dir) { Ok(mut b) => { update_config(&mut b); if let Err(e) = b.build() { error!("failed to build the book: {e:?}"); } else { post_build(); } book = b; } Err(e) => error!("failed to load book config: {e:?}"), } } } } fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> { if paths.is_empty() { return vec![]; } match super::find_gitignore(book_root) { Some(gitignore_path) => { let (ignore, err) = Gitignore::new(&gitignore_path); if let Some(err) = err { warn!( "error reading gitignore `{}`: {err}", gitignore_path.display() ); } filter_ignored_files(ignore, paths) } None => { // There is no .gitignore file. paths.iter().map(|path| path.to_path_buf()).collect() } } } // Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk. // For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981). fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> { let ignore_root = ignore .path() .canonicalize() .expect("ignore root canonicalize error"); paths .iter() .filter(|path| { let relative_path = pathdiff::diff_paths(&path, &ignore_root) .expect("One of the paths should be an absolute"); !ignore .matched_path_or_any_parents(&relative_path, relative_path.is_dir()) .is_ignore() }) .map(|path| path.to_path_buf()) .collect() } #[cfg(test)] mod tests { use super::*; use ignore::gitignore::GitignoreBuilder; use std::env; #[test] fn test_filter_ignored_files() { let current_dir = env::current_dir().unwrap(); let ignore = GitignoreBuilder::new(¤t_dir) .add_line(None, "*.html") .unwrap() .build() .unwrap(); let should_remain = current_dir.join("record.text"); let should_filter = current_dir.join("index.html"); let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]); assert_eq!(remain, vec![should_remain]) } #[test] fn filter_ignored_files_should_handle_parent_dir() { let current_dir = env::current_dir().unwrap(); let ignore = GitignoreBuilder::new(¤t_dir) .add_line(None, "*.html") .unwrap() .build() .unwrap(); let parent_dir = current_dir.join(".."); let should_remain = parent_dir.join("record.text"); let should_filter = parent_dir.join("index.html"); let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]); assert_eq!(remain, vec![should_remain]) } } ================================================ FILE: src/cmd/watch/poller.rs ================================================ //! A simple poll-based filesystem watcher. //! //! This exists because the native change notifications have historically had //! lots of problems. Various operating systems and different filesystems have //! had problems correctly reporting changes. use ignore::gitignore::Gitignore; use mdbook_driver::MDBook; use pathdiff::diff_paths; use std::collections::HashMap; use std::fs::FileType; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant, SystemTime}; use tracing::{debug, error, info, trace, warn}; use walkdir::WalkDir; /// Calls the closure when a book source file is changed, blocking indefinitely. pub fn rebuild_on_change( book_dir: &Path, update_config: &dyn Fn(&mut MDBook), post_build: &dyn Fn(), ) { let mut book = MDBook::load(book_dir).unwrap_or_else(|e| { error!("failed to load book: {e}"); std::process::exit(1); }); let mut watcher = Watcher::new(book_dir); info!("Watching for changes..."); // Scan once to initialize the starting point. watcher.set_roots(&book); watcher.scan(); // Track average scan time, to help investigate if the poller is taking // undesirably long. This is not a rigorous benchmark, just a rough // estimate. const AVG_SIZE: usize = 60; let mut avgs = vec![0.0; AVG_SIZE]; let mut avg_i = 0; loop { std::thread::sleep(Duration::new(1, 0)); let start = Instant::now(); let paths = watcher.scan(); let elapsed = start.elapsed().as_secs_f64(); avgs[avg_i] = elapsed; avg_i += 1; if avg_i >= AVG_SIZE { avg_i = 0; let avg = avgs.iter().sum::<f64>() / (avgs.len() as f64); trace!( "scan average time: {avg:.2}s, scan size is {}", watcher.path_data.len() ); } if !paths.is_empty() { info!("Files changed: {paths:?}"); match MDBook::load(book_dir) { Ok(mut b) => { update_config(&mut b); if let Err(e) = b.build() { error!("failed to build the book: {e:?}"); } else { post_build(); } book = b; watcher.set_roots(&book); } Err(e) => error!("failed to load book config: {e:?}"), } } } } #[derive(PartialEq)] struct PathData { file_type: FileType, mtime: SystemTime, size: u64, } /// A very simple poll-watcher that scans for modified files. #[derive(Default)] struct Watcher { /// The root paths where it will recursively scan for changes. root_paths: Vec<PathBuf>, /// Data about files on disk. path_data: HashMap<PathBuf, PathData>, /// Filters paths that will be watched. ignore: Option<(PathBuf, Gitignore)>, } impl Watcher { fn new(book_root: &Path) -> Watcher { // FIXME: ignore should be reloaded when it changes. let ignore = super::find_gitignore(book_root).map(|gitignore_path| { let (ignore, err) = Gitignore::new(&gitignore_path); if let Some(err) = err { warn!( "error reading gitignore `{}`: {err}", gitignore_path.display() ); } // Note: The usage of `canonicalize` may encounter occasional // failures on the Windows platform, presenting a potential risk. // For more details, refer to [Pull Request // #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981). let ignore_path = ignore .path() .canonicalize() .expect("ignore root canonicalize error"); (ignore_path, ignore) }); Watcher { ignore, ..Default::default() } } /// Sets the root directories where scanning will start. fn set_roots(&mut self, book: &MDBook) { let mut root_paths = vec![ book.source_dir(), book.theme_dir(), book.root.join("book.toml"), ]; root_paths.extend( book.config .build .extra_watch_dirs .iter() .map(|path| book.root.join(path)), ); if let Some(html_config) = book.config.html_config() { root_paths.extend( html_config .additional_css .iter() .chain(html_config.additional_js.iter()) .map(|path| book.root.join(path)), ); } self.root_paths = root_paths; } /// Scans for changes. /// /// Returns the paths that have changed. fn scan(&mut self) -> Vec<PathBuf> { let ignore = &self.ignore; let new_path_data: HashMap<_, _> = self .root_paths .iter() .filter(|root| root.exists()) .flat_map(|root| { WalkDir::new(root) .follow_links(true) .into_iter() .filter_entry(|entry| { if let Some((ignore_path, ignore)) = ignore { let path = entry.path(); // Canonicalization helps with removing `..` and // `.` entries, which can cause issues with // diff_paths. let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); let relative_path = diff_paths(&path, &ignore_path) .expect("One of the paths should be an absolute"); if ignore .matched_path_or_any_parents(&relative_path, relative_path.is_dir()) .is_ignore() { trace!("ignoring {path:?}"); return false; } } true }) .filter_map(move |entry| { let entry = match entry { Ok(e) => e, Err(e) => { debug!("failed to scan {root:?}: {e}"); return None; } }; if entry.file_type().is_dir() { // Changes to directories themselves aren't // particularly interesting. return None; } let path = entry.path().to_path_buf(); let meta = match entry.metadata() { Ok(meta) => meta, Err(e) => { debug!("failed to scan {path:?}: {e}"); return None; } }; let mtime = meta.modified().unwrap_or(SystemTime::UNIX_EPOCH); let pd = PathData { file_type: meta.file_type(), mtime, size: meta.len(), }; Some((path, pd)) }) }) .collect(); let mut paths = Vec::new(); for (new_path, new_data) in &new_path_data { match self.path_data.get(new_path) { Some(old_data) => { if new_data != old_data { paths.push(new_path.to_path_buf()); } } None => { paths.push(new_path.clone()); } } } for old_path in self.path_data.keys() { if !new_path_data.contains_key(old_path) { paths.push(old_path.to_path_buf()); } } self.path_data = new_path_data; paths } } #[cfg(test)] mod tests { use super::*; /// Helper for testing the watcher. fn check_watch_behavior( gitignore_path: &str, gitignore: &str, book_root_path: &str, ignored: &[&str], not_ignored: &[&str], extra_setup: &dyn Fn(&Path), ) { // Create the book and initialize things. let temp = tempfile::Builder::new() .prefix("mdbook-") .tempdir() .unwrap(); let root = temp.path(); let book_root = root.join(book_root_path); // eprintln!("book_root={book_root:?}",); MDBook::init(&book_root).build().unwrap(); std::fs::write(root.join(gitignore_path), gitignore).unwrap(); let create = |paths: &[&str]| { let mut paths = paths .iter() .map(|path| root.join(path)) .inspect(|path| { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(path, "initial content").unwrap(); }) .map(|path| path.canonicalize().unwrap()) .collect::<Vec<_>>(); paths.sort(); paths }; let ignored = create(ignored); let not_ignored = create(not_ignored); extra_setup(&book_root); // Create a watcher and check its behavior. let book = MDBook::load(&book_root).unwrap(); let mut watcher = Watcher::new(&book_root); watcher.set_roots(&book); // Do an initial scan to initialize its state. watcher.scan(); // Verify the steady state is empty. let changed = watcher.scan(); assert_eq!(changed, Vec::<PathBuf>::new()); // Modify all files, and verify that only not_ignored are detected. for path in ignored.iter().chain(not_ignored.iter()) { std::fs::write(path, "modified").unwrap(); } let changed = watcher.scan(); let mut changed = changed .into_iter() .map(|p| p.canonicalize().unwrap()) .collect::<Vec<_>>(); changed.sort(); assert_eq!(changed, not_ignored); // Verify again that steady state is empty. let changed = watcher.scan(); assert_eq!(changed, Vec::<PathBuf>::new()); } #[test] fn test_ignore() { // Basic gitignore test. check_watch_behavior( "foo/.gitignore", "*.tmp", "foo", &["foo/src/somefile.tmp"], &["foo/src/chapter.md"], &|_book_root| {}, ); } #[test] fn test_ignore_in_parent() { // gitignore is in the parent of the book check_watch_behavior( ".gitignore", "*.tmp\nsomedir/\n/inroot\n/foo/src/inbook\n", "foo", &[ "foo/src/somefile.tmp", "foo/src/somedir/somefile", "inroot/somefile", "foo/src/inbook/somefile", ], &["foo/src/inroot/somefile"], &|_book_root| {}, ); } #[test] fn test_ignore_canonical() { // test with path with .. check_watch_behavior( ".gitignore", "*.tmp\nsomedir/\n/foo/src/inbook\n", "bar/../foo", &[ "foo/src/somefile.tmp", "foo/src/somedir/somefile", "foo/src/inbook/somefile", ], &["foo/src/chapter.md"], &|_book_root| {}, ); } #[test] fn test_scan_extra_watch() { // Check behavior with extra-watch-dirs check_watch_behavior( ".gitignore", "*.tmp\n/outside-root/ignoreme\n/foo/examples/ignoreme\n", "foo", &[ "foo/src/somefile.tmp", "foo/examples/example.tmp", "outside-root/somefile.tmp", "outside-root/ignoreme", "foo/examples/ignoreme", ], &[ "foo/src/chapter.md", "foo/examples/example.rs", "foo/examples/example2.rs", "outside-root/image.png", ], &|book_root| { std::fs::write( book_root.join("book.toml"), r#" [book] title = "foo" [build] extra-watch-dirs = [ "examples", "../outside-root", ] "#, ) .unwrap(); }, ); } } ================================================ FILE: src/cmd/watch.rs ================================================ use super::command_prelude::*; use crate::{get_book_dir, open}; use anyhow::Result; use mdbook_driver::MDBook; use std::path::{Path, PathBuf}; use tracing::error; mod native; mod poller; // Create clap subcommand arguments pub fn make_subcommand() -> Command { Command::new("watch") .about("Watches a book's files and rebuilds it on changes") .arg_dest_dir() .arg_root_dir() .arg_open() .arg_watcher() } pub enum WatcherKind { Poll, Native, } impl WatcherKind { pub fn from_str(s: &str) -> WatcherKind { match s { "poll" => WatcherKind::Poll, "native" => WatcherKind::Native, _ => panic!("unsupported watcher {s}"), } } } // Watch command implementation pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let mut book = MDBook::load(&book_dir)?; let update_config = |book: &mut MDBook| { set_dest_dir(args, book); }; update_config(&mut book); if args.get_flag("open") { book.build()?; let path = book.build_dir_for("html").join("index.html"); if !path.exists() { error!("No chapter available to open"); std::process::exit(1) } open(path); } let watcher = WatcherKind::from_str(args.get_one::<String>("watcher").unwrap()); rebuild_on_change(watcher, &book_dir, &update_config, &|| {}); Ok(()) } pub fn rebuild_on_change( kind: WatcherKind, book_dir: &Path, update_config: &dyn Fn(&mut MDBook), post_build: &dyn Fn(), ) { match kind { WatcherKind::Poll => self::poller::rebuild_on_change(book_dir, update_config, post_build), WatcherKind::Native => self::native::rebuild_on_change(book_dir, update_config, post_build), } } fn find_gitignore(book_root: &Path) -> Option<PathBuf> { book_root .ancestors() .map(|p| p.join(".gitignore")) .find(|p| p.exists()) } ================================================ FILE: src/main.rs ================================================ //! The mdbook CLI. #![allow(unreachable_pub, reason = "not needed in a bin crate")] use anyhow::anyhow; use clap::{Arg, ArgMatches, Command}; use clap_complete::Shell; use mdbook_core::utils; use std::env; use std::ffi::OsStr; use std::path::PathBuf; use tracing::{error, info}; mod cmd; const VERSION: &str = concat!("v", clap::crate_version!()); fn main() { init_logger(); let command = create_clap_command(); // Check which subcommand the user ran... let res = match command.get_matches().subcommand() { Some(("init", sub_matches)) => cmd::init::execute(sub_matches), Some(("build", sub_matches)) => cmd::build::execute(sub_matches), Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches), #[cfg(feature = "watch")] Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches), #[cfg(feature = "serve")] Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches), Some(("test", sub_matches)) => cmd::test::execute(sub_matches), Some(("completions", sub_matches)) => (|| { let shell = sub_matches .get_one::<Shell>("shell") .ok_or_else(|| anyhow!("Shell name missing."))?; let mut complete_app = create_clap_command(); clap_complete::generate( *shell, &mut complete_app, "mdbook", &mut std::io::stdout().lock(), ); Ok(()) })(), _ => unreachable!(), }; if let Err(e) = res { utils::log_backtrace(&e); std::process::exit(101); } } /// Create a list of valid arguments and sub-commands fn create_clap_command() -> Command { let app = Command::new(clap::crate_name!()) .about(clap::crate_description!()) .author("Mathieu David <mathieudavid@mathieudavid.org>") .version(VERSION) .propagate_version(true) .arg_required_else_help(true) .after_help( "For more information about a specific command, try `mdbook <command> --help`\n\ The source code for mdBook is available at: https://github.com/rust-lang/mdBook", ) .subcommand(cmd::init::make_subcommand()) .subcommand(cmd::build::make_subcommand()) .subcommand(cmd::test::make_subcommand()) .subcommand(cmd::clean::make_subcommand()) .subcommand( Command::new("completions") .about("Generate shell completions for your shell to stdout") .arg( Arg::new("shell") .value_parser(clap::value_parser!(Shell)) .help("the shell to generate completions for") .value_name("SHELL") .required(true), ), ); #[cfg(feature = "watch")] let app = app.subcommand(cmd::watch::make_subcommand()); #[cfg(feature = "serve")] let app = app.subcommand(cmd::serve::make_subcommand()); app } fn init_logger() { let filter = tracing_subscriber::EnvFilter::builder() .with_env_var("MDBOOK_LOG") .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) .from_env_lossy(); let log_env = std::env::var("MDBOOK_LOG"); // Silence some particularly noisy dependencies unless the user // specifically asks for them. let silence_unless_specified = |filter: tracing_subscriber::EnvFilter, target| { if !log_env.as_ref().map_or(false, |s| { s.split(',').any(|directive| directive.starts_with(target)) }) { filter.add_directive(format!("{target}=warn").parse().unwrap()) } else { filter } }; let filter = silence_unless_specified(filter, "handlebars"); let filter = silence_unless_specified(filter, "html5ever"); // Don't show the target by default, since it generally isn't useful // unless you are overriding the level. let with_target = log_env.is_ok(); tracing_subscriber::fmt() .without_time() .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr())) .with_writer(std::io::stderr) .with_env_filter(filter) .with_target(with_target) .init(); } fn get_book_dir(args: &ArgMatches) -> PathBuf { if let Some(p) = args.get_one::<PathBuf>("dir") { // Check if path is relative from current dir, or absolute... if p.is_relative() { env::current_dir().unwrap().join(p) } else { p.to_path_buf() } } else { env::current_dir().expect("Unable to determine the current directory") } } fn open<P: AsRef<OsStr>>(path: P) { info!("Opening web browser"); if let Err(e) = opener::open(path) { error!("Error opening web browser: {}", e); } } #[test] fn verify_app() { create_clap_command().debug_assert(); } ================================================ FILE: tests/gui/books/all-summary/README.md ================================================ # All summary This GUI test book tests all the different kinds of book items in the summary. ================================================ FILE: tests/gui/books/all-summary/book.toml ================================================ [book] title = "all-summary" ================================================ FILE: tests/gui/books/all-summary/src/SUMMARY.md ================================================ # Summary [Prefix 1](prefix-1.md) [Prefix 2](prefix-2.md) - [Introduction](intro.md) - [Draft]() # Part 1 - [P1 C1](part-1/chapter-1.md) --- # Part 2 - [P2 C1](part-2/chapter-1.md) [Suffix 1](suffix-1.md) [Suffix 2](suffix-2.md) ================================================ FILE: tests/gui/books/all-summary/src/intro.md ================================================ # Introduction ================================================ FILE: tests/gui/books/all-summary/src/part-1/chapter-1.md ================================================ # P1 C1 ================================================ FILE: tests/gui/books/all-summary/src/part-2/chapter-1.md ================================================ # P2 C1 ================================================ FILE: tests/gui/books/all-summary/src/prefix-1.md ================================================ # Prefix 1 ================================================ FILE: tests/gui/books/all-summary/src/prefix-2.md ================================================ # Prefix 2 ================================================ FILE: tests/gui/books/all-summary/src/suffix-1.md ================================================ # Suffix 1 ================================================ FILE: tests/gui/books/all-summary/src/suffix-2.md ================================================ # Suffix 2 ================================================ FILE: tests/gui/books/basic/book.toml ================================================ [book] title = "basic" ================================================ FILE: tests/gui/books/basic/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/gui/books/basic/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/gui/books/heading-nav/README.md ================================================ # Heading nav This GUI test book is used for testing sidebar heading navigation. ================================================ FILE: tests/gui/books/heading-nav/book.toml ================================================ [book] title = "heading-nav" ================================================ FILE: tests/gui/books/heading-nav/src/SUMMARY.md ================================================ # Summary - [Empty page](empty.md) - [Large text before first heading](large-intro.md) - [Normal text before first heading](normal-intro.md) - [Collapsed headings](collapsed.md) - [Headings with markup](markup.md) - [Current scrolls to bottom](current-to-bottom.md) - [Unusual heading levels](unusual-heading-levels.md) - [Filtered headings](filtered-headings.md) ================================================ FILE: tests/gui/books/heading-nav/src/collapsed.md ================================================ # Collapsed headings Tests collapsed headings. ## Heading 1 1\ 2\ 3\ 4\ 5 ### Heading 1.1 1\ 2\ 3\ 4\ 5 ### Heading 1.2 1\ 2\ 3\ 4\ 5 #### Heading 1.2.1 1\ 2\ 3\ 4\ 5 #### Heading 1.2.2 1\ 2\ 3\ 4\ 5 ### Heading 1.3 1\ 2\ 3\ 4\ 5 ## Heading 2 1\ 2\ 3\ 4\ 5 ### Heading 2.1 1\ 2\ 3\ 4\ 5 #### Heading 2.1.1 1\ 2\ 3\ 4\ 5 ##### Heading 2.1.1.1 1\ 2\ 3\ 4\ 5 ###### Heading 2.1.1.1.1 1\ 2\ 3\ 4\ 5 ================================================ FILE: tests/gui/books/heading-nav/src/current-to-bottom.md ================================================ # Current scrolls to bottom Checks that the "current" header works even when there are headers near the bottom. ## First header <span id="scroll-to-1">1</span>\ <span id="scroll-to-2">2</span>\ <span id="scroll-to-3">3</span>\ <span id="scroll-to-4">4</span>\ <span id="scroll-to-5">5</span>\ <span id="scroll-to-6">6</span>\ <span id="scroll-to-7">7</span>\ <span id="scroll-to-8">8</span>\ <span id="scroll-to-9">9</span>\ <span id="scroll-to-10">10</span>\ <span id="scroll-to-11">11</span>\ <span id="scroll-to-12">12</span>\ <span id="scroll-to-13">13</span>\ <span id="scroll-to-14">14</span>\ <span id="scroll-to-15">15</span>\ <span id="scroll-to-16">16</span>\ <span id="scroll-to-17">17</span>\ <span id="scroll-to-18">18</span>\ <span id="scroll-to-19">19</span>\ <span id="scroll-to-20">20</span> ## Second header <span id="scroll-to-21">21</span> ### Second sub-header <span id="scroll-to-22">22</span> ## Third header <span id="scroll-to-23">23</span> ## Fourth header <span id="scroll-to-24">24</span> ## Fifth header <span id="scroll-to-25">25</span> ================================================ FILE: tests/gui/books/heading-nav/src/empty.md ================================================ # Empty page ================================================ FILE: tests/gui/books/heading-nav/src/filtered-headings.md ================================================ # Filtered headings ## Skateboard Checking for search marking. ================================================ FILE: tests/gui/books/heading-nav/src/large-intro.md ================================================ # Large text before first heading This tests what happens if there is a lot of text before the first header, which is off the bottom of the screen. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ## First header Text for first header. ================================================ FILE: tests/gui/books/heading-nav/src/markup.md ================================================ # Headings with markup Tests that heading markup gets copied to the sidebar. ## Heading with `code` or *italic* or **bold** or ~~strike~~ Basic markup should be copied. ## Heading with a [link](../index.html) Probably not super-wise to have headings with links, but at least they shouldn't explode. ## Heading with a custom id { #custom-id .custom-class } Make sure navigation works on a custom id. ## Heading with <span>html</span> What happens if there is inline HTML? ================================================ FILE: tests/gui/books/heading-nav/src/normal-intro.md ================================================ # Normal text before first heading This test is to ensure the first heading shows up as "current" on page load. ## The first heading 1 2 3 ## The second heading ### And a sub heading ================================================ FILE: tests/gui/books/heading-nav/src/unusual-heading-levels.md ================================================ # Unusual heading levels ### Heading 3 ## Heading 2 #### Heading 5 #### Heading 5.1 ================================================ FILE: tests/gui/books/heading-nav-folded/book.toml ================================================ [book] title = "heading-nav-folded" [output.html.fold] enable = true level = 0 ================================================ FILE: tests/gui/books/heading-nav-folded/src/SUMMARY.md ================================================ # Summary - [Introduction](./intro.md) - [Sub chapter](./sub/index.md) - [Sub inner](./sub/inner/index.md) - [Sub second chapter](./sub/second.md) - [Next main chapter](./next-main.md) ================================================ FILE: tests/gui/books/heading-nav-folded/src/intro.md ================================================ # Introduction ## Heading A ### Heading A2 ### Heading A3 ## Heading B ================================================ FILE: tests/gui/books/heading-nav-folded/src/next-main.md ================================================ # Next main chapter ================================================ FILE: tests/gui/books/heading-nav-folded/src/sub/index.md ================================================ # Sub chapter ## Sub-chapter heading ================================================ FILE: tests/gui/books/heading-nav-folded/src/sub/inner/index.md ================================================ # Sub inner ## Inner chapter heading ================================================ FILE: tests/gui/books/heading-nav-folded/src/sub/second.md ================================================ # Sub second chapter ## Second chapter heading ================================================ FILE: tests/gui/books/highlighting/README.md ================================================ # Syntax Highlighting This GUI test book is used for testing syntax highlighting. ================================================ FILE: tests/gui/books/highlighting/book.toml ================================================ [book] title = "Syntax Highlighting" ================================================ FILE: tests/gui/books/highlighting/src/SUMMARY.md ================================================ # Summary - [Languages](./languages.md) ================================================ FILE: tests/gui/books/highlighting/src/languages.md ================================================ # Syntax Highlights ## apache ```apache # rewrite`s rules for wordpress pretty url LoadModule rewrite_module modules/mod_rewrite.so RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.php [NC,L] ExpiresActive On ExpiresByType application/x-javascript "access plus 1 days" Order Deny,Allow Allow from All <Location /maps/> RewriteMap map txt:map.txt RewriteMap lower int:tolower RewriteCond %{REQUEST_URI} ^/([^/.]+)\.html$ [NC] RewriteCond ${map:${lower:%1}|NOT_FOUND} !NOT_FOUND RewriteRule .? /index.php?q=${map:${lower:%1}} [NC,L] </Location> 20.164.151.111 - - [20/Aug/2015:22:20:18 -0400] "GET /mywebpage/index.php HTTP/1.1" 403 772 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1" ``` ## armasm ```armasm .data /* Data segment: define our message string and calculate its length. */ msg: .ascii "Hello, ARM!\n" len = . - msg .text /* Our application's entry point. */ .globl _start _start: /* syscall write(int fd, const void *buf, size_t count) */ mov %r0, $1 /* fd := STDOUT_FILENO */ ldr %r1, =msg /* buf := msg */ ldr %r2, =len /* count := len */ mov %r7, $4 /* write is syscall #4 */ swi $0 /* invoke syscall */ /* syscall exit(int status) */ mov %r0, $0 /* status := 0 */ mov %r7, $1 /* exit is syscall #1 */ swi $0 /* invoke syscall */ ``` ## bash ```bash #!/bin/bash ###### CONFIG ACCEPTED_HOSTS="/root/.hag_accepted.conf" BE_VERBOSE=false if [ "$UID" -ne 0 ] then echo "Superuser rights required" exit 2 fi genApacheConf(){ echo -e "# Host ${HOME_DIR}$1/$2 :" } echo '"quoted"' | tr -d \" > text.txt ``` ## c ```c #include <stdio.h> void main(int argc,char ** argv){ printf("Hello World!"); } ``` ## coffeescript ```coffeescript grade = (student, period=(if b? then 7 else 6)) -> if student.excellentWork "A+" else if student.okayStuff if student.triedHard then "B" else "B-" else "C" class Animal extends Being constructor: (@name) -> move: (meters) -> alert @name + " moved #{meters}m." ``` ## cpp ```cpp #include <iostream> using namespace std; int main() { cout << "Hello, World!" << endl; // This prints Hello, World! return 0; } ``` ## csharp ```csharp using System; class App { static void Main() { Console.WriteLine("Hello World!"); } } ``` ## css ```css @font-face { font-family: Chunkfive; src: url('Chunkfive.otf'); } body, .usertext { color: #f0f0f0; background: #600; font-family: Chunkfive, sans; --heading-1: 30px/32px Helvetica, sans-serif; } @import url(print.css); @media print { a[href^='http']::after { content: attr(href); } } ``` ## d ```d /* This program prints a hello world message to the console. */ import std.stdio; void main() { writeln("Hello, World!"); } ``` ## diff ```diff Index: languages/ini.js =================================================================== --- languages/ini.js (revision 199) +++ languages/ini.js (revision 200) @@ -1,8 +1,7 @@ hljs.LANGUAGES.ini = { case_insensitive: true, - defaultMode: - { + defaultMode: { contains: ['comment', 'title', 'setting'], illegal: '[^\\s]' }, *** /path/to/original timestamp --- /path/to/new timestamp *************** *** 1,3 **** --- 1,9 ---- + This is an important + notice! It should + therefore be located at + the beginning of this + document! ! compress the size of the ! changes. It is important to spell ``` ## go ```go package main import "fmt" func main() { fmt.Println("Hello World!") } ``` ## handlebars ```handlebars <div class='entry'> {{! only show if author exists }} {{#if author}} <h1>{{firstName}} {{lastName}}</h1> {{/if}} </div> ``` ## haskell ```haskell main :: IO () main = putStrLn "Hello World!" ``` ## http ```http POST /task?id=1 HTTP/1.1 Host: example.org Content-Type: application/json; charset=utf-8 Content-Length: 137 { "status": "ok", "extended": true, "results": [ {"value": 0, "type": "int64"}, {"value": 1.0e+3, "type": "decimal"} ] } ``` ## ini ```ini ; boilerplate [package] name = "some_name" authors = ["Author"] description = "This is \ a description" [[lib]] name = ${NAME} default = True auto = no counter = 1_000 ``` ## java ```java class Main { public static void main(String[] args) { System.out.println("Hello World!"); } } ``` ## javascript ```javascript function $initHighlight(block, cls) { try { if (cls.search(/\bno\-highlight\b/) != -1) return process(block, true, 0x0F) + ` class="${cls}"`; } catch (e) { /* handle exception */ } for (var i = 0 / 2; i < classes.length; i++) { if (checkCondition(classes[i]) === undefined) console.log('undefined'); } return ( <div> <web-component>{block}</web-component> </div> ) } export $initHighlight; ``` ## json ```json [ { "title": "apples", "count": [12000, 20000], "description": { "text": "...", "sensitive": false } }, { "title": "oranges", "count": [17500, null], "description": { "text": "...", "sensitive": false } } ] ``` ## julia ```julia # function to calculate the volume of a sphere function sphere_vol(r) # julia allows Unicode names (in UTF-8 encoding) # so either "pi" or the symbol π can be used return 4/3*pi*r^3 end # functions can also be defined more succinctly quadratic(a, sqr_term, b) = (-b + sqr_term) / 2a # calculates x for 0 = a*x^2+b*x+c, arguments types can be defined in function definitions function quadratic2(a::Float64, b::Float64, c::Float64) # unlike other languages 2a is equivalent to 2*a # a^2 is used instead of a**2 or pow(a,2) sqr_term = sqrt(b^2-4a*c) r1 = quadratic(a, sqr_term, b) r2 = quadratic(a, -sqr_term, b) # multiple values can be returned from a function using tuples # if the return keyword is omitted, the last term is returned r1, r2 end vol = sphere_vol(3) ``` ## kotlin ```kotlin package org.kotlinlang.play fun main() { println("Hello, World!") } ``` ## less ```less @import 'fruits'; @rhythm: 1.5em; @media screen and (min-resolution: 2dppx) { body { font-size: 125%; } } section > .foo + #bar:hover [href*='less'] { margin: @rhythm 0 0 @rhythm; padding: calc(5% + 20px); background: #f00ba7 url(http://placehold.alpha-centauri/42.png) no-repeat; background-image: linear-gradient(-135deg, wheat, fuchsia) !important ; background-blend-mode: multiply; } @font-face { font-family: /* ? */ 'Omega'; src: url('../fonts/omega-webfont.woff?v=2.0.2'); } .icon-baz::before { display: inline-block; font-family: 'Omega', Alpha, sans-serif; content: '\f085'; color: rgba(98, 76 /* or 54 */, 231, 0.75); } ``` ## lua ```lua --[[ Simple signal/slot implementation ]] local signal_mt = { __index = { register = table.insert } } function signal_mt.__index:emit(... --[[ Comment in params ]]) for _, slot in ipairs(self) do slot(self, ...) end end local function create_signal() return setmetatable({}, signal_mt) end -- Signal test local signal = create_signal() signal:register(function(signal, ...) print(...) end) signal:emit('Answer to Life, the Universe, and Everything:', 42) --[==[ [=[ [[ Nested ]] multi-line ]=] comment ]==] [==[ Nested [=[ multi-line [[ string ]] ]=] ]==] ``` ## makefile ```makefile # Makefile BUILDDIR = _build EXTRAS ?= $(BUILDDIR)/extras .PHONY: main clean main: @echo "Building main facility..." build_main $(BUILDDIR) clean: rm -rf $(BUILDDIR)/* ``` ## markdown ```markdown # hello world you can write text [with links](http://example.com) inline or [link references][1]. - one _thing_ has *em*phasis - two **things** are **bold** [1]: http://example.com --- # hello world <this_is inline="xml"></this_is> > markdown is so cool so are code segments 1. one thing (yeah!) 2. two thing `i can write code`, and `more` wipee! ``` ## nginx ```nginx user www www; worker_processes 2; pid /var/run/nginx.pid; error_log /var/log/nginx.error_log debug | info | notice | warn | error | crit; events { connections 2000; use kqueue | rtsig | epoll | /dev/poll | select | poll; } http { log_format main '$remote_addr - $remote_user [$time_local] ' '"$request" $status $bytes_sent ' '"$http_referer" "$http_user_agent" ' '"$gzip_ratio"'; send_timeout 3m; client_header_buffer_size 1k; gzip on; gzip_min_length 1100; #lingering_time 30; server { server_name one.example.com www.one.example.com; access_log /var/log/nginx.access_log main; rewrite (.*) /index.php?page=$1 break; location / { proxy_pass http://127.0.0.1/; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; charset koi8-r; } location /api/ { fastcgi_pass 127.0.0.1:9000; } location ~* \.(jpg|jpeg|gif)$ { root /spool/www; } } } ``` ## nim ```nim from strutils import `%` const numDoors = 100 var doors {.compileTime.}: array[1..numDoors, bool] proc calcDoors(): string = for pass in 1..numDoors: for door in countup(pass, numDoors, pass): doors[door] = not doors[door] for door in 1..numDoors: result.add("Door $1 is $2.\n" % [$door, if doors[door]: "open" else: "closed"]) const outputString: string = calcDoors() echo outputString ``` ## objectivec ```objectivec #import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @mylak { NSLog(@"Hello World!"); } return 0; } ``` ## nix ```nix let world = "World!"; in "Hello " + world ``` ## perl ```perl print "Hello World!\n"; ``` ## php ```php <?php echo "Hello World!"; ?> ``` ## plaintext ```plaintext I think this is simply plain text? Hello World! ``` ## properties ```properties # .properties ! Exclamation mark = comments, too key1 = value1 key2 : value2 key3 value3 key\ spaces multiline\ value4 empty_key ! Key can contain escaped chars \:\= = value5 ``` ## python ```python @requires_authorization(roles=["ADMIN"]) def somefunc(param1='', param2=0): r'''A docstring''' if param1 > param2: # interesting print 'Gre\'ater' return (param2 - param1 + 1 + 0b10l) or None class SomeClass: pass >>> message = '''interpreter ... prompt''' ``` ## r ```r require(stats) #' Compute different averages #' #' @param x \code{numeric} vector of sample data #' @param type \code{character} vector of length 1 specifying the average type #' @return \code{centre} returns the sample average according to the chosen method. #' @examples #' centre(rcauchy(10), "mean") #' @export centre <- function(x, type) { switch(type, mean = mean(x), median = median(x), trimmed = mean(x, trim = .1)) } x <- rcauchy(10) centre(x, "mean") library(ggplot2) models <- tibble::tribble( ~model_name, ~ formula, "length-width", Sepal.Length ~ Petal.Width + Petal.Length, "interaction", Sepal.Length ~ Petal.Width * Petal.Length ) iris %>% nest_by(Species) %>% left_join(models, by = character()) %>% rowwise(Species, model_name) %>% mutate(model = list(lm(formula, data = data))) %>% summarise(broom::glance(model)) ``` ## ruby ```ruby # The Greeter class class Greeter def initialize(name) @name = name.capitalize end def salute puts "Hello #{@name}!" end end g = Greeter.new("world") g.salute ``` ## rust ```rust fn main()->(){ println!("Hello World!"); } ``` ## scala ```scala /** * A person has a name and an age. */ case class Person(name: String, age: Int) abstract class Vertical extends CaseJeu case class Haut(a: Int) extends Vertical case class Bas(name: String, b: Double) extends Vertical sealed trait Ior[+A, +B] case class Left[A](a: A) extends Ior[A, Nothing] case class Right[B](b: B) extends Ior[Nothing, B] case class Both[A, B](a: A, b: B) extends Ior[A, B] trait Functor[F[_]] { def map[A, B](fa: F[A], f: A => B): F[B] } // beware Int.MinValue def absoluteValue(n: Int): Int = if (n < 0) -n else n def interp(n: Int): String = s"there are $n ${color} balloons.\n" type ξ[A] = (A, A) trait Hist { lhs => def ⊕(rhs: Hist): Hist } def gsum[A: Ring](as: Seq[A]): A = as.foldLeft(Ring[A].zero)(_ + _) val actions: List[Symbol] = 'init :: 'read :: 'write :: 'close :: Nil ``` ## scss ```scss import "compass/reset"; // variables $colorGreen: #008000; $colorGreenDark: darken($colorGreen, 10); @mixin container { max-width: 980px; } // mixins with parameters @mixin button($color:green) { @if ($color == green) { background-color: #008000; } @else if ($color == red) { background-color: #B22222; } } button { @include button(red); } div, .navbar, #header, input[type="input"] { font-family: "Helvetica Neue", Arial, sans-serif; width: auto; margin: 0 auto; display: block; } .row-12 > [class*="spans"] { border-left: 1px solid #B5C583; } ``` ## shell ```shell $ echo $EDITOR vim $ git checkout main Switched to branch 'main' Your branch is up-to-date with 'origin/main'. $ git push Everything up-to-date $ echo 'All > done!' All done! ``` ## sql ```sql CREATE TABLE "topic" ( "id" integer NOT NULL PRIMARY KEY, "forum_id" integer NOT NULL, "subject" varchar(255) NOT NULL ); ALTER TABLE "topic" ADD CONSTRAINT forum_id FOREIGN KEY ("forum_id") REFERENCES "forum" ("id"); -- Initials insert into "topic" ("forum_id", "subject") values (2, 'D''artagnian'); ``` ## swift ```swift import Foundation @objc class Person: Entity { var name: String! var age: Int! init(name: String, age: Int) { /* /* ... */ */ } // Return a descriptive string for this person func description(offset: Int = 0) -> String { return "\(name) is \(age + offset) years old" } } ``` ## typescript ```typescript class MyClass { public static myValue: string; constructor(init: string) { this.myValue = init; } } import fs = require("fs"); module MyModule { export interface MyInterface extends Other { myProperty: any; } } declare magicNumber number; myArray.forEach(() => { }); // fat arrow syntax ``` ## x86asm ```x86asm section .text extern _MessageBoxA@16 %if __NASM_VERSION_ID__ >= 0x02030000 safeseh handler ; register handler as "safe handler" %endif handler: push dword 1 ; MB_OKCANCEL push dword caption push dword text push dword 0 call _MessageBoxA@16 sub eax,1 ; incidentally suits as return value ; for exception handler ret global _main _main: push dword handler push dword [fs:0] mov dword [fs:0], esp xor eax,eax mov eax, dword[eax] ; cause exception pop dword [fs:0] ; disengage exception handler add esp, 4 ret avx2: vzeroupper push rbx mov rbx, rsp sub rsp, 0h20 vmovdqa ymm0, [rcx] vpaddb ymm0, [rdx] leave ret text: db 'OK to rethrow, CANCEL to generate core dump',0 caption:db 'SEGV',0 section .drectve info db '/defaultlib:user32.lib /defaultlib:msvcrt.lib ' ``` ## xml ```xml <!DOCTYPE html> <title>Title

Title

``` ## yaml ```yaml --- # comment string_1: "Bar" string_2: 'bar' string_3: bar inline_keys_ignored: sompath/name/file.jpg keywords_in_yaml: - true - false - TRUE - FALSE - 21 - 21.0 - !!str 123 "quoted_key": &foobar bar: foo foo: "foo": bar reference: *foobar multiline_1: | Multiline String multiline_2: > Multiline String multiline_3: " Multiline string " ansible_variables: "foo {{variable}}" array_nested: - a - b: 1 c: 2 - b - comment ``` ================================================ FILE: tests/gui/books/redirect/README.md ================================================ # Redirect This GUI test book tests the redirect configuration. ================================================ FILE: tests/gui/books/redirect/book.toml ================================================ [book] title = "redirect" [output.html.redirect] "/inner/old.html" = "../new-chapter.html" # This is a source without a fragment, and one with a fragment that goes to # the same place. The redirect with the fragment is not necessary, since that # is the default behavior. "/pointless-fragment.html" = "new-chapter.html" "/pointless-fragment.html#foo" = "new-chapter.html#foo" "/rename-page-and-fragment.html" = "new-chapter.html" "/rename-page-and-fragment.html#orig" = "new-chapter.html#new" "/rename-page-fragment-elsewhere.html" = "new-chapter.html" "/rename-page-fragment-elsewhere.html#orig" = "other-chapter.html#new" # Rename fragment on an existing page. "/new-chapter.html#orig" = "new-chapter.html#new" # Rename fragment on an existing page to another page. "/new-chapter.html#orig-new-chapter" = "other-chapter.html#new" "/full-url-with-fragment.html" = "https://www.rust-lang.org/#fragment" "/full-url-with-fragment-map.html" = "https://www.rust-lang.org/" "/full-url-with-fragment-map.html#a" = "https://www.rust-lang.org/#new1" "/full-url-with-fragment-map.html#b" = "https://www.rust-lang.org/#new2" ================================================ FILE: tests/gui/books/redirect/src/SUMMARY.md ================================================ # Summary - [New chapter](new-chapter.md) - [Other chapter](other-chapter.md) ================================================ FILE: tests/gui/books/redirect/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/gui/books/redirect/src/new-chapter.md ================================================ # New chapter ================================================ FILE: tests/gui/books/redirect/src/other-chapter.md ================================================ # Other chapter ================================================ FILE: tests/gui/books/search/README.md ================================================ # Search This GUI test book is used for testing basic search interaction. ================================================ FILE: tests/gui/books/search/book.toml ================================================ [book] title = "search" ================================================ FILE: tests/gui/books/search/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) - [Chapter 2](./inner/chapter_2.md) ================================================ FILE: tests/gui/books/search/src/chapter_1.md ================================================ # Chapter 1 extraordinary refrigerator philosophical thunderstorm kaleidoscope ## Repeat on same page kaleidoscope ================================================ FILE: tests/gui/books/search/src/inner/chapter_2.md ================================================ # Chapter 2 championship mediterranean sophisticated tuberculosis photographer ## Repeat from other chapter extraordinary ================================================ FILE: tests/gui/books/sidebar-scroll/book.toml ================================================ [book] title = "sidebar-scroll" language = "en" ================================================ FILE: tests/gui/books/sidebar-scroll/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) - [Chapter 2](./chapter_2.md) - [Chapter 3](./chapter_3.md) - [Chapter 4](./chapter_4.md) - [Chapter 5](./chapter_5.md) - [Chapter 6](./chapter_6.md) - [Chapter 7](./chapter_7.md) - [Chapter 8](./chapter_8.md) - [Chapter 9](./chapter_9.md) - [Chapter 10](./chapter_10.md) - [Chapter 11](./chapter_11.md) - [Chapter 12](./chapter_12.md) - [Chapter 13](./chapter_13.md) - [Chapter 14](./chapter_14.md) - [Chapter 15](./chapter_15.md) - [Chapter 16](./chapter_16.md) - [Chapter 17](./chapter_17.md) - [Chapter 18](./chapter_18.md) - [Chapter 19](./chapter_19.md) - [Chapter 20](./chapter_20.md) - [Chapter 21](./chapter_21.md) - [Chapter 22](./chapter_22.md) - [Chapter 23](./chapter_23.md) - [Chapter 24](./chapter_24.md) - [Chapter 25](./chapter_25.md) - [Chapter 26](./chapter_26.md) - [Chapter 27](./chapter_27.md) - [Chapter 28](./chapter_28.md) - [Chapter 29](./chapter_29.md) - [Chapter 30](./chapter_30.md) - [Chapter 31](./chapter_31.md) - [Chapter 32](./chapter_32.md) - [Chapter 33](./chapter_33.md) - [Chapter 34](./chapter_34.md) - [Chapter 35](./chapter_35.md) - [Chapter 36](./chapter_36.md) - [Chapter 37](./chapter_37.md) - [Chapter 38](./chapter_38.md) - [Chapter 39](./chapter_39.md) - [Chapter 40](./chapter_40.md) - [Chapter 41](./chapter_41.md) - [Chapter 42](./chapter_42.md) - [Chapter 43](./chapter_43.md) - [Chapter 44](./chapter_44.md) - [Chapter 45](./chapter_45.md) - [Chapter 46](./chapter_46.md) - [Chapter 47](./chapter_47.md) - [Chapter 48](./chapter_48.md) - [Chapter 49](./chapter_49.md) - [Chapter 50](./chapter_50.md) - [Chapter 51](./chapter_51.md) - [Chapter 52](./chapter_52.md) - [Chapter 53](./chapter_53.md) - [Chapter 54](./chapter_54.md) - [Chapter 55](./chapter_55.md) - [Chapter 56](./chapter_56.md) - [Chapter 57](./chapter_57.md) - [Chapter 58](./chapter_58.md) - [Chapter 59](./chapter_59.md) - [Chapter 60](./chapter_60.md) - [Chapter 61](./chapter_61.md) - [Chapter 62](./chapter_62.md) - [Chapter 63](./chapter_63.md) - [Chapter 64](./chapter_64.md) - [Chapter 65](./chapter_65.md) - [Chapter 66](./chapter_66.md) - [Chapter 67](./chapter_67.md) - [Chapter 68](./chapter_68.md) - [Chapter 69](./chapter_69.md) - [Chapter 70](./chapter_70.md) - [Chapter 71](./chapter_71.md) - [Chapter 72](./chapter_72.md) - [Chapter 73](./chapter_73.md) - [Chapter 74](./chapter_74.md) - [Chapter 75](./chapter_75.md) - [Chapter 76](./chapter_76.md) - [Chapter 77](./chapter_77.md) - [Chapter 78](./chapter_78.md) - [Chapter 79](./chapter_79.md) - [Chapter 80](./chapter_80.md) - [Chapter 81](./chapter_81.md) - [Chapter 82](./chapter_82.md) - [Chapter 83](./chapter_83.md) - [Chapter 84](./chapter_84.md) - [Chapter 85](./chapter_85.md) - [Chapter 86](./chapter_86.md) - [Chapter 87](./chapter_87.md) - [Chapter 88](./chapter_88.md) - [Chapter 89](./chapter_89.md) - [Chapter 90](./chapter_90.md) - [Chapter 91](./chapter_91.md) - [Chapter 92](./chapter_92.md) - [Chapter 93](./chapter_93.md) - [Chapter 94](./chapter_94.md) - [Chapter 95](./chapter_95.md) - [Chapter 96](./chapter_96.md) - [Chapter 97](./chapter_97.md) - [Chapter 98](./chapter_98.md) - [Chapter 99](./chapter_99.md) - [Chapter 100](./chapter_100.md) ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_1.md ================================================ # Chapter 1 ## This has a single heading ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_10.md ================================================ # Chapter 10 ## Heading A ## Heading B ## Heading C ## Heading D ## Heading E ## Heading F ## Heading G ## Heading H ## Heading I ## Heading J ## Heading K ## Heading L ## Heading M ## Heading N ## Heading O ## Heading P ## Heading Q ## Heading R ## Heading S ## Heading T ## Heading U ## Heading V ## Heading W ## Heading X ## Heading Y ## Heading Z ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_100.md ================================================ # Chapter 100 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_11.md ================================================ # Chapter 11 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_12.md ================================================ # Chapter 12 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_13.md ================================================ # Chapter 13 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_14.md ================================================ # Chapter 14 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_15.md ================================================ # Chapter 15 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_16.md ================================================ # Chapter 16 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_17.md ================================================ # Chapter 17 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_18.md ================================================ # Chapter 18 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_19.md ================================================ # Chapter 19 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_2.md ================================================ # Chapter 2 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_20.md ================================================ # Chapter 20 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_21.md ================================================ # Chapter 21 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_22.md ================================================ # Chapter 22 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_23.md ================================================ # Chapter 23 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_24.md ================================================ # Chapter 24 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_25.md ================================================ # Chapter 25 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_26.md ================================================ # Chapter 26 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_27.md ================================================ # Chapter 27 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_28.md ================================================ # Chapter 28 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_29.md ================================================ # Chapter 29 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_3.md ================================================ # Chapter 3 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_30.md ================================================ # Chapter 30 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_31.md ================================================ # Chapter 31 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_32.md ================================================ # Chapter 32 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_33.md ================================================ # Chapter 33 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_34.md ================================================ # Chapter 34 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_35.md ================================================ # Chapter 35 ## Heading A ## Heading B ## Heading C ## Heading D ## Heading E ## Heading F ## Heading G ## Heading H ## Heading I ## Heading J ## Heading K ## Heading L ## Heading M ## Heading N ## Heading O ## Heading P ## Heading Q ## Heading R ## Heading S ## Heading T ## Heading U ## Heading V ## Heading W ## Heading X ## Heading Y ## Heading Z ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_36.md ================================================ # Chapter 36 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_37.md ================================================ # Chapter 37 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_38.md ================================================ # Chapter 38 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_39.md ================================================ # Chapter 39 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_4.md ================================================ # Chapter 4 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_40.md ================================================ # Chapter 40 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_41.md ================================================ # Chapter 41 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_42.md ================================================ # Chapter 42 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_43.md ================================================ # Chapter 43 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_44.md ================================================ # Chapter 44 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_45.md ================================================ # Chapter 45 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_46.md ================================================ # Chapter 46 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_47.md ================================================ # Chapter 47 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_48.md ================================================ # Chapter 48 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_49.md ================================================ # Chapter 49 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_5.md ================================================ # Chapter 5 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_50.md ================================================ # Chapter 50 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_51.md ================================================ # Chapter 51 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_52.md ================================================ # Chapter 52 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_53.md ================================================ # Chapter 53 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_54.md ================================================ # Chapter 54 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_55.md ================================================ # Chapter 55 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_56.md ================================================ # Chapter 56 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_57.md ================================================ # Chapter 57 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_58.md ================================================ # Chapter 58 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_59.md ================================================ # Chapter 59 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_6.md ================================================ # Chapter 6 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_60.md ================================================ # Chapter 60 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_61.md ================================================ # Chapter 61 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_62.md ================================================ # Chapter 62 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_63.md ================================================ # Chapter 63 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_64.md ================================================ # Chapter 64 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_65.md ================================================ # Chapter 65 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_66.md ================================================ # Chapter 66 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_67.md ================================================ # Chapter 67 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_68.md ================================================ # Chapter 68 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_69.md ================================================ # Chapter 69 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_7.md ================================================ # Chapter 7 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_70.md ================================================ # Chapter 70 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_71.md ================================================ # Chapter 71 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_72.md ================================================ # Chapter 72 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_73.md ================================================ # Chapter 73 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_74.md ================================================ # Chapter 74 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_75.md ================================================ # Chapter 75 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_76.md ================================================ # Chapter 76 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_77.md ================================================ # Chapter 77 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_78.md ================================================ # Chapter 78 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_79.md ================================================ # Chapter 79 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_8.md ================================================ # Chapter 8 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_80.md ================================================ # Chapter 80 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_81.md ================================================ # Chapter 81 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_82.md ================================================ # Chapter 82 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_83.md ================================================ # Chapter 83 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_84.md ================================================ # Chapter 84 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_85.md ================================================ # Chapter 85 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_86.md ================================================ # Chapter 86 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_87.md ================================================ # Chapter 87 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_88.md ================================================ # Chapter 88 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_89.md ================================================ # Chapter 89 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_9.md ================================================ # Chapter 9 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_90.md ================================================ # Chapter 90 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_91.md ================================================ # Chapter 91 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_92.md ================================================ # Chapter 92 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_93.md ================================================ # Chapter 93 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_94.md ================================================ # Chapter 94 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_95.md ================================================ # Chapter 95 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_96.md ================================================ # Chapter 96 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_97.md ================================================ # Chapter 97 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_98.md ================================================ # Chapter 98 ================================================ FILE: tests/gui/books/sidebar-scroll/src/chapter_99.md ================================================ # Chapter 99 ================================================ FILE: tests/gui/heading-nav-collapsed.goml ================================================ // Tests for collapsed heading sidebar navigation. set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/collapsed.html" assert-count: (".header-item", 11) assert-count: (".current-header", 1) assert-text: (".current-header", "Heading 1") // Collapsed elements do not have "expanded" class. assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item"}) assert-property: ("div.on-this-page", {"innerHTML": '
  1. Heading 1
    1. Heading 1.1
    2. Heading 1.2
      1. Heading 1.2.1
      2. Heading 1.2.2
    3. Heading 1.3
  2. Heading 2
    1. Heading 2.1
      1. Heading 2.1.1
        1. Heading 2.1.1.1
          1. Heading 2.1.1.1.1
'}) // Click 1.2, expands it. click: "a.header-in-summary[href='#heading-12']" assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item expanded"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item"}) assert-css: ("//a[@href='#heading-12']/../following-sibling::ol", {"display": "block"}) // Click 1.1, should collapse it. click: "a.header-in-summary[href='#heading-11']" assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item"}) assert-css: ("//a[@href='#heading-12']/../following-sibling::ol", {"display": "none"}) // Click the chevron, should expand it. click: "a.header-in-summary[href='#heading-12'] ~ a.header-toggle" assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item expanded"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item"}) assert-css: ("//a[@href='#heading-12']/../following-sibling::ol", {"display": "block"}) // Click 1.3 click: "a.header-in-summary[href='#heading-13']" // Everything should be collapsed assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item"}) assert-css: ("//a[@href='#heading-12']/../following-sibling::ol", {"display": "none"}) assert-css: ("//a[@href='#heading-21']/../following-sibling::ol", {"display": "none"}) assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-211'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-2111'])", {"class": "header-item"}) // Scroll to bottom of page press-key: 'PageDown' press-key: 'PageDown' press-key: 'PageDown' press-key: 'PageDown' // 2.1.1.1.1 should be visible, and all the chevrons should be open, and expanded should be on each one assert-attribute: ("li:has(> span > a[href='#heading-12'])", {"class": "header-item"}) assert-attribute: ("li:has(> span > a[href='#heading-21'])", {"class": "header-item expanded"}) assert-attribute: ("li:has(> span > a[href='#heading-211'])", {"class": "header-item expanded"}) assert-attribute: ("li:has(> span > a[href='#heading-2111'])", {"class": "header-item expanded"}) assert-css: ("//a[@href='#heading-12']/../following-sibling::ol", {"display": "none"}) assert-css: ("//a[@href='#heading-21']/../following-sibling::ol", {"display": "block"}) assert-css: ("//a[@href='#heading-211']/../following-sibling::ol", {"display": "block"}) assert-css: ("//a[@href='#heading-2111']/../following-sibling::ol", {"display": "block"}) ================================================ FILE: tests/gui/heading-nav-current-to-bottom.goml ================================================ // Checks that the "current" header works even when there are headers near the // bottom. set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/current-to-bottom.html" assert-count: (".current-header", 1) assert-text: (".current-header", "First header") scroll-to: "#scroll-to-1" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-2" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-3" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-4" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-5" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-6" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-7" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-8" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-9" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-10" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-11" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-12" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-13" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-14" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-15" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-16" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-17" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-18" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-19" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-20" assert-text: (".current-header", "First header") scroll-to: "#scroll-to-21" wait-for-text: (".current-header", "Second sub-header") scroll-to: "#scroll-to-22" assert-text: (".current-header", "Second sub-header") scroll-to: "#scroll-to-23" assert-text: (".current-header", "Second sub-header") scroll-to: "#scroll-to-24" assert-text: (".current-header", "Second sub-header") scroll-to: "#scroll-to-25" wait-for-text: (".current-header", "Fifth header") ================================================ FILE: tests/gui/heading-nav-empty.goml ================================================ // When there aren't any headings, there shouldn't be any header items in the sidebar. set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/empty.html" assert-count: (".header-item", 0) assert-count: (".current-header", 0) ================================================ FILE: tests/gui/heading-nav-filter.goml ================================================ // Tests for collapsed heading sidebar navigation. set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/filtered-headings.html?highlight=skateboard#skateboard" assert-property: ("//h2[@id='skateboard']", {"innerHTML": 'Skateboard'}) assert-property: ("//a[contains(@class, 'header-in-summary') and @href='#skateboard']", {"innerHTML": 'Skateboard'}) ================================================ FILE: tests/gui/heading-nav-folded.goml ================================================ // Tests when chapter folding is enabled. go-to: |DOC_PATH| + "heading-nav-folded/index.html" ================================================ FILE: tests/gui/heading-nav-large-intro.goml ================================================ // When there is a large intro, there shouldn't be any "current" headers until // you scroll down and make it visible on screen. set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/large-intro.html" assert-count: (".header-item", 1) assert-count: (".current-header", 0) scroll-to: "#first-header" wait-for-count: (".current-header", 1) assert-text: (".current-header", "First header") // Scrolling back to the top should set it to 0. scroll-to: (0, 0) wait-for-count: (".current-header", 0) ================================================ FILE: tests/gui/heading-nav-markup.goml ================================================ // When a header has various markup, the sidebar should replicate it. set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/markup.html" assert-count: (".header-item", 4) assert-count: (".current-header", 1) assert-text: (".current-header", "Heading with code or italic or bold or strike") assert-property: (".current-header", {"innerHTML": "Heading with code or italic or bold or strike"}) // Clicking the custom one should work and should make it current. click: "a.header-in-summary[href='#custom-id']" assert-count: (".current-header", 1) assert-text: (".current-header", "Heading with a custom id") // Click the one with HTML, and check it. click: "a.header-in-summary[href='#heading-with-html']" assert-count: (".current-header", 1) assert-text: (".current-header", "Heading with html") ================================================ FILE: tests/gui/heading-nav-normal-intro.goml ================================================ // When there is a normal-sized intro, when the page loads the first heading // should be "current". set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/normal-intro.html" assert-count: (".header-item", 3) assert-count: (".current-header", 1) assert-text: (".current-header", "The first heading") click: "a[href='#and-a-sub-heading']" wait-for-text: (".current-header", "And a sub heading") ================================================ FILE: tests/gui/heading-nav-unusual-levels.goml ================================================ // Tests for unusual heading levels set-window-size: (1400, 800) go-to: |DOC_PATH| + "heading-nav/unusual-heading-levels.html" assert-property: ("//a[@href='unusual-heading-levels.html']/../following-sibling::div", {"innerHTML": '
    1. Heading 3
  1. Heading 2
      1. Heading 5
      2. Heading 5.1
'}) ================================================ FILE: tests/gui/help.goml ================================================ // This GUI test checks help popup. go-to: |DOC_PATH| + "basic/index.html" assert-css: ("#mdbook-help-container", {"display": "none"}) press-key: '?' wait-for-css: ("#mdbook-help-container", {"display": "flex"}) press-key: 'Escape' wait-for-css: ("#mdbook-help-container", {"display": "none"}) press-key: '?' wait-for-css: ("#mdbook-help-container", {"display": "flex"}) // Click inside does nothing. click: "#mdbook-help-popup" wait-for-css: ("#mdbook-help-container", {"display": "flex"}) // Click outside dismisses. click: "*" wait-for-css: ("#mdbook-help-container", {"display": "none"}) ================================================ FILE: tests/gui/highlighting.goml ================================================ // This tests syntax highlighting. go-to: |DOC_PATH| + "highlighting/index.html" assert: "#apache ~ pre > code > span.hljs-comment" assert: "#armasm ~ pre > code > span.hljs-symbol" assert: "#bash ~ pre > code > span.hljs-meta" assert: "#c ~ pre > code > span.hljs-meta" assert: "#coffeescript ~ pre > code > span.hljs-title" assert: "#cpp ~ pre > code > span.hljs-meta" assert: "#csharp ~ pre > code > span.hljs-keyword" assert: "#css ~ pre > code > span.hljs-keyword" assert: "#d ~ pre > code > span.hljs-comment" assert: "#diff ~ pre > code > span.hljs-comment" assert: "#go ~ pre > code > span.hljs-keyword" // Not clear why this doesn't have the hljs- prefix. assert: "#handlebars ~ pre > code > span.xml" assert: "#haskell ~ pre > code > span.hljs-title" assert: "#http ~ pre > code > span.hljs-keyword" assert: "#ini ~ pre > code > span.hljs-comment" assert: "#java ~ pre > code > span.hljs-class" assert: "#javascript ~ pre > code > span.hljs-function" assert: "#json ~ pre > code > span.hljs-attr" assert: "#julia ~ pre > code > span.hljs-comment" assert: "#kotlin ~ pre > code > span.hljs-keyword" assert: "#less ~ pre > code > span.hljs-keyword" assert: "#lua ~ pre > code > span.hljs-comment" assert: "#makefile ~ pre > code > span.hljs-comment" assert: "#markdown ~ pre > code > span.hljs-section" assert: "#nginx ~ pre > code > span.hljs-attribute" assert: "#nim ~ pre > code > span.hljs-keyword" assert: "#objectivec ~ pre > code > span.hljs-meta" assert: "#nix ~ pre > code > span.hljs-keyword" assert: "#perl ~ pre > code > span.hljs-keyword" assert: "#php ~ pre > code > span.hljs-meta" assert: "#properties ~ pre > code > span.hljs-comment" assert: "#python ~ pre > code > span.hljs-meta" assert: "#r ~ pre > code > span.hljs-keyword" assert: "#ruby ~ pre > code > span.hljs-comment" assert: "#rust ~ pre > code > span.hljs-function" assert: "#scala ~ pre > code > span.hljs-comment" assert: "#scss ~ pre > code > span.hljs-comment" assert: "#shell ~ pre > code > span.hljs-meta" assert: "#sql ~ pre > code > span.hljs-keyword" assert: "#swift ~ pre > code > span.hljs-keyword" assert: "#typescript ~ pre > code > span.hljs-keyword" assert: "#x86asm ~ pre > code > span.hljs-meta" assert: "#xml ~ pre > code > span.hljs-meta" assert: "#yaml ~ pre > code > span.hljs-meta" ================================================ FILE: tests/gui/move-between-pages.goml ================================================ // This tests pressing the left and right arrows moving to previous and next page. go-to: |DOC_PATH| + "all-summary/index.html" // default page is the first chapter assert-text: ("title", "Prefix 1 - all-summary") // Trying to move to the left beyond the prefix pages - nothing changes press-key: 'ArrowLeft' assert-text: ("title", "Prefix 1 - all-summary") // Move left go-to: |DOC_PATH| + "all-summary/intro.html" assert-text: ("title", "Introduction - all-summary") press-key: 'ArrowLeft' assert-text: ("title", "Prefix 2 - all-summary") press-key: 'ArrowLeft' assert-text: ("title", "Prefix 1 - all-summary") // Move right press-key: 'ArrowRight' assert-text: ("title", "Prefix 2 - all-summary") press-key: 'ArrowRight' assert-text: ("title", "Introduction - all-summary") press-key: 'ArrowRight' assert-text: ("title", "P1 C1 - all-summary") press-key: 'ArrowRight' assert-text: ("title", "P2 C1 - all-summary") press-key: 'ArrowRight' assert-text: ("title", "Suffix 1 - all-summary") press-key: 'ArrowRight' assert-text: ("title", "Suffix 2 - all-summary") // Try to go beyond the last page press-key: 'ArrowRight' assert-text: ("title", "Suffix 2 - all-summary") ================================================ FILE: tests/gui/redirect.goml ================================================ go-to: |DOC_PATH| + "redirect/inner/old.html" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html"}) // Check that it preserves fragments when redirecting. go-to: |DOC_PATH| + "redirect/inner/old.html#fragment" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html#fragment"}) // The fragment one here isn't necessary, but should still work. go-to: |DOC_PATH| + "redirect/pointless-fragment.html" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html"}) go-to: |DOC_PATH| + "redirect/pointless-fragment.html#foo" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html#foo"}) // Page rename, and a fragment rename. go-to: |DOC_PATH| + "redirect/rename-page-and-fragment.html" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html"}) go-to: |DOC_PATH| + "redirect/rename-page-and-fragment.html#orig" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html#new"}) // Page rename, and the fragment goes to a *different* page from the default. go-to: |DOC_PATH| + "redirect/rename-page-fragment-elsewhere.html" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html"}) go-to: |DOC_PATH| + "redirect/rename-page-fragment-elsewhere.html#orig" assert-window-property: ({"location": |DOC_PATH| + "redirect/other-chapter.html#new"}) // Rename fragment on an existing page. go-to: |DOC_PATH| + "redirect/new-chapter.html#orig" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html#new"}) // Other fragments aren't affected. go-to: |DOC_PATH| + "redirect/index.html" // Reset page since redirects are processed on load. go-to: |DOC_PATH| + "redirect/new-chapter.html" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html"}) go-to: |DOC_PATH| + "redirect/index.html" // Reset page since redirects are processed on load. go-to: |DOC_PATH| + "redirect/new-chapter.html#dont-change" assert-window-property: ({"location": |DOC_PATH| + "redirect/new-chapter.html#dont-change"}) // Rename fragment on an existing page to another page. go-to: |DOC_PATH| + "redirect/index.html" // Reset page since redirects are processed on load. go-to: |DOC_PATH| + "redirect/new-chapter.html#orig-new-chapter" assert-window-property: ({"location": |DOC_PATH| + "redirect/other-chapter.html#new"}) ================================================ FILE: tests/gui/runner.rs ================================================ //! The GUI test runner. //! //! This uses the browser-ui-test npm package to use a headless Chrome to //! exercise the behavior of rendered books. See `CONTRIBUTING.md` for more //! information. use serde_json::Value; use std::fs::read_to_string; use std::path::Path; use std::process::{Command, Output}; fn get_available_browser_ui_test_version_inner(global: bool) -> Option { let mut command = Command::new("npm"); command .arg("list") .arg("--parseable") .arg("--long") .arg("--depth=0"); if global { command.arg("--global"); } let stdout = command.output().expect("`npm` command not found").stdout; let lines = String::from_utf8_lossy(&stdout); lines .lines() .find_map(|l| l.split(':').nth(1)?.strip_prefix("browser-ui-test@")) .map(std::borrow::ToOwned::to_owned) } fn get_available_browser_ui_test_version() -> Option { get_available_browser_ui_test_version_inner(false) .or_else(|| get_available_browser_ui_test_version_inner(true)) } fn expected_browser_ui_test_version() -> String { let content = read_to_string("package.json").expect("failed to read `package.json`"); let v: Value = serde_json::from_str(&content).expect("failed to parse `package.json`"); let Some(dependencies) = v.get("dependencies") else { panic!("Missing `dependencies` key in `package.json`"); }; let Some(browser_ui_test) = dependencies.get("browser-ui-test") else { panic!("Missing `browser-ui-test` key in \"dependencies\" object in `package.json`"); }; let Value::String(version) = browser_ui_test else { panic!("`browser-ui-test` version is not a string"); }; version.trim().to_string() } fn main() { let browser_ui_test_version = expected_browser_ui_test_version(); match get_available_browser_ui_test_version() { Some(version) => { if version != browser_ui_test_version { eprintln!( "⚠️ Installed version of browser-ui-test (`{version}`) is different than the \ one used in the CI (`{browser_ui_test_version}`) You can install this version \ using `npm update browser-ui-test` or by using `npm install browser-ui-test\ @{browser_ui_test_version}`", ); } } None => { panic!( "`browser-ui-test` is not installed. You can install this package using `npm \ update browser-ui-test` or by using `npm install browser-ui-test\ @{browser_ui_test_version}`", ); } } let out_dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("gui"); build_books(&out_dir); run_browser_ui_test(&out_dir); } fn build_books(out_dir: &Path) { let root = Path::new(env!("CARGO_MANIFEST_DIR")); let books_dir = root.join("tests/gui/books"); for entry in books_dir.read_dir().unwrap() { let entry = entry.unwrap(); let path = entry.path(); if !path.is_dir() { continue; } println!("Building `{}`", path.display()); let mut cmd = Command::new(env!("CARGO_BIN_EXE_mdbook")); let output = cmd .arg("build") .arg("--dest-dir") .arg(out_dir.join(path.file_name().unwrap())) .arg(&path) .output() .expect("mdbook should be built"); check_status(&cmd, &output); } } fn check_status(cmd: &Command, output: &Output) { if !output.status.success() { eprintln!("error: `{cmd:?}` failed"); let stdout = std::str::from_utf8(&output.stdout).expect("stdout is not utf8"); let stderr = std::str::from_utf8(&output.stderr).expect("stderr is not utf8"); eprintln!("\n--- stdout\n{stdout}\n--- stderr\n{stderr}"); std::process::exit(1); } } fn run_browser_ui_test(out_dir: &Path) { let mut command = Command::new("npx"); let mut doc_path = format!("file://{}", out_dir.display()); if !doc_path.ends_with('/') { doc_path.push('/'); } command .arg("browser-ui-test") .args(["--variable", "DOC_PATH", doc_path.as_str()]) .args(["--display-format", "compact"]); for arg in std::env::args().skip(1) { if arg == "--disable-headless-test" { command.arg("--no-headless"); } else if arg.starts_with("--") { command.arg(arg); } else { command.args(["--filter", arg.as_str()]); } } let test_dir = "tests/gui"; command.args(["--test-folder", test_dir]); // Then we run the GUI tests on it. let status = command.status().expect("failed to get command output"); assert!(status.success(), "{status:?}"); } ================================================ FILE: tests/gui/search.goml ================================================ // This tests basic search behavior. fail-on-js-error: true go-to: |DOC_PATH| + "search/index.html" define-function: ( "open-search", [], block { assert-css: ("#mdbook-search-wrapper", {"display": "none"}) press-key: 's' wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"}) } ) call-function: ("open-search", {}) assert-text: ("#mdbook-searchresults-header", "") write: "extraordinary" wait-for-text: ("#mdbook-searchresults-header", "2 search results for 'extraordinary':") // Close the search display press-key: 'Escape' wait-for-css: ("#mdbook-search-wrapper", {"display": "none"}) // Reopening the search should show the last value call-function: ("open-search", {}) assert-text: ("#mdbook-searchresults-header", "2 search results for 'extraordinary':") // Navigate to a sub-chapter go-to: "./inner/chapter_2.html" assert-text: ("#mdbook-searchresults-header", "") call-function: ("open-search", {}) write: "kaleidoscope" wait-for-text: ("#mdbook-searchresults-header", "2 search results for 'kaleidoscope':") // Now we test search shortcuts and more page changes. go-to: |DOC_PATH| + "search/index.html" // This check is to ensure that the search bar is inside the search wrapper. assert: "#mdbook-search-wrapper #mdbook-searchbar" assert-css: ("#mdbook-search-wrapper", {"display": "none"}) // Now we make sure the search input appear with the `S` shortcut. press-key: 's' wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"}) // We ensure the search bar has the focus. wait-for: "#mdbook-searchbar:focus" // Pressing a key will therefore update the search input. press-key: 't' assert-text: ("#mdbook-searchbar", "t") // Now we press `Escape` to ensure that the search input disappears again. press-key: 'Escape' wait-for-css: ("#mdbook-search-wrapper", {"display": "none"}) // Making it appear by clicking on the search button. click: "#mdbook-search-toggle" wait-for-css: ("#mdbook-search-wrapper", {"display": "block"}) // We ensure the search bar has the focus. assert: "#mdbook-searchbar:focus" // We input "thunder". write: "thunder" // The results should now appear. wait-for-text: ("#mdbook-searchresults-header", "1 search result for 'thunder':") assert: "#mdbook-searchresults" // Ensure that the URL was updated as well. assert-document-property: ({"URL": "?search=thunder"}, ENDS_WITH) // Now we ensure that when we land on the page with a "search in progress", the search results are // loaded and that the search input has focus. go-to: |DOC_PATH| + "search/index.html?search=thunder" wait-for-text: ("#mdbook-searchresults-header", "1 search result for 'thunder':") assert: "#mdbook-searchbar:focus" assert: "#mdbook-searchresults" // And now we press `Escape` to close everything. press-key: 'Escape' wait-for-css: ("#mdbook-search-wrapper", {"display": "none"}) ================================================ FILE: tests/gui/sidebar-active.goml ================================================ // This GUI test checks the active page sidebar highlight. go-to: |DOC_PATH| + "all-summary/index.html" assert-text: ("mdbook-sidebar-scrollbox a.active", "Prefix 1") go-to: |DOC_PATH| + "all-summary/part-1/chapter-1.html" assert-text: ("mdbook-sidebar-scrollbox a.active", "3. P1 C1") go-to: |DOC_PATH| + "all-summary/index.html?highlight=test" assert-text: ("mdbook-sidebar-scrollbox a.active", "Prefix 1") go-to: |DOC_PATH| + "all-summary/part-1/chapter-1.html?highlight=test" assert-text: ("mdbook-sidebar-scrollbox a.active", "3. P1 C1") ================================================ FILE: tests/gui/sidebar-nojs.goml ================================================ // This GUI test checks that the sidebar takes the whole height when it's inside // an iframe (because of JS disabled). // Regression test for . // We disable javascript javascript: false go-to: |DOC_PATH| + "basic/index.html" store-value: (height, 1028) set-window-size: (1028, |height|) within-iframe: (".sidebar-iframe-outer", block { assert-size: (" body", {"height": |height|}) }) ================================================ FILE: tests/gui/sidebar-scroll.goml ================================================ // This GUI test checks the sidebar scroll position when navigating. store-value: (windowHeight, 900) set-window-size: (1200, |windowHeight|) go-to: |DOC_PATH| + "sidebar-scroll/index.html" assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": 0}) click: ".chapter a[href='chapter_2.html']" assert-text: ("title", "Chapter 2 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": 0}) click: ".chapter a[href='chapter_10.html']" assert-text: ("title", "Chapter 10 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": 0}) // Check that heading nav of chapter 10 pushes 11 off the bottom. store-position: (".chapter a[href='chapter_11.html']", {"y": chapter_y}) assert: |chapter_y| > |windowHeight| // Clicking 11 should scroll back up because it is not possible for it to stay // in position since there are only a few lines above. click: ".chapter a[href='chapter_11.html']" assert-text: ("title", "Chapter 11 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": 0}) // Scroll down a little, and click on a chapter that can stay in place when // clicked. scroll-element-to: ("mdbook-sidebar-scrollbox", (0, 230)) store-property: ("mdbook-sidebar-scrollbox", {"scrollTop": sidebarScrollTop}) assert: |sidebarScrollTop| > 0 click: ".chapter a[href='chapter_35.html']" assert-text: ("title", "Chapter 35 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": |sidebarScrollTop|}) // Go to the next chapter, and verify that it scrolls to the middle. press-key: "ArrowRight" assert-text: ("title", "Chapter 36 - sidebar-scroll") // The active link should be roughly in the middle of the screen. store-position: (".chapter a.active", {"y": active_y}) // Approximate check just in case the browser has slight rendering differences. assert: |active_y| > 400 assert: |active_y| < 450 // Go to something near the top, it shouldn't scroll. store-property: ("mdbook-sidebar-scrollbox", {"scrollTop": sidebarScrollTop}) click: ".chapter a[href='chapter_24.html']" assert-text: ("title", "Chapter 24 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": |sidebarScrollTop|}) // Go to the last chapter, and verify it is scrolled to the bottom. go-to: |DOC_PATH| + "sidebar-scroll/chapter_100.html" store-property: ("mdbook-sidebar-scrollbox", {"scrollTop": scrollTop, "scrollHeight": scrollHeight, "clientHeight": clientHeight}) // This needs to be approximate since there is a slight difference. assert: |scrollTop| >= |scrollHeight| - |clientHeight| - 1 // Clicking upwards shouldn't scroll. store-property: ("mdbook-sidebar-scrollbox", {"scrollTop": sidebarScrollTop}) click: ".chapter a[href='chapter_97.html']" assert-text: ("title", "Chapter 97 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": |sidebarScrollTop|}) store-property: ("mdbook-sidebar-scrollbox", {"scrollTop": sidebarScrollTop}) click: ".chapter a[href='chapter_76.html']" assert-text: ("title", "Chapter 76 - sidebar-scroll") assert-property: ("mdbook-sidebar-scrollbox", {"scrollTop": |sidebarScrollTop|}) ================================================ FILE: tests/gui/sidebar.goml ================================================ // This GUI test checks sidebar hide/show and also its behaviour on smaller // width. go-to: |DOC_PATH| + "all-summary/index.html" set-window-size: (1100, 600) // Need to reload for the new size to be taken account by the JS. reload: store-value: (content_indent, 308) store-value: (sidebar_storage_value, "mdbook-sidebar") store-value: (sidebar_storage_hidden_value, "hidden") store-value: (sidebar_storage_displayed_value, "visible") define-function: ( "hide-sidebar", [], block { assert-position: ("#mdbook-page-wrapper", {"x": |content_indent|}) // We now hide the sidebar. click: "#mdbook-sidebar-toggle" wait-for-css: ("#mdbook-sidebar", {"display": "none"}) assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_hidden_value|} }, ) define-function: ( "show-sidebar", [], block { assert-css: ("#mdbook-sidebar", {"display": "none"}) assert-position: ("#mdbook-page-wrapper", {"x": 0}) // We expand the sidebar. click: "#mdbook-sidebar-toggle" wait-for-css-false: ("#mdbook-sidebar", {"display": "none"}) // `transform` is 0.3s so we need to wait a bit (0.5s) to ensure the animation is done. wait-for: 5000 assert-css-false: ("#mdbook-sidebar", {"transform": "matrix(1, 0, 0, 1, -308, 0)"}) // The page content should be moved to the right. assert-position: ("#mdbook-page-wrapper", {"x": |content_indent|}) assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|} }, ) // Since the sidebar is visible, we should be able to find this text. assert-find-text: ("3. P1 C1", {"case-sensitive": true}) call-function: ("hide-sidebar", {}) // Text should not be findeable anymore since the sidebar is collapsed. assert-find-text-false: ("3. P1 C1", {"case-sensitive": true}) call-function: ("show-sidebar", {}) // We should be able to find this text again. assert-find-text: ("3. P1 C1", {"case-sensitive": true}) // We now test on smaller width to ensure that the sidebar is collapsed by default. set-window-size: (900, 600) reload: call-function: ("show-sidebar", {}) call-function: ("hide-sidebar", {}) // We now test that if the sidebar is considered open and we reload the page, since // the width is small, it will still be collapsed. set-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|} reload: // The stored value shouldn't have changed. assert-local-storage: {|sidebar_storage_value|: |sidebar_storage_displayed_value|} // But the sidebar should be hidden anyway. assert-css: ("#mdbook-sidebar", {"display": "none"}) assert-position: ("#mdbook-page-wrapper", {"x": 0}) ================================================ FILE: tests/gui/theme.goml ================================================ // Basic theme switcher test. debug: true go-to: |DOC_PATH| + "all-summary/index.html" // TODO: Dark mode is automatic, how to check that here? assert-css: ("#mdbook-theme-list", {"display": "none"}) click: "#mdbook-theme-toggle" assert-css: ("#mdbook-theme-list", {"display": "block"}) click: "#mdbook-theme-rust" assert-attribute: ("html", {"class": "js rust"}) // Clicking a theme doesn't dismiss the popup. assert-css: ("#mdbook-theme-list", {"display": "block"}) assert-local-storage: {"mdbook-theme": "rust"} // Dismiss via toggle. click: "#mdbook-theme-toggle" assert-css: ("#mdbook-theme-list", {"display": "none"}) // Check for dismissal for click outside. click: "#mdbook-theme-toggle" assert-css: ("#mdbook-theme-list", {"display": "block"}) click: "main" assert-css: ("#mdbook-theme-list", {"display": "none"}) // Check for escape. click: "#mdbook-theme-toggle" assert-css: ("#mdbook-theme-list", {"display": "block"}) press-key: 'Escape' assert-css: ("#mdbook-theme-list", {"display": "none"}) // Check for navigation retains theme. go-to: "./part-1/chapter-1.html" assert-attribute: ("html", {"class": "rust js"}) ================================================ FILE: tests/testsuite/README.md ================================================ # Testsuite ## Introduction This is the main testsuite for exercising all functionality of mdBook. Tests should be organized into modules based around major features. Tests should use `BookTest` to drive the test. `BookTest` will set up a temp directory, and provides a variety of methods to help create a build books. ## Basic structure of a test Using `BookTest`, you typically use it to copy a directory into a temp directory, and then run mdbook commands in that temp directory. You can run the `mdbook` executable, or use the mdbook API to perform whatever tasks you need. Running the executable has the benefit of being able to validate the console output. See `build::basic_build` for a simple test example. I recommend reviewing the methods on `BookTest` to learn more, and reviewing some of the existing tests to get a feel for how they are structured. For example, let's say you are creating a new theme test. In the `testsuite/theme` directory, create a new directory with the book source that you want to exercise. At a minimum, this needs a `src/SUMMARY.md`, but often you'll also want `book.toml`. Then, in `testsuite/theme.rs`, add a test with `BookTest::from_dir("theme/mytest")`, and then use the methods to perform whatever actions you want. `BookTest` is designed to be able to chain a series of actions. For example, you can do something like: ```rust BookTest::from_dir("theme/mytest") .build() .check_main_file("book/index.html", str![["file contents"]]) .change_file("src/index.md", "new contents") .build() .check_main_file("book/index.html", str![["new contents"]]); ``` ## Snapbox The testsuite uses [`snapbox`] to drive most of the tests. This library provides the ability to compare strings using a variety of methods. These strings are written in the source code using either the [`str!`] or [`file!`] macros. The magic is that you can set the `SNAPSHOTS=overwrite` environment variable, and snapbox will automatically update the strings contents of `str!`, or the file contents of `file!`. This makes it easier to update tests. Snapbox provides nice diffing output, and quite a few other features. Expected contents can have wildcards like `...` (matches any lines) or `[..]` (matches any characters on a line). See [snapbox filters] for more info and other filters. Typically when writing a test, I'll just start with an empty `str!` or `file!`, and let snapbox fill it in. Then I review the contents to make sure they are what I expect. Note that there is some normalization applied to the strings. See `book_test::assert` for how some of these normalizations happen. [`snapbox`]: https://docs.rs/snapbox/latest/snapbox/ [`str!`]: https://docs.rs/snapbox/latest/snapbox/macro.str.html [`file!`]: https://docs.rs/snapbox/latest/snapbox/macro.file.html [snapbox filters]: https://docs.rs/snapbox/latest/snapbox/assert/struct.Assert.html#method.eq ================================================ FILE: tests/testsuite/book_test.rs ================================================ //! Utility for building and running tests against mdbook. use mdbook_core::utils::fs; use mdbook_driver::MDBook; use mdbook_driver::init::BookBuilder; use snapbox::IntoData; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::{AtomicU32, Ordering}; /// Test number used for generating unique temp directory names. static NEXT_TEST_ID: AtomicU32 = AtomicU32::new(0); #[derive(Clone, Copy, Eq, PartialEq)] enum StatusCode { Success, Failure, Code(i32), } /// Main helper for driving mdbook tests. pub struct BookTest { /// The temp directory where the test should perform its work. pub dir: PathBuf, /// The original source directory if created from [`BookTest::from_dir`]. original_source: Option, /// Snapshot assertion support. pub assert: snapbox::Assert, /// This indicates whether or not the book has been built. built: bool, } impl BookTest { /// Creates a new test, copying the contents from the given directory into /// a temp directory. pub fn from_dir(dir: &str) -> BookTest { // Copy this test book to a temp directory. let dir = Path::new("tests/testsuite").join(dir); assert!(dir.exists(), "{dir:?} should exist"); let tmp = Self::new_tmp(); mdbook_core::utils::fs::copy_files_except_ext( &dir, &tmp, true, Some(&PathBuf::from("book")), &[], ) .unwrap_or_else(|e| panic!("failed to copy test book {dir:?} to {tmp:?}: {e:?}")); Self::new(tmp, Some(dir)) } /// Creates a new test with an empty temp directory. pub fn empty() -> BookTest { Self::new(Self::new_tmp(), None) } /// Creates a new test with the given function to initialize a new book. /// /// The book itself is not built. pub fn init(f: impl Fn(&mut BookBuilder)) -> BookTest { let tmp = Self::new_tmp(); let mut bb = MDBook::init(&tmp); f(&mut bb); bb.build() .unwrap_or_else(|e| panic!("failed to initialize book at {tmp:?}: {e:?}")); Self::new(tmp, None) } fn new_tmp() -> PathBuf { let id = NEXT_TEST_ID.fetch_add(1, Ordering::SeqCst); let tmp = Path::new(env!("CARGO_TARGET_TMPDIR")) .join("ts") .join(format!("t{id}")); if tmp.exists() { std::fs::remove_dir_all(&tmp) .unwrap_or_else(|e| panic!("failed to remove {tmp:?}: {e:?}")); } fs::create_dir_all(&tmp).unwrap(); tmp } fn new(dir: PathBuf, original_source: Option) -> BookTest { let assert = assert(&dir); BookTest { dir, original_source, assert, built: false, } } /// Checks the contents of an HTML file that it has the given contents /// between the `
` tag. /// /// Normally the contents outside of the `
` tag aren't interesting, /// and they add a significant amount of noise. #[track_caller] pub fn check_main_file(&mut self, path: &str, expected: impl IntoData) -> &mut Self { if !self.built { self.build(); } let full_path = self.dir.join(path); let actual = read_to_string(&full_path); let start = actual .find("
") .unwrap_or_else(|| panic!("didn't find
for `{full_path:?}` in:\n{actual}")); let end = actual.find("
").unwrap(); let contents = actual[start + 6..end - 7].trim(); self.assert.eq(contents, expected); self } /// Verifies the HTML output of all chapters in a book. /// /// This calls [`BookTest::check_main_file`] for every `.html` file /// generated by building the book. All of expected files are stored in a /// director called "expected" in the original book source directory. /// /// This only works when created with [`BookTest::from_dir`]. /// /// `404.html`, `print.html`, and `toc.html` are not validated. The root /// `index.html` is also not validated (since it is often duplicated with /// the first chapter). If you need to validate it, call /// [`BookTest::check_main_file`] directly. #[track_caller] pub fn check_all_main_files(&mut self) -> &mut Self { if !self.built { self.build(); } let book_root = self.dir.join("book"); let mut files = list_all_files(&book_root); files.retain(|file| { file.extension().is_some_and(|ext| ext == "html") && !matches!( file.to_str().unwrap(), "index.html" | "404.html" | "print.html" | "toc.html" ) }); let expected_path = self .original_source .as_ref() .expect("created with BookTest::from_dir") .join("expected"); let mut expected_list = list_all_files(&expected_path); for file in &files { let expected = expected_path.join(file); let data = snapbox::Data::read_from(&expected, None); self.check_main_file(book_root.join(file).to_str().unwrap(), data); if let Some(i) = expected_list.iter().position(|p| p == file) { expected_list.remove(i); } } // Verify there aren't any unused expected files. if !expected_list.is_empty() { panic!( "extra expected files found in `{expected_path:?}:\n\ {expected_list:#?}\n\ Verify that these files are no longer needed and delete them." ); } self } /// Checks the summary contents of `toc.js` against the expected value. #[track_caller] pub fn check_toc_js(&mut self, expected: impl IntoData) -> &mut Self { if !self.built { self.build(); } let inner = self.toc_js_html(); // Would be nice if this were prettified, but a primitive wrapping will do for now. let inner = inner.replace("><", ">\n<"); self.assert.eq(inner, expected); self } /// Returns the summary contents from `toc.js`. #[track_caller] pub fn toc_js_html(&self) -> String { let toc_path = glob_one(&self.dir, "book/toc*.js"); let actual = read_to_string(&toc_path); let inner = actual .lines() .filter_map(|line| { let line = line.trim().strip_prefix("this.innerHTML = '")?; let line = line.strip_suffix("';")?; Some(line) }) .next() .expect("should have innerHTML"); inner.to_string() } /// Checks that the contents of the given file matches the expected value. /// /// The path can use glob-style wildcards, but it must match only a single file. #[track_caller] pub fn check_file(&mut self, path_pattern: &str, expected: impl IntoData) -> &mut Self { if !self.built { self.build(); } let path = glob_one(&self.dir, path_pattern); let actual = read_to_string(&path); self.assert.eq(actual, expected); self } /// Checks that the given file contains the given [`snapbox::Assert`] pattern somewhere. /// /// The path can use glob-style wildcards, but it must match only a single file. #[track_caller] pub fn check_file_contains(&mut self, path_pattern: &str, expected: &str) -> &mut Self { if !self.built { self.build(); } let path = glob_one(&self.dir, path_pattern); let actual = read_to_string(&path); let expected = format!("...\n[..]{expected}[..]\n...\n"); self.assert.eq(actual, expected); self } /// Checks that the given file does not contain the given string anywhere. /// /// Beware that using this is fragile, as it may be unable to catch /// regressions (it can't tell the difference between success, or the /// string being looked for changed). /// /// The path can use glob-style wildcards, but it must match only a single file. #[track_caller] pub fn check_file_doesnt_contain(&mut self, path_pattern: &str, string: &str) -> &mut Self { if !self.built { self.build(); } let path = glob_one(&self.dir, path_pattern); let actual = read_to_string(&path); assert!( !actual.contains(string), "Unexpectedly found {string:?} in {path:?}\n\n{actual}", ); self } /// Checks that the list of files at the given path matches the given value. #[track_caller] pub fn check_file_list(&mut self, path: &str, expected: impl IntoData) -> &mut Self { let mut all_paths: Vec<_> = walkdir::WalkDir::new(&self.dir.join(path)) .into_iter() // Skip the outer directory. .skip(1) .map(|e| { e.unwrap() .into_path() .strip_prefix(&self.dir) .unwrap() .to_str() .unwrap() .replace('\\', "/") }) .collect(); all_paths.sort(); let actual = all_paths.join("\n"); self.assert.eq(actual, expected); self } /// Loads an [`MDBook`] from the temp directory. pub fn load_book(&self) -> MDBook { MDBook::load(&self.dir).unwrap_or_else(|e| panic!("book failed to load: {e:?}")) } /// Builds the book in the temp directory. pub fn build(&mut self) -> &mut Self { let book = self.load_book(); book.build() .unwrap_or_else(|e| panic!("book failed to build: {e:?}")); self.built = true; self } /// Runs the `mdbook` binary in the temp directory. /// /// This runs `mdbook` with the given args. The args are split on spaces /// (if you need args with spaces, use the `args` method). The given /// callback receives a [`BookCommand`] for you to customize how the /// executable is run. pub fn run(&mut self, args: &str, f: impl Fn(&mut BookCommand)) -> &mut Self { let mut cmd = BookCommand { assert: self.assert.clone(), dir: self.dir.clone(), args: split_args(args), env: BTreeMap::new(), expect_status: StatusCode::Success, expect_stderr_data: None, expect_stdout_data: None, debug: None, }; f(&mut cmd); cmd.run(); // Ensure that `built` gets set if a build command is used so that all // the `check` methods do not overwrite the contents of what was just // built. if cmd.args.first().map(String::as_str) == Some("build") { self.built = true } self } /// Change a file's contents in the given path. pub fn change_file(&mut self, path: impl AsRef, body: &str) -> &mut Self { let path = self.dir.join(path); fs::write(&path, body).unwrap(); self } /// Removes a file or directory relative to the test root. pub fn rm_r(&mut self, path: impl AsRef) -> &mut Self { let path = self.dir.join(path.as_ref()); let meta = match path.symlink_metadata() { Ok(meta) => meta, Err(e) => panic!("failed to remove {path:?}, could not read: {e:?}"), }; // There is a race condition between fetching the metadata and // actually performing the removal, but we don't care all that much // for our tests. if meta.is_dir() { if let Err(e) = std::fs::remove_dir_all(&path) { panic!("failed to remove {path:?}: {e:?}"); } } else if let Err(e) = std::fs::remove_file(&path) { panic!("failed to remove {path:?}: {e:?}") } self } /// Builds a Rust program with the given src. /// /// The given path should be the path where to output the executable in /// the temp directory. pub fn rust_program(&mut self, path: &str, src: &str) -> &mut Self { let rs = self.dir.join(path).with_extension("rs"); let parent = rs.parent().unwrap(); if !parent.exists() { fs::create_dir_all(&parent).unwrap(); } fs::write(&rs, src).unwrap(); let status = std::process::Command::new("rustc") .arg(&rs) .current_dir(&parent) .status() .expect("rustc should run"); assert!(status.success()); self } } /// A builder for preparing to run the `mdbook` executable. /// /// By default, it expects the process to succeed. pub struct BookCommand { pub dir: PathBuf, assert: snapbox::Assert, args: Vec, env: BTreeMap>, expect_status: StatusCode, expect_stderr_data: Option, expect_stdout_data: Option, debug: Option, } impl BookCommand { /// Indicates that the process should fail. pub fn expect_failure(&mut self) -> &mut Self { self.expect_status = StatusCode::Failure; self } /// Indicates the process should fail with the given exit code. pub fn expect_code(&mut self, code: i32) -> &mut Self { self.expect_status = StatusCode::Code(code); self } /// Verifies that stderr matches the given value. pub fn expect_stderr(&mut self, expected: impl snapbox::IntoData) -> &mut Self { self.expect_stderr_data = Some(expected.into_data()); self } /// Verifies that stdout matches the given value. pub fn expect_stdout(&mut self, expected: impl snapbox::IntoData) -> &mut Self { self.expect_stdout_data = Some(expected.into_data()); self } /// Adds arguments to the command to run. pub fn args(&mut self, args: &[&str]) -> &mut Self { self.args.extend(args.into_iter().map(|t| t.to_string())); self } /// Specifies an environment variable to set on the executable. pub fn env>(&mut self, key: &str, value: T) -> &mut Self { self.env.insert(key.to_string(), Some(value.into())); self } /// Sets the directory used for running the command. pub fn current_dir>(&mut self, path: S) -> &mut Self { self.dir = self.dir.join(path.as_ref()); self } /// Use this to debug a command. /// /// Pass the value that you would normally pass to `MDBOOK_LOG`, and this /// will enable logging, print the command that runs and its output. /// /// This will fail if you use it in CI. #[allow(unused)] pub fn debug(&mut self, value: &str) -> &mut Self { if std::env::var_os("CI").is_some() { panic!("debug is not allowed on CI"); } self.debug = Some(value.into()); self } /// Runs the command, and verifies the output. fn run(&mut self) { let mut cmd = Command::new(env!("CARGO_BIN_EXE_mdbook")); cmd.current_dir(&self.dir) .args(&self.args) .env_remove("MDBOOK_LOG") // Don't read the system git config which is out of our control. .env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_GLOBAL", &self.dir) .env("GIT_CONFIG_SYSTEM", &self.dir) .env_remove("GIT_AUTHOR_EMAIL") .env_remove("GIT_AUTHOR_NAME") .env_remove("GIT_COMMITTER_EMAIL") .env_remove("GIT_COMMITTER_NAME"); if let Some(debug) = &self.debug { cmd.env("MDBOOK_LOG", debug); } for (k, v) in &self.env { match v { Some(v) => cmd.env(k, v), None => cmd.env_remove(k), }; } if self.debug.is_some() { eprintln!("running {cmd:#?}"); } let output = cmd.output().expect("mdbook should be runnable"); let stdout = std::str::from_utf8(&output.stdout).expect("stdout is not utf8"); let stderr = std::str::from_utf8(&output.stderr).expect("stderr is not utf8"); let render_output = || format!("\n--- stdout\n{stdout}\n--- stderr\n{stderr}"); match (self.expect_status, output.status.success()) { (StatusCode::Success, false) => { panic!("mdbook failed, but expected success{}", render_output()) } (StatusCode::Failure, true) => { panic!("mdbook succeeded, but expected failure{}", render_output()) } (StatusCode::Code(expected), _) => match output.status.code() { Some(actual) => assert_eq!( actual, expected, "process exit code did not match as expected" ), None => panic!("process exited via signal {:?}", output.status), }, _ => {} } if self.debug.is_some() { eprintln!("{}", render_output()); } self.expect_status = StatusCode::Success; // Reset to default. if let Some(expect_stderr_data) = &self.expect_stderr_data { if let Err(e) = self.assert.try_eq( Some(&"stderr"), stderr.into_data(), expect_stderr_data.clone(), ) { panic!("{e}"); } } if let Some(expect_stdout_data) = &self.expect_stdout_data { if let Err(e) = self.assert.try_eq( Some(&"stdout"), stdout.into_data(), expect_stdout_data.clone(), ) { panic!("{e}"); } } } } fn split_args(s: &str) -> Vec { s.split_whitespace() .map(|arg| { if arg.contains(&['"', '\''][..]) { panic!("shell-style argument parsing is not supported"); } String::from(arg) }) .collect() } static LITERAL_REDACTIONS: &[(&str, &str)] = &[ // Unix message for an entity was not found ("[NOT_FOUND]", "No such file or directory (os error 2)"), // Windows message for an entity was not found ( "[NOT_FOUND]", "The system cannot find the file specified. (os error 2)", ), ( "[NOT_FOUND]", "The system cannot find the path specified. (os error 3)", ), ("[NOT_FOUND]", "program not found"), // Unix message for exit status ("[EXIT_STATUS]", "exit status"), // Windows message for exit status ("[EXIT_STATUS]", "exit code"), ("[TAB]", "\t"), ("[EXE]", std::env::consts::EXE_SUFFIX), ]; fn assert(root: &Path) -> snapbox::Assert { let mut subs = snapbox::Redactions::new(); subs.insert("[ROOT]", root.to_path_buf()).unwrap(); subs.insert("[VERSION]", mdbook_core::MDBOOK_VERSION) .unwrap(); subs.extend(LITERAL_REDACTIONS.into_iter().cloned()) .unwrap(); snapbox::Assert::new() .action_env(snapbox::assert::DEFAULT_ACTION_ENV) .redact_with(subs) } /// Helper to read a string from the filesystem. #[track_caller] pub fn read_to_string>(path: P) -> String { let path = path.as_ref(); fs::read_to_string(path).unwrap() } /// Returns the first path from the given glob pattern. pub fn glob_one>(path: P, pattern: &str) -> PathBuf { let path = path.as_ref(); let mut matches = glob::glob(path.join(pattern).to_str().unwrap()).unwrap(); let Some(first) = matches.next() else { panic!("expected at least one file at `{path:?}` with pattern `{pattern}`, found none"); }; let first = first.unwrap(); if let Some(next) = matches.next() { panic!( "expected only one file for pattern `{pattern}` in `{path:?}`, \ found `{first:?}` and `{:?}`", next.unwrap() ); } first } /// Lists all files at the given directory. /// /// Recursively walks the tree. Paths are relative to the directory. pub fn list_all_files(dir: &Path) -> Vec { walkdir::WalkDir::new(dir) .sort_by_file_name() .into_iter() // Skip the outer directory. .skip(1) .map(|entry| { let entry = entry.unwrap(); let path = entry.path(); path.strip_prefix(dir).unwrap().to_path_buf() }) .collect() } ================================================ FILE: tests/testsuite/build/basic_build/README.md ================================================ # Basic book This GUI test book is the default book with a single chapter. ================================================ FILE: tests/testsuite/build/basic_build/book.toml ================================================ [book] title = "basic_build" ================================================ FILE: tests/testsuite/build/basic_build/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/build/basic_build/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/build/create_missing/book.toml ================================================ [book] title = "create_missing" ================================================ FILE: tests/testsuite/build/create_missing/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/build/missing_file/book.toml ================================================ [book] title = "missing_file" [build] create-missing = false ================================================ FILE: tests/testsuite/build/missing_file/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/build/no_reserved_filename/book.toml ================================================ [book] title = "no_reserved_filename" ================================================ FILE: tests/testsuite/build/no_reserved_filename/src/SUMMARY.md ================================================ # Summary - [Print](print.md) ================================================ FILE: tests/testsuite/build/no_reserved_filename/src/print.md ================================================ # Print ================================================ FILE: tests/testsuite/build.rs ================================================ //! General build tests. //! //! More specific tests should usually go into a module based on the feature. //! This module should just have general build tests, or misc small things. use crate::prelude::*; // Simple smoke test that building works. #[test] fn basic_build() { BookTest::from_dir("build/basic_build").run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }); } // Ensure building fails if `create-missing` is false and one of the files does // not exist. #[test] fn failure_on_missing_file() { BookTest::from_dir("build/missing_file").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" ERROR failed to read chapter `./chapter_1.md` [TAB]Caused by: [NOT_FOUND] "#]]); }); } // Ensure a missing file is created if `create-missing` is true. #[test] fn create_missing() { let test = BookTest::from_dir("build/create_missing"); assert!(test.dir.join("src/SUMMARY.md").exists()); assert!(!test.dir.join("src/chapter_1.md").exists()); test.load_book(); assert!(test.dir.join("src/chapter_1.md").exists()); } // Checks that it fails if the summary has a reserved filename. #[test] fn no_reserved_filename() { BookTest::from_dir("build/no_reserved_filename").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend ERROR Rendering failed [TAB]Caused by: print.md is reserved for internal use "#]]); }); } // Build without book.toml should be OK. #[test] fn book_toml_isnt_required() { let mut test = BookTest::init(|_| {}); std::fs::remove_file(test.dir.join("book.toml")).unwrap(); test.build(); test.check_main_file( "book/chapter_1.html", str![[r##"

Chapter 1

"##]], ); } // Dest dir relative path behavior. #[test] fn dest_dir_relative_path() { let mut test = BookTest::from_dir("build/basic_build"); let current_dir = test.dir.join("work"); std::fs::create_dir_all(¤t_dir).unwrap(); test.run("build", |cmd| { cmd.args(&["--dest-dir", "foo", ".."]) .current_dir(¤t_dir) .expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/work/foo` "#]]); }); assert!(current_dir.join("foo/index.html").exists()); } ================================================ FILE: tests/testsuite/cli.rs ================================================ //! Basic tests for mdbook's CLI. use crate::prelude::*; use snapbox::file; // Test with no args. #[test] #[cfg_attr( not(all(feature = "watch", feature = "serve")), ignore = "needs all features" )] fn no_args() { BookTest::empty().run("", |cmd| { cmd.expect_code(2) .expect_stdout(str![[""]]) .expect_stderr(file!["cli/no_args.term.svg"]); }); } // Help command. #[test] #[cfg_attr( not(all(feature = "watch", feature = "serve")), ignore = "needs all features" )] fn help() { BookTest::empty() .run("help", |cmd| { cmd.expect_stdout(file!["cli/help.term.svg"]) .expect_stderr(str![[""]]); }) .run("--help", |cmd| { cmd.expect_stdout(file!["cli/help.term.svg"]) .expect_stderr(str![[""]]); }); } ================================================ FILE: tests/testsuite/config/empty/book.toml ================================================ ================================================ FILE: tests/testsuite/config/empty/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/config/empty/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/config.rs ================================================ //! Tests for book configuration loading. use crate::prelude::*; // Test that config can load from environment variable. #[test] fn config_from_env() { BookTest::from_dir("config/empty") .run("build", |cmd| { cmd.env("MDBOOK_BOOK__TITLE", "Custom env title"); }) .check_file_contains( "book/index.html", "Chapter 1 - Custom env title", ); // json for some subtable // } // Test environment config with JSON. #[test] fn config_json_from_env() { // build table BookTest::from_dir("config/empty") .run("build", |cmd| { cmd.env( "MDBOOK_BOOK", r#"{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}"#, ); }) .check_file_contains( "book/index.html", "Chapter 1 - My Awesome Book", ); // book table BookTest::from_dir("config/empty") .run("build", |cmd| { cmd.env("MDBOOK_BUILD", r#"{"build-dir": "alt"}"#); }) .check_file_contains("alt/index.html", "Chapter 1"); } // Test that a preprocessor receives config set in the environment. #[test] fn preprocessor_cfg_from_env() { let mut test = BookTest::from_dir("config/empty"); test.rust_program( "cat-to-file", 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(); println!("{{\"items\": []}}"); } "#, ) .run("build", |cmd| { cmd.env( "MDBOOK_PREPROCESSOR__CAT_TO_FILE", r#"{"command":"./cat-to-file", "array": [1,2,3], "number": 123}"#, ); }); let out = read_to_string(test.dir.join("out.txt")); let (ctx, _book) = mdbook_preprocessor::parse_input(out.as_bytes()).unwrap(); let cfg: serde_json::Value = ctx.config.get("preprocessor.cat-to-file").unwrap().unwrap(); assert_eq!( cfg, serde_json::json!({ "command": "./cat-to-file", "array": [1,2,3], "number": 123, }) ); } // Test that a renderer receives config set in the environment. #[test] fn output_cfg_from_env() { let mut test = BookTest::from_dir("config/empty"); test.rust_program( "cat-to-file", 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(); } "#, ) .run("build", |cmd| { cmd.env( "MDBOOK_OUTPUT__CAT_TO_FILE", r#"{"command":"./cat-to-file", "array": [1,2,3], "number": 123}"#, ); }); let out = read_to_string(test.dir.join("book/out.txt")); let ctx = mdbook_renderer::RenderContext::from_json(out.as_bytes()).unwrap(); let cfg: serde_json::Value = ctx.config.get("output.cat-to-file").unwrap().unwrap(); assert_eq!( cfg, serde_json::json!({ "command": "./cat-to-file", "array": [1,2,3], "number": 123, }) ); } // An invalid key at the top level. #[test] fn bad_config_top_level() { BookTest::init(|_| {}) .change_file("book.toml", "foo = 123") .run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR Invalid configuration file [TAB]Caused by: TOML parse error at line 1, column 1 | 1 | foo = 123 | ^^^ unknown field `foo`, expected one of `book`, `build`, `rust`, `output`, `preprocessor` "#]]); }); } // An invalid table at the top level. #[test] fn bad_config_top_level_table() { BookTest::init(|_| {}) .change_file( "book.toml", "[other]\n\ foo = 123", ) .run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR Invalid configuration file [TAB]Caused by: TOML parse error at line 1, column 2 | 1 | [other] | ^^^^^ unknown field `other`, expected one of `book`, `build`, `rust`, `output`, `preprocessor` "#]]); }); } // An invalid key in the main book table. #[test] fn bad_config_in_book_table() { BookTest::init(|_| {}) .change_file( "book.toml", "[book]\n\ title = \"bad-config\"\n\ foo = 123" ) .run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR Invalid configuration file [TAB]Caused by: TOML parse error at line 3, column 1 | 3 | foo = 123 | ^^^ unknown field `foo`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction` "#]]); }); } // An invalid key in the main rust table. #[test] fn bad_config_in_rust_table() { BookTest::init(|_| {}) .change_file( "book.toml", "[rust]\n\ title = \"bad-config\"\n", ) .run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR Invalid configuration file [TAB]Caused by: TOML parse error at line 2, column 1 | 2 | title = "bad-config" | ^^^^^ unknown field `title`, expected `edition` "#]]); }); } // An invalid top-level key in the environment. #[test] fn env_invalid_config_key() { BookTest::from_dir("config/empty").run("build", |cmd| { cmd.env("MDBOOK_FOO", "testing") .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }); } // An invalid value in the environment. #[test] fn env_invalid_value() { BookTest::from_dir("config/empty") .run("build", |cmd| { cmd.env("MDBOOK_BOOK", r#"{"titlez": "typo"}"#) .expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR unknown field `titlez`, expected one of `title`, `authors`, `description`, `src`, `language`, `text-direction` "#]]); }) .run("build", |cmd| { cmd.env("MDBOOK_BOOK__TITLE", r#"{"looks like obj": "abc"}"#) .expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR invalid type: map, expected a string in `title` "#]]); }) // This is not valid JSON, so falls back to be interpreted as a string. .run("build", |cmd| { cmd.env("MDBOOK_BOOK__TITLE", r#"{braces}"#) .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }) .check_file_contains("book/index.html", "Chapter 1 - {braces}"); } // Replacing the entire book table from the environment. #[test] fn env_entire_book_table() { BookTest::init(|_| {}) .change_file( "book.toml", "[book]\n\ title = \"config title\"\n\ ", ) .run("build", |cmd| { cmd.env("MDBOOK_BOOK", r#"{"description": "custom description"}"#); }) // The book.toml title is removed. .check_file_contains("book/index.html", "Chapter 1") .check_file_contains( "book/index.html", r#""#, ); } // Replacing the entire output or preprocessor table from the environment. #[test] fn env_entire_output_preprocessor_table() { BookTest::from_dir("config/empty") .rust_program( "mdbook-my-preprocessor", r#" fn main() { let mut args = std::env::args().skip(1); if args.next().as_deref() == Some("supports") { return; } use std::io::Read; let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); assert!(s.contains("custom preprocessor config")); println!("{{\"items\": []}}"); } "#, ) .rust_program( "mdbook-my-output", r#" fn main() { use std::io::Read; let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); assert!(s.contains("custom output config")); eprintln!("preprocessor saw custom config"); } "#, ) .run("build", |cmd| { let mut paths: Vec<_> = std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()).collect(); paths.push(cmd.dir.clone()); let path = std::env::join_paths(paths).unwrap().into_string().unwrap(); cmd.env( "MDBOOK_OUTPUT", r#"{"my-output": {"foo": "custom output config"}}"#, ) .env( "MDBOOK_PREPROCESSOR", r#"{"my-preprocessor": {"foo": "custom preprocessor config"}}"#, ) .env("PATH", path) .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the my-output backend INFO Invoking the "my-output" renderer preprocessor saw custom config "#]]); }) // No HTML output .check_file_list("book", str![[""]]); } ================================================ FILE: tests/testsuite/includes/all_includes/book.toml ================================================ [book] title = "all_includes" ================================================ FILE: tests/testsuite/includes/all_includes/src/SUMMARY.md ================================================ # Summary - [Basic Includes](./includes.md) - [Relative Includes](./relative/includes.md) - [Recursive Includes](./recursive.md) - [Include Anchors](./anchors.md) - [Rustdoc Includes](./rustdoc.md) - [Playground Includes](./playground.md) ================================================ FILE: tests/testsuite/includes/all_includes/src/anchors.md ================================================ # Include Anchors ```rust {{#include nested-test-with-anchors.rs:myanchor}} ``` ================================================ FILE: tests/testsuite/includes/all_includes/src/example.rs ================================================ fn main() { println!("Hello World!"); # # // You can even hide lines! :D # println!("I am hidden! Expand the code snippet to see me"); } ================================================ FILE: tests/testsuite/includes/all_includes/src/includes.md ================================================ # Basic Includes {{#include sample.md}} ================================================ FILE: tests/testsuite/includes/all_includes/src/nested-test-with-anchors.rs ================================================ // This is a test of includes with anchors. // ANCHOR: myanchor // ANCHOR: unendinganchor let x = 1; // ANCHOR_END: myanchor ================================================ FILE: tests/testsuite/includes/all_includes/src/partially-included-test-with-anchors.rs ================================================ fn some_other_function() { // ANCHOR: unused-anchor-that-should-be-stripped println!("unused anchor"); // ANCHOR_END: unused-anchor-that-should-be-stripped } // ANCHOR: rustdoc-include-anchor fn main() { some_other_function(); } // ANCHOR_END: rustdoc-include-anchor ================================================ FILE: tests/testsuite/includes/all_includes/src/partially-included-test.rs ================================================ fn some_function() { println!("some function"); } fn main() { some_function(); } ================================================ FILE: tests/testsuite/includes/all_includes/src/playground.md ================================================ # Playground Includes {{#playground example.rs}} ================================================ FILE: tests/testsuite/includes/all_includes/src/recursive.md ================================================ Around the world, around the world {{#include recursive.md}} ================================================ FILE: tests/testsuite/includes/all_includes/src/relative/includes.md ================================================ # Relative Includes {{#include ../sample.md}} ================================================ FILE: tests/testsuite/includes/all_includes/src/rustdoc.md ================================================ # Rustdoc Includes ## 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}} ``` ================================================ FILE: tests/testsuite/includes/all_includes/src/sample.md ================================================ ## Sample This is a sample include. ================================================ FILE: tests/testsuite/includes.rs ================================================ //! Tests for include preprocessor. use crate::prelude::*; // Basic test for #include. #[test] fn include() { BookTest::from_dir("includes/all_includes") .check_main_file( "book/includes.html", str![[r##"

Basic Includes

Sample

This is a sample include.

"##]], ) .check_main_file( "book/relative/includes.html", str![[r##"

Relative Includes

Sample

This is a sample include.

"##]], ); } // Checks for anchored includes. #[test] fn anchored_include() { BookTest::from_dir("includes/all_includes").check_main_file( "book/anchors.html", str![[r##"

Include Anchors

#![allow(unused)]
fn main() {
let x = 1;
}
"##]], ); } // Checks behavior of recursive include. #[test] fn recursive_include() { BookTest::from_dir("includes/all_includes") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started ERROR Stack depth exceeded in recursive.md. Check for cyclic includes INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }) .check_main_file( "book/recursive.html", str![[r#"

Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world Around the world, around the world

"#]], ); } // Checks the behavior of `{{#playground}}` include. #[test] fn playground_include() { BookTest::from_dir("includes/all_includes") .check_main_file("book/playground.html", str![[r##"

Playground Includes

fn main() {
    println!("Hello World!");

   // You can even hide lines! :D
  println!("I am hidden! Expand the code snippet to see me");
}
"##]]); } // Checks the behavior of `{{#rustdoc_include}}`. #[test] fn rustdoc_include() { BookTest::from_dir("includes/all_includes") .check_main_file("book/rustdoc.html", str![[r##"

Rustdoc Includes

Rustdoc include adds the rest of the file as hidden

fn some_function() {
    println!("some function");
}

fn main() {
    some_function();
}

Rustdoc include works with anchors too

fn some_other_function() {
    println!("unused anchor");
}

fn main() {
    some_other_function();
}
"##]]); } ================================================ FILE: tests/testsuite/index/basic_readme/book.toml ================================================ [book] title = "basic_readme" ================================================ FILE: tests/testsuite/index/basic_readme/src/README.md ================================================ # Intro ================================================ FILE: tests/testsuite/index/basic_readme/src/SUMMARY.md ================================================ # Summary [Intro](./README.md) - [First](first/README) - [Second](second/Readme.md) ================================================ FILE: tests/testsuite/index/basic_readme/src/first/README ================================================ # First ================================================ FILE: tests/testsuite/index/basic_readme/src/second/Readme.md ================================================ # Second ================================================ FILE: tests/testsuite/index.rs ================================================ //! Tests for the index preprocessor. use crate::prelude::*; // Checks basic README to index.html conversion. #[test] fn readme_to_index() { let mut test = BookTest::from_dir("index/basic_readme"); test.check_main_file( "book/index.html", str![[r##"

Intro

"##]], ) .check_main_file( "book/first/index.html", str![[r##"

First

"##]], ) .check_main_file( "book/second/index.html", str![[r##"

Second

"##]], ) .check_toc_js(str![[r#"
  1. Intro
  2. First
  3. Second
"#]]); assert!(test.dir.join("book/index.html").exists()); assert!(!test.dir.join("book/README.html").exists()); } ================================================ FILE: tests/testsuite/init/init_from_summary/src/SUMMARY.md ================================================ # Summary [intro](intro.md) - [First chapter](first.md) [outro](outro.md) ================================================ FILE: tests/testsuite/init.rs ================================================ //! Tests for `mdbook init`. use crate::prelude::*; use mdbook_core::config::Config; use mdbook_driver::MDBook; use std::path::PathBuf; // Tests "init" with no args. #[test] fn basic_init() { let mut test = BookTest::empty(); test.run("init", |cmd| { cmd.expect_stdout(str![[r#" Do you want a .gitignore to be created? (y/n) What title would you like to give the book? All done, no errors... "#]]) .expect_stderr(str![[r#" INFO Creating a new book with stub content "#]]); }) .check_file( "book.toml", str![[r#" [book] authors = [] language = "en" "#]], ) .check_file( "src/SUMMARY.md", str![[r#" # Summary - [Chapter 1](./chapter_1.md) "#]], ) .check_file( "src/chapter_1.md", str![[r#" # Chapter 1 "#]], ) .check_main_file( "book/chapter_1.html", str![[r##"

Chapter 1

"##]], ); assert!(!test.dir.join(".gitignore").exists()); assert!(test.dir.join("book").exists()); } // Test init via API. This does a little less than the CLI does. #[test] fn init_api() { let mut test = BookTest::empty(); MDBook::init(&test.dir).build().unwrap(); test.check_file_list( ".", str![[r#" book book.toml src src/SUMMARY.md src/chapter_1.md "#]], ); } // Run `mdbook init` with `--force` to skip the confirmation prompts #[test] fn init_force() { let mut test = BookTest::empty(); test.run("init --force", |cmd| { cmd.expect_stdout(str![[r#" All done, no errors... "#]]) .expect_stderr(str![[r#" INFO Creating a new book with stub content "#]]); }) .check_file( "book.toml", str![[r#" [book] authors = [] language = "en" "#]], ); assert!(!test.dir.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 mut test = BookTest::empty(); test.run("init", |cmd| { cmd.expect_stdout(str![[r#" Do you want a .gitignore to be created? (y/n) All done, no errors... "#]]) .expect_stderr(str![[r#" INFO Creating a new book with stub content "#]]) .args(&["--title", "Example title"]); }) .check_file( "book.toml", str![[r#" [book] title = "Example title" authors = [] language = "en" "#]], ); assert!(!test.dir.join(".gitignore").exists()); } // Run `mdbook init` in a directory containing a SUMMARY.md should create the // files listed in the summary. #[test] fn init_from_summary() { BookTest::from_dir("init/init_from_summary") .run("init", |_| {}) .check_file( "src/intro.md", str![[r#" # intro "#]], ) .check_file( "src/first.md", str![[r#" # First chapter "#]], ) .check_file( "src/outro.md", str![[r#" # outro "#]], ); } // Set some custom arguments for where to place the source and destination // files, then call `mdbook init`. #[test] fn init_with_custom_book_and_src_locations() { let mut test = BookTest::empty(); let mut cfg = Config::default(); cfg.book.src = PathBuf::from("in"); cfg.build.build_dir = PathBuf::from("out"); MDBook::init(&test.dir).with_config(cfg).build().unwrap(); test.check_file( "book.toml", str![[r#" [book] authors = [] src = "in" language = "en" [build] build-dir = "out" create-missing = true use-default-preprocessors = true extra-watch-dirs = [] "#]], ) .check_file( "in/SUMMARY.md", str![[r#" # Summary - [Chapter 1](./chapter_1.md) "#]], ) .check_file( "in/chapter_1.md", str![[r#" # Chapter 1 "#]], ); assert!(test.dir.join("out").exists()); } // Copies the theme into the initialized directory. #[test] fn copy_theme() { BookTest::empty() .run("init --theme", |_| {}) .check_file_list( ".", str![[r#" book book.toml src src/SUMMARY.md src/chapter_1.md theme theme/book.js theme/css theme/css/chrome.css theme/css/general.css theme/css/print.css theme/css/variables.css theme/favicon.png theme/favicon.svg theme/fonts theme/fonts/OPEN-SANS-LICENSE.txt theme/fonts/SOURCE-CODE-PRO-LICENSE.txt theme/fonts/fonts.css theme/fonts/open-sans-v17-all-charsets-300.woff2 theme/fonts/open-sans-v17-all-charsets-300italic.woff2 theme/fonts/open-sans-v17-all-charsets-600.woff2 theme/fonts/open-sans-v17-all-charsets-600italic.woff2 theme/fonts/open-sans-v17-all-charsets-700.woff2 theme/fonts/open-sans-v17-all-charsets-700italic.woff2 theme/fonts/open-sans-v17-all-charsets-800.woff2 theme/fonts/open-sans-v17-all-charsets-800italic.woff2 theme/fonts/open-sans-v17-all-charsets-italic.woff2 theme/fonts/open-sans-v17-all-charsets-regular.woff2 theme/fonts/source-code-pro-v11-all-charsets-500.woff2 theme/highlight.css theme/highlight.js theme/index.hbs "#]], ); } ================================================ FILE: tests/testsuite/main.rs ================================================ //! Main testsuite for exercising all functionality of mdBook. //! //! See README.md for documentation. #![allow(unreachable_pub, reason = "not needed in an integration test crate")] mod book_test; mod build; mod cli; mod config; mod includes; mod index; mod init; mod markdown; mod playground; mod preprocessor; mod print; mod redirects; mod renderer; mod rendering; #[cfg(feature = "search")] mod search; mod test; mod theme; mod toc; mod prelude { pub use crate::book_test::{BookTest, glob_one, read_to_string}; pub use snapbox::str; } ================================================ FILE: tests/testsuite/markdown/admonitions/book.toml ================================================ [book] title = "admonitions" ================================================ FILE: tests/testsuite/markdown/admonitions/expected/admonitions.html ================================================

Admonitions

Note

This is a note.

There are multiple paragraphs.

Tip

This is a tip.

Important

This is important.

Warning

This is a warning.

Caution

This is a caution.

[!UNKNOWN] This is an unknown tag.

Important

This is an important admonition.

Note

This nested note should have its own color.

================================================ FILE: tests/testsuite/markdown/admonitions/expected_disabled/admonitions.html ================================================

Admonitions

[!NOTE] This is a note.

There are multiple paragraphs.

[!TIP] This is a tip.

[!IMPORTANT] This is important.

[!WARNING] This is a warning.

[!CAUTION] This is a caution.

[!UNKNOWN] This is an unknown tag.

[!IMPORTANT] This is an important admonition.

[!NOTE] This nested note should have its own color.

================================================ FILE: tests/testsuite/markdown/admonitions/src/SUMMARY.md ================================================ # Summary - [Admonitions](./admonitions.md) ================================================ FILE: tests/testsuite/markdown/admonitions/src/admonitions.md ================================================ # Admonitions > [!NOTE] > This is a note. > > There are multiple paragraphs. > [!TIP] > This is a tip. > [!IMPORTANT] > This is important. > [!WARNING] > This is a warning. > [!CAUTION] > This is a caution. > [!UNKNOWN] > This is an unknown tag. > [!IMPORTANT] > This is an important admonition. > > > [!NOTE] > > This nested note should have its own color. ================================================ FILE: tests/testsuite/markdown/basic_markdown/book.toml ================================================ [book] title = "basic_markdown" ================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/blank.html ================================================ ================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/blockquotes.html ================================================

Blockquotes

Empty:

Normal:

foo bar

Contains code block:

#![allow(unused)]
fn main() {
let x = 1;
}

Random stuff:

And now,

Let us introduce All kinds of

  • tags
  • etc
  • stuff
  1. In

  2. The

  3. blockquote

    cause we can

    Cause we can

================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/code-blocks.html ================================================

Code blocks

This is a codeblock
#![allow(unused)]
fn main() {
// This links to a playpen
}
# This is an editable codeblock
Text with different classes.
Indented
code
block.
#![allow(unused)]
fn main() {
let x = 1;
}
fn main() {
    println!("hello");
}
================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/html.html ================================================

Inline HTML

Comments

Void elements

alt text

Line
break

Wordbreak

Rule:


Blocks

A block HTML element trying to do *markup*.

A block HTML with spaces that cause it to be interleaved with markdown.

Scripts

Style

Manual headers

My Header

Another header

================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/images.html ================================================

Images

Image “alt” & " "text" & <stuff> url <em>html</em> — hard break

Image with title

================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/inlines.html ================================================

Inlines

emphasis bold bold emphasis

Some inline code.

Hard
break

Invisible hard
break

[escaped] <html> *here*

================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/links.html ================================================

Links

================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/lists.html ================================================

Lists

  1. A
  2. Normal
  3. Ordered
  4. List

  1. A
    1. Nested
    2. List
  2. But
  3. Still
  4. Normal

  1. Start list
  2. with a different number.

  • An
  • Unordered
  • Normal
  • List

  • Nested
    • Unordered
  • List

  • This
    1. Is
    2. Normal
  • ?!
================================================ FILE: tests/testsuite/markdown/basic_markdown/expected/svg.html ================================================

SVG

SVG ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/SUMMARY.md ================================================ # Summary - [Blank](./blank.md) - [Blockquotes](./blockquotes.md) - [Code blocks](./code-blocks.md) - [Lists](./lists.md) - [Inlines](./inlines.md) - [Links](./links.md) - [Images](./images.md) - [HTML](./html.md) - [SVG](./svg.md) ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/blank.md ================================================ ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/blockquotes.md ================================================ # Blockquotes Empty: > Normal: > foo > bar Contains code block: > ```rust > let x = 1; > ``` Random stuff: > ### And now, > > **Let us _introduce_** > All kinds of > > - tags > - etc > - stuff > > 1. In > 2. The > 3. blockquote > > > cause we can > > > > > Cause we can ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/code-blocks.md ================================================ # Code blocks ``` This is a codeblock ``` ```rust // This links to a playpen ``` ```bash,editable # This is an editable codeblock ``` ```text cls1,,cls2 cls3 Text with different classes. ``` Indented code block. ```rust,edition2021 let x = 1; ``` ```rust fn main() { println!("hello"); } ``` ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/html.md ================================================ # Inline HTML ## Comments ## Void elements alt text Line
break Wordbreak
Rule:
## Blocks
A block HTML element trying to do *markup*.
A block HTML with spaces that **cause** it to be interleaved with markdown.
## Scripts ## Style ## Manual headers

My Header

Another header

================================================ FILE: tests/testsuite/markdown/basic_markdown/src/images.md ================================================ # Images ![Image *"alt"* & \" `"text" & ` [url](/foo) html --- hard\ break ](https://rust-lang.org/logos/rust-logo-256x256.png) ![Image with title](https://rust-lang.org/logos/rust-logo-256x256.png "Some title") ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/inlines.md ================================================ # Inlines *emphasis* **bold** **_bold emphasis_** Some `inline code`. Hard\ break Invisible hard break \[escaped] \ \*here* ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/links.md ================================================ # Links - [Inline](https://example.com/inline) - [Collapsed] - [Emtpy reference][] - [Specific reference][specific] - - - [Titled](https://example.com/title "with title") - [Broken collapsed] - [Broken reference][missing] - [Markdown link](path/foo.md) - [Markdown link anchor](path/foo.md#anchor) - [Link anchor](#anchor) - [With md in anchor](path.html#phantomdata) [collapsed]: https://example.com/collapsed [specific]: https://example.com/specific ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/lists.md ================================================ # Lists 1. A 2. Normal 3. Ordered 4. List --- 1. A 1. Nested 2. List 2. But 3. Still 4. Normal --- 7. Start list 7. with a different number. --- - An - Unordered - Normal - List --- - Nested - Unordered - List --- - This 1. Is 2. Normal - ?! ================================================ FILE: tests/testsuite/markdown/basic_markdown/src/svg.md ================================================ # SVG SVG ================================================ FILE: tests/testsuite/markdown/custom_header_attributes/book.toml ================================================ [book] title = "custom_header_attributes" ================================================ FILE: tests/testsuite/markdown/custom_header_attributes/src/SUMMARY.md ================================================ # Summary - [Heading Attributes](./custom_header_attributes.md) ================================================ FILE: tests/testsuite/markdown/custom_header_attributes/src/custom_header_attributes.md ================================================ # Heading Attributes {#attrs} ## Heading with classes {.class1 .class2} ## Heading with id and classes {#both .class1 .class2} ## Heading with attribute {.myclass1 myattr #myh3 otherattr=value .myclass2} ================================================ FILE: tests/testsuite/markdown/definition_lists/book.toml ================================================ [book] title = "definition_lists" ================================================ FILE: tests/testsuite/markdown/definition_lists/expected/definition_lists.html ================================================

Definition Lists

apple
red fruit
orange
orange fruit
apple
red fruit
computer company
orange
orange fruit
telecom company
term
  1. Para one

    Para two

apple
red fruit

Multi-line term

a b
c

foo

Nested

level one
l1 level two
l2 level three
l3
level one
l1

Loose

apple

red fruit

computer company

orange

orange fruit

================================================ FILE: tests/testsuite/markdown/definition_lists/expected/html_definition_lists.html ================================================

HTML definition lists

Test for definition lists manually written in HTML.

Some tag
Some defintion
Another definition
A definition.
================================================ FILE: tests/testsuite/markdown/definition_lists/expected_disabled/definition_lists.html ================================================

Definition Lists

apple : red fruit

orange : orange fruit

apple : red fruit : computer company

orange : orange fruit : telecom company

term : 1. Para one

   Para two

apple : red fruit

Multi-line term

a b
c

: foo

Nested

level one : l1 level two : l2 level three : l3

level one : l1

Loose

apple

: red fruit : computer company

orange

: orange fruit

================================================ FILE: tests/testsuite/markdown/definition_lists/expected_disabled/html_definition_lists.html ================================================

HTML definition lists

Test for definition lists manually written in HTML.

Some tag
Some defintion
Another definition
A definition.
================================================ FILE: tests/testsuite/markdown/definition_lists/src/SUMMARY.md ================================================ # Summary - [Definition lists](./definition_lists.md) - [HTML definition lists](./html_definition_lists.md) ================================================ FILE: tests/testsuite/markdown/definition_lists/src/definition_lists.md ================================================ # Definition Lists apple : red fruit orange : orange fruit apple : red fruit : computer company orange : orange fruit : telecom company term : 1. Para one Para two ## Term with link [apple](some-page.md#apple) : red fruit ## Multi-line term a b\ c : foo ## Nested level one : l1 level two : l2 level three : l3 level one : l1 ## Loose apple : red fruit : computer company orange : orange fruit ================================================ FILE: tests/testsuite/markdown/definition_lists/src/html_definition_lists.md ================================================ # HTML definition lists Test for definition lists manually written in HTML.
Some tag
Some defintion
Another definition
A definition.
================================================ FILE: tests/testsuite/markdown/footnotes/expected/footnotes.html ================================================

Footnote tests

Footnote example1, or with a word2.

There are multiple references to word2.

Footnote without a paragraph3

Footnote with multiple paragraphs4

Footnote name with wacky characters5

Testing when referring to something earlier.6

Footnote that is defined multiple times.7

And another8 that references the duplicate again.7

Multiple footnotes in a row.9 10 11


  1. This is a footnote. ↩2

  2. A longer footnote. With multiple lines. Link to other. With a reference inside.1 ↩2

    1. Item one
      1. Sub-item
    2. Item two
  3. One

    Two

    Three

  4. Testing footnote id with special characters.

  5. This is defined before it is referred to.

  6. This is the first definition of the footnote with tag multiple-definitions ↩2

  7. Footnote between duplicates.

  8. Footnote 1

  9. Footnote 2

  10. Footnote 3

================================================ FILE: tests/testsuite/markdown/footnotes/src/SUMMARY.md ================================================ - [Footnotes](footnotes.md) ================================================ FILE: tests/testsuite/markdown/footnotes/src/footnotes.md ================================================ # Footnote tests Footnote example[^1], or with a word[^word]. [^1]: This is a footnote. [^word]: A longer footnote. With multiple lines. [Link to other](other.md). With a reference inside.[^1] There are multiple references to word[^word]. Footnote without a paragraph[^para] [^para]: 1. Item one 1. Sub-item 2. Item two Footnote with multiple paragraphs[^multiple] [^define-before-use]: This is defined before it is referred to. [^multiple]: One Two Three [^unused]: This footnote is defined by not used. Footnote name with wacky characters[^"wacky"] [^"wacky"]: Testing footnote id with special characters. Testing when referring to something earlier.[^define-before-use] Footnote that is defined multiple times.[^multiple-definitions] [^multiple-definitions]: This is the first definition of the footnote with tag multiple-definitions And another[^in-between] that references the duplicate again.[^multiple-definitions] [^in-between]: Footnote between duplicates. [^multiple-definitions]: This is the second definition of the footnote with tag multiple-definitions Multiple footnotes in a row.[^a][^b][^c] [^a]: Footnote 1 [^b]: Footnote 2 [^c]: Footnote 3 ================================================ FILE: tests/testsuite/markdown/smart_punctuation/src/SUMMARY.md ================================================ - [Smart Punctuation](smart_punctuation.md) ================================================ FILE: tests/testsuite/markdown/smart_punctuation/src/smart_punctuation.md ================================================ # Smart Punctuation - En dash: -- - Em dash: --- - Ellipsis: ... - Double quote: "quote" - Single quote: 'quote' - Quote in `"code"` ``` "quoted" ``` ================================================ FILE: tests/testsuite/markdown/strikethrough/src/SUMMARY.md ================================================ - [Strikethrough](strikethrough.md) ================================================ FILE: tests/testsuite/markdown/strikethrough/src/strikethrough.md ================================================ # Strikethrough ~~strikethrough example~~ ================================================ FILE: tests/testsuite/markdown/tables/src/SUMMARY.md ================================================ - [Tables](tables.md) ================================================ FILE: tests/testsuite/markdown/tables/src/tables.md ================================================ # Tables | foo | bar | | --- | --- | | baz | bim | | Backslash in code | `\` | | Double back in code | `\\` | | Pipe in code | `\|` | | Pipe in code2 | `test \| inside` | | Neither | Left | Center | Right | |---------|:-----|:------:|------:| | one | two | three | four | ================================================ FILE: tests/testsuite/markdown/tasklists/src/SUMMARY.md ================================================ - [Tasklists](tasklists.md) ================================================ FILE: tests/testsuite/markdown/tasklists/src/tasklists.md ================================================ ## Tasklisks - [X] Apples - [X] Broccoli - [ ] Carrots ================================================ FILE: tests/testsuite/markdown.rs ================================================ //! Tests for special markdown rendering. use crate::prelude::*; use snapbox::file; // Checks custom header id and classes. #[test] fn custom_header_attributes() { BookTest::from_dir("markdown/custom_header_attributes") .check_main_file("book/custom_header_attributes.html", str![[r##"

Heading Attributes

Heading with classes

Heading with id and classes

Heading with attribute

"##]]); } // Test for a variety of footnote renderings. #[test] fn footnotes() { BookTest::from_dir("markdown/footnotes") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend WARN footnote `multiple-definitions` in footnotes.md defined multiple times - not updating to new definition WARN footnote `unused` in `footnotes.md` is defined but not referenced INFO HTML book written to `[ROOT]/book` "#]]); }) .check_main_file( "book/footnotes.html", file!["markdown/footnotes/expected/footnotes.html"], ); } // Basic table test. #[test] fn tables() { BookTest::from_dir("markdown/tables").check_main_file( "book/tables.html", str![[r##"

Tables

foobar
bazbim
Backslash in code/
Double back in code//
Pipe in code|
Pipe in code2test | inside
NeitherLeftCenterRight
onetwothreefour
"##]], ); } // Strikethrough test. #[test] fn strikethrough() { BookTest::from_dir("markdown/strikethrough").check_main_file( "book/strikethrough.html", str![[r##"

Strikethrough

strikethrough example

"##]], ); } // Tasklist test. #[test] fn tasklists() { BookTest::from_dir("markdown/tasklists").check_main_file( "book/tasklists.html", str![[r##"

Tasklisks

  • Apples
  • Broccoli
  • Carrots
"##]], ); } // Smart punctuation test. #[test] fn smart_punctuation() { BookTest::from_dir("markdown/smart_punctuation") // Default is on. .check_main_file( "book/smart_punctuation.html", str![[r##"

Smart Punctuation

  • En dash: –
  • Em dash: —
  • Ellipsis: …
  • Double quote: “quote”
  • Single quote: ‘quote’
  • Quote in "code"
"quoted"
"##]], ) .run("build", |cmd| { cmd.env("MDBOOK_OUTPUT__HTML__SMART_PUNCTUATION", "false"); }) .check_main_file( "book/smart_punctuation.html", str![[r##"

Smart Punctuation

  • En dash: --
  • Em dash: ---
  • Ellipsis: ...
  • Double quote: "quote"
  • Single quote: 'quote'
  • Quote in "code"
"quoted"
"##]], ); } // Basic markdown syntax. // This doesn't try to cover the commonmark test suite, but maybe it could some day? #[test] fn basic_markdown() { BookTest::from_dir("markdown/basic_markdown").check_all_main_files(); } #[test] fn definition_lists() { BookTest::from_dir("markdown/definition_lists") .check_all_main_files() .run("build", |cmd| { cmd.env("MDBOOK_OUTPUT__HTML__DEFINITION_LISTS", "false"); }) .check_main_file( "book/definition_lists.html", file!["markdown/definition_lists/expected_disabled/definition_lists.html"], ) .check_main_file( "book/html_definition_lists.html", file!["markdown/definition_lists/expected_disabled/html_definition_lists.html"], ); } #[test] fn admonitions() { BookTest::from_dir("markdown/admonitions") .check_all_main_files() .run("build", |cmd| { cmd.env("MDBOOK_OUTPUT__HTML__ADMONITIONS", "false"); }) .check_main_file( "book/admonitions.html", file!["markdown/admonitions/expected_disabled/admonitions.html"], ); } ================================================ FILE: tests/testsuite/playground/disabled_playground/book.toml ================================================ [book] title = "playground_on_rust_code" [output.html.playground] runnable = false ================================================ FILE: tests/testsuite/playground/disabled_playground/src/SUMMARY.md ================================================ # Summary - [Rust Playground](./disabled-rust-playground.md) ================================================ FILE: tests/testsuite/playground/disabled_playground/src/disabled-rust-playground.md ================================================ # Rust Sample ```rust let x = 1; ``` ================================================ FILE: tests/testsuite/playground/playground_on_rust_code/book.toml ================================================ [book] title = "playground_on_rust_code" ================================================ FILE: tests/testsuite/playground/playground_on_rust_code/src/SUMMARY.md ================================================ # Summary - [Rust Playground](./rust-playground.md) ================================================ FILE: tests/testsuite/playground/playground_on_rust_code/src/rust-playground.md ================================================ # Rust Sample ```rust let x = 1; ``` ================================================ FILE: tests/testsuite/playground.rs ================================================ //! Tests for Rust playground support. use crate::prelude::*; // Verifies that a rust codeblock gets the playground class. #[test] fn playground_on_rust_code() { BookTest::from_dir("playground/playground_on_rust_code").check_main_file( "book/index.html", str![[r##"

Rust Sample

#![allow(unused)]
fn main() {
let x = 1;
}
"##]], ); } // When the playground is disabled, there should be no playground class. #[test] fn disabled_playground() { BookTest::from_dir("playground/disabled_playground").check_main_file( "book/index.html", str![[r##"

Rust Sample

let x = 1;
"##]], ); } ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/book.toml ================================================ [book] title = "extension_compatibility" [preprocessor.my-preprocessor] command = "./my-preprocessor" custom-config = true optional = true [preprocessor.my-preprocessor.custom-table] extra = "abc" [output.html] [output.my-renderer] command = "./my-renderer" custom-config = "renderer settings" optional = true [output.my-renderer.custom-table] extra = "xyz" ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/src/SUMMARY.md ================================================ # Summary [Prefix chapter](./prefix.md) - [Chapter 1](./chapter_1.md) - [Draft chapter]() # Part title - [Part chapter](./part/chapter.md) - [Part sub chapter](./part/sub-chapter.md) --- [Suffix chapter](./suffix.md) ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/src/part/chapter.md ================================================ # Part chapter ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/src/part/sub-chapter.md ================================================ # Part sub chapter ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/src/prefix.md ================================================ # Prefix chapter ================================================ FILE: tests/testsuite/preprocessor/extension_compatibility/src/suffix.md ================================================ # Suffix chapter ================================================ FILE: tests/testsuite/preprocessor/failing_preprocessor/book.toml ================================================ [preprocessor.nop-preprocessor] command = "cargo run --quiet --example nop-preprocessor --" blow-up = true ================================================ FILE: tests/testsuite/preprocessor/failing_preprocessor/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/preprocessor/missing_optional_not_fatal/book.toml ================================================ [preprocessor.missing] command = "trduyvbhijnorgevfuhn" optional = true ================================================ FILE: tests/testsuite/preprocessor/missing_optional_not_fatal/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/preprocessor/missing_preprocessor/book.toml ================================================ [preprocessor.missing] command = "trduyvbhijnorgevfuhn" ================================================ FILE: tests/testsuite/preprocessor/missing_preprocessor/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/preprocessor/nop_preprocessor/book.toml ================================================ [preprocessor.nop-preprocessor] command = "cargo run --quiet --example nop-preprocessor --" ================================================ FILE: tests/testsuite/preprocessor/nop_preprocessor/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/preprocessor.rs ================================================ //! Tests for custom preprocessors. use crate::book_test::list_all_files; use crate::prelude::*; use anyhow::Result; use mdbook_core::book::{Book, BookItem, Chapter}; use mdbook_driver::builtin_preprocessors::CmdPreprocessor; use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use snapbox::IntoData; use std::sync::{Arc, Mutex}; struct Spy(Arc>); #[derive(Debug, Default)] struct Inner { run_count: usize, rendered_with: Vec, } impl Preprocessor for Spy { fn name(&self) -> &str { "dummy" } fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result { let mut inner = self.0.lock().unwrap(); inner.run_count += 1; inner.rendered_with.push(ctx.renderer.clone()); Ok(book) } } // Test that preprocessor gets run. #[test] fn runs_preprocessors() { let test = BookTest::init(|_| {}); let spy: Arc> = Default::default(); let mut book = test.load_book(); 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, ["html"]); } // Run tests with a custom preprocessor. #[test] fn test_with_custom_preprocessor() { let test = BookTest::init(|_| {}); let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_preprocessor(Spy(Arc::clone(&spy))); book.test(vec![]).unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1); assert_eq!(inner.rendered_with, ["test"]); } // No-op preprocessor works. #[test] fn nop_preprocessor() { BookTest::from_dir("preprocessor/nop_preprocessor").run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }); } // Failing preprocessor generates an error. #[test] fn failing_preprocessor() { BookTest::from_dir("preprocessor/failing_preprocessor").run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started Boom!!1! ERROR The "nop-preprocessor" preprocessor exited unsuccessfully with [EXIT_STATUS]: 1 status "#]]); }); } fn example() -> CmdPreprocessor { CmdPreprocessor::new( "nop-preprocessor".to_string(), "cargo run --quiet --example nop-preprocessor --".to_string(), std::env::current_dir().unwrap(), false, ) } #[test] fn example_supports_whatever() { let cmd = example(); let got = cmd.supports_renderer("whatever").unwrap(); assert_eq!(got, true); } #[test] fn example_doesnt_support_not_supported() { let cmd = example(); let got = cmd.supports_renderer("not-supported").unwrap(); assert_eq!(got, false); } // Checks the behavior of a relative path to a preprocessor. #[test] fn relative_command_path() { let mut test = BookTest::init(|_| {}); test.rust_program( "preprocessors/my-preprocessor", r#" fn main() { let mut args = std::env::args().skip(1); if args.next().as_deref() == Some("supports") { std::fs::write("support-check", args.next().unwrap()).unwrap(); return; } use std::io::Read; let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); std::fs::write("preprocessor-ran", "test").unwrap(); println!("{{\"items\": []}}"); } "#, ) .change_file( "book.toml", "[preprocessor.my-preprocessor]\n\ command = 'preprocessors/my-preprocessor'\n", ) .run("build", |cmd| { cmd.expect_stdout(str![""]).expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }) .check_file("support-check", "html") .check_file("preprocessor-ran", "test") // Try again, but outside of the book root to check relative path behavior. .rm_r("support-check") .rm_r("preprocessor-ran") .run("build ..", |cmd| { cmd.current_dir(cmd.dir.join("src")) .expect_stdout(str![""]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/src/../book` "#]]); }) .check_file("support-check", "html") .check_file("preprocessor-ran", "test"); } // Preprocessor command is missing. #[test] fn missing_preprocessor() { BookTest::from_dir("preprocessor/missing_preprocessor").run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started ERROR The command `trduyvbhijnorgevfuhn` wasn't found, is the `missing` preprocessor installed? If you want to ignore this error when the `missing` preprocessor is not installed, set `optional = true` in the `[preprocessor.missing]` section of the book.toml configuration file. ERROR Unable to run the preprocessor `missing` [TAB]Caused by: [NOT_FOUND] "#]]); }); } // Optional missing is not an error. #[test] fn missing_optional_not_fatal() { BookTest::from_dir("preprocessor/missing_optional_not_fatal").run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started WARN The command `trduyvbhijnorgevfuhn` for preprocessor `missing` was not found, but is marked as optional. INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }); } // with_preprocessor of an existing name. #[test] fn with_preprocessor_same_name() { let mut test = BookTest::init(|_| {}); test.change_file( "book.toml", "[preprocessor.dummy]\n\ command = 'mdbook-preprocessor-does-not-exist'\n", ); let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_preprocessor(Spy(Arc::clone(&spy))); // Unfortunately this is unable to capture the output when using the API. book.build().unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1); assert_eq!(inner.rendered_with, ["html"]); } // Checks that the interface stays backwards compatible. The interface here // should not be changed to fix a compatibility issue unless there is a // major-semver version update to mdbook. // // Note that this tests both preprocessors and renderers. It's in this module // for lack of a better location. #[test] fn extension_compatibility() { // This is here to force you to look at this test if you alter any of // these types such as adding new fields/variants. This test should be // updated accordingly. For example, new `BookItem` variants should be // added to the extension_compatibility book, or new fields should be // added to the expected input/output. This is also a check that these // should only be changed in a semver-breaking release let chapter = Chapter { name: "example".to_string(), content: "content".to_string(), number: None, sub_items: Vec::new(), path: None, source_path: None, parent_names: Vec::new(), }; let item = BookItem::Chapter(chapter); match &item { BookItem::Chapter(_) => {} BookItem::Separator => {} BookItem::PartTitle(_) => {} } let items = vec![item]; let _book = Book { items }; let mut test = BookTest::from_dir("preprocessor/extension_compatibility"); // Run it once with the preprocessor disabled so that we can verify // that the built book is identical with the preprocessor enabled. test.run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started WARN The command `./my-preprocessor` for preprocessor `my-preprocessor` was not found, but is marked as optional. INFO Running the html backend INFO HTML book written to `[ROOT]/book/html` WARN The command `./my-preprocessor` for preprocessor `my-preprocessor` was not found, but is marked as optional. INFO Running the my-renderer backend INFO Invoking the "my-renderer" renderer WARN The command `./my-renderer` for backend `my-renderer` was not found, but is marked as optional. "#]]); }); let orig_dir = test.dir.join("book.orig"); let pre_dir = test.dir.join("book"); std::fs::rename(&pre_dir, &orig_dir).unwrap(); // **CAUTION** DO NOT modify this value unless this is a major-semver change. let book_output = serde_json::json!({ "items": [ { "Chapter": { "content": "# Prefix chapter\n", "name": "Prefix chapter", "number": null, "parent_names": [], "path": "prefix.md", "source_path": "prefix.md", "sub_items": [] } }, { "Chapter": { "content": "# Chapter 1\n", "name": "Chapter 1", "number": [ 1 ], "parent_names": [], "path": "chapter_1.md", "source_path": "chapter_1.md", "sub_items": [] } }, { "Chapter": { "content": "", "name": "Draft chapter", "number": [ 2 ], "parent_names": [], "path": null, "source_path": null, "sub_items": [] } }, { "PartTitle": "Part title" }, { "Chapter": { "content": "# Part chapter\n", "name": "Part chapter", "number": [ 3 ], "parent_names": [], "path": "part/chapter.md", "source_path": "part/chapter.md", "sub_items": [ { "Chapter": { "content": "# Part sub chapter\n", "name": "Part sub chapter", "number": [ 3, 1 ], "parent_names": [ "Part chapter" ], "path": "part/sub-chapter.md", "source_path": "part/sub-chapter.md", "sub_items": [] } } ] } }, "Separator", { "Chapter": { "content": "# Suffix chapter\n", "name": "Suffix chapter", "number": null, "parent_names": [], "path": "suffix.md", "source_path": "suffix.md", "sub_items": [] } } ] }); let output_str = serde_json::to_string(&book_output).unwrap(); // **CAUTION** The only updates allowed here in a semver-compatible // release is to add new fields. let expected_config = serde_json::json!({ "book": { "authors": [], "description": null, "language": "en", "text-direction": null, "title": "extension_compatibility" }, "output": { "html": {}, "my-renderer": { "command": "./my-renderer", "custom-config": "renderer settings", "custom-table": { "extra": "xyz" }, "optional": true } }, "preprocessor": { "my-preprocessor": { "command": "./my-preprocessor", "custom-config": true, "custom-table": { "extra": "abc" }, "optional": true } } }); // **CAUTION** The only updates allowed here in a semver-compatible // release is to add new fields. The output should not change. let expected_preprocessor_input = serde_json::json!([ { "config": expected_config, "mdbook_version": "[VERSION]", "renderer": "html", "root": "[ROOT]" }, book_output ]); let expected_renderer_input = serde_json::json!( { "version": "[VERSION]", "root": "[ROOT]", "book": book_output, "config": expected_config, "destination": "[ROOT]/book/my-renderer", } ); // This preprocessor writes its input to some files, and writes the // hard-coded output specified above. test.rust_program( "my-preprocessor", &r###" use std::fs::OpenOptions; use std::io::{Read, Write}; fn main() { let mut args = std::env::args().skip(1); if args.next().as_deref() == Some("supports") { let mut file = OpenOptions::new() .create(true) .append(true) .open("support-check") .unwrap(); let renderer = args.next().unwrap(); writeln!(file, "{renderer}").unwrap(); if renderer != "html" { std::process::exit(1); } return; } let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); std::fs::write("preprocessor-input", &s).unwrap(); let output = r##"OUTPUT_REPLACE"##; println!("{output}"); } "### .replace("OUTPUT_REPLACE", &output_str), ) // This renderer writes its input to a file. .rust_program( "my-renderer", &r#" fn main() { use std::io::Read; let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); std::fs::write("renderer-input", &s).unwrap(); } "#, ) .run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book/html` INFO Running the my-renderer backend INFO Invoking the "my-renderer" renderer "#]]); }) .check_file("support-check", "html\nmy-renderer\n") .check_file( "preprocessor-input", serde_json::to_string(&expected_preprocessor_input) .unwrap() .is_json(), ) .check_file( "book/my-renderer/renderer-input", serde_json::to_string(&expected_renderer_input) .unwrap() .is_json(), ); // Verify both directories have the exact same output. test.rm_r("book/my-renderer/renderer-input"); let orig_files = list_all_files(&orig_dir); let pre_files = list_all_files(&pre_dir); assert_eq!(orig_files, pre_files); for file in &orig_files { let orig_path = orig_dir.join(file); if orig_path.is_file() { let orig = std::fs::read(&orig_path).unwrap(); let pre = std::fs::read(&pre_dir.join(file)).unwrap(); test.assert.eq(pre, orig); } } } ================================================ FILE: tests/testsuite/print/chapter_no_h1/book.toml ================================================ [book] title = "chapter_no_h1" ================================================ FILE: tests/testsuite/print/chapter_no_h1/expected/print.html ================================================

Chapter 1

See chapter 2.

Chapter 2

See this.

H2 instead

H2 instead

This has H2 instead of H1.

================================================ FILE: tests/testsuite/print/chapter_no_h1/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) - [Chapter 2](./chapter_2.md) - [H2 instead](./h2-instead.md) ================================================ FILE: tests/testsuite/print/chapter_no_h1/src/chapter_1.md ================================================ # Chapter 1 See [chapter 2](chapter_2.md). ================================================ FILE: tests/testsuite/print/chapter_no_h1/src/chapter_2.md ================================================ See [this](./chapter_2.md). ================================================ FILE: tests/testsuite/print/chapter_no_h1/src/h2-instead.md ================================================ ## H2 instead This has H2 instead of H1. ================================================ FILE: tests/testsuite/print/duplicate_ids/book.toml ================================================ [book] title = "duplicate_ids" ================================================ FILE: tests/testsuite/print/duplicate_ids/expected/print.html ================================================

Chapter 1

Some title

See other

See this

See this anchor only

Chapter 2

Some title

See other

See this

See this anchor only

Works with HTML extension too

================================================ FILE: tests/testsuite/print/duplicate_ids/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) - [Chapter 2](./chapter_2.md) ================================================ FILE: tests/testsuite/print/duplicate_ids/src/chapter_1.md ================================================ # Chapter 1 ## Some title See [other](chapter_2.md#some-title) See [this](chapter_1.md#some-title) See [this anchor only](#some-title) ================================================ FILE: tests/testsuite/print/duplicate_ids/src/chapter_2.md ================================================ # Chapter 2 ## Some title See [other](chapter_1.md#some-title) See [this](chapter_2.md#some-title) See [this anchor only](#some-title) [Works with HTML extension too](chapter_1.html#some-title) ================================================ FILE: tests/testsuite/print/noindex/src/SUMMARY.md ================================================ - [Intro](index.md) ================================================ FILE: tests/testsuite/print/noindex/src/index.md ================================================ # Intro ================================================ FILE: tests/testsuite/print/relative_links/book.toml ================================================ [book] title = "relative_links" ================================================ FILE: tests/testsuite/print/relative_links/expected/print.html ================================================

First Chapter

First Nested

Testing relative links for the print page

When we link to the first section, it should work on both the print page and the non-print page.

The same link should work with an html extension.

A fragment link should work.

Link outside.

Link outside with anchor.

Link inside but doesn’t exist. Link inside but doesn’t exist with anchor. Link inside to html. Link inside to html with anchor.

Some image

HTML Link

raw html

Some section

Links with scheme shouldn’t be touched.

Non-html link

================================================ FILE: tests/testsuite/print/relative_links/src/SUMMARY.md ================================================ # Summary - [First Chapter](first/index.md) - [First Nested](first/nested.md) - [Second Chapter](second/nested.md) ================================================ FILE: tests/testsuite/print/relative_links/src/first/index.md ================================================ # First Chapter ================================================ FILE: tests/testsuite/print/relative_links/src/first/nested.md ================================================ # First Nested ================================================ FILE: tests/testsuite/print/relative_links/src/second/nested.md ================================================ # Testing relative links for the print page When we link to [the first section](../first/nested.md), it should work on both the print page and the non-print page. The same link should work [with an html extension](../first/nested.html). A [fragment link](#some-section) should work. Link [outside](../../std/foo/bar.html). Link [outside with anchor](../../std/foo/bar.html#panic). Link [inside but doesn't exist](../first/alpha/beta.md). Link [inside but doesn't exist with anchor](../first/alpha/beta.md#anchor). Link [inside to html](../first/alpha/gamma.html). Link [inside to html with anchor](../first/alpha/gamma.html#anchor). ![Some image](../images/picture.png) HTML Link raw html ## Some section [Links with scheme shouldn't be touched.](https://example.com/foo.html#bar) Non-html link ================================================ FILE: tests/testsuite/print.rs ================================================ //! Tests for print page. use crate::prelude::*; use snapbox::file; // Tests relative links from the print page. #[test] fn relative_links() { BookTest::from_dir("print/relative_links").check_main_file( "book/print.html", file!("print/relative_links/expected/print.html"), ); } // Test for duplicate IDs, and links to those duplicates. #[test] fn duplicate_ids() { BookTest::from_dir("print/duplicate_ids").check_main_file( "book/print.html", file!("print/duplicate_ids/expected/print.html"), ); } // Test for synthesized link to a chapter that does not have an h1. #[test] fn chapter_no_h1() { BookTest::from_dir("print/chapter_no_h1").check_main_file( "book/print.html", file!("print/chapter_no_h1/expected/print.html"), ); } // Checks that print.html is noindex. #[test] fn noindex() { let robots = r#""#; BookTest::from_dir("print/noindex") .check_file_contains("book/print.html", robots) .check_file_doesnt_contain("book/index.html", robots); } ================================================ FILE: tests/testsuite/redirects/redirect_existing_page/book.toml ================================================ [book] title = "redirect_existing_page" [output.html.redirect] "/chapter_1.html" = "other-page.html" ================================================ FILE: tests/testsuite/redirects/redirect_existing_page/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/redirects/redirect_existing_page/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/redirects/redirect_removed_with_fragments_only/book.toml ================================================ [book] title = "redirect_removed_with_fragments_only" [output.html.redirect] "/old-file.html#foo" = "chapter_1.html" ================================================ FILE: tests/testsuite/redirects/redirect_removed_with_fragments_only/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/redirects/redirect_removed_with_fragments_only/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/redirects/redirects_are_emitted_correctly/book.toml ================================================ [book] title = "redirects_are_emitted_correctly" [output.html.redirect] "/overview.html" = "index.html" "/overview.html#old" = "index.html#new" "/nested/page.html" = "https://rust-lang.org/" ================================================ FILE: tests/testsuite/redirects/redirects_are_emitted_correctly/expected/nested/page.html ================================================ Redirecting...

Redirecting to... https://rust-lang.org/.

================================================ FILE: tests/testsuite/redirects/redirects_are_emitted_correctly/expected/overview.html ================================================ Redirecting...

Redirecting to... index.html.

================================================ FILE: tests/testsuite/redirects/redirects_are_emitted_correctly/src/SUMMARY.md ================================================ # Redirects - [Chapter 1](chapter_1.md) - [Chapter 2](chapter_2.md) ================================================ FILE: tests/testsuite/redirects/redirects_are_emitted_correctly/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/redirects/redirects_are_emitted_correctly/src/chapter_2.md ================================================ # Chapter 2 ================================================ FILE: tests/testsuite/redirects.rs ================================================ //! Tests for the HTML redirect feature. use crate::prelude::*; use snapbox::file; // Basic check of redirects. #[test] fn redirects_are_emitted_correctly() { BookTest::from_dir("redirects/redirects_are_emitted_correctly") .check_file( "book/overview.html", file!["redirects/redirects_are_emitted_correctly/expected/overview.html"], ) .check_file( "book/nested/page.html", file!["redirects/redirects_are_emitted_correctly/expected/nested/page.html"], ); } // Invalid redirect with only fragments. #[test] fn redirect_removed_with_fragments_only() { BookTest::from_dir("redirects/redirect_removed_with_fragments_only").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend ERROR Rendering failed [TAB]Caused by: Unable to emit redirects [TAB]Caused by: redirect entry for `old-file.html` only has source paths with `#` fragments There must be an entry without the `#` fragment to determine the default destination. "#]]); }); } // Invalid redirect for an existing page. #[test] fn redirect_existing_page() { BookTest::from_dir("redirects/redirect_existing_page").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend ERROR Rendering failed [TAB]Caused by: redirect found for existing chapter at `/chapter_1.html` Either delete the redirect or remove the chapter. "#]]); }); } ================================================ FILE: tests/testsuite/renderer/backends_receive_render_context_via_stdin/book.toml ================================================ [output.cat-to-file] command = "./cat-to-file" ================================================ FILE: tests/testsuite/renderer/backends_receive_render_context_via_stdin/src/SUMMARY.md ================================================ - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/renderer/backends_receive_render_context_via_stdin/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/renderer/missing_optional_not_fatal/book.toml ================================================ [output.missing] command = "trduyvbhijnorgevfuhn" optional = true ================================================ FILE: tests/testsuite/renderer/missing_optional_not_fatal/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/renderer/missing_renderer/book.toml ================================================ [output.missing] command = "trduyvbhijnorgevfuhn" ================================================ FILE: tests/testsuite/renderer/missing_renderer/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/renderer/renderer_with_arguments/book.toml ================================================ [output.arguments] command = "./arguments arg1 arg2" ================================================ FILE: tests/testsuite/renderer/renderer_with_arguments/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/renderer.rs ================================================ //! Tests for custom renderers. use crate::prelude::*; use anyhow::Result; use mdbook_renderer::{RenderContext, Renderer}; use snapbox::IntoData; use std::fs::File; use std::sync::{Arc, Mutex}; struct Spy(Arc>); #[derive(Debug, Default)] struct Inner { run_count: usize, } 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 that renderer gets run. #[test] fn runs_renderers() { let test = BookTest::init(|_| {}); let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_renderer(Spy(Arc::clone(&spy))); book.build().unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1); } // Test renderer with a failing command fails. #[test] fn failing_command() { BookTest::init(|_| {}) .rust_program( "failing", r#" fn main() { // Read from stdin to avoid random pipe failures on Linux. use std::io::Read; let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); std::process::exit(1); } "#, ) .change_file( "book.toml", "[output.failing]\n\ command = './failing'\n", ) .run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the failing backend INFO Invoking the "failing" renderer ERROR Renderer exited with non-zero return code. ERROR Rendering failed [TAB]Caused by: The "failing" renderer failed "#]]); }); } // Renderer command is missing. #[test] fn missing_renderer() { BookTest::from_dir("renderer/missing_renderer").run("build", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the missing backend INFO Invoking the "missing" renderer ERROR The command `trduyvbhijnorgevfuhn` wasn't found, is the `missing` backend installed? If you want to ignore this error when the `missing` backend is not installed, set `optional = true` in the `[output.missing]` section of the book.toml configuration file. ERROR Rendering failed [TAB]Caused by: Unable to run the backend `missing` [TAB]Caused by: [NOT_FOUND] "#]]); }); } // Optional missing is not an error. #[test] fn missing_optional_not_fatal() { BookTest::from_dir("renderer/missing_optional_not_fatal").run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started INFO Running the missing backend INFO Invoking the "missing" renderer WARN The command `trduyvbhijnorgevfuhn` for backend `missing` was not found, but is marked as optional. "#]]); }); } // Command can include arguments. #[test] fn renderer_with_arguments() { BookTest::from_dir("renderer/renderer_with_arguments") .rust_program( "arguments", r#" fn main() { let args: Vec<_> = std::env::args().skip(1).collect(); assert_eq!(args, &["arg1", "arg2"]); println!("Hello World!"); use std::io::Read; let mut s = String::new(); std::io::stdin().read_to_string(&mut s).unwrap(); } "#, ) .run("build", |cmd| { cmd.expect_stdout(str![[r#" Hello World! "#]]) .expect_stderr(str![[r#" INFO Book building has started INFO Running the arguments backend INFO Invoking the "arguments" renderer "#]]); }); } // Checks the render context received by the renderer. #[test] fn backends_receive_render_context_via_stdin() { let mut test = BookTest::from_dir("renderer/backends_receive_render_context_via_stdin"); test.rust_program( "cat-to-file", 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(); } "#, ) .run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started INFO Running the cat-to-file backend INFO Invoking the "cat-to-file" renderer "#]]); }) .check_file( "book/out.txt", str![[r##" { "book": { "items": [ { "Chapter": { "content": "# Chapter 1\n", "name": "Chapter 1", "number": [ 1 ], "parent_names": [], "path": "chapter_1.md", "source_path": "chapter_1.md", "sub_items": [] } } ] }, "config": { "book": { "authors": [], "description": null, "language": "en", "text-direction": null, "title": null }, "output": { "cat-to-file": { "command": "./cat-to-file" } } }, "destination": "[ROOT]/book", "root": "[ROOT]", "version": "[VERSION]" } "##]] .is_json(), ); // Can round-trip. let f = File::open(test.dir.join("book/out.txt")).unwrap(); RenderContext::from_json(f).unwrap(); } // Verifies that a relative path for the renderer command is relative to the // book root. #[test] fn relative_command_path() { let mut test = BookTest::init(|_| {}); test.rust_program( "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("output", "test").unwrap(); } "#, ) .change_file( "book.toml", "[output.myrenderer]\n\ command = 'renderers/myrenderer'\n", ) .run("build", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Book building has started INFO Running the myrenderer backend INFO Invoking the "myrenderer" renderer "#]]); }) .check_file("book/output", "test"); } // with_renderer of an existing name. #[test] fn with_renderer_same_name() { let mut test = BookTest::init(|_| {}); test.change_file( "book.toml", "[output.dummy]\n\ command = 'mdbook-renderer-does-not-exist'\n", ); let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_renderer(Spy(Arc::clone(&spy))); // Unfortunately this is unable to capture the output when using the API. book.build().unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1); } ================================================ FILE: tests/testsuite/rendering/code_blocks_fenced_with_indent/book.toml ================================================ [book] title = "code_blocks_fenced_with_indent" ================================================ FILE: tests/testsuite/rendering/code_blocks_fenced_with_indent/expected/code-blocks-fenced-with-indent.html ================================================

Code blocks fenced with indent

#![allow(unused)]
fn main() {
    // This has a first line that is indented.
    println!("hello");
}
================================================ FILE: tests/testsuite/rendering/code_blocks_fenced_with_indent/src/SUMMARY.md ================================================ # Summary - [Code blocks fenced with indent](./code-blocks-fenced-with-indent.md) ================================================ FILE: tests/testsuite/rendering/code_blocks_fenced_with_indent/src/code-blocks-fenced-with-indent.md ================================================ # Code blocks fenced with indent ```rust // This has a first line that is indented. println!("hello"); ``` ================================================ FILE: tests/testsuite/rendering/default_rust_edition/book.toml ================================================ [book] title = "default_rust_edition" [rust] edition = "2021" ================================================ FILE: tests/testsuite/rendering/default_rust_edition/expected/default-rust-edition.html ================================================

Chapter 1

#![allow(unused)]
fn main() {
let x = 2021;
}
#![allow(unused)]
fn main() {
let x = 2021;
}
#![allow(unused)]
fn main() {
let x = 2024;
}
================================================ FILE: tests/testsuite/rendering/default_rust_edition/src/SUMMARY.md ================================================ # Summary - [Default rust edition](./default-rust-edition.md) ================================================ FILE: tests/testsuite/rendering/default_rust_edition/src/default-rust-edition.md ================================================ # Chapter 1 ```rust let x = 2021; ``` ```rust,edition2021 let x = 2021; ``` ```rust,edition2024 let x = 2024; ``` ================================================ FILE: tests/testsuite/rendering/edit_url_template/book.toml ================================================ [book] title = "edit_url_template" [output.html] edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" ================================================ FILE: tests/testsuite/rendering/edit_url_template/src/SUMMARY.md ================================================ - [Intro](README.md) ================================================ FILE: tests/testsuite/rendering/edit_url_template_explicit_src/book.toml ================================================ [book] title = "edit_url_template" src = "src2" [output.html] edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" ================================================ FILE: tests/testsuite/rendering/edit_url_template_explicit_src/src2/SUMMARY.md ================================================ - [Intro](README.md) ================================================ FILE: tests/testsuite/rendering/editable_rust_block/book.toml ================================================ [book] title = "editable_rust_block" [output.html.playground] editable = true ================================================ FILE: tests/testsuite/rendering/editable_rust_block/expected/editable-rust.html ================================================

Chapter 1

fn f() {
    println!("hello");
}
#![allow(unused)]
fn main() {
// Not editable.
}
================================================ FILE: tests/testsuite/rendering/editable_rust_block/src/SUMMARY.md ================================================ # Summary - [Editable rust blocks](./editable-rust.md) ================================================ FILE: tests/testsuite/rendering/editable_rust_block/src/editable-rust.md ================================================ # Chapter 1 ```rust,editable fn f() { println!("hello"); } ``` ```rust // Not editable. ``` ================================================ FILE: tests/testsuite/rendering/first_chapter_is_copied_as_index_even_if_not_first_elem/src/SUMMARY.md ================================================ # Summary --- - [None of these should be treated as the "index chapter"]() # Part 1 - [Not this either]() - [Chapter 1](./chapter_1.md) - [And not this]() ================================================ FILE: tests/testsuite/rendering/fontawesome/book.toml ================================================ [book] title = "fontawesome" ================================================ FILE: tests/testsuite/rendering/fontawesome/expected/fa.html ================================================

Chapter 1

Text prevents translation.

================================================ FILE: tests/testsuite/rendering/fontawesome/src/SUMMARY.md ================================================ # Summary - [Font Awesome](./fa.md) ================================================ FILE: tests/testsuite/rendering/fontawesome/src/fa.md ================================================ # Chapter 1 Text prevents translation. ================================================ FILE: tests/testsuite/rendering/fontawesome_error/book.toml ================================================ [book] title = "fontawesome_error" [output.html] git-repository-url = "https://github.com/example/test" git-repository-icon = "fa-github" ================================================ FILE: tests/testsuite/rendering/fontawesome_error/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/rendering/fontawesome_error/src/chapter_1.md ================================================ # Chapter 1 Hello, world! ================================================ FILE: tests/testsuite/rendering/header_links/book.toml ================================================ [book] title = "header_links" ================================================ FILE: tests/testsuite/rendering/header_links/expected/header_links.html ================================================

Header Links

Foo^bar

Repeat

Repeat

Repeat

Repeat 1

================================================ FILE: tests/testsuite/rendering/header_links/src/SUMMARY.md ================================================ # Summary - [Header Links](./header_links.md) ================================================ FILE: tests/testsuite/rendering/header_links/src/header_links.md ================================================ # Header Links ## Foo^bar ### #### ## Hï ## Repeat ## Repeat ## Repeat ## Repeat 1 ## With *emphasis* **bold** **_bold_emphasis_** `code` \ html [link] [link]: https://example.com/link ================================================ FILE: tests/testsuite/rendering/hidelines/book.toml ================================================ [book] title = "hidelines" [output.html.code.hidelines] python = "~" ================================================ FILE: tests/testsuite/rendering/hidelines/expected/hide-lines.html ================================================

Hide Lines

hidden()
nothidden():
    hidden()
    hidden()
    nothidden()
hidden()
nothidden():
    hidden()
    hidden()
    nothidden()
#![allow(unused)]
#![allow(something)]
fn main() {

#hidden();
hidden();
# not_hidden();
#[not_hidden]
not_hidden();
}
================================================ FILE: tests/testsuite/rendering/hidelines/src/SUMMARY.md ================================================ # Summary - [Hide Lines](./hide-lines.md) ================================================ FILE: tests/testsuite/rendering/hidelines/src/hide-lines.md ================================================ # Hide Lines ```python ~hidden() nothidden(): ~ hidden() ~hidden() nothidden() ``` ```python,hidelines=!!! !!!hidden() nothidden(): !!! hidden() !!!hidden() nothidden() ``` ```rust #![allow(something)] # #hidden(); # hidden(); ## not_hidden(); #[not_hidden] not_hidden(); ``` ================================================ FILE: tests/testsuite/rendering/html_blocks/book.toml ================================================ [book] title = "html_blocks" ================================================ FILE: tests/testsuite/rendering/html_blocks/expected/comment-in-list.html ================================================
  • List

================================================ FILE: tests/testsuite/rendering/html_blocks/expected/script-in-block.html ================================================
HTML block start < still in block
================================================ FILE: tests/testsuite/rendering/html_blocks/src/SUMMARY.md ================================================ # Summary - [Comment in list](./comment-in-list.md) - [Script in block](./script-in-block.md) ================================================ FILE: tests/testsuite/rendering/html_blocks/src/comment-in-list.md ================================================ * List ================================================ FILE: tests/testsuite/rendering/html_blocks/src/script-in-block.md ================================================
HTML block start < still in block
================================================ FILE: tests/testsuite/rendering.rs ================================================ //! Tests for HTML rendering. //! //! Note that markdown-specific rendering tests are in the `markdown` module. use crate::prelude::*; // Checks that edit-url-template works. #[test] fn edit_url_template() { BookTest::from_dir("rendering/edit_url_template").check_file_contains( "book/index.html", "", ); } // Checks that an alternate `src` setting works with the edit url template. #[test] fn edit_url_template_explicit_src() { BookTest::from_dir("rendering/edit_url_template_explicit_src").check_file_contains( "book/index.html", "", ); } // Checks that index.html is generated correctly, even when the first few // chapters are drafts. #[test] fn first_chapter_is_copied_as_index_even_if_not_first_elem() { BookTest::from_dir("rendering/first_chapter_is_copied_as_index_even_if_not_first_elem") // These two files should be equal. .check_main_file( "book/chapter_1.html", str![[ r##"

Chapter 1

"## ]], ) .check_main_file( "book/index.html", str![[ r##"

Chapter 1

"## ]], ); } // Fontawesome `` tag support. #[test] fn fontawesome() { BookTest::from_dir("rendering/fontawesome") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend WARN failed to find Font Awesome icon for icon `does-not-exist` with type `regular` in `fa.md`: Invalid Font Awesome icon name: visit https://fontawesome.com/icons?d=gallery&m=free to see valid names INFO HTML book written to `[ROOT]/book` "#]]); }) .check_all_main_files(); } // Verifies that an invalid `git-repository-icon` in book.toml produces a // helpful error message with the icon name, type, and a link to FontAwesome. #[test] fn fontawesome_error_message() { BookTest::from_dir("rendering/fontawesome_error") .run("build", |cmd| { cmd.expect_failure(); cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend ERROR Rendering failed [TAB]Caused by: Error rendering "index" line [..], col [..]: Unknown Font Awesome icon `github` for type `regular`. Hint: check the icon name and prefix (fas (solid), fab (brands), or far (regular)) at https://fontawesome.com/v6/search?m=free [TAB]Caused by: Unknown Font Awesome icon `github` for type `regular`. Hint: check the icon name and prefix (fas (solid), fab (brands), or far (regular)) at https://fontawesome.com/v6/search?m=free "#]]); }); } // Tests the rendering when setting the default rust edition. #[test] fn default_rust_edition() { BookTest::from_dir("rendering/default_rust_edition").check_all_main_files(); } // Tests the rendering for editable code blocks. #[test] fn editable_rust_block() { BookTest::from_dir("rendering/editable_rust_block").check_all_main_files(); } // Tests for custom hide lines. #[test] fn hidelines() { BookTest::from_dir("rendering/hidelines").check_all_main_files(); } // Tests for code blocks of basic rust code. #[test] fn language_rust_playground() { fn expect(input: &str, info: &str, expected: impl snapbox::IntoData) { BookTest::init(|_| {}) .change_file("book.toml", "output.html.playground.editable = true") .change_file("src/chapter_1.md", &format!("```rust {info}\n{input}\n```")) .check_main_file("book/chapter_1.html", expected); } // No-main should be wrapped in `fn main` boring lines. expect( "x()", "", str![[r#"
#![allow(unused)]
fn main() {
x()
}
"#]], ); // `fn main` should not be wrapped, not boring. expect( "fn main() {}", "", str![[r#"
fn main() {}
"#]], ); // Lines starting with `#` are boring. expect( "let s = \"foo\n # bar\n\";", "editable", str![[r#"
let s = "foo
 bar
";
"#]], ); // `##` is not boring and is used as an escape. expect( "let s = \"foo\n ## bar\n\";", "editable", str![[r#"
let s = "foo
 # bar
";
"#]], ); // `#` on a line by itself is boring. expect( "let s = \"foo\n # bar\n#\n\";", "editable", str![[r#"
let s = "foo
 bar

";
"#]], ); // `#` must be followed by a space to be boring. expect( "#x;", "", str![[r#"
#![allow(unused)]
fn main() {
#x;
}
"#]], ); // Other classes like "ignore" should not change things, and the class is // included in the code tag. expect( "let s = \"foo\n # bar\n\";", "ignore", str![[r#"
let s = "foo
 bar
";
"#]], ); // Inner attributes and normal attributes are not boring. expect( "#![no_std]\nlet s = \"foo\";\n #[some_attr]", "editable", str![[r#"
#![no_std]
let s = "foo";
 #[some_attr]
"#]], ); } // Rust code block in a list. #[test] fn code_block_in_list() { BookTest::init(|_| {}) .change_file( "src/chapter_1.md", r#"- inside list ```rust fn foo() { let x = 1; } ``` "#, ) .check_main_file( "book/chapter_1.html", str![[r#"
  • inside list

    #![allow(unused)]
    fn main() {
    fn foo() {
      let x = 1;
    }
    }
"#]], ); } // Checks the rendering of links added to headers. #[test] fn header_links() { BookTest::from_dir("rendering/header_links").check_all_main_files(); } // A corrupted HTML end tag. #[test] fn busted_end_tag() { BookTest::init(|_| {}) .change_file("src/chapter_1.md", "
xfooy
") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend WARN html parse error in `chapter_1.md`: Self-closing end tag Html text was:
xfooy
INFO HTML book written to `[ROOT]/book` "#]]); }) .check_main_file("book/chapter_1.html", str!["
xfooy
"]); } // Various html blocks. #[test] fn html_blocks() { BookTest::from_dir("rendering/html_blocks").check_all_main_files(); } // Test for a fenced code block that is also indented. #[test] fn code_block_fenced_with_indent() { BookTest::from_dir("rendering/code_blocks_fenced_with_indent").check_all_main_files(); } // Unclosed HTML tags. // // Note that the HTML parsing algorithm is much more complicated than what // this is checking. #[test] fn unclosed_html_tags() { BookTest::init(|_| {}) .change_file("src/chapter_1.md", "
xfooxyz") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend WARN unclosed HTML tag `` found in `chapter_1.md` WARN unclosed HTML tag `` found in `chapter_1.md` WARN unclosed HTML tag `
` found in `chapter_1.md` INFO HTML book written to `[ROOT]/book` "#]]); }) .check_main_file( "book/chapter_1.html", str!["
xfooxyz
"], ); } // Test for HTML tags out of sync. #[test] fn unbalanced_html_tags() { BookTest::init(|_| {}) .change_file("src/chapter_1.md", "
xfoo
") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend WARN unexpected HTML end tag `
` found in `chapter_1.md` Check that the HTML tags are properly balanced. WARN unclosed HTML tag `
` found in `chapter_1.md` INFO HTML book written to `[ROOT]/book` "#]]); }) .check_main_file("book/chapter_1.html", str!["
xfoo
"]); } // Test for bug with unbalanced HTML handling in the heading. #[test] fn heading_with_unbalanced_html() { BookTest::init(|_| {}) .change_file("src/chapter_1.md", "### Option") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend WARN unclosed HTML tag `` found in `chapter_1.md` while exiting Heading(H3) HTML tags must be closed before exiting a markdown element. INFO HTML book written to `[ROOT]/book` "#]]); }) .check_main_file( "book/chapter_1.html", str![[r##"

Option

"##]], ); } ================================================ FILE: tests/testsuite/search/chapter_settings_validation_error/book.toml ================================================ [book] title = "Search Test" [output.html.search.chapter] "does-not-exist" = { enable = false } ================================================ FILE: tests/testsuite/search/chapter_settings_validation_error/src/SUMMARY.md ================================================ ================================================ FILE: tests/testsuite/search/disable_search_chapter/book.toml ================================================ [book] title = "disable_search_chapter" [output.html.search.chapter] "second" = { enable = false } "first/disable_me.md" = { enable = false } ================================================ FILE: tests/testsuite/search/disable_search_chapter/src/SUMMARY.md ================================================ # Summary - [Keep Me](first/keep_me.md) - [Disable Me](first/disable_me.md) - [Second](second.md) - [Second Nested](second/nested.md) ================================================ FILE: tests/testsuite/search/disable_search_chapter/src/first/disable_me.md ================================================ # Disable Me ================================================ FILE: tests/testsuite/search/disable_search_chapter/src/first/keep_me.md ================================================ # Keep Me ================================================ FILE: tests/testsuite/search/disable_search_chapter/src/second/nested.md ================================================ # Second Nested ================================================ FILE: tests/testsuite/search/disable_search_chapter/src/second.md ================================================ # Second ================================================ FILE: tests/testsuite/search/reasonable_search_index/expected_index.js ================================================ window.search = Object.assign(window.search, JSON.parse('{"doc_urls":["intro.html#introduction","intro.html#sneaky","first/index.html#first-chapter","first/index.html#some-section","first/includes.html#includes","first/includes.html#summary","first/unicode.html#unicode-stress-tests","first/no-headers.html","first/duplicate-headers.html#duplicate-headers","first/duplicate-headers.html#header-text","first/duplicate-headers.html#header-text-1","first/duplicate-headers.html#header-text-2","first/heading-attributes.html#attrs","first/heading-attributes.html#heading-with-classes","first/heading-attributes.html#both"],"index":{"documentStore":{"docInfo":{"0":{"body":3,"breadcrumbs":2,"title":1},"1":{"body":9,"breadcrumbs":2,"title":1},"10":{"body":0,"breadcrumbs":6,"title":2},"11":{"body":0,"breadcrumbs":6,"title":2},"12":{"body":0,"breadcrumbs":6,"title":2},"13":{"body":0,"breadcrumbs":6,"title":2},"14":{"body":0,"breadcrumbs":7,"title":3},"2":{"body":2,"breadcrumbs":4,"title":2},"3":{"body":0,"breadcrumbs":3,"title":1},"4":{"body":0,"breadcrumbs":4,"title":1},"5":{"body":10,"breadcrumbs":4,"title":1},"6":{"body":29,"breadcrumbs":6,"title":3},"7":{"body":6,"breadcrumbs":3,"title":2},"8":{"body":5,"breadcrumbs":6,"title":2},"9":{"body":0,"breadcrumbs":6,"title":2}},"docs":{"0":{"body":"Here’s some interesting text…","breadcrumbs":"Introduction » Introduction","id":"0","title":"Introduction"},"1":{"body":"I put in here! Sneaky inline event . But regular inline is indexed.","breadcrumbs":"Introduction » Sneaky","id":"1","title":"Sneaky"},"10":{"body":"","breadcrumbs":"First Chapter » Duplicate Headers » Header Text","id":"10","title":"Header Text"},"11":{"body":"","breadcrumbs":"First Chapter » Duplicate Headers » header-text","id":"11","title":"header-text"},"12":{"body":"","breadcrumbs":"First Chapter » Heading Attributes » Heading Attributes","id":"12","title":"Heading Attributes"},"13":{"body":"","breadcrumbs":"First Chapter » Heading Attributes » Heading with classes","id":"13","title":"Heading with classes"},"14":{"body":"","breadcrumbs":"First Chapter » Heading Attributes » Heading with id and classes","id":"14","title":"Heading with id and classes"},"2":{"body":"more text.","breadcrumbs":"First Chapter » First Chapter","id":"2","title":"First Chapter"},"3":{"body":"","breadcrumbs":"First Chapter » Some Section","id":"3","title":"Some Section"},"4":{"body":"","breadcrumbs":"First Chapter » Includes » Includes","id":"4","title":"Includes"},"5":{"body":"Introduction First Chapter Includes Unicode No Headers Duplicate Headers Heading Attributes","breadcrumbs":"First Chapter » Includes » Summary","id":"5","title":"Summary"},"6":{"body":"Please be careful editing, this contains carefully crafted characters. Two byte character: spatiëring Combining character: spatiëring Three byte character: 书こんにちは Four byte character: 𐌀‮𐌁‮𐌂‮𐌃‮𐌄‮𐌅‮𐌆‮𐌇‮𐌈‬ Right-to-left: مرحبا Emoticons: 🔊 😍 💜 1️⃣ right-to-left mark: hello באמת!‏ Zalgo: ǫ̛̖̱̗̝͈̋͒͋̏ͥͫ̒̆ͩ̏͌̾͊͐ͪ̾̚","breadcrumbs":"First Chapter » Unicode » Unicode stress tests","id":"6","title":"Unicode stress tests"},"7":{"body":"Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex.","breadcrumbs":"First Chapter » No Headers","id":"7","title":"First Chapter"},"8":{"body":"This page validates behaviour of duplicate headers.","breadcrumbs":"First Chapter » Duplicate Headers » Duplicate headers","id":"8","title":"Duplicate headers"},"9":{"body":"","breadcrumbs":"First Chapter » Duplicate Headers » Header Text","id":"9","title":"Header Text"}},"length":15,"save":true},"fields":["title","body","breadcrumbs"],"index":{"body":{"root":{"1":{"df":1,"docs":{"6":{"tf":1.0}}},"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":2,"docs":{"12":{"tf":1.0},"5":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"8":{"tf":1.0}}}}}}}},"df":0,"docs":{}}},"y":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":1,"docs":{"6":{"tf":1.7320508075688772}}}}}},"c":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"y":{"b":{"a":{"df":0,"docs":{},"r":{"a":{"df":1,"docs":{"7":{"tf":2.449489742783178}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"r":{"df":0,"docs":{},"e":{"df":1,"docs":{"6":{"tf":1.0}},"f":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":1,"docs":{"6":{"tf":1.0}}}}}}}}}},"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":3,"docs":{"2":{"tf":1.0},"5":{"tf":1.0},"7":{"tf":1.0}}}}}},"r":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":2.23606797749979}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"l":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"13":{"tf":1.0},"14":{"tf":1.0}}}}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}},"n":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}}}},"r":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":2,"docs":{"5":{"tf":1.0},"8":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"1":{"tf":1.0}}}}}}},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":3,"docs":{"2":{"tf":1.0},"5":{"tf":1.0},"7":{"tf":1.0}}}}}},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"6":{"tf":1.0}}}}}},"h":{"df":0,"docs":{},"e":{"a":{"d":{"df":4,"docs":{"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"5":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":5,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"5":{"tf":1.4142135623730951},"8":{"tf":1.4142135623730951},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"6":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"e":{"df":1,"docs":{"1":{"tf":1.0}},"’":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"t":{"df":0,"docs":{},"m":{"df":0,"docs":{},"l":{"df":1,"docs":{"1":{"tf":1.0}}}}}},"i":{"d":{"df":1,"docs":{"14":{"tf":1.0}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"d":{"df":2,"docs":{"4":{"tf":1.0},"5":{"tf":1.0}}},"df":0,"docs":{}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":1,"docs":{"1":{"tf":1.0}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"1":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}},"r":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"u":{"c":{"df":0,"docs":{},"t":{"df":2,"docs":{"0":{"tf":1.0},"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.4142135623730951}}}}}},"m":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"k":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":1,"docs":{"2":{"tf":1.0}}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":1,"docs":{"8":{"tf":1.0}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"s":{"df":1,"docs":{"6":{"tf":1.0}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"t":{"df":1,"docs":{"1":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"1":{"tf":1.0}}}},"df":0,"docs":{}}}}},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.4142135623730951}}}}}}},"s":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":1,"docs":{"3":{"tf":1.0}}}}}}},"df":0,"docs":{}},"n":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"df":0,"docs":{},"i":{"df":1,"docs":{"1":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"̈":{"df":0,"docs":{},"r":{"df":1,"docs":{"6":{"tf":1.0}}}}},"ë":{"df":0,"docs":{},"r":{"df":1,"docs":{"6":{"tf":1.0}}}}}}},"df":0,"docs":{}},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"5":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.0}}}},"x":{"df":0,"docs":{},"t":{"df":5,"docs":{"0":{"tf":1.0},"10":{"tf":1.0},"11":{"tf":1.0},"2":{"tf":1.0},"9":{"tf":1.0}}}}},"h":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":1,"docs":{"6":{"tf":1.0}}}}}},"w":{"df":0,"docs":{},"o":{"df":1,"docs":{"6":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"5":{"tf":1.0},"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"v":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"d":{"df":1,"docs":{"8":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"z":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"g":{"df":0,"docs":{},"o":{"df":1,"docs":{"6":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"breadcrumbs":{"root":{"1":{"df":1,"docs":{"6":{"tf":1.0}}},"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":4,"docs":{"12":{"tf":1.7320508075688772},"13":{"tf":1.0},"14":{"tf":1.0},"5":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"b":{"df":0,"docs":{},"e":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"v":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"8":{"tf":1.0}}}}}}}},"df":0,"docs":{}}},"y":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":1,"docs":{"6":{"tf":1.7320508075688772}}}}}},"c":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"y":{"b":{"a":{"df":0,"docs":{},"r":{"a":{"df":1,"docs":{"7":{"tf":2.449489742783178}}},"df":0,"docs":{}}},"df":0,"docs":{}},"df":0,"docs":{}}},"r":{"df":0,"docs":{},"e":{"df":1,"docs":{"6":{"tf":1.0}},"f":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":1,"docs":{"6":{"tf":1.0}}}}}}}}}},"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":13,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"2":{"tf":1.7320508075688772},"3":{"tf":1.0},"4":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.0},"7":{"tf":1.4142135623730951},"8":{"tf":1.0},"9":{"tf":1.0}}}}}},"r":{"a":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":2.23606797749979}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"df":0,"docs":{}},"l":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"13":{"tf":1.4142135623730951},"14":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}},"o":{"df":0,"docs":{},"m":{"b":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}},"n":{"df":0,"docs":{},"t":{"a":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}}}},"r":{"a":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":5,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"5":{"tf":1.0},"8":{"tf":2.0},"9":{"tf":1.0}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"e":{"d":{"df":0,"docs":{},"i":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{},"m":{"df":0,"docs":{},"o":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"v":{"df":0,"docs":{},"e":{"df":0,"docs":{},"n":{"df":0,"docs":{},"t":{"df":1,"docs":{"1":{"tf":1.0}}}}}}},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":13,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0},"2":{"tf":1.7320508075688772},"3":{"tf":1.0},"4":{"tf":1.0},"5":{"tf":1.4142135623730951},"6":{"tf":1.0},"7":{"tf":1.4142135623730951},"8":{"tf":1.0},"9":{"tf":1.0}}}}}},"o":{"df":0,"docs":{},"u":{"df":0,"docs":{},"r":{"df":1,"docs":{"6":{"tf":1.0}}}}}},"h":{"df":0,"docs":{},"e":{"a":{"d":{"df":4,"docs":{"12":{"tf":1.7320508075688772},"13":{"tf":1.7320508075688772},"14":{"tf":1.7320508075688772},"5":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":6,"docs":{"10":{"tf":1.7320508075688772},"11":{"tf":1.7320508075688772},"5":{"tf":1.4142135623730951},"7":{"tf":1.0},"8":{"tf":2.0},"9":{"tf":1.7320508075688772}}}}},"df":0,"docs":{}},"df":0,"docs":{},"l":{"df":0,"docs":{},"l":{"df":0,"docs":{},"o":{"df":1,"docs":{"6":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"e":{"df":1,"docs":{"1":{"tf":1.0}},"’":{"df":1,"docs":{"0":{"tf":1.0}}}}}},"t":{"df":0,"docs":{},"m":{"df":0,"docs":{},"l":{"df":1,"docs":{"1":{"tf":1.0}}}}}},"i":{"d":{"df":1,"docs":{"14":{"tf":1.4142135623730951}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"d":{"df":2,"docs":{"4":{"tf":1.7320508075688772},"5":{"tf":1.4142135623730951}}},"df":0,"docs":{}}}},"d":{"df":0,"docs":{},"e":{"df":0,"docs":{},"x":{"df":1,"docs":{"1":{"tf":1.0}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"df":0,"docs":{},"n":{"df":1,"docs":{"1":{"tf":1.4142135623730951}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}}}}},"r":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"u":{"c":{"df":0,"docs":{},"t":{"df":3,"docs":{"0":{"tf":1.7320508075688772},"1":{"tf":1.0},"5":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"l":{"df":0,"docs":{},"e":{"df":0,"docs":{},"f":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.4142135623730951}}}}}},"m":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"k":{"df":1,"docs":{"6":{"tf":1.0}}}}},"df":0,"docs":{},"o":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":1,"docs":{"2":{"tf":1.0}}}}}},"p":{"a":{"df":0,"docs":{},"g":{"df":0,"docs":{},"e":{"df":1,"docs":{"8":{"tf":1.0}}}}},"df":0,"docs":{},"l":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"s":{"df":1,"docs":{"6":{"tf":1.0}}}},"df":0,"docs":{}}},"u":{"df":0,"docs":{},"t":{"df":1,"docs":{"1":{"tf":1.0}}}}},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"g":{"df":0,"docs":{},"u":{"df":0,"docs":{},"l":{"a":{"df":0,"docs":{},"r":{"df":1,"docs":{"1":{"tf":1.0}}}},"df":0,"docs":{}}}}},"i":{"df":0,"docs":{},"g":{"df":0,"docs":{},"h":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.4142135623730951}}}}}}},"s":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":1,"docs":{"3":{"tf":1.4142135623730951}}}}}}},"df":0,"docs":{}},"n":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"df":0,"docs":{},"i":{"df":1,"docs":{"1":{"tf":1.7320508075688772}}}}},"df":0,"docs":{}}},"p":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"e":{"df":0,"docs":{},"̈":{"df":0,"docs":{},"r":{"df":1,"docs":{"6":{"tf":1.0}}}}},"ë":{"df":0,"docs":{},"r":{"df":1,"docs":{"6":{"tf":1.0}}}}}}},"df":0,"docs":{}},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"6":{"tf":1.4142135623730951}}}}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"5":{"tf":1.4142135623730951}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.4142135623730951}}}},"x":{"df":0,"docs":{},"t":{"df":5,"docs":{"0":{"tf":1.0},"10":{"tf":1.4142135623730951},"11":{"tf":1.4142135623730951},"2":{"tf":1.0},"9":{"tf":1.4142135623730951}}}}},"h":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"e":{"df":1,"docs":{"6":{"tf":1.0}}}}}},"w":{"df":0,"docs":{},"o":{"df":1,"docs":{"6":{"tf":1.0}}}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"o":{"d":{"df":2,"docs":{"5":{"tf":1.0},"6":{"tf":1.7320508075688772}}},"df":0,"docs":{}}},"df":0,"docs":{}}}},"v":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"d":{"df":1,"docs":{"8":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{}},"z":{"a":{"df":0,"docs":{},"l":{"df":0,"docs":{},"g":{"df":0,"docs":{},"o":{"df":1,"docs":{"6":{"tf":1.0}}}}}},"df":0,"docs":{}}}},"title":{"root":{"a":{"df":0,"docs":{},"t":{"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"b":{"df":0,"docs":{},"u":{"df":0,"docs":{},"t":{"df":1,"docs":{"12":{"tf":1.0}}}}},"df":0,"docs":{}}}}}},"c":{"df":0,"docs":{},"h":{"a":{"df":0,"docs":{},"p":{"df":0,"docs":{},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"r":{"df":2,"docs":{"2":{"tf":1.0},"7":{"tf":1.0}}}}}}},"df":0,"docs":{}},"l":{"a":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":2,"docs":{"13":{"tf":1.0},"14":{"tf":1.0}}}}},"df":0,"docs":{}}},"d":{"df":0,"docs":{},"u":{"df":0,"docs":{},"p":{"df":0,"docs":{},"l":{"df":0,"docs":{},"i":{"c":{"df":1,"docs":{"8":{"tf":1.0}}},"df":0,"docs":{}}}}}},"df":0,"docs":{},"f":{"df":0,"docs":{},"i":{"df":0,"docs":{},"r":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":2,"docs":{"2":{"tf":1.0},"7":{"tf":1.0}}}}}}},"h":{"df":0,"docs":{},"e":{"a":{"d":{"df":3,"docs":{"12":{"tf":1.0},"13":{"tf":1.0},"14":{"tf":1.0}},"e":{"df":0,"docs":{},"r":{"df":4,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"8":{"tf":1.0},"9":{"tf":1.0}}}}},"df":0,"docs":{}},"df":0,"docs":{}}},"i":{"d":{"df":1,"docs":{"14":{"tf":1.0}}},"df":0,"docs":{},"n":{"c":{"df":0,"docs":{},"l":{"df":0,"docs":{},"u":{"d":{"df":1,"docs":{"4":{"tf":1.0}}},"df":0,"docs":{}}}},"df":0,"docs":{},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"o":{"d":{"df":0,"docs":{},"u":{"c":{"df":0,"docs":{},"t":{"df":1,"docs":{"0":{"tf":1.0}}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}},"s":{"df":0,"docs":{},"e":{"c":{"df":0,"docs":{},"t":{"df":0,"docs":{},"i":{"df":0,"docs":{},"o":{"df":0,"docs":{},"n":{"df":1,"docs":{"3":{"tf":1.0}}}}}}},"df":0,"docs":{}},"n":{"df":0,"docs":{},"e":{"a":{"df":0,"docs":{},"k":{"df":0,"docs":{},"i":{"df":1,"docs":{"1":{"tf":1.0}}}}},"df":0,"docs":{}}},"t":{"df":0,"docs":{},"r":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"s":{"df":1,"docs":{"6":{"tf":1.0}}}}}}},"u":{"df":0,"docs":{},"m":{"df":0,"docs":{},"m":{"a":{"df":0,"docs":{},"r":{"df":0,"docs":{},"i":{"df":1,"docs":{"5":{"tf":1.0}}}}},"df":0,"docs":{}}}}},"t":{"df":0,"docs":{},"e":{"df":0,"docs":{},"s":{"df":0,"docs":{},"t":{"df":1,"docs":{"6":{"tf":1.0}}}},"x":{"df":0,"docs":{},"t":{"df":3,"docs":{"10":{"tf":1.0},"11":{"tf":1.0},"9":{"tf":1.0}}}}}},"u":{"df":0,"docs":{},"n":{"df":0,"docs":{},"i":{"c":{"df":0,"docs":{},"o":{"d":{"df":1,"docs":{"6":{"tf":1.0}}},"df":0,"docs":{}}},"df":0,"docs":{}}}}}}},"lang":"English","pipeline":["trimmer","stopWordFilter","stemmer"],"ref":"id","version":"0.9.5"},"results_options":{"limit_results":30,"teaser_word_count":30},"search_options":{"bool":"OR","expand":true,"fields":{"body":{"boost":1},"breadcrumbs":{"boost":1},"title":{"boost":2}}}}')); ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/SUMMARY.md ================================================ # Summary [Introduction](intro.md) - [First Chapter](first/index.md) - [Includes](first/includes.md) - [Unicode](first/unicode.md) - [No Headers](first/no-headers.md) - [Duplicate Headers](first/duplicate-headers.md) - [Heading Attributes](first/heading-attributes.md) ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/first/duplicate-headers.md ================================================ # Duplicate headers This page validates behaviour of duplicate headers. # Header Text # Header Text # header-text ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/first/heading-attributes.md ================================================ # Heading Attributes {#attrs} ## Heading with classes {.class1 .class2} ## Heading with id and classes {#both .class1 .class2} ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/first/includes.md ================================================ # Includes {{#include ../SUMMARY.md::}} ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/first/index.md ================================================ # First Chapter more text. ## Some Section ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/first/no-headers.md ================================================ Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex. ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/first/unicode.md ================================================ # Unicode stress tests Please be careful editing, this contains carefully crafted characters. Two byte character: spatiëring Combining character: spatiëring Three byte character: 书こんにちは Four byte character: 𐌀‮𐌁‮𐌂‮𐌃‮𐌄‮𐌅‮𐌆‮𐌇‮𐌈‬ Right-to-left: مرحبا Emoticons: 🔊 😍 💜 1️⃣ right-to-left mark: hello באמת!‏ Zalgo: ǫ̛̖̱̗̝͈̋͒͋̏ͥͫ̒̆ͩ̏͌̾͊͐ͪ̾̚ ================================================ FILE: tests/testsuite/search/reasonable_search_index/src/intro.md ================================================ # Introduction Here's some interesting text... ## Sneaky

I put <HTML> in here!

Sneaky inline event . But regular inline is indexed. ================================================ FILE: tests/testsuite/search.rs ================================================ //! Tests for search support. use crate::prelude::*; use mdbook_core::book::{BookItem, Chapter}; use snapbox::file; use std::path::Path; fn read_book_index(root: &Path) -> serde_json::Value { let index_path = glob_one(root, "book/searchindex*.js"); let index = read_to_string(&index_path); let index = index.trim_start_matches("window.search = Object.assign(window.search, JSON.parse('"); let index = index.trim_end_matches("'));"); // We need unescape the string as it's supposed to be an escaped JS string. serde_json::from_str(&index.replace("\\'", "'").replace("\\\\", "\\")).unwrap() } // Some spot checks for the generation of the search index. #[test] fn reasonable_search_index() { let mut test = BookTest::from_dir("search/reasonable_search_index"); test.build(); let index = read_book_index(&test.dir); let doc_urls = index["doc_urls"].as_array().unwrap(); eprintln!("doc_urls={doc_urls:#?}",); let get_doc_ref = |url: &str| -> String { doc_urls .iter() .position(|s| s == url) .unwrap_or_else(|| panic!("failed to find {url}")) .to_string() }; let first_chapter = get_doc_ref("first/index.html#first-chapter"); let introduction = get_doc_ref("intro.html#introduction"); let some_section = get_doc_ref("first/index.html#some-section"); let summary = get_doc_ref("first/includes.html#summary"); let no_headers = get_doc_ref("first/no-headers.html"); let duplicate_headers_1 = get_doc_ref("first/duplicate-headers.html#header-text-1"); let heading_attrs = get_doc_ref("first/heading-attributes.html#both"); let sneaky = get_doc_ref("intro.html#sneaky"); let bodyidx = &index["index"]["index"]["body"]["root"]; let textidx = &bodyidx["t"]["e"]["x"]["t"]; assert_eq!(textidx["df"], 5); assert_eq!(textidx["docs"][&first_chapter]["tf"], 1.0); assert_eq!(textidx["docs"][&introduction]["tf"], 1.0); let docs = &index["index"]["documentStore"]["docs"]; assert_eq!(docs[&first_chapter]["body"], "more text."); assert_eq!(docs[&some_section]["body"], ""); assert_eq!( docs[&summary]["body"], "Introduction First Chapter Includes Unicode No Headers Duplicate Headers Heading Attributes" ); assert_eq!( docs[&summary]["breadcrumbs"], "First Chapter » Includes » Summary" ); // See note about InlineHtml in search.rs. Ideally the `alert()` part // should not be in the index, but we don't have a way to scrub inline // html. assert_eq!( docs[&sneaky]["body"], "I put in here! Sneaky inline event . But regular inline is indexed." ); assert_eq!( docs[&no_headers]["breadcrumbs"], "First Chapter » No Headers" ); assert_eq!( docs[&duplicate_headers_1]["breadcrumbs"], "First Chapter » Duplicate Headers » Header Text" ); assert_eq!( docs[&no_headers]["body"], "Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex." ); assert_eq!( docs[&heading_attrs]["breadcrumbs"], "First Chapter » Heading Attributes » Heading with id and classes" ); } // This test is here to catch any unexpected changes to the search index. #[test] fn search_index_hasnt_changed_accidentally() { BookTest::from_dir("search/reasonable_search_index").check_file( "book/searchindex*.js", file!["search/reasonable_search_index/expected_index.js"], ); } // Ability to disable search chapters. #[test] fn can_disable_individual_chapters() { let mut test = BookTest::from_dir("search/disable_search_chapter"); test.build(); let index = read_book_index(&test.dir); let doc_urls = index["doc_urls"].as_array().unwrap(); let contains = |path| { doc_urls .iter() .any(|p| p.as_str().unwrap().starts_with(path)) }; assert!(contains("second.html")); assert!(!contains("second/")); assert!(!contains("first/disable_me.html")); assert!(contains("first/keep_me.html")); } // Test for a regression where search would fail if source_path is None. // https://github.com/rust-lang/mdBook/pull/2550 #[test] fn with_no_source_path() { let test = BookTest::from_dir("search/reasonable_search_index"); let mut book = test.load_book(); let chapter = Chapter::new("Sample chapter", String::new(), "sample.html", vec![]); book.book.items.push(BookItem::Chapter(chapter)); book.build().unwrap(); } // Checks that invalid settings in search chapter is rejected. #[test] fn chapter_settings_validation_error() { BookTest::from_dir("search/chapter_settings_validation_error").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend ERROR Rendering failed [TAB]Caused by: [output.html.search.chapter] key `does-not-exist` does not match any chapter paths "#]]); }); } ================================================ FILE: tests/testsuite/test/failing_tests/book.toml ================================================ [book] title = "failing_tests" ================================================ FILE: tests/testsuite/test/failing_tests/src/SUMMARY.md ================================================ # Summary - [Failing Tests](./failing.md) - [Failing Include](./failing_include.md) ================================================ FILE: tests/testsuite/test/failing_tests/src/failing.md ================================================ # Failing Tests ```rust panic!("fail"); ``` ================================================ FILE: tests/testsuite/test/failing_tests/src/failing_include.md ================================================ # Failing Include ```rust {{#include test1.rs:FAILING}} ``` ================================================ FILE: tests/testsuite/test/failing_tests/src/test1.rs ================================================ fn test2() { println!("test2"); } // ANCHOR: PASSING println!("passing!"); // ANCHOR_END: PASSING // ANCHOR: FAILING panic!("failing!"); // ANCHOR_END: FAILING ================================================ FILE: tests/testsuite/test/passing_tests/book.toml ================================================ [book] title = "passing_tests" ================================================ FILE: tests/testsuite/test/passing_tests/src/SUMMARY.md ================================================ # Summary [Intro](./intro.md) - [Passing 1](./passing1.md) - [Passing 2](./passing2.md) ================================================ FILE: tests/testsuite/test/passing_tests/src/passing1.md ================================================ # Passing Tests 1 ```rust assert!(true); ``` ```rust println!("hello!"); ``` ## Also check includes ```rust {{#include test1.rs}} ``` ```rust {{#include test2.rs:2}} ``` ```rust {{#include test2.rs:PASSING}} ``` ```rust {{#rustdoc_include test3.rs:2}} ``` {{#playground test1.rs}} ================================================ FILE: tests/testsuite/test/passing_tests/src/passing2.md ================================================ # Passing Tests 2 ```rust println!("also passing"); ``` ================================================ FILE: tests/testsuite/test/passing_tests/src/test1.rs ================================================ println!("test1"); ================================================ FILE: tests/testsuite/test/passing_tests/src/test2.rs ================================================ fn test2() { println!("test2"); } // ANCHOR: PASSING println!("passing!"); // ANCHOR_END: PASSING // ANCHOR: FAILING panic!("failing!"); // ANCHOR_END: FAILING ================================================ FILE: tests/testsuite/test/passing_tests/src/test3.rs ================================================ println!("test3"); ================================================ FILE: tests/testsuite/test.rs ================================================ //! Tests for the `mdbook test` command. use crate::prelude::*; // Simple test for passing tests. #[test] fn passing_tests() { BookTest::from_dir("test/passing_tests").run("test", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Testing chapter 'Intro': "intro.md" INFO Testing chapter 'Passing 1': "passing1.md" INFO Testing chapter 'Passing 2': "passing2.md" "#]]); }); } // Test for a test failure #[test] fn failing_tests() { BookTest::from_dir("test/failing_tests").run("test", |cmd| { cmd.expect_code(101) .expect_stdout(str![[""]]) // This redacts a large number of lines that come from rustdoc and // libtest. If the output from those ever changes, then it would not // make it possible to test against different versions of Rust. This // still includes a little bit of output, so if that is a problem, // add more redactions. .expect_stderr(str![[r#" INFO Testing chapter 'Failing Tests': "failing.md" ERROR rustdoc returned an error: --- stdout ... test failing.md - Failing_Tests (line 3) ... FAILED ... thread [..] panicked at failing.md:3:1: fail ... INFO Testing chapter 'Failing Include': "failing_include.md" ERROR rustdoc returned an error: --- stdout ... test failing_include.md - Failing_Include (line 3) ... FAILED ... thread [..] panicked at failing_include.md:3:1: failing! ... ERROR One or more tests failed "#]]); }); } // Test with a specific chapter. #[test] fn test_individual_chapter() { let mut test = BookTest::from_dir("test/passing_tests"); test.run("test -c", |cmd| { cmd.args(&["Passing 1"]) .expect_stdout(str![[""]]) .expect_stderr(str![[r#" INFO Testing chapter 'Passing 1': "passing1.md" "#]]); }) // Can also be a source path. .run("test -c passing2.md", |cmd| { cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#" INFO Testing chapter 'Passing 2': "passing2.md" "#]]); }); } // Unknown chapter name. #[test] fn chapter_not_found() { BookTest::from_dir("test/passing_tests").run("test -c bogus", |cmd| { cmd.expect_failure() .expect_stdout(str![[""]]) .expect_stderr(str![[r#" ERROR Chapter not found: bogus "#]]); }); } ================================================ FILE: tests/testsuite/theme/custom_fonts_css/book.toml ================================================ [book] title = "custom_fonts_css" ================================================ FILE: tests/testsuite/theme/custom_fonts_css/src/SUMMARY.md ================================================ - [Intro](index.md) ================================================ FILE: tests/testsuite/theme/custom_fonts_css/theme/fonts/fonts.css ================================================ /*custom*/ ================================================ FILE: tests/testsuite/theme/empty_fonts_css/book.toml ================================================ [book] title = "empty_fonts_css" ================================================ FILE: tests/testsuite/theme/empty_fonts_css/src/SUMMARY.md ================================================ - [Intro](index.md) ================================================ FILE: tests/testsuite/theme/empty_fonts_css/theme/fonts/fonts.css ================================================ ================================================ FILE: tests/testsuite/theme/empty_theme/book.toml ================================================ [book] title = "empty_theme" [output.html] theme = "./theme" ================================================ FILE: tests/testsuite/theme/empty_theme/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/theme/empty_theme/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/theme/fonts_css/src/SUMMARY.md ================================================ - [With Fonts](index.md) ================================================ FILE: tests/testsuite/theme/fonts_css/theme/fonts/fonts.css ================================================ /*custom*/ ================================================ FILE: tests/testsuite/theme/missing_theme/book.toml ================================================ [book] title = "missing_theme" [output.html] theme = "./non-existent-directory" ================================================ FILE: tests/testsuite/theme/missing_theme/src/SUMMARY.md ================================================ # Summary - [Chapter 1](./chapter_1.md) ================================================ FILE: tests/testsuite/theme/missing_theme/src/chapter_1.md ================================================ # Chapter 1 ================================================ FILE: tests/testsuite/theme/override_index/src/SUMMARY.md ================================================ - [Intro](index.md) ================================================ FILE: tests/testsuite/theme/override_index/theme/index.hbs ================================================ This is a modified index.hbs! ================================================ FILE: tests/testsuite/theme.rs ================================================ //! Tests for theme handling. use crate::prelude::*; // Checks what happens if the theme directory is missing. #[test] fn missing_theme() { BookTest::from_dir("theme/missing_theme").run("build", |cmd| { cmd.expect_failure().expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend ERROR Rendering failed [TAB]Caused by: theme dir [ROOT]/./non-existent-directory does not exist "#]]); }); } // Checks what happens if the theme directory is empty. #[test] fn empty_theme() { BookTest::from_dir("theme/empty_theme").run("build", |cmd| { std::fs::create_dir(cmd.dir.join("theme")).unwrap(); cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }); } // Checks overriding index.hbs. #[test] fn override_index() { BookTest::from_dir("theme/override_index").check_file( "book/index.html", str![[r#" This is a modified index.hbs! "#]], ); } // After building, what are the default set of fonts? #[test] fn default_fonts() { BookTest::init(|_| {}) .check_file_contains("book/index.html", "fonts/fonts-[..].css") .check_file_list( "book/fonts", str![[r#" book/fonts/OPEN-SANS-LICENSE.txt book/fonts/SOURCE-CODE-PRO-LICENSE.txt book/fonts/fonts-[..].css book/fonts/open-sans-v17-all-charsets-300-[..].woff2 book/fonts/open-sans-v17-all-charsets-300italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-600-[..].woff2 book/fonts/open-sans-v17-all-charsets-600italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-700-[..].woff2 book/fonts/open-sans-v17-all-charsets-700italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-800-[..].woff2 book/fonts/open-sans-v17-all-charsets-800italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-regular-[..].woff2 book/fonts/source-code-pro-v11-all-charsets-500-[..].woff2 "#]], ); } // When the theme is initialized, what does the fonts list look like? #[test] fn theme_fonts_copied() { BookTest::init(|bb| { bb.copy_theme(true); }) .check_file_contains("book/index.html", "fonts/fonts-[..].css") .check_file_list( "theme/fonts", str![[r#" theme/fonts/OPEN-SANS-LICENSE.txt theme/fonts/SOURCE-CODE-PRO-LICENSE.txt theme/fonts/fonts.css theme/fonts/open-sans-v17-all-charsets-300.woff2 theme/fonts/open-sans-v17-all-charsets-300italic.woff2 theme/fonts/open-sans-v17-all-charsets-600.woff2 theme/fonts/open-sans-v17-all-charsets-600italic.woff2 theme/fonts/open-sans-v17-all-charsets-700.woff2 theme/fonts/open-sans-v17-all-charsets-700italic.woff2 theme/fonts/open-sans-v17-all-charsets-800.woff2 theme/fonts/open-sans-v17-all-charsets-800italic.woff2 theme/fonts/open-sans-v17-all-charsets-italic.woff2 theme/fonts/open-sans-v17-all-charsets-regular.woff2 theme/fonts/source-code-pro-v11-all-charsets-500.woff2 "#]], ) // Note that license files get hashed, which is not like the behavior when // the theme directory is empty. It kinda makes sense, but is weird. .check_file_list( "book/fonts", str![[r#" book/fonts/OPEN-SANS-LICENSE-[..].txt book/fonts/SOURCE-CODE-PRO-LICENSE-[..].txt book/fonts/fonts-[..].css book/fonts/open-sans-v17-all-charsets-300-[..].woff2 book/fonts/open-sans-v17-all-charsets-300italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-600-[..].woff2 book/fonts/open-sans-v17-all-charsets-600italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-700-[..].woff2 book/fonts/open-sans-v17-all-charsets-700italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-800-[..].woff2 book/fonts/open-sans-v17-all-charsets-800italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-italic-[..].woff2 book/fonts/open-sans-v17-all-charsets-regular-[..].woff2 book/fonts/source-code-pro-v11-all-charsets-500-[..].woff2 "#]], ); } // Custom fonts.css. #[test] fn fonts_css() { BookTest::from_dir("theme/fonts_css") .check_file_contains("book/index.html", "fonts/fonts-[..].css") .check_file( "book/fonts/fonts-*.css", str![[r#" /*custom*/ "#]], ) .check_file("book/fonts/myfont-*.woff", str![[""]]) .check_file_list( "book/fonts", str![[r#" book/fonts/fonts-[..].css book/fonts/myfont-[..].woff "#]], ); } // Empty fonts.css should not copy the default fonts. #[test] fn empty_fonts_css() { BookTest::from_dir("theme/empty_fonts_css") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }) .check_file_contains("book/index.html", "fonts.css") .check_file_list("book/fonts", str![[""]]); } // Custom fonts.css file shouldn't copy default fonts. #[test] fn custom_fonts_css() { BookTest::from_dir("theme/custom_fonts_css") .run("build", |cmd| { cmd.expect_stderr(str![[r#" INFO Book building has started INFO Running the html backend INFO HTML book written to `[ROOT]/book` "#]]); }) .check_file_contains("book/index.html", "fonts-[..].css") .check_file_list( "book/fonts", str![[r#" book/fonts/fonts-[..].css book/fonts/myfont-[..].woff "#]], ); } ================================================ FILE: tests/testsuite/toc/basic_toc/book.toml ================================================ [book] title = "basic_toc" ================================================ FILE: tests/testsuite/toc/basic_toc/src/README.md ================================================ # With Readme ================================================ FILE: tests/testsuite/toc/basic_toc/src/SUMMARY.md ================================================ # Summary [Prefix 1](prefix1.md) [Prefix 2](prefix2.md) - [With Readme](README.md) - [Nested Index](nested/index.md) - [Nested two](nested/two.md) - [Draft]() --- # Deep Nest - [Deep Nest 1](deep/index.md) - [Deep Nest 2](deep/a/index.md) - [Deep Nest 3](deep/a/b/index.md) [Deep Nest 4](deep/a/b/c/index.md) --- [Suffix 1](suffix1.md) [Suffix 2](suffix2.md) ================================================ FILE: tests/testsuite/toc/basic_toc/src/deep/a/b/index.md ================================================ # Deep Nest 3 ================================================ FILE: tests/testsuite/toc/basic_toc/src/deep/a/index.md ================================================ # Deep Nest 2 ================================================ FILE: tests/testsuite/toc/basic_toc/src/deep/index.md ================================================ # Deep Nest 1 ================================================ FILE: tests/testsuite/toc/basic_toc/src/nested/index.md ================================================ # Nested Index ================================================ FILE: tests/testsuite/toc/basic_toc/src/nested/two.md ================================================ # Nested two ================================================ FILE: tests/testsuite/toc/basic_toc/src/prefix1.md ================================================ # Prefix 1 ================================================ FILE: tests/testsuite/toc/basic_toc/src/prefix2.md ================================================ # Prefix 2 ================================================ FILE: tests/testsuite/toc/basic_toc/src/suffix1.md ================================================ # Suffix 1 ================================================ FILE: tests/testsuite/toc/basic_toc/src/suffix2.md ================================================ # Suffix 2 ================================================ FILE: tests/testsuite/toc/summary_with_markdown_formatting/src/SUMMARY.md ================================================ # Summary formatting tests - [*Italic* `code` \*escape\* \`escape2\`](formatted-summary.md) - [Soft line break](soft.md) - [\](escaped-tag.md) ================================================ FILE: tests/testsuite/toc.rs ================================================ //! Tests for table of contents (sidebar). use crate::prelude::*; use anyhow::Result; use select::document::Document; use select::predicate::{Attr, Class, Name, Predicate}; const TOC_TOP_LEVEL: &[&str] = &[ "1. With Readme", "3. Deep Nest 1", "Prefix 1", "Prefix 2", "Suffix 1", "Suffix 2", ]; const TOC_SECOND_LEVEL: &[&str] = &[ "1.1. Nested Index", "1.2. Nested two", "3.1. Deep Nest 2", "3.1.1. Deep Nest 3", ]; /// Apply a series of predicates to some root predicate, where each /// successive predicate is the descendant of the last one. Similar to how you /// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list. macro_rules! descendants { ($root:expr, $($child:expr),*) => { $root $( .descendant($child) )* }; } /// Read the TOC (`book/toc.js`) nested HTML and expose it as a DOM which we /// can search with the `select` crate fn toc_js_html() -> Document { let mut test = BookTest::from_dir("toc/basic_toc"); test.build(); let html = test.toc_js_html(); Document::from(html.as_str()) } /// Read the TOC fallback (`book/toc.html`) HTML and expose it as a DOM which we /// can search with the `select` crate fn toc_fallback_html() -> Result { let mut test = BookTest::from_dir("toc/basic_toc"); test.build(); let toc_path = test.dir.join("book").join("toc.html"); let html = read_to_string(toc_path); Ok(Document::from(html.as_str())) } #[test] fn check_second_toc_level() { let doc = toc_js_html(); let mut should_be = Vec::from(TOC_SECOND_LEVEL); should_be.sort_unstable(); let pred = descendants!( Class("chapter"), Name("li"), Name("li"), Name("a").and(Class("toggle").not()) ); let mut children_of_children: Vec<_> = doc .find(pred) .map(|elem| elem.text().trim().to_string()) .collect(); children_of_children.sort(); assert_eq!(children_of_children, should_be); } #[test] fn check_first_toc_level() { let doc = toc_js_html(); let mut should_be = Vec::from(TOC_TOP_LEVEL); should_be.extend(TOC_SECOND_LEVEL); should_be.sort_unstable(); let pred = descendants!( Class("chapter"), Name("li"), Name("a").and(Class("toggle").not()) ); let mut children: Vec<_> = doc .find(pred) .map(|elem| elem.text().trim().to_string()) .collect(); children.sort(); assert_eq!(children, should_be); } #[test] fn check_spacers() { let doc = toc_js_html(); let should_be = 2; let num_spacers = doc .find(Class("chapter").descendant(Name("li").and(Class("spacer")))) .count(); assert_eq!(num_spacers, should_be); } // don't use target="_parent" in JS #[test] fn check_link_target_js() { let doc = toc_js_html(); let num_parent_links = doc .find( Class("chapter") .descendant(Name("li")) .descendant(Name("a").and(Attr("target", "_parent"))), ) .count(); assert_eq!(num_parent_links, 0); } // don't use target="_parent" in IFRAME #[test] fn check_link_target_fallback() { let doc = toc_fallback_html().unwrap(); let num_parent_links = doc .find( Class("chapter") .descendant(Name("li")) .descendant(Name("a").and(Attr("target", "_parent"))), ) .count(); assert_eq!( num_parent_links, TOC_TOP_LEVEL.len() + TOC_SECOND_LEVEL.len() ); } // Checks formatting of summary names with inline elements. #[test] fn summary_with_markdown_formatting() { BookTest::from_dir("toc/summary_with_markdown_formatting") .check_toc_js(str![[r#"
  1. Italic code *escape* `escape2`
  2. Soft line break
  3. <escaped tag>
"#]]) .check_file( "src/formatted-summary.md", str![[r#" # Italic code *escape* `escape2` "#]], ) .check_file( "src/soft.md", str![[r#" # Soft line break "#]], ) .check_file( "src/escaped-tag.md", str![[r#" # <escaped tag> "#]], ); } ================================================ FILE: triagebot.toml ================================================ # This will allow users to self assign, and/or drop assignment [assign] # Allows @rustbot ready, review, author, or blocked [shortcut] # Closes/reopens PRs created by the GitHub Actions bot so that checks will run. [bot-pull-requests] # When rebasing, this will add a diff link in a comment. [range-diff] [relabel] allow-unauthenticated = [ # For Issue areas "A-*", # Categories "C-*", # Commands "Command-*", # Status "S-*", "regression", "Breaking Change", "msrv-bump", ] [autolabel."S-waiting-on-review"] new_pr = true [merge-conflicts] remove = [] add = ["S-waiting-on-author"] unless = ["S-blocked", "S-waiting-on-review"] [review-submitted] reviewed_label = "S-waiting-on-author" review_labels = ["S-waiting-on-review"] [review-requested] remove_labels = ["S-waiting-on-author"] add_labels = ["S-waiting-on-review"]