Add Top TOC support to rustdoc

This commit adds the headers for the top level documentation to
rustdoc's existing table of contents, along with associated items.

It only show two levels of headers. Going further would require the
sidebar to be wider, and that seems unnecessary (the crates that
have manually-built TOCs usually don't need deeply nested headers).
This commit is contained in:
Michael Howell 2024-02-06 10:22:24 -07:00
parent 5aea14073e
commit 1aebff96ad
15 changed files with 271 additions and 133 deletions

View File

@ -506,9 +506,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
}
@ -536,9 +533,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
}

View File

@ -55,7 +55,7 @@
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;
@ -101,6 +101,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,
@ -532,9 +533,9 @@ fn next(&mut self) -> Option<Self::Item> {
let id = self.id_map.derive(id);
if let Some(ref mut builder) = self.toc {
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());
let mut text_header = String::new();
plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
let sec = builder.push(level as u32, text_header, id.clone());
self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
}
@ -1415,10 +1416,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);
@ -1432,7 +1446,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=\"TOC\">{toc}</nav>{s}", toc = toc.print())
}
}
@ -1611,7 +1629,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) => {
@ -1626,8 +1653,6 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
_ => (),
}
}
s
}
#[derive(Debug)]

View File

@ -616,7 +616,8 @@ 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());
let bar = Sidebar {
title_prefix: "",
title: "",

View File

@ -7,12 +7,13 @@
use rustc_hir::def_id::DefIdSet;
use rustc_middle::ty::{self, TyCtxt};
use crate::{
clean,
formats::{item_type::ItemType, Impl},
html::{format::Buffer, markdown::IdMap, markdown::MarkdownWithToc},
};
use super::{item_ty_to_section, Context, ItemSection};
use crate::clean;
use crate::formats::item_type::ItemType;
use crate::formats::Impl;
use crate::html::format::Buffer;
use crate::html::markdown::IdMap;
#[derive(Template)]
#[template(path = "sidebar.html")]
@ -66,11 +67,13 @@ pub(crate) struct Link<'a> {
name: 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![] }
}
pub fn empty() -> Link<'static> {
Link::new("", "")
@ -94,17 +97,19 @@ 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)),
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),
@ -112,15 +117,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 ",
@ -162,30 +161,75 @@ 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,
custom_code_classes_in_docs: cx.tcx().features().custom_code_classes_in_docs,
}
.into_parts();
let links: Vec<Link<'_>> = toc
.entries
.into_iter()
.map(|entry| {
Link {
name: entry.name.into(),
href: entry.id.into(),
children: entry
.children
.entries
.into_iter()
.map(|entry| Link {
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,
@ -222,19 +266,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(
@ -250,20 +295,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);
}
}
@ -271,8 +313,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 {
@ -294,19 +336,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
@ -345,33 +386,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"),
@ -380,8 +394,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);
}
}
@ -471,7 +507,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)
@ -479,24 +516,24 @@ 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,
) -> LinkBlock<'static> {
let item_sections = 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)
}
fn sidebar_module(items: &[clean::Item]) -> LinkBlock<'static> {
fn sidebar_module(items: &[clean::Item], ids: &mut IdMap) -> LinkBlock<'static> {
let item_sections_in_use: FxHashSet<_> = items
.iter()
.filter(|it| {
@ -517,13 +554,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)
}
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
@ -533,7 +572,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();
@ -558,7 +598,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",
@ -574,7 +614,7 @@ fn sidebar_render_assoc_items(
"blanket-implementation",
blanket,
),
]
]);
}
fn get_next_url(used_links: &mut FxHashSet<String>, url: String) -> String {

View File

@ -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 {
margin-left: 12px;
}
.sidebar-elems a,
.sidebar > h2 a {
display: block;

View File

@ -15,14 +15,23 @@
{% 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{% if !block.class.is_empty() +%} class="{{block.class}}"{% endif %}> {# #}
<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}}">{{link.name}}</a> {# #}
{% if !link.children.is_empty() %}
<ul> {# #}
{% for child in link.children %}
<li><a href="#{{child.href|safe}}">{{child.name}}</a></li>
{% endfor %}
</ul> {# #}
{% endif %}
</li> {# #}
{% endfor %}
</ul>
{% endif %}

View File

@ -1,4 +1,5 @@
//! Table-of-contents creation.
use crate::html::escape::EscapeBodyText;
/// 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,11 @@ 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,
pub(crate) name: String,
pub(crate) id: String,
pub(crate) children: Toc,
}
/// Progressive construction of a table of contents.
@ -173,7 +174,7 @@ fn print_inner(&self, v: &mut String) {
"\n<li><a href=\"#{id}\">{num} {name}</a>",
id = entry.id,
num = entry.sec_number,
name = entry.name
name = EscapeBodyText(&entry.name)
);
entry.children.print_inner(&mut *v);
v.push_str("</li>");

View File

@ -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,

View File

@ -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:
@ -136,7 +136,7 @@ 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 .location", "Module sub_sub_module")
assert-text: (".sidebar > .sidebar-elems > h2", "In lib2::module::sub_module")
assert-property: (".sidebar > .sidebar-elems > h2 > a", {
"href": "/module/sub_module/index.html",

View File

@ -0,0 +1,19 @@
// ignore-tidy-linelength
#![crate_name = "foo"]
#![feature(lazy_type_alias)]
#![allow(incomplete_features)]
//! # Basic [link](https://example.com) and *emphasis*
//!
//! 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="TOC"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-and-emphasis"]' 'Basic link and emphasis'
// @count - '//section[@id="TOC"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-and-emphasis"]/em' 0
// @count - '//section[@id="TOC"]/ul[@class="block top-toc"]/li/a[@href="#basic-link-and-emphasis"]/a' 0

View 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="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="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="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="TOC"]/ul[@class="block top-toc"]//a' 2
// Built-in header
// @has - '//section[@id="TOC"]/h3/a[@href="#fields"]' 'Fields'
// @has - '//section[@id="main-content"]/h2[@id="fields"]' 'Fields'
pub struct MyStruct {
pub fields: i32,
}