about summary refs log tree commit diff
path: root/src/librustdoc/html
diff options
context:
space:
mode:
Diffstat (limited to 'src/librustdoc/html')
-rw-r--r--src/librustdoc/html/format.rs16
-rw-r--r--src/librustdoc/html/highlight.rs16
-rw-r--r--src/librustdoc/html/markdown.rs86
-rw-r--r--src/librustdoc/html/markdown/tests.rs2
-rw-r--r--src/librustdoc/html/render/context.rs14
-rw-r--r--src/librustdoc/html/render/mod.rs80
-rw-r--r--src/librustdoc/html/render/print_item.rs43
-rw-r--r--src/librustdoc/html/render/search_index.rs11
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css62
-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/html/static/js/main.js118
-rw-r--r--src/librustdoc/html/static/js/search.js19
14 files changed, 229 insertions, 244 deletions
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index 8a7a8ea5fd1..314f0612249 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -34,6 +34,7 @@ use crate::clean::{
 use crate::formats::item_type::ItemType;
 use crate::html::escape::Escape;
 use crate::html::render::Context;
+use crate::passes::collect_intra_doc_links::UrlFragment;
 
 use super::url_parts_builder::estimate_item_path_byte_length;
 use super::url_parts_builder::UrlPartsBuilder;
@@ -768,6 +769,21 @@ pub(crate) fn href_relative_parts<'fqp>(
     }
 }
 
+pub(crate) fn link_tooltip(did: DefId, fragment: &Option<UrlFragment>, cx: &Context<'_>) -> String {
+    let cache = cx.cache();
+    let Some((fqp, shortty)) = cache.paths.get(&did)
+        .or_else(|| cache.external_paths.get(&did))
+        else { return String::new() };
+    let fqp = fqp.iter().map(|sym| sym.as_str()).join("::");
+    if let &Some(UrlFragment::Item(id)) = fragment {
+        let name = cx.tcx().item_name(id);
+        let descr = cx.tcx().def_kind(id).descr(id);
+        format!("{descr} {fqp}::{name}")
+    } else {
+        format!("{shortty} {fqp}")
+    }
+}
+
 /// Used to render a [`clean::Path`].
 fn resolved_path<'cx>(
     w: &mut fmt::Formatter<'_>,
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs
index 1644d1c5a29..3c10eb524d7 100644
--- a/src/librustdoc/html/highlight.rs
+++ b/src/librustdoc/html/highlight.rs
@@ -96,13 +96,19 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, to
     );
 
     if tooltip != Tooltip::None {
+        let edition_code;
         write!(
             out,
-            "<div class='tooltip'{}>ⓘ</div>",
-            if let Tooltip::Edition(edition_info) = tooltip {
-                format!(" data-edition=\"{}\"", edition_info)
-            } else {
-                String::new()
+            "<a href=\"#\" class=\"tooltip\" title=\"{}\">ⓘ</a>",
+            match tooltip {
+                Tooltip::Ignore => "This example is not tested",
+                Tooltip::CompileFail => "This example deliberately fails to compile",
+                Tooltip::ShouldPanic => "This example panics",
+                Tooltip::Edition(edition) => {
+                    edition_code = format!("This example runs with edition {edition}");
+                    &edition_code
+                }
+                Tooltip::None => unreachable!(),
             },
         );
     }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index fb7c34118a4..e4adee6ae4d 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -28,6 +28,7 @@
 use rustc_data_structures::fx::FxHashMap;
 use rustc_hir::def_id::DefId;
 use rustc_middle::ty::TyCtxt;
+pub(crate) use rustc_resolve::rustdoc::main_body_opts;
 use rustc_span::edition::Edition;
 use rustc_span::{Span, Symbol};
 
@@ -46,6 +47,7 @@ use crate::html::escape::Escape;
 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 pulldown_cmark::{
@@ -57,15 +59,6 @@ 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
-        | Options::ENABLE_FOOTNOTES
-        | Options::ENABLE_STRIKETHROUGH
-        | Options::ENABLE_TASKLISTS
-        | Options::ENABLE_SMART_PUNCTUATION
-}
-
 /// Options for rendering Markdown in summaries (e.g., in search results).
 pub(crate) fn summary_opts() -> Options {
     Options::ENABLE_TABLES
@@ -294,47 +287,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
                 doctest::make_test(&test, krate, false, &Default::default(), edition, None);
             let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
 
-            // These characters don't need to be escaped in a URI.
-            // See https://url.spec.whatwg.org/#query-percent-encode-set
-            // and https://url.spec.whatwg.org/#urlencoded-parsing
-            // and https://url.spec.whatwg.org/#url-code-points
-            fn dont_escape(c: u8) -> bool {
-                (b'a' <= c && c <= b'z')
-                    || (b'A' <= c && c <= b'Z')
-                    || (b'0' <= c && c <= b'9')
-                    || c == b'-'
-                    || c == b'_'
-                    || c == b'.'
-                    || c == b','
-                    || c == b'~'
-                    || c == b'!'
-                    || c == b'\''
-                    || c == b'('
-                    || c == b')'
-                    || c == b'*'
-                    || c == b'/'
-                    || c == b';'
-                    || c == b':'
-                    || c == b'?'
-                    // As described in urlencoded-parsing, the
-                    // first `=` is the one that separates key from
-                    // value. Following `=`s are part of the value.
-                    || c == b'='
-            }
-            let mut test_escaped = String::new();
-            for b in test.bytes() {
-                if dont_escape(b) {
-                    test_escaped.push(char::from(b));
-                } else if b == b' ' {
-                    // URL queries are decoded with + replaced with SP
-                    test_escaped.push('+');
-                } else if b == b'%' {
-                    test_escaped.push('%');
-                    test_escaped.push('%');
-                } else {
-                    write!(test_escaped, "%{:02X}", b).unwrap();
-                }
-            }
+            let test_escaped = small_url_encode(test);
             Some(format!(
                 r#"<a class="test-arrow" target="_blank" href="{}?code={}{}&amp;edition={}">Run</a>"#,
                 url, test_escaped, channel, edition,
@@ -407,6 +360,9 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
                     trace!("it matched");
                     assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
                     self.shortcut_link = Some(link);
+                    if title.is_empty() && !link.tooltip.is_empty() {
+                        *title = CowStr::Borrowed(link.tooltip.as_ref());
+                    }
                 }
             }
             // Now that we're done with the shortcut link, don't replace any more text.
@@ -457,9 +413,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
             }
             // If this is a link, but not a shortcut link,
             // replace the URL, since the broken_link_callback was not called.
-            Some(Event::Start(Tag::Link(_, dest, _))) => {
+            Some(Event::Start(Tag::Link(_, dest, title))) => {
                 if let Some(link) = self.links.iter().find(|&link| *link.original_text == **dest) {
                     *dest = CowStr::Borrowed(link.href.as_ref());
+                    if title.is_empty() && !link.tooltip.is_empty() {
+                        *title = CowStr::Borrowed(link.tooltip.as_ref());
+                    }
                 }
             }
             // Anything else couldn't have been a valid Rust path, so no need to replace the text.
@@ -787,11 +746,7 @@ pub(crate) fn find_testable_code<T: doctest::Tester>(
             }
             Event::Text(ref s) if register_header.is_some() => {
                 let level = register_header.unwrap();
-                if s.is_empty() {
-                    tests.register_header("", level);
-                } else {
-                    tests.register_header(s, level);
-                }
+                tests.register_header(s, level);
                 register_header = None;
             }
             _ => {}
@@ -1027,7 +982,7 @@ impl Markdown<'_> {
             links
                 .iter()
                 .find(|link| link.original_text.as_str() == &*broken_link.reference)
-                .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+                .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));
@@ -1110,7 +1065,7 @@ impl MarkdownSummaryLine<'_> {
             links
                 .iter()
                 .find(|link| link.original_text.as_str() == &*broken_link.reference)
