about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorManish Goregaokar <manishsmail@gmail.com>2021-10-06 12:33:20 -0700
committerGitHub <noreply@github.com>2021-10-06 12:33:20 -0700
commit7d6feb421ed159ee8f110741d4fb8a43b4ba7ff5 (patch)
tree7beae401b7e3df6a02d416d6891b059d2ebf1ae6 /src
parentb01594051cdb7bcd2ccc9f3957fdd243c7d66ef8 (diff)
parent1f86a8e2a058d7f12b25406a53576b08817677fe (diff)
downloadrust-7d6feb421ed159ee8f110741d4fb8a43b4ba7ff5.tar.gz
rust-7d6feb421ed159ee8f110741d4fb8a43b4ba7ff5.zip
Rollup merge of #89506 - yaymukund:docblock-headings, r=GuillaumeGomez
librustdoc: Use correct heading levels.

Closes #89309

This fixes the `<h#>` header tags throughout the docs to reflect a semantic hierarchy.

- I ran a script to manually check that we don't have any files with multiple `<h1>` tags.
- Also checked that we never incorrectly nest e.g. a `<h2>` under an `<h3>`.
- I also spot-checked a bunch of pages (`trait.Read`, `enum.Ordering`, `primitive.isize`, `trait.Iterator`).
Diffstat (limited to 'src')
-rw-r--r--src/librustdoc/externalfiles.rs24
-rw-r--r--src/librustdoc/html/markdown.rs72
-rw-r--r--src/librustdoc/html/markdown/tests.rs52
-rw-r--r--src/librustdoc/html/render/mod.rs90
-rw-r--r--src/librustdoc/html/render/print_item.rs44
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css19
-rw-r--r--src/librustdoc/html/static/css/themes/ayu.css2
-rw-r--r--src/librustdoc/html/static/css/themes/dark.css2
-rw-r--r--src/librustdoc/html/static/css/themes/light.css2
-rw-r--r--src/librustdoc/markdown.rs15
-rw-r--r--src/test/rustdoc/external-cross.rs2
-rw-r--r--src/test/rustdoc/external-doc.rs6
-rw-r--r--src/test/rustdoc/issue-42760.rs2
-rw-r--r--src/test/rustdoc/issue-89309-heading-levels.rs29
-rw-r--r--src/test/rustdoc/short-docblock.rs4
-rw-r--r--src/test/rustdoc/smart-punct.rs2
-rw-r--r--src/tools/error_index_generator/main.rs19
17 files changed, 264 insertions, 122 deletions
diff --git a/src/librustdoc/externalfiles.rs b/src/librustdoc/externalfiles.rs
index 56d2ca57218..302fc5a6777 100644
--- a/src/librustdoc/externalfiles.rs
+++ b/src/librustdoc/externalfiles.rs
@@ -1,4 +1,4 @@
-use crate::html::markdown::{ErrorCodes, IdMap, Markdown, Playground};
+use crate::html::markdown::{ErrorCodes, HeadingOffset, IdMap, Markdown, Playground};
 use crate::rustc_span::edition::Edition;
 use std::fs;
 use std::path::Path;
@@ -39,14 +39,32 @@ impl ExternalHtml {
         let bc = format!(
             "{}{}",
             bc,
-            Markdown(&m_bc, &[], id_map, codes, edition, playground).into_string()
+            Markdown {
+                content: &m_bc,
+                links: &[],
+                ids: id_map,
+                error_codes: codes,
+                edition,
+                playground,
+                heading_offset: HeadingOffset::H2,
+            }
+            .into_string()
         );
         let ac = load_external_files(after_content, diag)?;
         let m_ac = load_external_files(md_after_content, diag)?;
         let ac = format!(
             "{}{}",
             ac,
-            Markdown(&m_ac, &[], id_map, codes, edition, playground).into_string()
+            Markdown {
+                content: &m_ac,
+                links: &[],
+                ids: id_map,
+                error_codes: codes,
+                edition,
+                playground,
+                heading_offset: HeadingOffset::H2,
+            }
+            .into_string()
         );
         Some(ExternalHtml { in_header: ih, before_content: bc, after_content: ac })
     }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index fda2512a050..9f2e282fce1 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -8,11 +8,19 @@
 //! extern crate rustc_span;
 //!
 //! use rustc_span::edition::Edition;
-//! use rustdoc::html::markdown::{IdMap, Markdown, ErrorCodes};
+//! use rustdoc::html::markdown::{HeadingOffset, IdMap, Markdown, ErrorCodes};
 //!
 //! let s = "My *markdown* _text_";
 //! let mut id_map = IdMap::new();
-//! let md = Markdown(s, &[], &mut id_map, ErrorCodes::Yes, Edition::Edition2015, &None);
+//! let md = Markdown {
+//!     content: s,
+//!     links: &[],
+//!     ids: &mut id_map,
+//!     error_codes: ErrorCodes::Yes,
+//!     edition: Edition::Edition2015,
+//!     playground: &None,
+//!     heading_offset: HeadingOffset::H2,
+//! };
 //! let html = md.into_string();
 //! // ... something using html
 //! ```
