Rollup merge of #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:
* 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](06137bde8c/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 https://github.com/rust-lang/rust/pull/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.
This commit is contained in:
commit
e1da72c6e8
@ -2,7 +2,7 @@
|
||||
|
||||
<!-- Completely hide the TOC and the section numbers -->
|
||||
<style type="text/css">
|
||||
#TOC { display: none; }
|
||||
#rustdoc-toc { display: none; }
|
||||
.header-section-number { display: none; }
|
||||
li {list-style-type: none; }
|
||||
#search-input {
|
||||
|
@ -512,9 +512,6 @@ pub(crate) fn is_crate(&self) -> bool {
|
||||
pub(crate) fn is_mod(&self) -> bool {
|
||||
self.type_() == ItemType::Module
|
||||
}
|
||||
pub(crate) fn is_trait(&self) -> bool {
|
||||
self.type_() == ItemType::Trait
|
||||
}
|
||||
pub(crate) fn is_struct(&self) -> bool {
|
||||
self.type_() == ItemType::Struct
|
||||
}
|
||||
@ -542,9 +539,6 @@ pub(crate) fn is_method(&self) -> bool {
|
||||
pub(crate) fn is_ty_method(&self) -> bool {
|
||||
self.type_() == ItemType::TyMethod
|
||||
}
|
||||
pub(crate) fn is_type_alias(&self) -> bool {
|
||||
self.type_() == ItemType::TypeAlias
|
||||
}
|
||||
pub(crate) fn is_primitive(&self) -> bool {
|
||||
self.type_() == ItemType::Primitive
|
||||
}
|
||||
|
@ -51,12 +51,12 @@
|
||||
use crate::clean::RenderedLink;
|
||||
use crate::doctest;
|
||||
use crate::doctest::GlobalTestOptions;
|
||||
use crate::html::escape::Escape;
|
||||
use crate::html::escape::{Escape, EscapeBodyText};
|
||||
use crate::html::format::Buffer;
|
||||
use crate::html::highlight;
|
||||
use crate::html::length_limit::HtmlWithLimit;
|
||||
use crate::html::render::small_url_encode;
|
||||
use crate::html::toc::TocBuilder;
|
||||
use crate::html::toc::{Toc, TocBuilder};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -102,6 +102,7 @@ pub struct Markdown<'a> {
|
||||
/// A struct like `Markdown` that renders the markdown with a table of contents.
|
||||
pub(crate) struct MarkdownWithToc<'a> {
|
||||
pub(crate) content: &'a str,
|
||||
pub(crate) links: &'a [RenderedLink],
|
||||
pub(crate) ids: &'a mut IdMap,
|
||||
pub(crate) error_codes: ErrorCodes,
|
||||
pub(crate) edition: Edition,
|
||||
@ -533,9 +534,11 @@ fn next(&mut self) -> Option<Self::Item> {
|
||||
let id = self.id_map.derive(id);
|
||||
|
||||
if let Some(ref mut builder) = self.toc {
|
||||
let mut text_header = String::new();
|
||||
plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
|
||||
let mut html_header = String::new();
|
||||
html::push_html(&mut html_header, self.buf.iter().map(|(ev, _)| ev.clone()));
|
||||
let sec = builder.push(level as u32, html_header, id.clone());
|
||||
html_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut html_header);
|
||||
let sec = builder.push(level as u32, text_header, html_header, id.clone());
|
||||
self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
|
||||
}
|
||||
|
||||
@ -1412,10 +1415,23 @@ pub fn into_string(self) -> String {
|
||||
}
|
||||
|
||||
impl MarkdownWithToc<'_> {
|
||||
pub(crate) fn into_string(self) -> String {
|
||||
let MarkdownWithToc { content: md, ids, error_codes: codes, edition, playground } = self;
|
||||
pub(crate) fn into_parts(self) -> (Toc, String) {
|
||||
let MarkdownWithToc { content: md, links, ids, error_codes: codes, edition, playground } =
|
||||
self;
|
||||
|
||||
let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
|
||||
// This is actually common enough to special-case
|
||||
if md.is_empty() {
|
||||
return (Toc { entries: Vec::new() }, String::new());
|
||||
}
|
||||
let mut replacer = |broken_link: BrokenLink<'_>| {
|
||||
links
|
||||
.iter()
|
||||
.find(|link| &*link.original_text == &*broken_link.reference)
|
||||
.map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
|
||||
};
|
||||
|
||||
let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
|
||||
let p = p.into_offset_iter();
|
||||
|
||||
let mut s = String::with_capacity(md.len() * 3 / 2);
|
||||
|
||||
@ -1429,7 +1445,11 @@ pub(crate) fn into_string(self) -> String {
|
||||
html::push_html(&mut s, p);
|
||||
}
|
||||
|
||||
format!("<nav id=\"TOC\">{toc}</nav>{s}", toc = toc.into_toc().print())
|
||||
(toc.into_toc(), s)
|
||||
}
|
||||
pub(crate) fn into_string(self) -> String {
|
||||
let (toc, s) = self.into_parts();
|
||||
format!("<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1608,7 +1628,16 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
|
||||
|
||||
let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
|
||||
|
||||
for event in p {
|
||||
plain_text_from_events(p, &mut s);
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
pub(crate) fn plain_text_from_events<'a>(
|
||||
events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
|
||||
s: &mut String,
|
||||
) {
|
||||
for event in events {
|
||||
match &event {
|
||||
Event::Text(text) => s.push_str(text),
|
||||
Event::Code(code) => {
|
||||
@ -1623,8 +1652,29 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
pub(crate) fn html_text_from_events<'a>(
|
||||
events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
|
||||
s: &mut String,
|
||||
) {
|
||||
for event in events {
|
||||
match &event {
|
||||
Event::Text(text) => {
|
||||
write!(s, "{}", EscapeBodyText(text)).expect("string alloc infallible")
|
||||
}
|
||||
Event::Code(code) => {
|
||||
s.push_str("<code>");
|
||||
write!(s, "{}", EscapeBodyText(code)).expect("string alloc infallible");
|
||||
s.push_str("</code>");
|
||||
}
|
||||
Event::HardBreak | Event::SoftBreak => s.push(' '),
|
||||
Event::Start(Tag::CodeBlock(..)) => break,
|
||||
Event::End(TagEnd::Paragraph) => break,
|
||||
Event::End(TagEnd::Heading(..)) => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -1975,7 +2025,8 @@ fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {
|
||||
map.insert("default-settings".into(), 1);
|
||||
map.insert("sidebar-vars".into(), 1);
|
||||
map.insert("copy-path".into(), 1);
|
||||
map.insert("TOC".into(), 1);
|
||||
map.insert("rustdoc-toc".into(), 1);
|
||||
map.insert("rustdoc-modnav".into(), 1);
|
||||
// This is the list of IDs used by rustdoc sections (but still generated by
|
||||
// rustdoc).
|
||||
map.insert("fields".into(), 1);
|
||||
|
@ -15,7 +15,7 @@
|
||||
use tracing::info;
|
||||
|
||||
use super::print_item::{full_path, item_path, print_item};
|
||||
use super::sidebar::{print_sidebar, sidebar_module_like, Sidebar};
|
||||
use super::sidebar::{print_sidebar, sidebar_module_like, ModuleLike, Sidebar};
|
||||
use super::write_shared::write_shared;
|
||||
use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath};
|
||||
use crate::clean::types::ExternalLocation;
|
||||
@ -617,12 +617,14 @@ fn after_krate(&mut self) -> Result<(), Error> {
|
||||
let all = shared.all.replace(AllTypes::new());
|
||||
let mut sidebar = Buffer::html();
|
||||
|
||||
let blocks = sidebar_module_like(all.item_sections());
|
||||
// all.html is not customizable, so a blank id map is fine
|
||||
let blocks = sidebar_module_like(all.item_sections(), &mut IdMap::new(), ModuleLike::Crate);
|
||||
let bar = Sidebar {
|
||||
title_prefix: "",
|
||||
title: "",
|
||||
is_crate: false,
|
||||
is_mod: false,
|
||||
parent_is_crate: false,
|
||||
blocks: vec![blocks],
|
||||
path: String::new(),
|
||||
};
|
||||
|
@ -13,7 +13,24 @@
|
||||
use crate::formats::item_type::ItemType;
|
||||
use crate::formats::Impl;
|
||||
use crate::html::format::Buffer;
|
||||
use crate::html::markdown::IdMap;
|
||||
use crate::html::markdown::{IdMap, MarkdownWithToc};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ModuleLike {
|
||||
Module,
|
||||
Crate,
|
||||
}
|
||||
|
||||
impl ModuleLike {
|
||||
pub(crate) fn is_crate(self) -> bool {
|
||||
matches!(self, ModuleLike::Crate)
|
||||
}
|
||||
}
|
||||
impl<'a> From<&'a clean::Item> for ModuleLike {
|
||||
fn from(it: &'a clean::Item) -> ModuleLike {
|
||||
if it.is_crate() { ModuleLike::Crate } else { ModuleLike::Module }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "sidebar.html")]
|
||||
@ -21,6 +38,7 @@ pub(super) struct Sidebar<'a> {
|
||||
pub(super) title_prefix: &'static str,
|
||||
pub(super) title: &'a str,
|
||||
pub(super) is_crate: bool,
|
||||
pub(super) parent_is_crate: bool,
|
||||
pub(super) is_mod: bool,
|
||||
pub(super) blocks: Vec<LinkBlock<'a>>,
|
||||
pub(super) path: String,
|
||||
@ -63,15 +81,19 @@ pub fn should_render(&self) -> bool {
|
||||
/// A link to an item. Content should not be escaped.
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone)]
|
||||
pub(crate) struct Link<'a> {
|
||||
/// The content for the anchor tag
|
||||
/// The content for the anchor tag and title attr
|
||||
name: Cow<'a, str>,
|
||||
/// The content for the anchor tag (if different from name)
|
||||
name_html: Option<Cow<'a, str>>,
|
||||
/// The id of an anchor within the page (without a `#` prefix)
|
||||
href: Cow<'a, str>,
|
||||
/// Nested list of links (used only in top-toc)
|
||||
children: Vec<Link<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Link<'a> {
|
||||
pub fn new(href: impl Into<Cow<'a, str>>, name: impl Into<Cow<'a, str>>) -> Self {
|
||||
Self { href: href.into(), name: name.into() }
|
||||
Self { href: href.into(), name: name.into(), children: vec![], name_html: None }
|
||||
}
|
||||
pub fn empty() -> Link<'static> {
|
||||
Link::new("", "")
|
||||
@ -95,17 +117,21 @@ pub(crate) fn wrapped<T>(v: T) -> rinja::Result<Safe<impl Display>>
|
||||
}
|
||||
|
||||
pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buffer) {
|
||||
let blocks: Vec<LinkBlock<'_>> = match *it.kind {
|
||||
clean::StructItem(ref s) => sidebar_struct(cx, it, s),
|
||||
clean::TraitItem(ref t) => sidebar_trait(cx, it, t),
|
||||
clean::PrimitiveItem(_) => sidebar_primitive(cx, it),
|
||||
clean::UnionItem(ref u) => sidebar_union(cx, it, u),
|
||||
clean::EnumItem(ref e) => sidebar_enum(cx, it, e),
|
||||
clean::TypeAliasItem(ref t) => sidebar_type_alias(cx, it, t),
|
||||
clean::ModuleItem(ref m) => vec![sidebar_module(&m.items)],
|
||||
clean::ForeignTypeItem => sidebar_foreign_type(cx, it),
|
||||
_ => vec![],
|
||||
};
|
||||
let mut ids = IdMap::new();
|
||||
let mut blocks: Vec<LinkBlock<'_>> = docblock_toc(cx, it, &mut ids).into_iter().collect();
|
||||
match *it.kind {
|
||||
clean::StructItem(ref s) => sidebar_struct(cx, it, s, &mut blocks),
|
||||
clean::TraitItem(ref t) => sidebar_trait(cx, it, t, &mut blocks),
|
||||
clean::PrimitiveItem(_) => sidebar_primitive(cx, it, &mut blocks),
|
||||
clean::UnionItem(ref u) => sidebar_union(cx, it, u, &mut blocks),
|
||||
clean::EnumItem(ref e) => sidebar_enum(cx, it, e, &mut blocks),
|
||||
clean::TypeAliasItem(ref t) => sidebar_type_alias(cx, it, t, &mut blocks),
|
||||
clean::ModuleItem(ref m) => {
|
||||
blocks.push(sidebar_module(&m.items, &mut ids, ModuleLike::from(it)))
|
||||
}
|
||||
clean::ForeignTypeItem => sidebar_foreign_type(cx, it, &mut blocks),
|
||||
_ => {}
|
||||
}
|
||||
// The sidebar is designed to display sibling functions, modules and
|
||||
// other miscellaneous information. since there are lots of sibling
|
||||
// items (and that causes quadratic growth in large modules),
|
||||
@ -113,15 +139,9 @@ pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buf
|
||||
// still, we don't move everything into JS because we want to preserve
|
||||
// as much HTML as possible in order to allow non-JS-enabled browsers
|
||||
// to navigate the documentation (though slightly inefficiently).
|
||||
let (title_prefix, title) = if it.is_struct()
|
||||
|| it.is_trait()
|
||||
|| it.is_primitive()
|
||||
|| it.is_union()
|
||||
|| it.is_enum()
|
||||
// crate title is displayed as part of logo lockup
|
||||
|| (it.is_mod() && !it.is_crate())
|
||||
|| it.is_type_alias()
|
||||
{
|
||||
//
|
||||
// crate title is displayed as part of logo lockup
|
||||
let (title_prefix, title) = if !blocks.is_empty() && !it.is_crate() {
|
||||
(
|
||||
match *it.kind {
|
||||
clean::ModuleItem(..) => "Module ",
|
||||
@ -146,8 +166,15 @@ pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buf
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
let sidebar =
|
||||
Sidebar { title_prefix, title, is_mod: it.is_mod(), is_crate: it.is_crate(), blocks, path };
|
||||
let sidebar = Sidebar {
|
||||
title_prefix,
|
||||
title,
|
||||
is_mod: it.is_mod(),
|
||||
is_crate: it.is_crate(),
|
||||
parent_is_crate: sidebar_path.len() == 1,
|
||||
blocks,
|
||||
path,
|
||||
};
|
||||
sidebar.render_into(buffer).unwrap();
|
||||
}
|
||||
|
||||
@ -163,30 +190,80 @@ fn get_struct_fields_name<'a>(fields: &'a [clean::Item]) -> Vec<Link<'a>> {
|
||||
fields
|
||||
}
|
||||
|
||||
fn docblock_toc<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
ids: &mut IdMap,
|
||||
) -> Option<LinkBlock<'a>> {
|
||||
let (toc, _) = MarkdownWithToc {
|
||||
content: &it.doc_value(),
|
||||
links: &it.links(cx),
|
||||
ids,
|
||||
error_codes: cx.shared.codes,
|
||||
edition: cx.shared.edition(),
|
||||
playground: &cx.shared.playground,
|
||||
}
|
||||
.into_parts();
|
||||
let links: Vec<Link<'_>> = toc
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
Link {
|
||||
name_html: if entry.html == entry.name { None } else { Some(entry.html.into()) },
|
||||
name: entry.name.into(),
|
||||
href: entry.id.into(),
|
||||
children: entry
|
||||
.children
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| Link {
|
||||
name_html: if entry.html == entry.name {
|
||||
None
|
||||
} else {
|
||||
Some(entry.html.into())
|
||||
},
|
||||
name: entry.name.into(),
|
||||
href: entry.id.into(),
|
||||
// Only a single level of nesting is shown here.
|
||||
// Going the full six could break the layout,
|
||||
// so we have to cut it off somewhere.
|
||||
children: vec![],
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if links.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(LinkBlock::new(Link::new("", "Sections"), "top-toc", links))
|
||||
}
|
||||
}
|
||||
|
||||
fn sidebar_struct<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
s: &'a clean::Struct,
|
||||
) -> Vec<LinkBlock<'a>> {
|
||||
items: &mut Vec<LinkBlock<'a>>,
|
||||
) {
|
||||
let fields = get_struct_fields_name(&s.fields);
|
||||
let field_name = match s.ctor_kind {
|
||||
Some(CtorKind::Fn) => Some("Tuple Fields"),
|
||||
None => Some("Fields"),
|
||||
_ => None,
|
||||
};
|
||||
let mut items = vec![];
|
||||
if let Some(name) = field_name {
|
||||
items.push(LinkBlock::new(Link::new("fields", name), "structfield", fields));
|
||||
}
|
||||
sidebar_assoc_items(cx, it, &mut items);
|
||||
items
|
||||
sidebar_assoc_items(cx, it, items);
|
||||
}
|
||||
|
||||
fn sidebar_trait<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
t: &'a clean::Trait,
|
||||
) -> Vec<LinkBlock<'a>> {
|
||||
blocks: &mut Vec<LinkBlock<'a>>,
|
||||
) {
|
||||
fn filter_items<'a>(
|
||||
items: &'a [clean::Item],
|
||||
filt: impl Fn(&clean::Item) -> bool,
|
||||
@ -223,19 +300,20 @@ fn filter_items<'a>(
|
||||
foreign_impls.sort();
|
||||
}
|
||||
|
||||
let mut blocks: Vec<LinkBlock<'_>> = [
|
||||
("required-associated-types", "Required Associated Types", req_assoc),
|
||||
("provided-associated-types", "Provided Associated Types", prov_assoc),
|
||||
("required-associated-consts", "Required Associated Constants", req_assoc_const),
|
||||
("provided-associated-consts", "Provided Associated Constants", prov_assoc_const),
|
||||
("required-methods", "Required Methods", req_method),
|
||||
("provided-methods", "Provided Methods", prov_method),
|
||||
("foreign-impls", "Implementations on Foreign Types", foreign_impls),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(id, title, items)| LinkBlock::new(Link::new(id, title), "", items))
|
||||
.collect();
|
||||
sidebar_assoc_items(cx, it, &mut blocks);
|
||||
blocks.extend(
|
||||
[
|
||||
("required-associated-types", "Required Associated Types", req_assoc),
|
||||
("provided-associated-types", "Provided Associated Types", prov_assoc),
|
||||
("required-associated-consts", "Required Associated Constants", req_assoc_const),
|
||||
("provided-associated-consts", "Provided Associated Constants", prov_assoc_const),
|
||||
("required-methods", "Required Methods", req_method),
|
||||
("provided-methods", "Provided Methods", prov_method),
|
||||
("foreign-impls", "Implementations on Foreign Types", foreign_impls),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(id, title, items)| LinkBlock::new(Link::new(id, title), "", items)),
|
||||
);
|
||||
sidebar_assoc_items(cx, it, blocks);
|
||||
|
||||
if !t.is_object_safe(cx.tcx()) {
|
||||
blocks.push(LinkBlock::forced(
|
||||
@ -251,20 +329,17 @@ fn filter_items<'a>(
|
||||
"impl-auto",
|
||||
));
|
||||
}
|
||||
blocks
|
||||
}
|
||||
|
||||
fn sidebar_primitive<'a>(cx: &'a Context<'_>, it: &'a clean::Item) -> Vec<LinkBlock<'a>> {
|
||||
fn sidebar_primitive<'a>(cx: &'a Context<'_>, it: &'a clean::Item, items: &mut Vec<LinkBlock<'a>>) {
|
||||
if it.name.map(|n| n.as_str() != "reference").unwrap_or(false) {
|
||||
let mut items = vec![];
|
||||
sidebar_assoc_items(cx, it, &mut items);
|
||||
items
|
||||
sidebar_assoc_items(cx, it, items);
|
||||
} else {
|
||||
let shared = Rc::clone(&cx.shared);
|
||||
let (concrete, synthetic, blanket_impl) =
|
||||
super::get_filtered_impls_for_reference(&shared, it);
|
||||
|
||||
sidebar_render_assoc_items(cx, &mut IdMap::new(), concrete, synthetic, blanket_impl).into()
|
||||
sidebar_render_assoc_items(cx, &mut IdMap::new(), concrete, synthetic, blanket_impl, items);
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,8 +347,8 @@ fn sidebar_type_alias<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
t: &'a clean::TypeAlias,
|
||||
) -> Vec<LinkBlock<'a>> {
|
||||
let mut items = vec![];
|
||||
items: &mut Vec<LinkBlock<'a>>,
|
||||
) {
|
||||
if let Some(inner_type) = &t.inner_type {
|
||||
items.push(LinkBlock::forced(Link::new("aliased-type", "Aliased type"), "type"));
|
||||
match inner_type {
|
||||
@ -295,19 +370,18 @@ fn sidebar_type_alias<'a>(
|
||||
}
|
||||
}
|
||||
}
|
||||
sidebar_assoc_items(cx, it, &mut items);
|
||||
items
|
||||
sidebar_assoc_items(cx, it, items);
|
||||
}
|
||||
|
||||
fn sidebar_union<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
u: &'a clean::Union,
|
||||
) -> Vec<LinkBlock<'a>> {
|
||||
items: &mut Vec<LinkBlock<'a>>,
|
||||
) {
|
||||
let fields = get_struct_fields_name(&u.fields);
|
||||
let mut items = vec![LinkBlock::new(Link::new("fields", "Fields"), "structfield", fields)];
|
||||
sidebar_assoc_items(cx, it, &mut items);
|
||||
items
|
||||
items.push(LinkBlock::new(Link::new("fields", "Fields"), "structfield", fields));
|
||||
sidebar_assoc_items(cx, it, items);
|
||||
}
|
||||
|
||||
/// Adds trait implementations into the blocks of links
|
||||
@ -346,33 +420,6 @@ fn sidebar_assoc_items<'a>(
|
||||
methods.sort();
|
||||
}
|
||||
|
||||
let mut deref_methods = Vec::new();
|
||||
let [concrete, synthetic, blanket] = if v.iter().any(|i| i.inner_impl().trait_.is_some()) {
|
||||
if let Some(impl_) =
|
||||
v.iter().find(|i| i.trait_did() == cx.tcx().lang_items().deref_trait())
|
||||
{
|
||||
let mut derefs = DefIdSet::default();
|
||||
derefs.insert(did);
|
||||
sidebar_deref_methods(
|
||||
cx,
|
||||
&mut deref_methods,
|
||||
impl_,
|
||||
v,
|
||||
&mut derefs,
|
||||
&mut used_links,
|
||||
);
|
||||
}
|
||||
|
||||
let (synthetic, concrete): (Vec<&Impl>, Vec<&Impl>) =
|
||||
v.iter().partition::<Vec<_>, _>(|i| i.inner_impl().kind.is_auto());
|
||||
let (blanket_impl, concrete): (Vec<&Impl>, Vec<&Impl>) =
|
||||
concrete.into_iter().partition::<Vec<_>, _>(|i| i.inner_impl().kind.is_blanket());
|
||||
|
||||
sidebar_render_assoc_items(cx, &mut id_map, concrete, synthetic, blanket_impl)
|
||||
} else {
|
||||
std::array::from_fn(|_| LinkBlock::new(Link::empty(), "", vec![]))
|
||||
};
|
||||
|
||||
let mut blocks = vec![
|
||||
LinkBlock::new(
|
||||
Link::new("implementations", "Associated Constants"),
|
||||
@ -381,8 +428,30 @@ fn sidebar_assoc_items<'a>(
|
||||
),
|
||||
LinkBlock::new(Link::new("implementations", "Methods"), "method", methods),
|
||||
];
|
||||
blocks.append(&mut deref_methods);
|
||||
blocks.extend([concrete, synthetic, blanket]);
|
||||
|
||||
if v.iter().any(|i| i.inner_impl().trait_.is_some()) {
|
||||
if let Some(impl_) =
|
||||
v.iter().find(|i| i.trait_did() == cx.tcx().lang_items().deref_trait())
|
||||
{
|
||||
let mut derefs = DefIdSet::default();
|
||||
derefs.insert(did);
|
||||
sidebar_deref_methods(cx, &mut blocks, impl_, v, &mut derefs, &mut used_links);
|
||||
}
|
||||
|
||||
let (synthetic, concrete): (Vec<&Impl>, Vec<&Impl>) =
|
||||
v.iter().partition::<Vec<_>, _>(|i| i.inner_impl().kind.is_auto());
|
||||
let (blanket_impl, concrete): (Vec<&Impl>, Vec<&Impl>) =
|
||||
concrete.into_iter().partition::<Vec<_>, _>(|i| i.inner_impl().kind.is_blanket());
|
||||
|
||||
sidebar_render_assoc_items(
|
||||
cx,
|
||||
&mut id_map,
|
||||
concrete,
|
||||
synthetic,
|
||||
blanket_impl,
|
||||
&mut blocks,
|
||||
);
|
||||
}
|
||||
links.append(&mut blocks);
|
||||
}
|
||||
}
|
||||
@ -472,7 +541,8 @@ fn sidebar_enum<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
e: &'a clean::Enum,
|
||||
) -> Vec<LinkBlock<'a>> {
|
||||
items: &mut Vec<LinkBlock<'a>>,
|
||||
) {
|
||||
let mut variants = e
|
||||
.variants()
|
||||
.filter_map(|v| v.name)
|
||||
@ -480,24 +550,37 @@ fn sidebar_enum<'a>(
|
||||
.collect::<Vec<_>>();
|
||||
variants.sort_unstable();
|
||||
|
||||
let mut items = vec![LinkBlock::new(Link::new("variants", "Variants"), "variant", variants)];
|
||||
sidebar_assoc_items(cx, it, &mut items);
|
||||
items
|
||||
items.push(LinkBlock::new(Link::new("variants", "Variants"), "variant", variants));
|
||||
sidebar_assoc_items(cx, it, items);
|
||||
}
|
||||
|
||||
pub(crate) fn sidebar_module_like(
|
||||
item_sections_in_use: FxHashSet<ItemSection>,
|
||||
ids: &mut IdMap,
|
||||
module_like: ModuleLike,
|
||||
) -> LinkBlock<'static> {
|
||||
let item_sections = ItemSection::ALL
|
||||
let item_sections: Vec<Link<'_>> = ItemSection::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|sec| item_sections_in_use.contains(sec))
|
||||
.map(|sec| Link::new(sec.id(), sec.name()))
|
||||
.map(|sec| Link::new(ids.derive(sec.id()), sec.name()))
|
||||
.collect();
|
||||
LinkBlock::new(Link::empty(), "", item_sections)
|
||||
let header = if let Some(first_section) = item_sections.get(0) {
|
||||
Link::new(
|
||||
first_section.href.to_owned(),
|
||||
if module_like.is_crate() { "Crate Items" } else { "Module Items" },
|
||||
)
|
||||
} else {
|
||||
Link::empty()
|
||||
};
|
||||
LinkBlock::new(header, "", item_sections)
|
||||
}
|
||||
|
||||
fn sidebar_module(items: &[clean::Item]) -> LinkBlock<'static> {
|
||||
fn sidebar_module(
|
||||
items: &[clean::Item],
|
||||
ids: &mut IdMap,
|
||||
module_like: ModuleLike,
|
||||
) -> LinkBlock<'static> {
|
||||
let item_sections_in_use: FxHashSet<_> = items
|
||||
.iter()
|
||||
.filter(|it| {
|
||||
@ -518,13 +601,15 @@ fn sidebar_module(items: &[clean::Item]) -> LinkBlock<'static> {
|
||||
.map(|it| item_ty_to_section(it.type_()))
|
||||
.collect();
|
||||
|
||||
sidebar_module_like(item_sections_in_use)
|
||||
sidebar_module_like(item_sections_in_use, ids, module_like)
|
||||
}
|
||||
|
||||
fn sidebar_foreign_type<'a>(cx: &'a Context<'_>, it: &'a clean::Item) -> Vec<LinkBlock<'a>> {
|
||||
let mut items = vec![];
|
||||
sidebar_assoc_items(cx, it, &mut items);
|
||||
items
|
||||
fn sidebar_foreign_type<'a>(
|
||||
cx: &'a Context<'_>,
|
||||
it: &'a clean::Item,
|
||||
items: &mut Vec<LinkBlock<'a>>,
|
||||
) {
|
||||
sidebar_assoc_items(cx, it, items);
|
||||
}
|
||||
|
||||
/// Renders the trait implementations for this type
|
||||
@ -534,7 +619,8 @@ fn sidebar_render_assoc_items(
|
||||
concrete: Vec<&Impl>,
|
||||
synthetic: Vec<&Impl>,
|
||||
blanket_impl: Vec<&Impl>,
|
||||
) -> [LinkBlock<'static>; 3] {
|
||||
items: &mut Vec<LinkBlock<'_>>,
|
||||
) {
|
||||
let format_impls = |impls: Vec<&Impl>, id_map: &mut IdMap| {
|
||||
let mut links = FxHashSet::default();
|
||||
|
||||
@ -559,7 +645,7 @@ fn sidebar_render_assoc_items(
|
||||
let concrete = format_impls(concrete, id_map);
|
||||
let synthetic = format_impls(synthetic, id_map);
|
||||
let blanket = format_impls(blanket_impl, id_map);
|
||||
[
|
||||
items.extend([
|
||||
LinkBlock::new(
|
||||
Link::new("trait-implementations", "Trait Implementations"),
|
||||
"trait-implementation",
|
||||
@ -575,7 +661,7 @@ fn sidebar_render_assoc_items(
|
||||
"blanket-implementation",
|
||||
blanket,
|
||||
),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
fn get_next_url(used_links: &mut FxHashSet<String>, url: String) -> String {
|
||||
|
@ -568,12 +568,16 @@ img {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
ul.block, .block li {
|
||||
ul.block, .block li, .block ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.block ul a {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-elems a,
|
||||
.sidebar > h2 a {
|
||||
display: block;
|
||||
@ -585,6 +589,14 @@ ul.block, .block li {
|
||||
background-clip: border-box;
|
||||
}
|
||||
|
||||
.hide-toc #rustdoc-toc, .hide-toc .in-crate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-modnav #rustdoc-modnav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
text-wrap: balance;
|
||||
overflow-wrap: anywhere;
|
||||
|
@ -499,7 +499,7 @@ function preLoadCss(cssUrl) {
|
||||
if (!window.SIDEBAR_ITEMS) {
|
||||
return;
|
||||
}
|
||||
const sidebar = document.getElementsByClassName("sidebar-elems")[0];
|
||||
const sidebar = document.getElementById("rustdoc-modnav");
|
||||
|
||||
/**
|
||||
* Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items.
|
||||
@ -885,7 +885,7 @@ function preLoadCss(cssUrl) {
|
||||
if (!window.ALL_CRATES) {
|
||||
return;
|
||||
}
|
||||
const sidebarElems = document.getElementsByClassName("sidebar-elems")[0];
|
||||
const sidebarElems = document.getElementById("rustdoc-modnav");
|
||||
if (!sidebarElems) {
|
||||
return;
|
||||
}
|
||||
|
@ -36,6 +36,20 @@
|
||||
removeClass(document.documentElement, "hide-sidebar");
|
||||
}
|
||||
break;
|
||||
case "hide-toc":
|
||||
if (value === true) {
|
||||
addClass(document.documentElement, "hide-toc");
|
||||
} else {
|
||||
removeClass(document.documentElement, "hide-toc");
|
||||
}
|
||||
break;
|
||||
case "hide-modnav":
|
||||
if (value === true) {
|
||||
addClass(document.documentElement, "hide-modnav");
|
||||
} else {
|
||||
removeClass(document.documentElement, "hide-modnav");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +116,11 @@
|
||||
let output = "";
|
||||
|
||||
for (const setting of settings) {
|
||||
if (setting === "hr") {
|
||||
output += "<hr>";
|
||||
continue;
|
||||
}
|
||||
|
||||
const js_data_name = setting["js_name"];
|
||||
const setting_name = setting["name"];
|
||||
|
||||
@ -198,6 +217,16 @@
|
||||
"js_name": "hide-sidebar",
|
||||
"default": false,
|
||||
},
|
||||
{
|
||||
"name": "Hide table of contents",
|
||||
"js_name": "hide-toc",
|
||||
"default": false,
|
||||
},
|
||||
{
|
||||
"name": "Hide module navigation",
|
||||
"js_name": "hide-modnav",
|
||||
"default": false,
|
||||
},
|
||||
{
|
||||
"name": "Disable keyboard shortcuts",
|
||||
"js_name": "disable-shortcuts",
|
||||
|
@ -196,16 +196,21 @@ updateTheme();
|
||||
// This needs to be done here because this JS is render-blocking,
|
||||
// so that the sidebar doesn't "jump" after appearing on screen.
|
||||
// The user interaction to change this is set up in main.js.
|
||||
//
|
||||
// At this point in page load, `document.body` is not available yet.
|
||||
// Set a class on the `<html>` element instead.
|
||||
if (getSettingValue("source-sidebar-show") === "true") {
|
||||
// At this point in page load, `document.body` is not available yet.
|
||||
// Set a class on the `<html>` element instead.
|
||||
addClass(document.documentElement, "src-sidebar-expanded");
|
||||
}
|
||||
if (getSettingValue("hide-sidebar") === "true") {
|
||||
// At this point in page load, `document.body` is not available yet.
|
||||
// Set a class on the `<html>` element instead.
|
||||
addClass(document.documentElement, "hide-sidebar");
|
||||
}
|
||||
if (getSettingValue("hide-toc") === "true") {
|
||||
addClass(document.documentElement, "hide-toc");
|
||||
}
|
||||
if (getSettingValue("hide-modnav") === "true") {
|
||||
addClass(document.documentElement, "hide-modnav");
|
||||
}
|
||||
function updateSidebarWidth() {
|
||||
const desktopSidebarWidth = getSettingValue("desktop-sidebar-width");
|
||||
if (desktopSidebarWidth && desktopSidebarWidth !== "null") {
|
||||
|
@ -1,8 +1,3 @@
|
||||
{% if !title.is_empty() %}
|
||||
<h2 class="location"> {# #}
|
||||
<a href="#">{{title_prefix}}{{title|wrapped|safe}}</a> {# #}
|
||||
</h2>
|
||||
{% endif %}
|
||||
<div class="sidebar-elems">
|
||||
{% if is_crate %}
|
||||
<ul class="block"> {# #}
|
||||
@ -11,18 +6,46 @@
|
||||
{% endif %}
|
||||
|
||||
{% if self.should_render_blocks() %}
|
||||
<section>
|
||||
<section id="rustdoc-toc">
|
||||
{% if !title.is_empty() %}
|
||||
<h2 class="location"> {# #}
|
||||
<a href="#">{{title_prefix}}{{title|wrapped|safe}}</a> {# #}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{% for block in blocks %}
|
||||
{% if block.should_render() %}
|
||||
{% if !block.heading.name.is_empty() %}
|
||||
<h3><a href="#{{block.heading.href|safe}}"> {# #}
|
||||
{{block.heading.name|wrapped|safe}} {# #}
|
||||
</a></h3> {# #}
|
||||
<h3> {# #}
|
||||
<a href="#{{block.heading.href|safe}}">{{block.heading.name|wrapped|safe}}</a> {# #}
|
||||
</h3>
|
||||
{% endif %}
|
||||
{% if !block.links.is_empty() %}
|
||||
<ul class="block{% if !block.class.is_empty() +%} {{+block.class}}{% endif %}">
|
||||
{% for link in block.links %}
|
||||
<li><a href="#{{link.href|safe}}">{{link.name}}</a></li>
|
||||
<li> {# #}
|
||||
<a href="#{{link.href|safe}}" title="{{link.name}}">
|
||||
{% match link.name_html %}
|
||||
{% when Some with (html) %}
|
||||
{{html|safe}}
|
||||
{% else %}
|
||||
{{link.name}}
|
||||
{% endmatch %}
|
||||
</a> {# #}
|
||||
{% if !link.children.is_empty() %}
|
||||
<ul>
|
||||
{% for child in link.children %}
|
||||
<li><a href="#{{child.href|safe}}" title="{{child.name}}">
|
||||
{% match child.name_html %}
|
||||
{% when Some with (html) %}
|
||||
{{html|safe}}
|
||||
{% else %}
|
||||
{{child.name}}
|
||||
{% endmatch %}
|
||||
</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@ -30,7 +53,11 @@
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<div id="rustdoc-modnav">
|
||||
{% if !path.is_empty() %}
|
||||
<h2><a href="{% if is_mod %}../{% endif %}index.html">In {{+ path|wrapped|safe}}</a></h2>
|
||||
<h2{% if parent_is_crate +%} class="in-crate"{% endif %}> {# #}
|
||||
<a href="{% if is_mod %}../{% endif %}index.html">In {{+ path|wrapped|safe}}</a> {# #}
|
||||
</h2>
|
||||
{% endif %}
|
||||
</div> {# #}
|
||||
</div>
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Table-of-contents creation.
|
||||
use crate::html::escape::Escape;
|
||||
|
||||
/// A (recursive) table of contents
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -16,7 +17,7 @@ pub(crate) struct Toc {
|
||||
/// ### A
|
||||
/// ## B
|
||||
/// ```
|
||||
entries: Vec<TocEntry>,
|
||||
pub(crate) entries: Vec<TocEntry>,
|
||||
}
|
||||
|
||||
impl Toc {
|
||||
@ -27,11 +28,16 @@ fn count_entries_with_level(&self, level: u32) -> usize {
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct TocEntry {
|
||||
level: u32,
|
||||
sec_number: String,
|
||||
name: String,
|
||||
id: String,
|
||||
children: Toc,
|
||||
pub(crate) level: u32,
|
||||
pub(crate) sec_number: String,
|
||||
// name is a plain text header that works in a `title` tag
|
||||
// html includes `<code>` tags
|
||||
// the tooltip is used so that, when a toc is truncated,
|
||||
// you can mouse over it to see the whole thing
|
||||
pub(crate) name: String,
|
||||
pub(crate) html: String,
|
||||
pub(crate) id: String,
|
||||
pub(crate) children: Toc,
|
||||
}
|
||||
|
||||
/// Progressive construction of a table of contents.
|
||||
@ -115,7 +121,7 @@ fn fold_until(&mut self, level: u32) {
|
||||
/// Push a level `level` heading into the appropriate place in the
|
||||
/// hierarchy, returning a string containing the section number in
|
||||
/// `<num>.<num>.<num>` format.
|
||||
pub(crate) fn push(&mut self, level: u32, name: String, id: String) -> &str {
|
||||
pub(crate) fn push(&mut self, level: u32, name: String, html: String, id: String) -> &str {
|
||||
assert!(level >= 1);
|
||||
|
||||
// collapse all previous sections into their parents until we
|
||||
@ -149,6 +155,7 @@ pub(crate) fn push(&mut self, level: u32, name: String, id: String) -> &str {
|
||||
self.chain.push(TocEntry {
|
||||
level,
|
||||
name,
|
||||
html,
|
||||
sec_number,
|
||||
id,
|
||||
children: Toc { entries: Vec::new() },
|
||||
@ -170,10 +177,11 @@ fn print_inner(&self, v: &mut String) {
|
||||
// recursively format this table of contents
|
||||
let _ = write!(
|
||||
v,
|
||||
"\n<li><a href=\"#{id}\">{num} {name}</a>",
|
||||
"\n<li><a href=\"#{id}\" title=\"{name}\">{num} {html}</a>",
|
||||
id = entry.id,
|
||||
num = entry.sec_number,
|
||||
name = entry.name
|
||||
name = Escape(&entry.name),
|
||||
html = &entry.html,
|
||||
);
|
||||
entry.children.print_inner(&mut *v);
|
||||
v.push_str("</li>");
|
||||
|
@ -9,7 +9,10 @@ fn builder_smoke() {
|
||||
// there's been no macro mistake.
|
||||
macro_rules! push {
|
||||
($level: expr, $name: expr) => {
|
||||
assert_eq!(builder.push($level, $name.to_string(), "".to_string()), $name);
|
||||
assert_eq!(
|
||||
builder.push($level, $name.to_string(), $name.to_string(), "".to_string()),
|
||||
$name
|
||||
);
|
||||
};
|
||||
}
|
||||
push!(2, "0.1");
|
||||
@ -48,6 +51,7 @@ macro_rules! toc {
|
||||
TocEntry {
|
||||
level: $level,
|
||||
name: $name.to_string(),
|
||||
html: $name.to_string(),
|
||||
sec_number: $name.to_string(),
|
||||
id: "".to_string(),
|
||||
children: toc!($($sub),*)
|
||||
|
@ -72,6 +72,7 @@ pub(crate) fn render<P: AsRef<Path>>(
|
||||
let text = if !options.markdown_no_toc {
|
||||
MarkdownWithToc {
|
||||
content: text,
|
||||
links: &[],
|
||||
ids: &mut ids,
|
||||
error_codes,
|
||||
edition,
|
||||
|
44
tests/rustdoc-gui/sidebar-modnav-position.goml
Normal file
44
tests/rustdoc-gui/sidebar-modnav-position.goml
Normal file
@ -0,0 +1,44 @@
|
||||
// Verifies that, when TOC is hidden, modnav is always in exactly the same spot
|
||||
// This is driven by a reasonably common use case:
|
||||
//
|
||||
// - There are three or more items that might meet my needs.
|
||||
// - I open the first one, decide it's not what I want, switch to the second one using the sidebar.
|
||||
// - The second one also doesn't meet my needs, so I switch to the third.
|
||||
// - The third also doesn't meet my needs, so...
|
||||
//
|
||||
// because the sibling module nav is in exactly the same place every time,
|
||||
// it's very easy to find and switch between pages that way.
|
||||
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/enum.WhoLetTheDogOut.html"
|
||||
show-text: true
|
||||
set-local-storage: {"rustdoc-hide-toc": "true"}
|
||||
|
||||
define-function: (
|
||||
"check-positions",
|
||||
[url],
|
||||
block {
|
||||
go-to: "file://" + |DOC_PATH| + |url|
|
||||
// Checking results colors.
|
||||
assert-position: ("#rustdoc-modnav > h2", {"x": |h2_x|, "y": |h2_y|})
|
||||
assert-position: (
|
||||
"#rustdoc-modnav > ul:first-of-type > li:first-of-type",
|
||||
{"x": |x|, "y": |y|}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// First, at test_docs root
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/enum.WhoLetTheDogOut.html"
|
||||
store-position: ("#rustdoc-modnav > h2", {"x": h2_x, "y": h2_y})
|
||||
store-position: ("#rustdoc-modnav > ul:first-of-type > li:first-of-type", {"x": x, "y": y})
|
||||
call-function: ("check-positions", {"url": "/test_docs/enum.WhoLetTheDogOut.html"})
|
||||
call-function: ("check-positions", {"url": "/test_docs/struct.StructWithPublicUndocumentedFields.html"})
|
||||
call-function: ("check-positions", {"url": "/test_docs/codeblock_sub/index.html"})
|
||||
|
||||
// Now in a submodule
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/fields/struct.Struct.html"
|
||||
store-position: ("#rustdoc-modnav > h2", {"x": h2_x, "y": h2_y})
|
||||
store-position: ("#rustdoc-modnav > ul:first-of-type > li:first-of-type", {"x": x, "y": y})
|
||||
call-function: ("check-positions", {"url": "/test_docs/fields/struct.Struct.html"})
|
||||
call-function: ("check-positions", {"url": "/test_docs/fields/union.Union.html"})
|
||||
call-function: ("check-positions", {"url": "/test_docs/fields/enum.Enum.html"})
|
@ -118,7 +118,7 @@ assert-false: ".sidebar-elems > .crate"
|
||||
go-to: "./module/index.html"
|
||||
assert-property: (".sidebar", {"clientWidth": "200"})
|
||||
assert-text: (".sidebar > .sidebar-crate > h2 > a", "lib2")
|
||||
assert-text: (".sidebar > .location", "Module module")
|
||||
assert-text: (".sidebar .location", "Module module")
|
||||
assert-count: (".sidebar .location", 1)
|
||||
assert-text: (".sidebar-elems ul.block > li.current > a", "module")
|
||||
// Module page requires three headings:
|
||||
@ -126,8 +126,8 @@ assert-text: (".sidebar-elems ul.block > li.current > a", "module")
|
||||
// - Module name, followed by TOC for module headings
|
||||
// - "In crate [name]" parent pointer, followed by sibling navigation
|
||||
assert-count: (".sidebar h2", 3)
|
||||
assert-text: (".sidebar > .sidebar-elems > h2", "In crate lib2")
|
||||
assert-property: (".sidebar > .sidebar-elems > h2 > a", {
|
||||
assert-text: (".sidebar > .sidebar-elems > #rustdoc-modnav > h2", "In crate lib2")
|
||||
assert-property: (".sidebar > .sidebar-elems > #rustdoc-modnav > h2 > a", {
|
||||
"href": "/lib2/index.html",
|
||||
}, ENDS_WITH)
|
||||
// We check that we don't have the crate list.
|
||||
@ -136,9 +136,9 @@ assert-false: ".sidebar-elems > .crate"
|
||||
go-to: "./sub_module/sub_sub_module/index.html"
|
||||
assert-property: (".sidebar", {"clientWidth": "200"})
|
||||
assert-text: (".sidebar > .sidebar-crate > h2 > a", "lib2")
|
||||
assert-text: (".sidebar > .location", "Module sub_sub_module")
|
||||
assert-text: (".sidebar > .sidebar-elems > h2", "In lib2::module::sub_module")
|
||||
assert-property: (".sidebar > .sidebar-elems > h2 > a", {
|
||||
assert-text: (".sidebar .location", "Module sub_sub_module")
|
||||
assert-text: (".sidebar > .sidebar-elems > #rustdoc-modnav > h2", "In lib2::module::sub_module")
|
||||
assert-property: (".sidebar > .sidebar-elems > #rustdoc-modnav > h2 > a", {
|
||||
"href": "/module/sub_module/index.html",
|
||||
}, ENDS_WITH)
|
||||
assert-text: (".sidebar-elems ul.block > li.current > a", "sub_sub_module")
|
||||
@ -198,3 +198,36 @@ assert-position-false: (".sidebar-crate > h2 > a", {"x": -3})
|
||||
// when line-wrapped, see that it becomes flush-left again
|
||||
drag-and-drop: ((205, 100), (108, 100))
|
||||
assert-position: (".sidebar-crate > h2 > a", {"x": -3})
|
||||
|
||||
// Configuration option to show TOC in sidebar.
|
||||
set-local-storage: {"rustdoc-hide-toc": "true"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/enum.WhoLetTheDogOut.html"
|
||||
assert-css: ("#rustdoc-toc", {"display": "none"})
|
||||
assert-css: (".sidebar .in-crate", {"display": "none"})
|
||||
set-local-storage: {"rustdoc-hide-toc": "false"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/enum.WhoLetTheDogOut.html"
|
||||
assert-css: ("#rustdoc-toc", {"display": "block"})
|
||||
assert-css: (".sidebar .in-crate", {"display": "block"})
|
||||
|
||||
set-local-storage: {"rustdoc-hide-modnav": "true"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/enum.WhoLetTheDogOut.html"
|
||||
assert-css: ("#rustdoc-modnav", {"display": "none"})
|
||||
set-local-storage: {"rustdoc-hide-modnav": "false"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/enum.WhoLetTheDogOut.html"
|
||||
assert-css: ("#rustdoc-modnav", {"display": "block"})
|
||||
|
||||
set-local-storage: {"rustdoc-hide-toc": "true"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
assert-css: ("#rustdoc-toc", {"display": "none"})
|
||||
assert-false: ".sidebar .in-crate"
|
||||
set-local-storage: {"rustdoc-hide-toc": "false"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
assert-css: ("#rustdoc-toc", {"display": "block"})
|
||||
assert-false: ".sidebar .in-crate"
|
||||
|
||||
set-local-storage: {"rustdoc-hide-modnav": "true"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
assert-css: ("#rustdoc-modnav", {"display": "none"})
|
||||
set-local-storage: {"rustdoc-hide-modnav": "false"}
|
||||
go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
|
||||
assert-css: ("#rustdoc-modnav", {"display": "block"})
|
||||
|
16
tests/rustdoc/sidebar/module.rs
Normal file
16
tests/rustdoc/sidebar/module.rs
Normal file
@ -0,0 +1,16 @@
|
||||
#![crate_name = "foo"]
|
||||
|
||||
//@ has 'foo/index.html'
|
||||
//@ has - '//section[@id="rustdoc-toc"]/h3' 'Crate Items'
|
||||
|
||||
//@ has 'foo/bar/index.html'
|
||||
//@ has - '//section[@id="rustdoc-toc"]/h3' 'Module Items'
|
||||
pub mod bar {
|
||||
//@ has 'foo/bar/struct.Baz.html'
|
||||
//@ !has - '//section[@id="rustdoc-toc"]/h3' 'Module Items'
|
||||
pub struct Baz;
|
||||
}
|
||||
|
||||
//@ has 'foo/baz/index.html'
|
||||
//@ !has - '//section[@id="rustdoc-toc"]/h3' 'Module Items'
|
||||
pub mod baz {}
|
23
tests/rustdoc/sidebar/top-toc-html.rs
Normal file
23
tests/rustdoc/sidebar/top-toc-html.rs
Normal file
@ -0,0 +1,23 @@
|
||||
// ignore-tidy-linelength
|
||||
|
||||
#![crate_name = "foo"]
|
||||
#![feature(lazy_type_alias)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
//! # Basic [link](https://example.com), *emphasis*, **_very emphasis_** and `code`
|
||||
//!
|
||||
//! This test case covers TOC entries with rich text inside.
|
||||
//! Rustdoc normally supports headers with links, but for the
|
||||
//! TOC, that would break the layout.
|
||||
//!
|
||||
//! For consistency, emphasis is also filtered out.
|
||||
|
||||
//@ has foo/index.html
|
||||
// User header
|
||||
//@ has - '//section[@id="rustdoc-toc"]/h3' 'Sections'
|
||||
//@ has - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-emphasis-very-emphasis-and-code"]/@title' 'Basic link, emphasis, very emphasis and `code`'
|
||||
//@ has - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-emphasis-very-emphasis-and-code"]' 'Basic link, emphasis, very emphasis and code'
|
||||
//@ count - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-emphasis-very-emphasis-and-code"]/em' 0
|
||||
//@ count - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-emphasis-very-emphasis-and-code"]/a' 0
|
||||
//@ count - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-emphasis-very-emphasis-and-code"]/code' 1
|
||||
//@ has - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-emphasis-very-emphasis-and-code"]/code' 'code'
|
44
tests/rustdoc/sidebar/top-toc-idmap.rs
Normal file
44
tests/rustdoc/sidebar/top-toc-idmap.rs
Normal file
@ -0,0 +1,44 @@
|
||||
#![crate_name = "foo"]
|
||||
#![feature(lazy_type_alias)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
//! # Structs
|
||||
//!
|
||||
//! This header has the same name as a built-in header,
|
||||
//! and we need to make sure they're disambiguated with
|
||||
//! suffixes.
|
||||
//!
|
||||
//! Module-like headers get derived from the internal ID map,
|
||||
//! so the *internal* one gets a suffix here. To make sure it
|
||||
//! works right, the one in the `top-toc` needs to match the one
|
||||
//! in the `top-doc`, and the one that's not in the `top-doc`
|
||||
//! needs to match the one that isn't in the `top-toc`.
|
||||
|
||||
//@ has foo/index.html
|
||||
// User header
|
||||
//@ has - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#structs"]' 'Structs'
|
||||
//@ has - '//details[@class="toggle top-doc"]/div[@class="docblock"]/h2[@id="structs"]' 'Structs'
|
||||
// Built-in header
|
||||
//@ has - '//section[@id="rustdoc-toc"]/ul[@class="block"]/li/a[@href="#structs-1"]' 'Structs'
|
||||
//@ has - '//section[@id="main-content"]/h2[@id="structs-1"]' 'Structs'
|
||||
|
||||
/// # Fields
|
||||
/// ## Fields
|
||||
/// ### Fields
|
||||
///
|
||||
/// The difference between struct-like headers and module-like headers
|
||||
/// is strange, but not actually a problem as long as we're consistent.
|
||||
|
||||
//@ has foo/struct.MyStruct.html
|
||||
// User header
|
||||
//@ has - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]/li/a[@href="#fields-1"]' 'Fields'
|
||||
//@ has - '//details[@class="toggle top-doc"]/div[@class="docblock"]/h2[@id="fields-1"]' 'Fields'
|
||||
// Only one level of nesting
|
||||
//@ count - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]//a' 2
|
||||
// Built-in header
|
||||
//@ has - '//section[@id="rustdoc-toc"]/h3/a[@href="#fields"]' 'Fields'
|
||||
//@ has - '//section[@id="main-content"]/h2[@id="fields"]' 'Fields'
|
||||
|
||||
pub struct MyStruct {
|
||||
pub fields: i32,
|
||||
}
|
7
tests/rustdoc/sidebar/top-toc-nil.rs
Normal file
7
tests/rustdoc/sidebar/top-toc-nil.rs
Normal file
@ -0,0 +1,7 @@
|
||||
#![crate_name = "foo"]
|
||||
|
||||
//! This test case covers missing top TOC entries.
|
||||
|
||||
//@ has foo/index.html
|
||||
// User header
|
||||
//@ !has - '//section[@id="rustdoc-toc"]/ul[@class="block top-toc"]' 'Basic link and emphasis'
|
@ -1 +1 @@
|
||||
<ul class="block variant"><li><a href="#variant.Shown">Shown</a></li></ul>
|
||||
<ul class="block variant"><li><a href="#variant.Shown" title="Shown">Shown</a></li></ul>
|
Loading…
Reference in New Issue
Block a user