-                .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
         };
 
         let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
@@ -1157,7 +1112,7 @@ fn markdown_summary_with_limit(
         link_names
             .iter()
             .find(|link| link.original_text.as_str() == &*broken_link.reference)
-            .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
     };
 
     let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
@@ -1227,14 +1182,23 @@ pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]
 /// - Headings, links, and formatting are stripped.
 /// - Inline code is rendered as-is, surrounded by backticks.
 /// - HTML and code blocks are ignored.
-pub(crate) fn plain_text_summary(md: &str) -> String {
+pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String {
     if md.is_empty() {
         return String::new();
     }
 
     let mut s = String::with_capacity(md.len() * 3 / 2);
 
-    for event in Parser::new_ext(md, summary_opts()) {
+    let mut replacer = |broken_link: BrokenLink<'_>| {
+        link_names
+            .iter()
+            .find(|link| link.original_text.as_str() == &*broken_link.reference)
+            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
+    };
+
+    let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
+
+    for event in p {
         match &event {
             Event::Text(text) => s.push_str(text),
             Event::Code(code) => {
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 5878c58264e..e05635a0207 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -249,7 +249,7 @@ fn test_short_markdown_summary() {
 #[test]
 fn test_plain_text_summary() {
     fn t(input: &str, expect: &str) {
-        let output = plain_text_summary(input);
+        let output = plain_text_summary(input, &[]);
         assert_eq!(output, expect, "original: {}", input);
     }
 
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index b59645ec2e2..6762fba9275 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -18,7 +18,7 @@ use super::search_index::build_index;
 use super::write_shared::write_shared;
 use super::{
     collect_spans_and_sources, print_sidebar, scrape_examples_help, sidebar_module_like, AllTypes,
-    LinkFromSrc, NameDoc, StylePath,
+    LinkFromSrc, StylePath,
 };
 
 use crate::clean::{self, types::ExternalLocation, ExternalCrate};
@@ -182,7 +182,10 @@ impl<'tcx> Context<'tcx> {
         };
         title.push_str(" - Rust");
         let tyname = it.type_();
-        let desc = it.doc_value().as_ref().map(|doc| plain_text_summary(doc));
+        let desc = it
+            .doc_value()
+            .as_ref()
+            .map(|doc| plain_text_summary(doc, &it.link_names(&self.cache())));
         let desc = if let Some(desc) = desc {
             desc
         } else if it.is_crate() {
@@ -256,7 +259,7 @@ impl<'tcx> Context<'tcx> {
     }
 
     /// Construct a map of items shown in the sidebar to a plain-text summary of their docs.
-    fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<NameDoc>> {
+    fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<String>> {
         // BTreeMap instead of HashMap to get a sorted output
         let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new();
         let mut inserted: FxHashMap<ItemType, FxHashSet<Symbol>> = FxHashMap::default();
@@ -274,10 +277,7 @@ impl<'tcx> Context<'tcx> {
             if inserted.entry(short).or_default().insert(myname) {
                 let short = short.to_string();
                 let myname = myname.to_string();
-                map.entry(short).or_default().push((
-                    myname,
-                    Some(item.doc_value().map_or_else(String::new, |s| plain_text_summary(&s))),
-                ));
+                map.entry(short).or_default().push(myname);
             }
         }
 
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 816a8f4e274..2086faf78ac 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -38,7 +38,7 @@ pub(crate) use self::span_map::{collect_spans_and_sources, LinkFromSrc};
 
 use std::collections::VecDeque;
 use std::default::Default;
-use std::fmt;
+use std::fmt::{self, Write};
 use std::fs;
 use std::iter::Peekable;
 use std::path::PathBuf;
@@ -83,9 +83,6 @@ use crate::scrape_examples::{CallData, CallLocation};
 use crate::try_none;
 use crate::DOC_RUST_LANG_ORG_CHANNEL;
 
-/// A pair of name and its optional document.
-pub(crate) type NameDoc = (String, Option<String>);
-
 pub(crate) fn ensure_trailing_slash(v: &str) -> impl fmt::Display + '_ {
     crate::html::format::display_fn(move |f| {
         if !v.ends_with('/') && !v.is_empty() { write!(f, "{}/", v) } else { f.write_str(v) }
@@ -1313,7 +1310,7 @@ pub(crate) fn notable_traits_button(ty: &clean::Type, cx: &mut Context<'_>) -> O
     if has_notable_trait {
         cx.types_with_notable_traits.insert(ty.clone());
         Some(format!(
-            " <a href=\"#\" class=\"notable-traits\" data-ty=\"{ty}\">ⓘ</a>",
+            " <a href=\"#\" class=\"tooltip\" data-notable-ty=\"{ty}\">ⓘ</a>",
             ty = Escape(&format!("{:#}", ty.print(cx))),
         ))
     } else {
@@ -2020,31 +2017,60 @@ fn get_associated_constants(
         .collect::<Vec<_>>()
 }
 
-// The point is to url encode any potential character from a type with genericity.
-fn small_url_encode(s: String) -> String {
+pub(crate) fn small_url_encode(s: String) -> String {
+    // These characters don't need to be escaped in a URI.
+    // See https://url.spec.whatwg.org/#query-percent-encode-set
+    // and https://url.spec.whatwg.org/#urlencoded-parsing
+    // and https://url.spec.whatwg.org/#url-code-points
+    fn dont_escape(c: u8) -> bool {
+        (b'a' <= c && c <= b'z')
+            || (b'A' <= c && c <= b'Z')
+            || (b'0' <= c && c <= b'9')
+            || c == b'-'
+            || c == b'_'
+            || c == b'.'
+            || c == b','
+            || c == b'~'
+            || c == b'!'
+            || c == b'\''
+            || c == b'('
+            || c == b')'
+            || c == b'*'
+            || c == b'/'
+            || c == b';'
+            || c == b':'
+            || c == b'?'
+            // As described in urlencoded-parsing, the
+            // first `=` is the one that separates key from
+            // value. Following `=`s are part of the value.
+            || c == b'='
+    }
     let mut st = String::new();
     let mut last_match = 0;
-    for (idx, c) in s.char_indices() {
-        let escaped = match c {
-            '<' => "%3C",
-            '>' => "%3E",
-            ' ' => "%20",
-            '?' => "%3F",
-            '\'' => "%27",
-            '&' => "%26",
-            ',' => "%2C",
-            ':' => "%3A",
-            ';' => "%3B",
-            '[' => "%5B",
-            ']' => "%5D",
-            '"' => "%22",
-            _ => continue,
-        };
+    for (idx, b) in s.bytes().enumerate() {
+        if dont_escape(b) {
+            continue;
+        }
 
-        st += &s[last_match..idx];
-        st += escaped;
-        // NOTE: we only expect single byte characters here - which is fine as long as we
-        // only match single byte characters
+        if last_match != idx {
+            // Invariant: `idx` must be the first byte in a character at this point.
+            st += &s[last_match..idx];
+        }
+        if b == b' ' {
+            // URL queries are decoded with + replaced with SP.
+            // While the same is not true for hashes, rustdoc only needs to be
+            // consistent with itself when encoding them.
+            st += "+";
+        } else if b == b'%' {
+            st += "%%";
+        } else {
+            write!(st, "%{:02X}", b).unwrap();
+        }
+        // Invariant: if the current byte is not at the start of a multi-byte character,
+        // we need to get down here so that when the next turn of the loop comes around,
+        // last_match winds up equalling idx.
+        //
+        // In other words, dont_escape must always return `false` in multi-byte character.
         last_match = idx + 1;
     }
 
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index c2d24e51484..4ff1c93dc5d 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -10,7 +10,7 @@ use rustc_middle::ty::layout::LayoutError;
 use rustc_middle::ty::{self, Adt, TyCtxt};
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::{kw, sym, Symbol};
-use rustc_target::abi::{LayoutS, Primitive, TagEncoding, VariantIdx, Variants};
+use rustc_target::abi::{LayoutS, Primitive, TagEncoding, Variants};
 use std::cmp::Ordering;
 use std::fmt;
 use std::rc::Rc;
@@ -1081,10 +1081,10 @@ fn item_typedef(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clea
     fn write_content(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Typedef) {
         wrap_item(w, |w| {
             render_attributes_in_pre(w, it, "");
-            write!(w, "{}", visibility_print_with_space(it.visibility(cx.tcx()), it.item_id, cx));
             write!(
                 w,
-                "type {}{}{where_clause} = {type_};",
+                "{}type {}{}{where_clause} = {type_};",
+                visibility_print_with_space(it.visibility(cx.tcx()), it.item_id, cx),
                 it.name.unwrap(),
                 t.generics.print(cx),
                 where_clause = print_where_clause(&t.generics, cx, 0, Ending::Newline),
@@ -1109,7 +1109,7 @@ fn item_typedef(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clea
 fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean::Union) {
     wrap_item(w, |w| {
         render_attributes_in_pre(w, it, "");
-        render_union(w, it, Some(&s.generics), &s.fields, "", cx);
+        render_union(w, it, Some(&s.generics), &s.fields, cx);
     });
 
     document(w, cx, it, None, HeadingOffset::H2);
@@ -1138,13 +1138,11 @@ fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean:
                      <a href=\"#{id}\" class=\"anchor field\">§</a>\
                      <code>{name}: {ty}</code>\
                  </span>",
-                id = id,
-                name = name,
                 shortty = ItemType::StructField,
                 ty = ty.print(cx),
             );
             if let Some(stability_class) = field.stability_class(cx.tcx()) {
-                write!(w, "<span class=\"stab {stab}\"></span>", stab = stability_class);
+                write!(w, "<span class=\"stab {stability_class}\"></span>");
             }
             document(w, cx, field, Some(it), HeadingOffset::H3);
         }
@@ -1242,7 +1240,6 @@ fn item_enum(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, e: &clean::
                 w,
                 "<section id=\"{id}\" class=\"variant\">\
                     <a href=\"#{id}\" class=\"anchor\">§</a>",
-                id = id,
             );
             render_stability_since_raw_with_extra(
                 w,
@@ -1280,8 +1277,11 @@ fn item_enum(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, e: &clean::
             if let Some((heading, fields)) = heading_and_fields {
                 let variant_id =
                     cx.derive_id(format!("{}.{}.fields", ItemType::Variant, variant.name.unwrap()));
-                write!(w, "<div class=\"sub-variant\" id=\"{id}\">", id = variant_id);
-                write!(w, "<h4>{heading}</h4>", heading = heading);
+                write!(
+                    w,
+                    "<div class=\"sub-variant\" id=\"{variant_id}\">\
+                        <h4>{heading}</h4>",
+                );
                 document_non_exhaustive(w, variant);
                 for field in fields {
                     match *field.kind {
@@ -1299,7 +1299,6 @@ fn item_enum(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, e: &clean::
                                      <a href=\"#{id}\" class=\"anchor field\">§</a>\
                                      <code>{f}: {t}</code>\
                                  </span>",
-                                id = id,
                                 f = field.name.unwrap(),
                                 t = ty.print(cx)
                             );
@@ -1450,11 +1449,9 @@ fn item_struct(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean
                     w,
                     "<span id=\"{id}\" class=\"{item_type} small-section-header\">\
                          <a href=\"#{id}\" class=\"anchor field\">§</a>\
-                         <code>{name}: {ty}</code>\
+                         <code>{field_name}: {ty}</code>\
                      </span>",
                     item_type = ItemType::StructField,
-                    id = id,
-                    name = field_name,
                     ty = ty.print(cx)
                 );
                 document(w, cx, field, Some(it), HeadingOffset::H3);
@@ -1628,7 +1625,6 @@ fn render_union(
     it: &clean::Item,
     g: Option<&clean::Generics>,
     fields: &[clean::Item],
-    tab: &str,
     cx: &Context<'_>,
 ) {
     let tcx = cx.tcx();
@@ -1651,7 +1647,7 @@ fn render_union(
         w.write_str(" ");
     }
 
-    write!(w, "{{\n{}", tab);
+    write!(w, "{{\n");
     let count_fields =
         fields.iter().filter(|f| matches!(*f.kind, clean::StructFieldItem(..))).count();
     let toggle = should_hide_fields(count_fields);
@@ -1663,17 +1659,16 @@ fn render_union(
         if let clean::StructFieldItem(ref ty) = *field.kind {
             write!(
                 w,
-                "    {}{}: {},\n{}",
+                "    {}{}: {},\n",
                 visibility_print_with_space(field.visibility(tcx), field.item_id, cx),
                 field.name.unwrap(),
-                ty.print(cx),
-                tab
+                ty.print(cx)
             );
         }
     }
 
     if it.has_stripped_entries().unwrap() {
-        write!(w, "    /* private fields */\n{}", tab);
+        write!(w, "    /* private fields */\n");
     }
     if toggle {
         toggle_close(w);
@@ -1838,12 +1833,12 @@ fn document_non_exhaustive(w: &mut Buffer, item: &clean::Item) {
 }
 
 fn document_type_layout(w: &mut Buffer, cx: &Context<'_>, ty_def_id: DefId) {
-    fn write_size_of_layout(w: &mut Buffer, layout: &LayoutS<VariantIdx>, tag_size: u64) {
+    fn write_size_of_layout(w: &mut Buffer, layout: &LayoutS, tag_size: u64) {
         if layout.abi.is_unsized() {
             write!(w, "(unsized)");
         } else {
-            let bytes = layout.size.bytes() - tag_size;
-            write!(w, "{size} byte{pl}", size = bytes, pl = if bytes == 1 { "" } else { "s" },);
+            let size = layout.size.bytes() - tag_size;
+            write!(w, "{size} byte{pl}", pl = if size == 1 { "" } else { "s" },);
         }
     }
 
@@ -1898,7 +1893,7 @@ fn document_type_layout(w: &mut Buffer, cx: &Context<'_>, ty_def_id: DefId) {
 
                     for (index, layout) in variants.iter_enumerated() {
                         let name = adt.variant(index).name;
-                        write!(w, "<li><code>{name}</code>: ", name = name);
+                        write!(w, "<li><code>{name}</code>: ");
                         write_size_of_layout(w, layout, tag_size);
                         writeln!(w, "</li>");
                     }
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index 5b0caac099b..090ea2cb157 100644
--- a/src/librustdoc/html/render/search_index.rs
+++ b/src/librustdoc/html/render/search_index.rs
@@ -236,7 +236,16 @@ pub(crate) fn build_index<'tcx>(
             crate_data.serialize_field("doc", &self.doc)?;
             crate_data.serialize_field(
                 "t",
-                &self.items.iter().map(|item| &item.ty).collect::<Vec<_>>(),
+                &self
+                    .items
+                    .iter()
+                    .map(|item| {
+                        let n = item.ty as u8;
+                        let c = char::try_from(n + b'A').expect("item types must fit in ASCII");
+                        assert!(c <= 'z', "item types must fit within ASCII printables");
+                        c
+                    })
+                    .collect::<String>(),
             )?;
             crate_data.serialize_field(
                 "n",
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 2a9548712f0..476dd606680 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -174,6 +174,14 @@ h1, h2, h3, h4 {
 .top-doc .docblock > h4 {
 	border-bottom: 1px solid var(--headings-border-bottom-color);
 }
+/* while line-height 1.5 is required for any "block of text",
+	which WCAG defines as more than one sentence, it looks weird for
+	very large main headers */
+h1, h2 {
+	line-height: 1.25;
+	padding-top: 3px;
+	padding-bottom: 9px;
+}
 h3.code-header {
 	font-size: 1.125rem; /* 18px */
 }
@@ -707,8 +715,8 @@ h2.small-section-header > .anchor {
 .main-heading a:hover,
 .example-wrap > pre.rust a:hover,
 .all-items a:hover,
-.docblock a:not(.test-arrow):not(.scrape-help):hover,
-.docblock-short a:not(.test-arrow):not(.scrape-help):hover,
+.docblock a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover,
+.docblock-short a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover,
 .item-info a {
 	text-decoration: underline;
 }
@@ -1101,44 +1109,8 @@ pre.rust .doccomment {
 	display: block;
 	left: -25px;
 	top: 5px;
-}
-
-.example-wrap .tooltip:hover::after {
-	padding: 5px 3px 3px 3px;
-	border-radius: 6px;
-	margin-left: 5px;
-	font-size: 1rem;
-	border: 1px solid var(--border-color);
-	position: absolute;
-	width: max-content;
-	top: -2px;
-	z-index: 1;
-	background-color: var(--tooltip-background-color);
-	color: var(--tooltip-color);
-}
-
-.example-wrap .tooltip:hover::before {
-	content: " ";
-	position: absolute;
-	top: 50%;
-	left: 16px;
-	margin-top: -5px;
-	z-index: 1;
-	border: 5px solid transparent;
-	border-right-color: var(--tooltip-background-color);
-}
-
-.example-wrap.ignore .tooltip:hover::after {
-	content: "This example is not tested";
-}
-.example-wrap.compile_fail .tooltip:hover::after {
-	content: "This example deliberately fails to compile";
-}
-.example-wrap.should_panic .tooltip:hover::after {
-	content: "This example panics";
-}
-.example-wrap.edition .tooltip:hover::after {
-	content: "This code runs with edition " attr(data-edition);
+	margin: 0;
+	line-height: 1;
 }
 
 .example-wrap.compile_fail .tooltip,
@@ -1205,7 +1177,7 @@ a.test-arrow:hover {
 	border-right: 3px solid var(--target-border-color);
 }
 
-.notable-traits {
+.code-header a.tooltip {
 	color: inherit;
 	margin-right: 15px;
 	position: relative;
@@ -1214,7 +1186,7 @@ a.test-arrow:hover {
 /* placeholder thunk so that the mouse can easily travel from "(i)" to popover
 	the resulting "hover tunnel" is a stepped triangle, approximating
 	https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown */
-.notable-traits:hover::after {
+a.tooltip:hover::after {
 	position: absolute;
 	top: calc(100% - 10px);
 	left: -15px;
@@ -1223,11 +1195,11 @@ a.test-arrow:hover {
 	content: "\00a0";
 }
 
-.notable .content {
+.popover.tooltip .content {
 	margin: 0.25em 0.5em;
 }
 
-.notable .content pre, .notable .content code {
+.popover.tooltip .content pre, .popover.tooltip .content code {
 	background: transparent;
 	margin: 0;
 	padding: 0;
@@ -1235,7 +1207,7 @@ a.test-arrow:hover {
 	white-space: pre-wrap;
 }
 
-.notable .content > h3:first-child {
+.popover.tooltip .content > h3:first-child {
 	margin: 0 0 5px 0;
 }
 
diff --git a/src/librustdoc/html/static/css/themes/ayu.css b/src/librustdoc/html/static/css/themes/ayu.css
index b5a2cf7f28b..472a725f053 100644
--- a/src/librustdoc/html/static/css/themes/ayu.css
+++ b/src/librustdoc/html/static/css/themes/ayu.css
@@ -74,8 +74,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
 	--test-arrow-hover-background-color: rgba(57, 175, 215, 0.368);
 	--target-background-color: rgba(255, 236, 164, 0.06);
 	--target-border-color: rgba(255, 180, 76, 0.85);
-	--tooltip-background-color: #314559;
-	--tooltip-color: #c5c5c5;
 	--kbd-color: #c5c5c5;
 	--kbd-background: #314559;
 	--kbd-box-shadow-color: #5c6773;
diff --git a/src/librustdoc/html/static/css/themes/dark.css b/src/librustdoc/html/static/css/themes/dark.css
index b84d87c4a54..5612bde96a8 100644
--- a/src/librustdoc/html/static/css/themes/dark.css
+++ b/src/librustdoc/html/static/css/themes/dark.css
@@ -69,8 +69,6 @@
 	--test-arrow-hover-background-color: #4e8bca;
 	--target-background-color: #494a3d;
 	--target-border-color: #bb7410;
-	--tooltip-background-color: #000;
-	--tooltip-color: #fff;
 	--kbd-color: #000;
 	--kbd-background: #fafbfc;
 	--kbd-box-shadow-color: #c6cbd1;
diff --git a/src/librustdoc/html/static/css/themes/light.css b/src/librustdoc/html/static/css/themes/light.css
index 342274e6767..34b35c405a8 100644
--- a/src/librustdoc/html/static/css/themes/light.css
+++ b/src/librustdoc/html/static/css/themes/light.css
@@ -69,8 +69,6 @@
 	--test-arrow-hover-background-color: #4e8bca;
 	--target-background-color: #fdffd3;
 	--target-border-color: #ad7c37;
-	--tooltip-background-color: #000;
-	--tooltip-color: #fff;
 	--kbd-color: #000;
 	--kbd-background: #fafbfc;
 	--kbd-box-shadow-color: #c6cbd1;
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index c66f500f423..5e8c0e8d10c 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -378,7 +378,7 @@ function loadCss(cssUrl) {
         }
         ev.preventDefault();
         searchState.defocus();
-        window.hideAllModals(true); // true = reset focus for notable traits
+        window.hideAllModals(true); // true = reset focus for tooltips
     }
 
     function handleShortcut(ev) {
@@ -455,10 +455,7 @@ function loadCss(cssUrl) {
             const ul = document.createElement("ul");
             ul.className = "block " + shortty;
 
-            for (const item of filtered) {
-                const name = item[0];
-                const desc = item[1]; // can be null
-
+            for (const name of filtered) {
                 let path;
                 if (shortty === "mod") {
                     path = name + "/index.html";
@@ -468,7 +465,6 @@ function loadCss(cssUrl) {
                 const current_page = document.location.href.split("/").pop();
                 const link = document.createElement("a");
                 link.href = path;
-                link.title = desc;
                 if (path === current_page) {
                     link.className = "current";
                 }
@@ -788,17 +784,17 @@ function loadCss(cssUrl) {
             // we need to switch away from mobile mode and make the main content area scrollable.
             hideSidebar();
         }
-        if (window.CURRENT_NOTABLE_ELEMENT) {
-            // As a workaround to the behavior of `contains: layout` used in doc togglers, the
-            // notable traits popup is positioned using javascript.
+        if (window.CURRENT_TOOLTIP_ELEMENT) {
+            // As a workaround to the behavior of `contains: layout` used in doc togglers,
+            // tooltip popovers are positioned using javascript.
             //
             // This means when the window is resized, we need to redo the layout.
-            const base = window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE;
-            const force_visible = base.NOTABLE_FORCE_VISIBLE;
-            hideNotable(false);
+            const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE;
+            const force_visible = base.TOOLTIP_FORCE_VISIBLE;
+            hideTooltip(false);
             if (force_visible) {
-                showNotable(base);
-                base.NOTABLE_FORCE_VISIBLE = true;
+                showTooltip(base);
+                base.TOOLTIP_FORCE_VISIBLE = true;
             }
         }
     });
@@ -826,27 +822,35 @@ function loadCss(cssUrl) {
         });
     });
 
-    function showNotable(e) {
-        if (!window.NOTABLE_TRAITS) {
+    function showTooltip(e) {
+        const notable_ty = e.getAttribute("data-notable-ty");
+        if (!window.NOTABLE_TRAITS && notable_ty) {
             const data = document.getElementById("notable-traits-data");
             if (data) {
                 window.NOTABLE_TRAITS = JSON.parse(data.innerText);
             } else {
-                throw new Error("showNotable() called on page without any notable traits!");
+                throw new Error("showTooltip() called with notable without any notable traits!");
             }
         }
-        if (window.CURRENT_NOTABLE_ELEMENT && window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE === e) {
+        if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
             // Make this function idempotent.
             return;
         }
         window.hideAllModals(false);
-        const ty = e.getAttribute("data-ty");
         const wrapper = document.createElement("div");
-        wrapper.innerHTML = "<div class=\"content\">" + window.NOTABLE_TRAITS[ty] + "</div>";
-        wrapper.className = "notable popover";
+        if (notable_ty) {
+            wrapper.innerHTML = "<div class=\"content\">" +
+                window.NOTABLE_TRAITS[notable_ty] + "</div>";
+        } else if (e.getAttribute("title") !== undefined) {
+            const titleContent = document.createElement("div");
+            titleContent.className = "content";
+            titleContent.appendChild(document.createTextNode(e.getAttribute("title")));
+            wrapper.appendChild(titleContent);
+        }
+        wrapper.className = "tooltip popover";
         const focusCatcher = document.createElement("div");
         focusCatcher.setAttribute("tabindex", "0");
-        focusCatcher.onfocus = hideNotable;
+        focusCatcher.onfocus = hideTooltip;
         wrapper.appendChild(focusCatcher);
         const pos = e.getBoundingClientRect();
         // 5px overlap so that the mouse can easily travel from place to place
@@ -868,62 +872,62 @@ function loadCss(cssUrl) {
             );
         }
         wrapper.style.visibility = "";
-        window.CURRENT_NOTABLE_ELEMENT = wrapper;
-        window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE = e;
+        window.CURRENT_TOOLTIP_ELEMENT = wrapper;
+        window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
         wrapper.onpointerleave = function(ev) {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            if (!e.NOTABLE_FORCE_VISIBLE && !elemIsInParent(event.relatedTarget, e)) {
-                hideNotable(true);
+            if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(event.relatedTarget, e)) {
+                hideTooltip(true);
             }
         };
     }
 
-    function notableBlurHandler(event) {
-        if (window.CURRENT_NOTABLE_ELEMENT &&
-            !elemIsInParent(document.activeElement, window.CURRENT_NOTABLE_ELEMENT) &&
-            !elemIsInParent(event.relatedTarget, window.CURRENT_NOTABLE_ELEMENT) &&
-            !elemIsInParent(document.activeElement, window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE) &&
-            !elemIsInParent(event.relatedTarget, window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE)
+    function tooltipBlurHandler(event) {
+        if (window.CURRENT_TOOLTIP_ELEMENT &&
+            !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT) &&
+            !elemIsInParent(event.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT) &&
+            !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE) &&
+            !elemIsInParent(event.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE)
         ) {
             // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari.
-            // When I click the button on an already-opened notable trait popover, Safari
+            // When I click the button on an already-opened tooltip popover, Safari
             // hides the popover and then immediately shows it again, while everyone else hides it
             // and it stays hidden.
             //
             // To work around this, make sure the click finishes being dispatched before
-            // hiding the popover. Since `hideNotable()` is idempotent, this makes Safari behave
+            // hiding the popover. Since `hideTooltip()` is idempotent, this makes Safari behave
             // consistently with the other two.
-            setTimeout(() => hideNotable(false), 0);
+            setTimeout(() => hideTooltip(false), 0);
         }
     }
 
-    function hideNotable(focus) {
-        if (window.CURRENT_NOTABLE_ELEMENT) {
-            if (window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE.NOTABLE_FORCE_VISIBLE) {
+    function hideTooltip(focus) {
+        if (window.CURRENT_TOOLTIP_ELEMENT) {
+            if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
                 if (focus) {
-                    window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE.focus();
+                    window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus();
                 }
-                window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE.NOTABLE_FORCE_VISIBLE = false;
+                window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false;
             }
             const body = document.getElementsByTagName("body")[0];
-            body.removeChild(window.CURRENT_NOTABLE_ELEMENT);
-            window.CURRENT_NOTABLE_ELEMENT = null;
+            body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
+            window.CURRENT_TOOLTIP_ELEMENT = null;
         }
     }
 
-    onEachLazy(document.getElementsByClassName("notable-traits"), e => {
+    onEachLazy(document.getElementsByClassName("tooltip"), e => {
         e.onclick = function() {
-            this.NOTABLE_FORCE_VISIBLE = this.NOTABLE_FORCE_VISIBLE ? false : true;
-            if (window.CURRENT_NOTABLE_ELEMENT && !this.NOTABLE_FORCE_VISIBLE) {
-                hideNotable(true);
+            this.TOOLTIP_FORCE_VISIBLE = this.TOOLTIP_FORCE_VISIBLE ? false : true;
+            if (window.CURRENT_TOOLTIP_ELEMENT && !this.TOOLTIP_FORCE_VISIBLE) {
+                hideTooltip(true);
             } else {
-                showNotable(this);
-                window.CURRENT_NOTABLE_ELEMENT.setAttribute("tabindex", "0");
-                window.CURRENT_NOTABLE_ELEMENT.focus();
-                window.CURRENT_NOTABLE_ELEMENT.onblur = notableBlurHandler;
+                showTooltip(this);
+                window.CURRENT_TOOLTIP_ELEMENT.setAttribute("tabindex", "0");
+                window.CURRENT_TOOLTIP_ELEMENT.focus();
+                window.CURRENT_TOOLTIP_ELEMENT.onblur = tooltipBlurHandler;
             }
             return false;
         };
@@ -932,16 +936,16 @@ function loadCss(cssUrl) {
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            showNotable(this);
+            showTooltip(this);
         };
         e.onpointerleave = function(ev) {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            if (!this.NOTABLE_FORCE_VISIBLE &&
-                !elemIsInParent(ev.relatedTarget, window.CURRENT_NOTABLE_ELEMENT)) {
-                hideNotable(true);
+            if (!this.TOOLTIP_FORCE_VISIBLE &&
+                !elemIsInParent(ev.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT)) {
+                hideTooltip(true);
             }
         };
     });
@@ -1043,14 +1047,14 @@ function loadCss(cssUrl) {
     }
 
     /**
-     * Hide popover menus, notable trait tooltips, and the sidebar (if applicable).
+     * Hide popover menus, clickable tooltips, and the sidebar (if applicable).
      *
-     * Pass "true" to reset focus for notable traits.
+     * Pass "true" to reset focus for tooltip popovers.
      */
     window.hideAllModals = function(switchFocus) {
         hideSidebar();
         window.hidePopoverMenus();
-        hideNotable(switchFocus);
+        hideTooltip(switchFocus);
     };
 
     /**
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index 251e806c2d9..1e6c94d29ba 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -142,13 +142,11 @@ function initSearch(rawSearchIndex) {
     }
 
     function itemTypeFromName(typename) {
-        for (let i = 0, len = itemTypes.length; i < len; ++i) {
-            if (itemTypes[i] === typename) {
-                return i;
-            }
+        const index = itemTypes.findIndex(i => i === typename);
+        if (index < 0) {
+            throw new Error("Unknown type filter `" + typename + "`");
         }
-
-        throw new Error("Unknown type filter `" + typename + "`");
+        return index;
     }
 
     /**
@@ -1941,6 +1939,7 @@ function initSearch(rawSearchIndex) {
          * @type {Array<string>}
          */
         const searchWords = [];
+        const charA = "A".charCodeAt(0);
         let i, word;
         let currentIndex = 0;
         let id = 0;
@@ -1955,7 +1954,7 @@ function initSearch(rawSearchIndex) {
             /**
              * The raw search data for a given crate. `n`, `t`, `d`, and `q`, `i`, and `f`
              * are arrays with the same length. n[i] contains the name of an item.
-             * t[i] contains the type of that item (as a small integer that represents an
+             * t[i] contains the type of that item (as a string of characters that represent an
              * offset in `itemTypes`). d[i] contains the description of that item.
              *
              * q[i] contains the full path of the item, or an empty string indicating
@@ -1982,7 +1981,7 @@ function initSearch(rawSearchIndex) {
              *   doc: string,
              *   a: Object,
              *   n: Array<string>,
-             *   t: Array<Number>,
+             *   t: String,
              *   d: Array<string>,
              *   q: Array<string>,
              *   i: Array<Number>,
@@ -2011,7 +2010,7 @@ function initSearch(rawSearchIndex) {
             searchIndex.push(crateRow);
             currentIndex += 1;
 
-            // an array of (Number) item types
+            // a String of one character item type codes
             const itemTypes = crateCorpus.t;
             // an array of (String) item names
             const itemNames = crateCorpus.n;
@@ -2062,7 +2061,7 @@ function initSearch(rawSearchIndex) {
                 }
                 const row = {
                     crate: crate,
-                    ty: itemTypes[i],
+                    ty: itemTypes.charCodeAt(i) - charA,
                     name: itemNames[i],
                     path: itemPaths[i] ? itemPaths[i] : lastPath,
                     desc: itemDescs[i],