@@ -47,6 +55,8 @@ use pulldown_cmark::{
 #[cfg(test)]
 mod tests;
 
+const MAX_HEADER_LEVEL: u32 = 6;
+
 /// Options for rendering Markdown in the main body of documentation.
 pub(crate) fn main_body_opts() -> Options {
     Options::ENABLE_TABLES
@@ -65,20 +75,33 @@ pub(crate) fn summary_opts() -> Options {
         | Options::ENABLE_SMART_PUNCTUATION
 }
 
+#[derive(Debug, Clone, Copy)]
+pub enum HeadingOffset {
+    H1 = 0,
+    H2,
+    H3,
+    H4,
+    H5,
+    H6,
+}
+
 /// When `to_string` is called, this struct will emit the HTML corresponding to
 /// the rendered version of the contained markdown string.
-pub struct Markdown<'a>(
-    pub &'a str,
+pub struct Markdown<'a> {
+    pub content: &'a str,
     /// A list of link replacements.
-    pub &'a [RenderedLink],
+    pub links: &'a [RenderedLink],
     /// The current list of used header IDs.
-    pub &'a mut IdMap,
+    pub ids: &'a mut IdMap,
     /// Whether to allow the use of explicit error codes in doctest lang strings.
-    pub ErrorCodes,
+    pub error_codes: ErrorCodes,
     /// Default edition to use when parsing doctests (to add a `fn main`).
-    pub Edition,
-    pub &'a Option<Playground>,
-);
+    pub edition: Edition,
+    pub playground: &'a Option<Playground>,
+    /// Offset at which we render headings.
+    /// E.g. if `heading_offset: HeadingOffset::H2`, then `# something` renders an `<h2>`.
+    pub heading_offset: HeadingOffset,
+}
 /// A tuple struct like `Markdown` that renders the markdown with a table of contents.
 crate struct MarkdownWithToc<'a>(
     crate &'a str,
@@ -489,11 +512,17 @@ struct HeadingLinks<'a, 'b, 'ids, I> {
     toc: Option<&'b mut TocBuilder>,
     buf: VecDeque<SpannedEvent<'a>>,
     id_map: &'ids mut IdMap,
+    heading_offset: HeadingOffset,
 }
 
 impl<'a, 'b, 'ids, I> HeadingLinks<'a, 'b, 'ids, I> {
-    fn new(iter: I, toc: Option<&'b mut TocBuilder>, ids: &'ids mut IdMap) -> Self {
-        HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids }
+    fn new(
+        iter: I,
+        toc: Option<&'b mut TocBuilder>,
+        ids: &'ids mut IdMap,
+        heading_offset: HeadingOffset,
+    ) -> Self {
+        HeadingLinks { inner: iter, toc, buf: VecDeque::new(), id_map: ids, heading_offset }
     }
 }
 
@@ -530,6 +559,7 @@ impl<'a, 'b, 'ids, I: Iterator<Item = SpannedEvent<'a>>> Iterator
                 self.buf.push_front((Event::Html(format!("{} ", sec).into()), 0..0));
             }
 
