Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e0decce

Browse files
authoredSep 5, 2024
Unrolled build for rust-lang#120736
Rollup merge of rust-lang#120736 - notriddle:notriddle/toc, r=t-rustdoc 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: * rust-lang#80858 * rust-lang#28056 * rust-lang#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](rust-lang#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](https://github.com/ruby/rdoc/blob/06137bde8ccc48cd502bc28178bcd8f2dfe37624/History.rdoc#400--2013-02-24-). Haskell seems to mix text and functions even more freely than Elixir. For example, this [Naming conventions](https://hackage.haskell.org/package/base-4.19.0.0/docs/Control-Monad.html#g:3) is plain text, and is immediately followed by functions. And the [Pandoc top level](https://hackage.haskell.org/package/pandoc-3.1.11.1/docs/Text-Pandoc.html) has items split up by function, rather than by kind. Their TOC matches exactly with the contents of the page. ### Doc generators that don't have header TOC, but still have headers Elm, interestingly enough, seems to have the same setup that Rust used to have: sibling navigation between modules, and no index within a single page. [They keep Haskell's habit of named sections with machine-generated type signatures](https://package.elm-lang.org/packages/elm/browser/latest/Browser-Dom), though. [PHP](https://www.php.net/manual/en/book.datetime.php), like elm, also has a right-hand sidebar with sibling navigation. However, PHP has a single page for a single method, unlike Rust's page for an entire "class." So even though these pages have headers, it's never more than ten at most. And when they have guides, those guides are also multi-page. ## Unresolved questions * Writing recommendations for anyone who wants to take advantage of this. * Right now, it does not line wrap. That might be a bad idea: a lot of these are getting truncated. * Split sidebars, which I [tried implementing](https://rust-lang.zulipchat.com/#narrow/stream/266220-t-rustdoc/topic/Table.20of.20contents), are not required. The TOC can be turned off, if it's really a problem. Implemented in rust-lang#120818, but needs more, separate, discussion. ## Future possibilities I would like to do a better job of distinguishing global navigation from local navigation. Rustdoc has a pretty reasonable information architecture, if only we did a better job of communicating it. This PR aims, mostly, to help doc authors help their users by writing docs that can be more effectively skimmed. But it doesn't do anything to make it easier to tell the TOC and the Module Nav apart.
2 parents 009e738 + bead042 commit e0decce

24 files changed

+539
-153
lines changed
 

‎src/doc/not_found.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<!-- Completely hide the TOC and the section numbers -->
44
<style type="text/css">
5-
#TOC { display: none; }
5+
#rustdoc-toc { display: none; }
66
.header-section-number { display: none; }
77
li {list-style-type: none; }
88
#search-input {

‎src/librustdoc/clean/types.rs

-6
Original file line numberDiff line numberDiff line change
@@ -512,9 +512,6 @@ impl Item {
512512
pub(crate) fn is_mod(&self) -> bool {
513513
self.type_() == ItemType::Module
514514
}
515-
pub(crate) fn is_trait(&self) -> bool {
516-
self.type_() == ItemType::Trait
517-
}
518515
pub(crate) fn is_struct(&self) -> bool {
519516
self.type_() == ItemType::Struct
520517
}
@@ -542,9 +539,6 @@ impl Item {
542539
pub(crate) fn is_ty_method(&self) -> bool {
543540
self.type_() == ItemType::TyMethod
544541
}
545-
pub(crate) fn is_type_alias(&self) -> bool {
546-
self.type_() == ItemType::TypeAlias
547-
}
548542
pub(crate) fn is_primitive(&self) -> bool {
549543
self.type_() == ItemType::Primitive
550544
}

‎src/librustdoc/html/markdown.rs

+62-11
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ use tracing::{debug, trace};
5151
use crate::clean::RenderedLink;
5252
use crate::doctest;
5353
use crate::doctest::GlobalTestOptions;
54-
use crate::html::escape::Escape;
54+
use crate::html::escape::{Escape, EscapeBodyText};
5555
use crate::html::format::Buffer;
5656
use crate::html::highlight;
5757
use crate::html::length_limit::HtmlWithLimit;
5858
use crate::html::render::small_url_encode;
59-
use crate::html::toc::TocBuilder;
59+
use crate::html::toc::{Toc, TocBuilder};
6060

6161
#[cfg(test)]
6262
mod tests;
@@ -102,6 +102,7 @@ pub struct Markdown<'a> {
102102
/// A struct like `Markdown` that renders the markdown with a table of contents.
103103
pub(crate) struct MarkdownWithToc<'a> {
104104
pub(crate) content: &'a str,
105+
pub(crate) links: &'a [RenderedLink],
105106
pub(crate) ids: &'a mut IdMap,
106107
pub(crate) error_codes: ErrorCodes,
107108
pub(crate) edition: Edition,
@@ -533,9 +534,11 @@ impl<'a, 'b, 'ids, I: Iterator<Item = SpannedEvent<'a>>> Iterator
533534
let id = self.id_map.derive(id);
534535

535536
if let Some(ref mut builder) = self.toc {
537+
let mut text_header = String::new();
538+
plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
536539
let mut html_header = String::new();
537-
html::push_html(&mut html_header, self.buf.iter().map(|(ev, _)| ev.clone()));
538-
let sec = builder.push(level as u32, html_header, id.clone());
540+
html_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut html_header);
541+
let sec = builder.push(level as u32, text_header, html_header, id.clone());
539542
self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
540543
}
541544

@@ -1412,10 +1415,23 @@ impl Markdown<'_> {
14121415
}
14131416

