e1da72c6e8
rustdoc: add header map to the table of contents
## Summary
Add header sections to the sidebar TOC.
### Preview
![image](https://github.com/user-attachments/assets/eae4df02-86aa-4df4-8c61-a95685cd8829)
* http://notriddle.com/rustdoc-html-demo-9/toc/rust/std/index.html
* http://notriddle.com/rustdoc-html-demo-9/toc/rust-derive-builder/derive_builder/index.html
## Motivation
Some pages are very wordy, like these.
| crate | word count |
|--|--|
| [std::option](https://doc.rust-lang.org/stable/std/option/index.html) | 2,138
| [derive_builder](https://docs.rs/derive_builder/0.13.0/derive_builder/index.html) | 2,403
| [tracing](https://docs.rs/tracing/0.1.40/tracing/index.html) | 3,912
| [regex](https://docs.rs/regex/1.10.3/regex/index.html) | 8,412
This kind of very long document is more navigable with a table of contents, like Wikipedia's or the one [GitHub recently added](https://github.blog/changelog/2021-04-13-table-of-contents-support-in-markdown-files/) for READMEs.
In fact, the use case is so compelling, that it's been requested multiple times and implemented in an extension:
* https://github.com/rust-lang/rust/issues/80858
* https://github.com/rust-lang/rust/issues/28056
* https://github.com/rust-lang/rust/issues/14475
* https://rust.extension.sh/#show-table-of-content
(Some of these issues ask for more than this, so don’t close them.)
It's also been implemented by hand in some crates, because the author really thought it was needed. Protip: for a more exhaustive list, run [`site:docs.rs table of contents`](https://duckduckgo.com/?t=ffab&q=site%3Adocs.rs+table+of+contents&ia=web), though some of them are false positives.
* https://docs.rs/figment/0.10.14/figment/index.html#table-of-contents
* https://docs.rs/csv/1.3.0/csv/tutorial/index.html#table-of-contents
* https://docs.rs/axum/0.7.4/axum/response/index.html#table-of-contents
* https://docs.rs/regex-automata/0.4.5/regex_automata/index.html#table-of-contents
Unfortunately for these hand-built ToCs, because they're just part of the docs, there's no consistent way to turn them off if the reader doesn't want them. It's also more complicated to ensure they stay in sync with the docs they're supposed to describe, and they don't stay with you when you scroll like Wikipedia's [does now](https://uxdesign.cc/design-notes-on-the-2023-wikipedia-redesign-d6573b9af28d).
## Guide-level explanation
When writing docs for a top-level item, the first and second level of headers will be shown in an outline in the sidebar. In this context, "top level" means "not associated".
This means, if you're writing very long guides or explanations, and you want it to have a table of contents in the sidebar for its headings, the ideal place to attach it is usually the *module* or *crate*, because this page has fewer other things on it (and is the ideal place to describe "cross-cutting concerns" for its child items).
If you're reading documentation, and want to get rid of the table of contents, open the ![image](https://github.com/rust-lang/rust/assets/1593513/2ad82466-5fe3-4684-b1c2-6be4c99a8666) Settings panel and checkmark "Hide table of contents."
## Reference-level explanation
Top-level items have an outline generated. This works for potentially-malformed header trees by pairing a header with the nearest header with a higher level. For example:
```markdown
## A
# B
# C
## D
## E
```
A, B, and C are all siblings, and D and E are children of C.
Rustdoc only presents two layers of tree, but it tracks up to the full depth of 6 while preparing it.
That means that these two doc comment both generate the same outline:
```rust
/// # First
/// ## Second
struct One;
/// ## First
/// ### Second
struct Two;
```
## Drawbacks
The biggest drawback is adding more stuff to the sidebar.
My crawl through docs.rs shows this to, surprisingly, be less of a problem than I thought. The manually-built tables of contents, and the pages with dozens of headers, usually seem to be modules or crates, not types (where extreme scrolling would become a problem, since they already have methods to deal with).
The best example of a type with many headers is [vec::Vec](https://doc.rust-lang.org/1.75.0/std/vec/struct.Vec.html), which still only has five headers, not dozens like [axum::extract](https://docs.rs/axum/0.7.4/axum/extract/index.html).
## Rationale and alternatives
### Why in the existing sidebar?
The method links and the top-doc header links have more in common with each other than either of them do with the "In [parent module]" links, and should go together.
### Why limited to two levels?
The sidebar is pretty narrow, and I don't want too much space used by indentation. Making the sidebar wider, while it has some upsides, also takes up more space on middling-sized screens or tiled WMs.
### Why not line wrap?
That behaves strangely when resizing.
## Prior art
### Doc generators that have TOC for headers
https://hexdocs.pm/phoenix/Phoenix.Controller.html is very close, in the sense that it also has header sections directly alongside functions and types.
Another example, referenced as part of the [early sidebar discussion](https://github.com/rust-lang/rust/issues/37856) that added methods, Ruby will show a table of contents in the sidebar (for example, on the [ARGF](https://docs.ruby-lang.org/en/master/ARGF.html) class). According to their changelog, [they added it in 2013](
|
||
---|---|---|
.. | ||
src | ||
anchor-navigable.goml | ||
anchors.goml | ||
basic-code.goml | ||
check_info_sign_position.goml | ||
check-code-blocks-margin.goml | ||
check-stab-in-docblock.goml | ||
code-blocks-overflow.goml | ||
code-color.goml | ||
code-example-buttons.goml | ||
code-sidebar-toggle.goml | ||
code-tags.goml | ||
codeblock-sub.goml | ||
codeblock-tooltip.goml | ||
copy-code.goml | ||
copy-path.goml | ||
cursor.goml | ||
default-settings.goml | ||
deref-block.goml | ||
docblock-big-code-mobile.goml | ||
docblock-code-block-line-number.goml | ||
docblock-details.goml | ||
docblock-table-overflow.goml | ||
docblock-table.goml | ||
duplicate-macro-reexport.goml | ||
enum-variants.goml | ||
escape-key.goml | ||
extend-css.goml | ||
fields.goml | ||
font-weight.goml | ||
globals.goml | ||
go-to-collapsed-elem.goml | ||
hash-item-expansion.goml | ||
headers-color.goml | ||
headings-anchor.goml | ||
headings.goml | ||
help-page.goml | ||
hide-mobile-topbar.goml | ||
highlight-colors.goml | ||
huge-collection-of-constants.goml | ||
huge-logo.goml | ||
impl_on_foreign_order.goml | ||
impl-default-expansion.goml | ||
impl-doc.goml | ||
implementors.goml | ||
item-decl-colors.goml | ||
item-decl-comment-highlighting.goml | ||
item-info-alignment.goml | ||
item-info-overflow.goml | ||
item-info.goml | ||
item-name-wrap.goml | ||
item-summary-table.goml | ||
javascript-disabled.goml | ||
jump-to-def-background.goml | ||
label-next-to-symbol.goml | ||
links-color.goml | ||
list_code_block.goml | ||
method-margins.goml | ||
mobile-crate-name.goml | ||
mobile.goml | ||
module-items-font.goml | ||
no-docblock.goml | ||
notable-trait.goml | ||
overflow-tooltip-information.goml | ||
pocket-menu.goml | ||
README.md | ||
rust-logo.goml | ||
scrape-examples-button-focus.goml | ||
scrape-examples-color.goml | ||
scrape-examples-fonts.goml | ||
scrape-examples-layout.goml | ||
scrape-examples-toggle.goml | ||
search-corrections.goml | ||
search-error.goml | ||
search-filter.goml | ||
search-form-elements.goml | ||
search-input-mobile.goml | ||
search-keyboard.goml | ||
search-no-result.goml | ||
search-reexport.goml | ||
search-result-color.goml | ||
search-result-description.goml | ||
search-result-display.goml | ||
search-result-go-to-first.goml | ||
search-result-impl-disambiguation.goml | ||
search-result-keyword.goml | ||
search-tab-change-title-fn-sig.goml | ||
search-tab.goml | ||
search-title.goml | ||
setting-auto-hide-content-large-items.goml | ||
setting-auto-hide-item-methods-docs.goml | ||
setting-auto-hide-trait-implementations.goml | ||
setting-go-to-only-result.goml | ||
settings-button.goml | ||
settings.goml | ||
shortcuts.goml | ||
sidebar-links-color.goml | ||
sidebar-macro-reexport.goml | ||
sidebar-mobile-scroll.goml | ||
sidebar-mobile.goml | ||
sidebar-modnav-position.goml | ||
sidebar-resize-close-popover.goml | ||
sidebar-resize-setting.goml | ||
sidebar-resize-window.goml | ||
sidebar-resize.goml | ||
sidebar-source-code-display.goml | ||
sidebar-source-code.goml | ||
sidebar.goml | ||
source-anchor-scroll.goml | ||
source-code-page-code-scroll.goml | ||
source-code-page.goml | ||
src-font-size.goml | ||
stab-badge.goml | ||
stab-in-doc.goml | ||
struct-fields.goml | ||
target.goml | ||
theme-change.goml | ||
theme-defaults.goml | ||
theme-in-history.goml | ||
toggle-click-deadspace.goml | ||
toggle-docs-mobile.goml | ||
toggle-docs.goml | ||
toggle-implementors.goml | ||
toggled-open-implementations.goml | ||
tooltip-over-sidebar.goml | ||
trait-sidebar-item-order.goml | ||
trait-with-bounds.goml | ||
type-declation-overflow.goml | ||
type-impls.goml | ||
unsafe-fn.goml | ||
utils.goml | ||
warning-block.goml | ||
where-whitespace.goml |
The tests present here are used to test the generated HTML from rustdoc. The goal is to prevent unsound/unexpected GUI changes.
This is using the browser-ui-test framework to do so. It works as follows:
It wraps puppeteer to send commands to a web browser in order to navigate and test what's being currently displayed in the web page.
You can find more information and its documentation in its repository.
If you need to have more information on the tests run, you can use --test-args
:
$ ./x.py test tests/rustdoc-gui --stage 1 --test-args --debug
If you don't want to run in headless mode (helpful to debug sometimes), you can use
--no-headless
:
$ ./x.py test tests/rustdoc-gui --stage 1 --test-args --no-headless
To see the supported options, use --help
.
Important to be noted: if the chromium instance crashes when you run it, you might need to
use --no-sandbox
to make it work:
$ ./x.py test tests/rustdoc-gui --stage 1 --test-args --no-sandbox