+            let level = std::cmp::min(level + (self.heading_offset as u32), MAX_HEADER_LEVEL);
             self.buf.push_back((Event::Html(format!("</a></h{}>", level).into()), 0..0));
 
             let start_tags = format!(
@@ -1005,7 +1035,15 @@ impl LangString {
 
 impl Markdown<'_> {
     pub fn into_string(self) -> String {
-        let Markdown(md, links, mut ids, codes, edition, playground) = self;
+        let Markdown {
+            content: md,
+            links,
+            mut ids,
+            error_codes: codes,
+            edition,
+            playground,
+            heading_offset,
+        } = self;
 
         // This is actually common enough to special-case
         if md.is_empty() {
@@ -1026,7 +1064,7 @@ impl Markdown<'_> {
 
         let mut s = String::with_capacity(md.len() * 3 / 2);
 
-        let p = HeadingLinks::new(p, None, &mut ids);
+        let p = HeadingLinks::new(p, None, &mut ids, heading_offset);
         let p = Footnotes::new(p);
         let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
         let p = TableWrapper::new(p);
@@ -1048,7 +1086,7 @@ impl MarkdownWithToc<'_> {
         let mut toc = TocBuilder::new();
 
         {
-            let p = HeadingLinks::new(p, Some(&mut toc), &mut ids);
+            let p = HeadingLinks::new(p, Some(&mut toc), &mut ids, HeadingOffset::H1);
             let p = Footnotes::new(p);
             let p = TableWrapper::new(p.map(|(ev, _)| ev));
             let p = CodeBlocks::new(p, codes, edition, playground);
@@ -1077,7 +1115,7 @@ impl MarkdownHtml<'_> {
 
         let mut s = String::with_capacity(md.len() * 3 / 2);
 
-        let p = HeadingLinks::new(p, None, &mut ids);
+        let p = HeadingLinks::new(p, None, &mut ids, HeadingOffset::H1);
         let p = Footnotes::new(p);
         let p = TableWrapper::new(p.map(|(ev, _)| ev));
         let p = CodeBlocks::new(p, codes, edition, playground);
@@ -1295,7 +1333,7 @@ crate fn markdown_links(md: &str) -> Vec<MarkdownLink> {
     // There's no need to thread an IdMap through to here because
     // the IDs generated aren't going to be emitted anywhere.
     let mut ids = IdMap::new();
-    let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids));
+    let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids, HeadingOffset::H1));
 
     for ev in iter {
         if let Event::Start(Tag::Link(kind, dest, _)) = ev.0 {
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 1e4cf3381f6..68ab002f138 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -1,5 +1,5 @@
 use super::{find_testable_code, plain_text_summary, short_markdown_summary};
-use super::{ErrorCodes, IdMap, Ignore, LangString, Markdown, MarkdownHtml};
+use super::{ErrorCodes, HeadingOffset, IdMap, Ignore, LangString, Markdown, MarkdownHtml};
 use rustc_span::edition::{Edition, DEFAULT_EDITION};
 
 #[test]
@@ -147,33 +147,41 @@ fn test_lang_string_tokenizer() {
 fn test_header() {
     fn t(input: &str, expect: &str) {
         let mut map = IdMap::new();
-        let output =
-            Markdown(input, &[], &mut map, ErrorCodes::Yes, DEFAULT_EDITION, &None).into_string();
+        let output = Markdown {
+            content: input,
+            links: &[],
+            ids: &mut map,
+            error_codes: ErrorCodes::Yes,
+            edition: DEFAULT_EDITION,
+            playground: &None,
+            heading_offset: HeadingOffset::H2,
+        }
+        .into_string();
         assert_eq!(output, expect, "original: {}", input);
     }
 
     t(
         "# Foo bar",
-        "<h1 id=\"foo-bar\" class=\"section-header\"><a href=\"#foo-bar\">Foo bar</a></h1>",
+        "<h2 id=\"foo-bar\" class=\"section-header\"><a href=\"#foo-bar\">Foo bar</a></h2>",
     );
     t(
         "## Foo-bar_baz qux",
-        "<h2 id=\"foo-bar_baz-qux\" class=\"section-header\">\
-         <a href=\"#foo-bar_baz-qux\">Foo-bar_baz qux</a></h2>",
+        "<h3 id=\"foo-bar_baz-qux\" class=\"section-header\">\
+         <a href=\"#foo-bar_baz-qux\">Foo-bar_baz qux</a></h3>",
     );
     t(
         "### **Foo** *bar* baz!?!& -_qux_-%",
-        "<h3 id=\"foo-bar-baz--qux-\" class=\"section-header\">\
+        "<h4 id=\"foo-bar-baz--qux-\" class=\"section-header\">\
             <a href=\"#foo-bar-baz--qux-\"><strong>Foo</strong> \
             <em>bar</em> baz!?!&amp; -<em>qux</em>-%</a>\
-         </h3>",
+         </h4>",
     );
     t(
         "#### **Foo?** & \\*bar?!*  _`baz`_ ❤ #qux",
-        "<h4 id=\"foo--bar--baz--qux\" class=\"section-header\">\
+        "<h5 id=\"foo--bar--baz--qux\" class=\"section-header\">\
              <a href=\"#foo--bar--baz--qux\"><strong>Foo?</strong> &amp; *bar?!*  \
              <em><code>baz</code></em> ❤ #qux</a>\
-         </h4>",
+         </h5>",
     );
 }
 
@@ -181,40 +189,48 @@ fn test_header() {
 fn test_header_ids_multiple_blocks() {
     let mut map = IdMap::new();
     fn t(map: &mut IdMap, input: &str, expect: &str) {
-        let output =
-            Markdown(input, &[], map, ErrorCodes::Yes, DEFAULT_EDITION, &None).into_string();
+        let output = Markdown {
+            content: input,
+            links: &[],
+            ids: map,
+            error_codes: ErrorCodes::Yes,
+            edition: DEFAULT_EDITION,
+            playground: &None,
+            heading_offset: HeadingOffset::H2,
+        }
+        .into_string();
         assert_eq!(output, expect, "original: {}", input);
     }
 
     t(
         &mut map,
         "# Example",
-        "<h1 id=\"example\" class=\"section-header\"><a href=\"#example\">Example</a></h1>",
+        "<h2 id=\"example\" class=\"section-header\"><a href=\"#example\">Example</a></h2>",
     );
     t(
         &mut map,
         "# Panics",
-        "<h1 id=\"panics\" class=\"section-header\"><a href=\"#panics\">Panics</a></h1>",
+        "<h2 id=\"panics\" class=\"section-header\"><a href=\"#panics\">Panics</a></h2>",
     );
     t(
         &mut map,
         "# Example",
-        "<h1 id=\"example-1\" class=\"section-header\"><a href=\"#example-1\">Example</a></h1>",
+        "<h2 id=\"example-1\" class=\"section-header\"><a href=\"#example-1\">Example</a></h2>",
     );
     t(
         &mut map,
         "# Main",
-        "<h1 id=\"main-1\" class=\"section-header\"><a href=\"#main-1\">Main</a></h1>",
+        "<h2 id=\"main-1\" class=\"section-header\"><a href=\"#main-1\">Main</a></h2>",
     );
     t(
         &mut map,
         "# Example",
-        "<h1 id=\"example-2\" class=\"section-header\"><a href=\"#example-2\">Example</a></h1>",
+        "<h2 id=\"example-2\" class=\"section-header\"><a href=\"#example-2\">Example</a></h2>",
     );
     t(
         &mut map,
         "# Panics",
-        "<h1 id=\"panics-1\" class=\"section-header\"><a href=\"#panics-1\">Panics</a></h1>",
+        "<h2 id=\"panics-1\" class=\"section-header\"><a href=\"#panics-1\">Panics</a></h2>",
     );
 }
 
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 5045a99800a..11682afdf89 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -67,7 +67,7 @@ use crate::html::format::{
     href, print_abi_with_space, print_constness_with_space, print_default_space,
     print_generic_bounds, print_where_clause, Buffer, HrefError, PrintWithSpace,
 };
-use crate::html::markdown::{Markdown, MarkdownHtml, MarkdownSummaryLine};
+use crate::html::markdown::{HeadingOffset, Markdown, MarkdownHtml, MarkdownSummaryLine};
 
 /// A pair of name and its optional document.
 crate type NameDoc = (String, Option<String>);
@@ -470,32 +470,45 @@ fn settings(root_path: &str, suffix: &str, themes: &[StylePath]) -> Result<Strin
     ))
 }
 
-fn document(w: &mut Buffer, cx: &Context<'_>, item: &clean::Item, parent: Option<&clean::Item>) {
+fn document(
+    w: &mut Buffer,
+    cx: &Context<'_>,
+    item: &clean::Item,
+    parent: Option<&clean::Item>,
+    heading_offset: HeadingOffset,
+) {
     if let Some(ref name) = item.name {
         info!("Documenting {}", name);
     }
     document_item_info(w, cx, item, parent);
     if parent.is_none() {
-        document_full_collapsible(w, item, cx);
+        document_full_collapsible(w, item, cx, heading_offset);
     } else {
-        document_full(w, item, cx);
+        document_full(w, item, cx, heading_offset);
     }
 }
 
 /// Render md_text as markdown.
-fn render_markdown(w: &mut Buffer, cx: &Context<'_>, md_text: &str, links: Vec<RenderedLink>) {
+fn render_markdown(
+    w: &mut Buffer,
+    cx: &Context<'_>,
+    md_text: &str,
+    links: Vec<RenderedLink>,
+    heading_offset: HeadingOffset,
+) {
     let mut ids = cx.id_map.borrow_mut();
     write!(
         w,
         "<div class=\"docblock\">{}</div>",
-        Markdown(
-            md_text,
-            &links,
-            &mut ids,
-            cx.shared.codes,
-            cx.shared.edition(),
-            &cx.shared.playground
-        )
+        Markdown {
+            content: md_text,
+            links: &links,
+            ids: &mut ids,
+            error_codes: cx.shared.codes,
+            edition: cx.shared.edition(),
+            playground: &cx.shared.playground,
+            heading_offset,
+        }
         .into_string()
     )
 }
@@ -531,15 +544,31 @@ fn document_short(
     }
 }
 
-fn document_full_collapsible(w: &mut Buffer, item: &clean::Item, cx: &Context<'_>) {
-    document_full_inner(w, item, cx, true);
+fn document_full_collapsible(
+    w: &mut Buffer,
+    item: &clean::Item,
+    cx: &Context<'_>,
+    heading_offset: HeadingOffset,
+) {
+    document_full_inner(w, item, cx, true, heading_offset);
 }
 
-fn document_full(w: &mut Buffer, item: &clean::Item, cx: &Context<'_>) {
-    document_full_inner(w, item, cx, false);
+fn document_full(
+    w: &mut Buffer,
+    item: &clean::Item,
+    cx: &Context<'_>,
+    heading_offset: HeadingOffset,
+) {
+    document_full_inner(w, item, cx, false, heading_offset);
 }
 
-fn document_full_inner(w: &mut Buffer, item: &clean::Item, cx: &Context<'_>, is_collapsible: bool) {
+fn document_full_inner(
+    w: &mut Buffer,
+    item: &clean::Item,
+    cx: &Context<'_>,
+    is_collapsible: bool,
+    heading_offset: HeadingOffset,
+) {
     if let Some(s) = cx.shared.maybe_collapsed_doc_value(item) {
         debug!("Doc block: =====\n{}\n=====", s);
         if is_collapsible {
@@ -549,10 +578,10 @@ fn document_full_inner(w: &mut Buffer, item: &clean::Item, cx: &Context<'_>, is_
                      <span>Expand description</span>\
                 </summary>",
             );
-            render_markdown(w, cx, &s, item.links(cx));
+            render_markdown(w, cx, &s, item.links(cx), heading_offset);
             w.write_str("</details>");
         } else {
-            render_markdown(w, cx, &s, item.links(cx));
+            render_markdown(w, cx, &s, item.links(cx), heading_offset);
         }
     }
 }
@@ -1321,7 +1350,7 @@ fn render_impl(
                         // because impls can't have a stability.
                         if item.doc_value().is_some() {
                             document_item_info(&mut info_buffer, cx, it, Some(parent));
-                            document_full(&mut doc_buffer, item, cx);
+                            document_full(&mut doc_buffer, item, cx, HeadingOffset::H5);
                             short_documented = false;
                         } else {
                             // In case the item isn't documented,
@@ -1339,7 +1368,7 @@ fn render_impl(
                 } else {
                     document_item_info(&mut info_buffer, cx, item, Some(parent));
                     if rendering_params.show_def_docs {
-                        document_full(&mut doc_buffer, item, cx);
+                        document_full(&mut doc_buffer, item, cx, HeadingOffset::H5);
                         short_documented = false;
                     }
                 }
@@ -1573,14 +1602,15 @@ fn render_impl(
             write!(
                 w,
                 "<div class=\"docblock\">{}</div>",
-                Markdown(
-                    &*dox,
-                    &i.impl_item.links(cx),
-                    &mut ids,
-                    cx.shared.codes,
-                    cx.shared.edition(),
-                    &cx.shared.playground
-                )
+                Markdown {
+                    content: &*dox,
+                    links: &i.impl_item.links(cx),
+                    ids: &mut ids,
+                    error_codes: cx.shared.codes,
+                    edition: cx.shared.edition(),
+                    playground: &cx.shared.playground,
+                    heading_offset: HeadingOffset::H2
+                }
                 .into_string()
             );
         }
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index 28b2eded7cc..1275fa4e156 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -30,7 +30,7 @@ use crate::html::format::{
 };
 use crate::html::highlight;
 use crate::html::layout::Page;
-use crate::html::markdown::MarkdownSummaryLine;
+use crate::html::markdown::{HeadingOffset, MarkdownSummaryLine};
 
 const ITEM_TABLE_OPEN: &'static str = "<div class=\"item-table\">";
 const ITEM_TABLE_CLOSE: &'static str = "</div>";
@@ -175,7 +175,7 @@ fn toggle_close(w: &mut Buffer) {
 }
 
 fn item_module(w: &mut Buffer, cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) {
-    document(w, cx, item, None);
+    document(w, cx, item, None, HeadingOffset::H2);
 
     let mut indices = (0..items.len()).filter(|i| !items[*i].is_stripped()).collect::<Vec<usize>>();
 
@@ -482,7 +482,7 @@ fn item_function(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, f: &clean::
             notable_traits = notable_traits_decl(&f.decl, cx),
         );
     });
-    document(w, cx, it, None)
+    document(w, cx, it, None, HeadingOffset::H2)
 }
 
 fn item_trait(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Trait) {
@@ -605,7 +605,7 @@ fn item_trait(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Tra
     });
 
     // Trait documentation
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     fn write_small_section_header(w: &mut Buffer, id: &str, title: &str, extra_content: &str) {
         write!(
@@ -623,7 +623,7 @@ fn item_trait(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Tra
         let item_type = m.type_();
         let id = cx.derive_id(format!("{}.{}", item_type, name));
         let mut content = Buffer::empty_from(w);
-        document(&mut content, cx, m, Some(t));
+        document(&mut content, cx, m, Some(t), HeadingOffset::H5);
         let toggled = !content.is_empty();
         if toggled {
             write!(w, "<details class=\"rustdoc-toggle\" open><summary>");
@@ -837,7 +837,7 @@ fn item_trait_alias(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clea
         );
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     // Render any items associated directly to this alias, as otherwise they
     // won't be visible anywhere in the docs. It would be nice to also show
@@ -859,7 +859,7 @@ fn item_opaque_ty(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean:
         );
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     // Render any items associated directly to this alias, as otherwise they
     // won't be visible anywhere in the docs. It would be nice to also show
@@ -890,7 +890,7 @@ fn item_typedef(
         );
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     let def_id = it.def_id.expect_def_id();
     // Render any items associated directly to this alias, as otherwise they
@@ -908,7 +908,7 @@ fn item_union(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Uni
         });
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     let mut fields = s
         .fields
@@ -941,7 +941,7 @@ fn item_union(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Uni
             if let Some(stability_class) = field.stability_class(cx.tcx()) {
                 write!(w, "<span class=\"stab {stab}\"></span>", stab = stability_class);
             }
-            document(w, cx, field, Some(it));
+            document(w, cx, field, Some(it), HeadingOffset::H2);
         }
     }
     let def_id = it.def_id.expect_def_id();
@@ -1023,7 +1023,7 @@ fn item_enum(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, e: &clean::Enum
         });
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     if !e.variants.is_empty() {
         write!(
@@ -1052,7 +1052,7 @@ fn item_enum(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, e: &clean::Enum
             w.write_str("</code>");
             render_stability_since(w, variant, it, cx.tcx());
             w.write_str("</div>");
-            document(w, cx, variant, Some(it));
+            document(w, cx, variant, Some(it), HeadingOffset::H2);
             document_non_exhaustive(w, variant);
 
             use crate::clean::Variant;
@@ -1092,7 +1092,7 @@ fn item_enum(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, e: &clean::Enum
                                 f = field.name.as_ref().unwrap(),
                                 t = ty.print(cx)
                             );
-                            document(w, cx, field, Some(variant));
+                            document(w, cx, field, Some(variant), HeadingOffset::H2);
                         }
                         _ => unreachable!(),
                     }
@@ -1119,7 +1119,7 @@ fn item_macro(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Mac
             None,
         );
     });
-    document(w, cx, it, None)
+    document(w, cx, it, None, HeadingOffset::H2)
 }
 
 fn item_proc_macro(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, m: &clean::ProcMacro) {
@@ -1149,11 +1149,11 @@ fn item_proc_macro(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, m: &clean
             });
         }
     }
-    document(w, cx, it, None)
+    document(w, cx, it, None, HeadingOffset::H2)
 }
 
 fn item_primitive(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item) {
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
     render_assoc_items(w, cx, it, it.def_id.expect_def_id(), AssocItemRender::All)
 }
 
@@ -1192,7 +1192,7 @@ fn item_constant(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, c: &clean::
         }
     });
 
-    document(w, cx, it, None)
+    document(w, cx, it, None, HeadingOffset::H2)
 }
 
 fn item_struct(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::Struct) {
@@ -1203,7 +1203,7 @@ fn item_struct(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::St
         });
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     let mut fields = s
         .fields