14141417
impl MarkdownWithToc<'_> {
1415-
pub(crate) fn into_string(self) -> String {
1416-
let MarkdownWithToc { content: md, ids, error_codes: codes, edition, playground } = self;
1418+
pub(crate) fn into_parts(self) -> (Toc, String) {
1419+
let MarkdownWithToc { content: md, links, ids, error_codes: codes, edition, playground } =
1420+
self;
14171421

1418-
let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1422+
// This is actually common enough to special-case
1423+
if md.is_empty() {
1424+
return (Toc { entries: Vec::new() }, String::new());
1425+
}
1426+
let mut replacer = |broken_link: BrokenLink<'_>| {
1427+
links
1428+
.iter()
1429+
.find(|link| &*link.original_text == &*broken_link.reference)
1430+
.map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1431+
};
1432+
1433+
let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
1434+
let p = p.into_offset_iter();
14191435

14201436
let mut s = String::with_capacity(md.len() * 3 / 2);
14211437

@@ -1429,7 +1445,11 @@ impl MarkdownWithToc<'_> {
14291445
html::push_html(&mut s, p);
14301446
}
14311447

1432-
format!("<nav id=\"TOC\">{toc}</nav>{s}", toc = toc.into_toc().print())
1448+
(toc.into_toc(), s)
1449+
}
1450+
pub(crate) fn into_string(self) -> String {
1451+
let (toc, s) = self.into_parts();
1452+
format!("<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
14331453
}
14341454
}
14351455

@@ -1608,7 +1628,16 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
16081628

16091629
let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
16101630

1611-
for event in p {
1631+
plain_text_from_events(p, &mut s);
1632+
1633+
s
1634+
}
1635+
1636+
pub(crate) fn plain_text_from_events<'a>(
1637+
events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1638+
s: &mut String,
1639+
) {
1640+
for event in events {
16121641
match &event {
16131642
Event::Text(text) => s.push_str(text),
16141643
Event::Code(code) => {
@@ -1623,8 +1652,29 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
16231652
_ => (),
16241653
}
16251654
}
1655+
}
16261656

1627-
s
1657+
pub(crate) fn html_text_from_events<'a>(
1658+
events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
1659+
s: &mut String,
1660+
) {
1661+
for event in events {
1662+
match &event {
1663+
Event::Text(text) => {
1664+
write!(s, "{}", EscapeBodyText(text)).expect("string alloc infallible")
1665+
}
1666+
Event::Code(code) => {
1667+
s.push_str("<code>");
1668+
write!(s, "{}", EscapeBodyText(code)).expect("string alloc infallible");
1669+
s.push_str("</code>");
1670+
}
1671+
Event::HardBreak | Event::SoftBreak => s.push(' '),
1672+
Event::Start(Tag::CodeBlock(..)) => break,
1673+
Event::End(TagEnd::Paragraph) => break,
1674+
Event::End(TagEnd::Heading(..)) => break,
1675+
_ => (),
1676+
}
1677+
}
16281678
}
16291679

16301680
#[derive(Debug)]
@@ -1975,7 +2025,8 @@ fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {
19752025
map.insert("default-settings".into(), 1);
19762026
map.insert("sidebar-vars".into(), 1);
19772027
map.insert("copy-path".into(), 1);
1978-
map.insert("TOC".into(), 1);
2028+
map.insert("rustdoc-toc".into(), 1);
2029+
map.insert("rustdoc-modnav".into(), 1);
19792030
// This is the list of IDs used by rustdoc sections (but still generated by
19802031
// rustdoc).
19812032
map.insert("fields".into(), 1);

‎src/librustdoc/html/render/context.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use rustc_span::{sym, FileName, Symbol};
1515
use tracing::info;
1616

1717
use super::print_item::{full_path, item_path, print_item};
18-
use super::sidebar::{print_sidebar, sidebar_module_like, Sidebar};
18+
use super::sidebar::{print_sidebar, sidebar_module_like, ModuleLike, Sidebar};
1919
use super::write_shared::write_shared;
2020
use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath};
2121
use crate::clean::types::ExternalLocation;
@@ -617,12 +617,14 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
617617
let all = shared.all.replace(AllTypes::new());
618618
let mut sidebar = Buffer::html();
619619

620-
let blocks = sidebar_module_like(all.item_sections());
620+
// all.html is not customizable, so a blank id map is fine
621+
let blocks = sidebar_module_like(all.item_sections(), &mut IdMap::new(), ModuleLike::Crate);
621622
let bar = Sidebar {
622623
title_prefix: "",
623624
title: "",
624625
is_crate: false,
625626
is_mod: false,
627+
parent_is_crate: false,
626628
blocks: vec![blocks],
627629
path: String::new(),
628630
};
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.