@@ -1239,7 +1239,7 @@ fn item_struct(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::St
                     name = field_name,
                     ty = ty.print(cx)
                 );
-                document(w, cx, field, Some(it));
+                document(w, cx, field, Some(it), HeadingOffset::H2);
             }
         }
     }
@@ -1260,7 +1260,7 @@ fn item_static(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, s: &clean::St
             typ = s.type_.print(cx)
         );
     });
-    document(w, cx, it, None)
+    document(w, cx, it, None, HeadingOffset::H2)
 }
 
 fn item_foreign_type(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item) {
@@ -1275,13 +1275,13 @@ fn item_foreign_type(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item) {
         );
     });
 
-    document(w, cx, it, None);
+    document(w, cx, it, None, HeadingOffset::H2);
 
     render_assoc_items(w, cx, it, it.def_id.expect_def_id(), AssocItemRender::All)
 }
 
 fn item_keyword(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item) {
-    document(w, cx, it, None)
+    document(w, cx, it, None, HeadingOffset::H2)
 }
 
 /// Compare two strings treating multi-digit numbers as single units (i.e. natural sort order).
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 341d9b80fd8..5d33681847a 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -126,7 +126,7 @@ h2 {
 h3 {
 	font-size: 1.3em;
 }
-h1, h2, h3, h4 {
+h1, h2, h3, h4, h5, h6 {
 	font-weight: 500;
 	margin: 20px 0 15px 0;
 	padding-bottom: 6px;
@@ -179,7 +179,7 @@ div.impl-items > div {
 	padding-left: 0;
 }
 
-h1, h2, h3, h4,
+h1, h2, h3, h4, h5, h6,
 .sidebar, a.source, .search-input, .search-results .result-name,
 .content table td:first-child > a,
 .item-left > a,
@@ -501,21 +501,20 @@ nav.sub {
 	white-space: pre-wrap;
 }
 
-.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5 {
+.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5, .docblock h6 {
 	border-bottom: 1px solid;
 }
 
-.top-doc .docblock h1 { font-size: 1.3em; }
-.top-doc .docblock h2 { font-size: 1.15em; }
-.top-doc .docblock h3,
+.top-doc .docblock h2 { font-size: 1.3em; }
+.top-doc .docblock h3 { font-size: 1.15em; }
 .top-doc .docblock h4,
-.top-doc .docblock h5 {
+.top-doc .docblock h5,
+.top-doc .docblock h6 {
 	font-size: 1em;
 }
 
-.docblock h1 { font-size: 1em; }
-.docblock h2 { font-size: 0.95em; }
-.docblock h3, .docblock h4, .docblock h5 { font-size: 0.9em; }
+.docblock h5 { font-size: 1em; }
+.docblock h6 { font-size: 0.95em; }
 
 .docblock {
 	margin-left: 24px;
diff --git a/src/librustdoc/html/static/css/themes/ayu.css b/src/librustdoc/html/static/css/themes/ayu.css
index c79801e8308..0fd6462a8f5 100644
--- a/src/librustdoc/html/static/css/themes/ayu.css
+++ b/src/librustdoc/html/static/css/themes/ayu.css
@@ -136,7 +136,7 @@ pre, .rustdoc.source .example-wrap {
 	border-right: 1px solid #ffb44c;
 }
 
-.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5 {
+.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5, .docblock h6 {
 	border-bottom-color: #5c6773;
 }
 
diff --git a/src/librustdoc/html/static/css/themes/dark.css b/src/librustdoc/html/static/css/themes/dark.css
index d2e54070acd..d863701dd73 100644
--- a/src/librustdoc/html/static/css/themes/dark.css
+++ b/src/librustdoc/html/static/css/themes/dark.css
@@ -93,7 +93,7 @@ pre, .rustdoc.source .example-wrap {
 	background-color: #0a042f !important;
 }
 
-.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5 {
+.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5, .docblock h6 {
 	border-bottom-color: #DDD;
 }
 
diff --git a/src/librustdoc/html/static/css/themes/light.css b/src/librustdoc/html/static/css/themes/light.css
index 25d810560c1..28d2e99a3d0 100644
--- a/src/librustdoc/html/static/css/themes/light.css
+++ b/src/librustdoc/html/static/css/themes/light.css
@@ -93,7 +93,7 @@ pre, .rustdoc.source .example-wrap {
 	background-color: #f6fdb0 !important;
 }
 
-.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5 {
+.docblock h1, .docblock h2, .docblock h3, .docblock h4, .docblock h5, .docblock h6 {
 	border-bottom-color: #ddd;
 }
 
diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs
index 2ae4897dc34..47b24d40edc 100644
--- a/src/librustdoc/markdown.rs
+++ b/src/librustdoc/markdown.rs
@@ -10,7 +10,9 @@ use crate::config::{Options, RenderOptions};
 use crate::doctest::{Collector, TestOptions};
 use crate::html::escape::Escape;
 use crate::html::markdown;
-use crate::html::markdown::{find_testable_code, ErrorCodes, IdMap, Markdown, MarkdownWithToc};
+use crate::html::markdown::{
+    find_testable_code, ErrorCodes, HeadingOffset, IdMap, Markdown, MarkdownWithToc,
+};
 
 /// Separate any lines at the start of the file that begin with `# ` or `%`.
 fn extract_leading_metadata(s: &str) -> (Vec<&str>, &str) {
@@ -70,7 +72,16 @@ crate fn render<P: AsRef<Path>>(
     let text = if !options.markdown_no_toc {
         MarkdownWithToc(text, &mut ids, error_codes, edition, &playground).into_string()
     } else {
-        Markdown(text, &[], &mut ids, error_codes, edition, &playground).into_string()
+        Markdown {
+            content: text,
+            links: &[],
+            ids: &mut ids,
+            error_codes,
+            edition,
+            playground: &playground,
+            heading_offset: HeadingOffset::H1,
+        }
+        .into_string()
     };
 
     let err = write!(
diff --git a/src/test/rustdoc/external-cross.rs b/src/test/rustdoc/external-cross.rs
index 056ed353462..3f8e1688291 100644
--- a/src/test/rustdoc/external-cross.rs
+++ b/src/test/rustdoc/external-cross.rs
@@ -6,5 +6,5 @@
 extern crate external_cross;
 
 // @has host/struct.NeedMoreDocs.html
-// @has - '//h1' 'Cross-crate imported docs'
+// @has - '//h2' 'Cross-crate imported docs'
 pub use external_cross::NeedMoreDocs;
diff --git a/src/test/rustdoc/external-doc.rs b/src/test/rustdoc/external-doc.rs
index fc29cb252e2..bd322d67a37 100644
--- a/src/test/rustdoc/external-doc.rs
+++ b/src/test/rustdoc/external-doc.rs
@@ -1,6 +1,6 @@
 // @has external_doc/struct.IncludeStrDocs.html
-// @has - '//h1' 'External Docs'
-// @has - '//h2' 'Inline Docs'
+// @has - '//h2' 'External Docs'
+// @has - '//h3' 'Inline Docs'
 #[doc = include_str!("auxiliary/external-doc.md")]
 /// ## Inline Docs
 pub struct IncludeStrDocs;
@@ -8,7 +8,7 @@ pub struct IncludeStrDocs;
 macro_rules! dir { () => { "auxiliary" } }
 
 // @has external_doc/struct.EagerExpansion.html
-// @has - '//h1' 'External Docs'
+// @has - '//h2' 'External Docs'
 #[doc = include_str!(concat!(dir!(), "/external-doc.md"))]
 /// ## Inline Docs
 pub struct EagerExpansion;
diff --git a/src/test/rustdoc/issue-42760.rs b/src/test/rustdoc/issue-42760.rs
index b07dc3f6e96..4944f815701 100644
--- a/src/test/rustdoc/issue-42760.rs
+++ b/src/test/rustdoc/issue-42760.rs
@@ -1,5 +1,5 @@
 // @has issue_42760/struct.NonGen.html
-// @has - '//h1' 'Example'
+// @has - '//h2' 'Example'
 
 /// Item docs.
 ///
diff --git a/src/test/rustdoc/issue-89309-heading-levels.rs b/src/test/rustdoc/issue-89309-heading-levels.rs
new file mode 100644
index 00000000000..bb706c28ffa
--- /dev/null
+++ b/src/test/rustdoc/issue-89309-heading-levels.rs
@@ -0,0 +1,29 @@
+#![crate_name = "foo"]
+
+// @has foo/trait.Read.html
+// @has - '//h2' 'Trait examples'
+/// # Trait examples
+pub trait Read {
+    // @has - '//h5' 'Function examples'
+    /// # Function examples
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()>;
+}
+
+pub struct Foo;
+
+// @has foo/struct.Foo.html
+impl Foo {
+    // @has - '//h5' 'Implementation header'
+    /// # Implementation header
+    pub fn bar(&self) -> usize {
+        1
+    }
+}
+
+impl Read for Foo {
+    // @has - '//h5' 'Trait implementation header'
+    /// # Trait implementation header
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()> {
+        Ok(1)
+    }
+}
diff --git a/src/test/rustdoc/short-docblock.rs b/src/test/rustdoc/short-docblock.rs
index 74fa783174d..17c44eab091 100644
--- a/src/test/rustdoc/short-docblock.rs
+++ b/src/test/rustdoc/short-docblock.rs
@@ -2,7 +2,7 @@
 
 // @has foo/index.html '//*[@class="item-right docblock-short"]/p' 'fooo'
 // @!has foo/index.html '//*[@class="item-right docblock-short"]/p/h1' 'fooo'
-// @has foo/fn.foo.html '//h1[@id="fooo"]/a[@href="#fooo"]' 'fooo'
+// @has foo/fn.foo.html '//h2[@id="fooo"]/a[@href="#fooo"]' 'fooo'
 
 /// # fooo
 ///
@@ -11,7 +11,7 @@ pub fn foo() {}
 
 // @has foo/index.html '//*[@class="item-right docblock-short"]/p' 'mooood'
 // @!has foo/index.html '//*[@class="item-right docblock-short"]/p/h2' 'mooood'
-// @has foo/foo/index.html '//h2[@id="mooood"]/a[@href="#mooood"]' 'mooood'
+// @has foo/foo/index.html '//h3[@id="mooood"]/a[@href="#mooood"]' 'mooood'
 
 /// ## mooood
 ///
diff --git a/src/test/rustdoc/smart-punct.rs b/src/test/rustdoc/smart-punct.rs
index 5319892c99c..7ae5bd69945 100644
--- a/src/test/rustdoc/smart-punct.rs
+++ b/src/test/rustdoc/smart-punct.rs
@@ -21,7 +21,7 @@
 //! ```
 
 // @has "foo/index.html" "//p" "This is the “start” of the ‘document’! How’d you know that “it’s” the start?"
-// @has "foo/index.html" "//h1" "Header with “smart punct’”"
+// @has "foo/index.html" "//h2" "Header with “smart punct’”"
 // @has "foo/index.html" '//a[@href="https://www.rust-lang.org"]' "link with “smart punct’” – yessiree!"
 // @has "foo/index.html" '//code' "this inline code -- it shouldn't have \"smart punct\""
 // @has "foo/index.html" '//pre' "let x = \"don't smart-punct me -- please!\";"
diff --git a/src/tools/error_index_generator/main.rs b/src/tools/error_index_generator/main.rs
index 0386d8be167..39498c99e64 100644
--- a/src/tools/error_index_generator/main.rs
+++ b/src/tools/error_index_generator/main.rs
@@ -14,7 +14,7 @@ use std::path::PathBuf;
 
 use rustc_span::edition::DEFAULT_EDITION;
 
-use rustdoc::html::markdown::{ErrorCodes, IdMap, Markdown, Playground};
+use rustdoc::html::markdown::{ErrorCodes, HeadingOffset, IdMap, Markdown, Playground};
 
 pub struct ErrorMetadata {
     pub description: Option<String>,
@@ -119,14 +119,15 @@ impl Formatter for HTMLFormatter {
                 write!(
                     output,
                     "{}",
-                    Markdown(
-                        desc,
-                        &[],
-                        &mut id_map,
-                        ErrorCodes::Yes,
-                        DEFAULT_EDITION,
-                        &Some(playground)
-                    )
+                    Markdown {
+                        content: desc,
+                        links: &[],
+                        ids: &mut id_map,
+                        error_codes: ErrorCodes::Yes,
+                        edition: DEFAULT_EDITION,
+                        playground: &Some(playground),
+                        heading_offset: HeadingOffset::H1,
+                    }
                     .into_string()
                 )?
             }