diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 642265f5f6bef..bad1511dfd29e 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -530,7 +530,6 @@ impl<'a, 'b, 'ids, I: Iterator>> Iterator for event in &mut self.inner { match &event.0 { Event::End(Tag::Heading(..)) => break, - Event::Start(Tag::Link(_, _, _)) | Event::End(Tag::Link(..)) => {} Event::Text(text) | Event::Code(text) => { id.extend(text.chars().filter_map(slugify)); self.buf.push_back(event); @@ -549,12 +548,10 @@ impl<'a, 'b, 'ids, I: Iterator>> Iterator let level = std::cmp::min(level as u32 + (self.heading_offset as u32), MAX_HEADER_LEVEL); - self.buf.push_back((Event::Html(format!("").into()), 0..0)); + self.buf.push_back((Event::Html(format!("").into()), 0..0)); - let start_tags = format!( - "\ - ", - ); + let start_tags = + format!("§"); return Some((Event::Html(start_tags.into()), 0..0)); } event diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index 5eba1d0609f38..4dd176b3a692a 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -311,26 +311,38 @@ fn test_header() { assert_eq!(output, expect, "original: {}", input); } - t("# Foo bar", "

Foo bar

"); + t( + "# Foo bar", + "

§Foo bar

", + ); t( "## Foo-bar_baz qux", "

\ - Foo-bar_baz qux

", + §\ + Foo-bar_baz qux\ + ", ); t( "### **Foo** *bar* baz!?!& -_qux_-%", "

\ - Foo \ - bar baz!?!& -qux-%\ + §\ + Foo bar baz!?!& -qux-%\

", ); t( "#### **Foo?** & \\*bar?!* _`baz`_ ❤ #qux", "
\ - Foo? & *bar?!* \ - baz ❤ #qux\ + §\ + Foo? & *bar?!* baz ❤ #qux\
", ); + t( + "# Foo [bar](https://hello.yo)", + "

\ + §\ + Foo bar\ +

", + ); } #[test] @@ -351,12 +363,36 @@ fn test_header_ids_multiple_blocks() { assert_eq!(output, expect, "original: {}", input); } - t(&mut map, "# Example", "

Example

"); - t(&mut map, "# Panics", "

Panics

"); - t(&mut map, "# Example", "

Example

"); - t(&mut map, "# Search", "

Search

"); - t(&mut map, "# Example", "

Example

"); - t(&mut map, "# Panics", "

Panics

"); + t( + &mut map, + "# Example", + "

§Example

", + ); + t( + &mut map, + "# Panics", + "

§Panics

", + ); + t( + &mut map, + "# Example", + "

§Example

", + ); + t( + &mut map, + "# Search", + "

§Search

", + ); + t( + &mut map, + "# Example", + "

§Example

", + ); + t( + &mut map, + "# Panics", + "

§Panics

", + ); } #[test] diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index bea5ccd7c860d..ac7ae291d29b3 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -1207,17 +1207,31 @@ impl<'a> AssocItemLink<'a> { } } -fn write_impl_section_heading(mut w: impl fmt::Write, title: &str, id: &str) { +pub fn write_section_heading( + w: &mut impl fmt::Write, + title: &str, + id: &str, + extra_class: Option<&str>, + extra: impl fmt::Display, +) { + let (extra_class, whitespace) = match extra_class { + Some(extra) => (extra, " "), + None => ("", ""), + }; write!( w, - "

\ + "

\ {title}\ §\ -

" + {extra}", ) .unwrap(); } +fn write_impl_section_heading(w: &mut impl fmt::Write, title: &str, id: &str) { + write_section_heading(w, title, id, None, "") +} + pub(crate) fn render_all_impls( mut w: impl Write, cx: &mut Context<'_>, diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 3b91fbdcb29dd..71186319e07dd 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -19,8 +19,8 @@ use super::{ item_ty_to_section, notable_traits_button, notable_traits_json, render_all_impls, render_assoc_item, render_assoc_items, render_attributes_in_code, render_attributes_in_pre, render_impl, render_rightside, render_stability_since_raw, - render_stability_since_raw_with_extra, AssocItemLink, AssocItemRender, Context, - ImplRenderingParameters, RenderMode, + render_stability_since_raw_with_extra, write_section_heading, AssocItemLink, AssocItemRender, + Context, ImplRenderingParameters, RenderMode, }; use crate::clean; use crate::config::ModuleSorting; @@ -425,13 +425,12 @@ fn item_module(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Item, items: w.write_str(ITEM_TABLE_CLOSE); } last_section = Some(my_section); - write!( + write_section_heading( w, - "

\ - {name}\ -

{ITEM_TABLE_OPEN}", - id = cx.derive_id(my_section.id()), - name = my_section.name(), + my_section.name(), + &cx.derive_id(my_section.id()), + None, + ITEM_TABLE_OPEN, ); } @@ -814,16 +813,6 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: // Trait documentation write!(w, "{}", document(cx, it, None, HeadingOffset::H2)); - fn write_small_section_header(w: &mut Buffer, id: &str, title: &str, extra_content: &str) { - write!( - w, - "

\ - {1}§\ -

{2}", - id, title, extra_content - ) - } - fn trait_item(w: &mut Buffer, cx: &mut Context<'_>, m: &clean::Item, t: &clean::Item) { let name = m.name.unwrap(); info!("Documenting {name} on {ty_name:?}", ty_name = t.name); @@ -857,10 +846,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: } if !required_types.is_empty() { - write_small_section_header( + write_section_heading( w, - "required-associated-types", "Required Associated Types", + "required-associated-types", + None, "
", ); for t in required_types { @@ -869,10 +859,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: w.write_str("
"); } if !provided_types.is_empty() { - write_small_section_header( + write_section_heading( w, - "provided-associated-types", "Provided Associated Types", + "provided-associated-types", + None, "
", ); for t in provided_types { @@ -882,10 +873,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: } if !required_consts.is_empty() { - write_small_section_header( + write_section_heading( w, - "required-associated-consts", "Required Associated Constants", + "required-associated-consts", + None, "
", ); for t in required_consts { @@ -894,10 +886,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: w.write_str("
"); } if !provided_consts.is_empty() { - write_small_section_header( + write_section_heading( w, - "provided-associated-consts", "Provided Associated Constants", + "provided-associated-consts", + None, "
", ); for t in provided_consts { @@ -908,10 +901,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: // Output the documentation for each function individually if !required_methods.is_empty() || must_implement_one_of_functions.is_some() { - write_small_section_header( + write_section_heading( w, - "required-methods", "Required Methods", + "required-methods", + None, "
", ); @@ -929,10 +923,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: w.write_str("
"); } if !provided_methods.is_empty() { - write_small_section_header( + write_section_heading( w, - "provided-methods", "Provided Methods", + "provided-methods", + None, "
", ); for m in provided_methods { @@ -949,10 +944,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: let mut extern_crates = FxHashSet::default(); if !t.is_object_safe(cx.tcx()) { - write_small_section_header( + write_section_heading( w, - "object-safety", "Object Safety", + "object-safety", + None, &format!( "
This trait is not \ \ @@ -996,7 +992,7 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: foreign.sort_by_cached_key(|i| ImplString::new(i, cx)); if !foreign.is_empty() { - write_small_section_header(w, "foreign-impls", "Implementations on Foreign Types", ""); + write_section_heading(w, "Implementations on Foreign Types", "foreign-impls", None, ""); for implementor in foreign { let provided_methods = implementor.inner_impl().provided_trait_methods(tcx); @@ -1021,10 +1017,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: } } - write_small_section_header( + write_section_heading( w, - "implementors", "Implementors", + "implementors", + None, "
", ); for implementor in concrete { @@ -1033,10 +1030,11 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: w.write_str("
"); if t.is_auto(tcx) { - write_small_section_header( + write_section_heading( w, - "synthetic-implementors", "Auto implementors", + "synthetic-implementors", + None, "
", ); for implementor in synthetic { @@ -1054,18 +1052,20 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: } else { // even without any implementations to write in, we still want the heading and list, so the // implementors javascript file pulled in below has somewhere to write the impls into - write_small_section_header( + write_section_heading( w, - "implementors", "Implementors", + "implementors", + None, "
", ); if t.is_auto(tcx) { - write_small_section_header( + write_section_heading( w, - "synthetic-implementors", "Auto implementors", + "synthetic-implementors", + None, "
", ); } @@ -1248,11 +1248,7 @@ fn item_type_alias(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &c write!(w, "{}", document(cx, it, None, HeadingOffset::H2)); if let Some(inner_type) = &t.inner_type { - write!( - w, - "

\ - Aliased Type§

" - ); + write_section_heading(w, "Aliased Type", "aliased-type", None, ""); match inner_type { clean::TypeAliasInnerType::Enum { variants, is_non_exhaustive } => { @@ -1673,16 +1669,14 @@ fn item_variants( enum_def_id: DefId, ) { let tcx = cx.tcx(); - write!( + write_section_heading( w, - "

\ - Variants{}§\ -

\ - {}\ -
", - document_non_exhaustive_header(it), - document_non_exhaustive(it) + &format!("Variants{}", document_non_exhaustive_header(it)), + "variants", + Some("variants"), + format!("{}
", document_non_exhaustive(it)), ); + let should_show_enum_discriminant = should_show_enum_discriminant(cx, enum_def_id, variants); for (index, variant) in variants.iter_enumerated() { if variant.is_stripped() { @@ -1930,16 +1924,12 @@ fn item_fields( .peekable(); if let None | Some(CtorKind::Fn) = ctor_kind { if fields.peek().is_some() { - write!( - w, - "

\ - {}{}§\ -

\ - {}", + let title = format!( + "{}{}", if ctor_kind.is_none() { "Fields" } else { "Tuple Fields" }, document_non_exhaustive_header(it), - document_non_exhaustive(it) ); + write_section_heading(w, &title, "fields", Some("fields"), document_non_exhaustive(it)); for (index, (field, ty)) in fields.enumerate() { let field_name = field.name.map_or_else(|| index.to_string(), |sym| sym.as_str().to_string()); diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index cd53fcb8b7c16..9c593aa85d987 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -849,11 +849,30 @@ nav.sub { h2.section-header > .anchor { padding-right: 6px; } +a.doc-anchor { + color: var(--main-color); + display: none; + position: absolute; + left: -17px; + /* We add this padding so that when the cursor moves from the heading's text to the anchor, + the anchor doesn't disappear. */ + padding-right: 5px; + /* And this padding is used to make the anchor larger and easier to click on. */ + padding-left: 3px; +} +*:hover > .doc-anchor { + display: block; +} +/* If the first element of the top doc block is a heading, we don't want to ever display its anchor +because of the `[-]` element which would overlap with it. */ +.top-doc > .docblock > *:first-child > .doc-anchor { + display: none !important; +} .main-heading a:hover, .example-wrap .rust a:hover, .all-items a:hover, -.docblock a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover, +.docblock a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover:not(.doc-anchor), .docblock-short a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover, .item-info a { text-decoration: underline; diff --git a/tests/rustdoc-gui/docblock-details.goml b/tests/rustdoc-gui/docblock-details.goml index 8e6d2ba824f73..4b8f5b54fac5d 100644 --- a/tests/rustdoc-gui/docblock-details.goml +++ b/tests/rustdoc-gui/docblock-details.goml @@ -6,7 +6,7 @@ reload: // We first check that the headers in the `.top-doc` doc block still have their // bottom border. -assert-text: (".top-doc .docblock > h3", "Hello") +assert-text: (".top-doc .docblock > h3", "§Hello") assert-css: ( ".top-doc .docblock > h3", {"border-bottom": "1px solid #d2d2d2"}, diff --git a/tests/rustdoc-gui/headers-color.goml b/tests/rustdoc-gui/headers-color.goml index 19185818f407e..80d11c9c849cf 100644 --- a/tests/rustdoc-gui/headers-color.goml +++ b/tests/rustdoc-gui/headers-color.goml @@ -1,4 +1,4 @@ -// This test check for headers text and background colors for the different themes. +// This test check for headings text and background colors for the different themes. define-function: ( "check-colors", @@ -45,7 +45,7 @@ call-function: ( "color": "#c5c5c5", "code_header_color": "#e6e1cf", "focus_background_color": "rgba(255, 236, 164, 0.06)", - "headings_color": "#39afd7", + "headings_color": "#c5c5c5", }, ) call-function: ( @@ -55,7 +55,7 @@ call-function: ( "color": "#ddd", "code_header_color": "#ddd", "focus_background_color": "#494a3d", - "headings_color": "#d2991d", + "headings_color": "#ddd", }, ) call-function: ( @@ -65,6 +65,6 @@ call-function: ( "color": "black", "code_header_color": "black", "focus_background_color": "#fdffd3", - "headings_color": "#3873ad", + "headings_color": "black", }, ) diff --git a/tests/rustdoc-gui/headings-anchor.goml b/tests/rustdoc-gui/headings-anchor.goml new file mode 100644 index 0000000000000..f568caa3b07fe --- /dev/null +++ b/tests/rustdoc-gui/headings-anchor.goml @@ -0,0 +1,32 @@ +// Test to ensure that the headings anchor behave as expected. +go-to: "file://" + |DOC_PATH| + "/test_docs/struct.HeavilyDocumentedStruct.html" +show-text: true + +define-function: ( + "check-heading-anchor", + (heading_id), + block { + // The anchor should not be displayed by default. + assert-css: ("#" + |heading_id| + " .doc-anchor", { "display": "none" }) + // We ensure that hovering the heading makes the anchor visible. + move-cursor-to: "#" + |heading_id| + assert-css: ("#" + |heading_id| + ":hover .doc-anchor", { "display": "block" }) + // We then ensure that moving from the heading to the anchor doesn't make the anchor + // disappear. + move-cursor-to: "#" + |heading_id| + " .doc-anchor" + assert-css: ("#" + |heading_id| + " .doc-anchor:hover", { + "display": "block", + // We also ensure that there is no underline decoration. + "text-decoration-line": "none", + }) + } +) + +move-cursor-to: "#top-doc-prose-title" +// If the top documentation block first element is a heading, we should never display its anchor +// to prevent it from overlapping with the `[-]` element. +assert-css: ("#top-doc-prose-title:hover .doc-anchor", { "display": "none" }) + +call-function: ("check-heading-anchor", ("top-doc-prose-sub-heading")) +call-function: ("check-heading-anchor", ("top-doc-prose-sub-sub-heading")) +call-function: ("check-heading-anchor", ("you-know-the-drill")) diff --git a/tests/rustdoc/disambiguate-anchors-header-29449.rs b/tests/rustdoc/disambiguate-anchors-header-29449.rs index 38a4954fc1395..1388af7df4b2e 100644 --- a/tests/rustdoc/disambiguate-anchors-header-29449.rs +++ b/tests/rustdoc/disambiguate-anchors-header-29449.rs @@ -5,18 +5,23 @@ pub struct Foo; impl Foo { - // @has - '//*[@id="examples"]//a' 'Examples' - // @has - '//*[@id="panics"]//a' 'Panics' + // @has - '//*[@id="examples"]' 'Examples' + // @has - '//*[@id="examples"]/a[@href="#examples"]' '§' + // @has - '//*[@id="panics"]' 'Panics' + // @has - '//*[@id="panics"]/a[@href="#panics"]' '§' /// # Examples /// # Panics pub fn bar() {} - // @has - '//*[@id="examples-1"]//a' 'Examples' + // @has - '//*[@id="examples-1"]' 'Examples' + // @has - '//*[@id="examples-1"]/a[@href="#examples-1"]' '§' /// # Examples pub fn bar_1() {} - // @has - '//*[@id="examples-2"]//a' 'Examples' - // @has - '//*[@id="panics-1"]//a' 'Panics' + // @has - '//*[@id="examples-2"]' 'Examples' + // @has - '//*[@id="examples-2"]/a[@href="#examples-2"]' '§' + // @has - '//*[@id="panics-1"]' 'Panics' + // @has - '//*[@id="panics-1"]/a[@href="#panics-1"]' '§' /// # Examples /// # Panics pub fn bar_2() {} diff --git a/tests/rustdoc/links-in-headings.rs b/tests/rustdoc/links-in-headings.rs new file mode 100644 index 0000000000000..c5bee1a79750c --- /dev/null +++ b/tests/rustdoc/links-in-headings.rs @@ -0,0 +1,14 @@ +#![crate_name = "foo"] + +//! # Heading with [a link](https://a.com) inside +//! +//! And even with +//! +//! ## [multiple](https://b.com) [links](https://c.com) +//! +//! ! + +// @has 'foo/index.html' +// @has - '//h2/a[@href="https://a.com"]' 'a link' +// @has - '//h3/a[@href="https://b.com"]' 'multiple' +// @has - '//h3/a[@href="https://c.com"]' 'links' diff --git a/tests/rustdoc/remove-url-from-headings.rs b/tests/rustdoc/remove-url-from-headings.rs index 599c429a6e1de..8f4770286192e 100644 --- a/tests/rustdoc/remove-url-from-headings.rs +++ b/tests/rustdoc/remove-url-from-headings.rs @@ -1,9 +1,12 @@ +// It actually checks that the link is kept in the headings as expected now. + #![crate_name = "foo"] // @has foo/fn.foo.html -// @!has - '//a[@href="http://a.a"]' '' -// @has - '//a[@href="#implementing-stuff-somewhere"]' 'Implementing stuff somewhere' -// @has - '//a[@href="#another-one-urg"]' 'Another one urg' +// @has - '//a[@href="http://a.a"]' 'stuff' +// @has - '//*[@id="implementing-stuff-somewhere"]' 'Implementing stuff somewhere' +// @has - '//a[@href="http://b.b"]' 'one' +// @has - '//*[@id="another-one-urg"]' 'Another one urg' /// fooo /// @@ -13,5 +16,5 @@ /// /// # Another [one][two] urg /// -/// [two]: http://a.a +/// [two]: http://b.b pub fn foo() {} diff --git a/tests/rustdoc/short-docblock.rs b/tests/rustdoc/short-docblock.rs index 791d3547c9fec..151a42a9c9ee5 100644 --- a/tests/rustdoc/short-docblock.rs +++ b/tests/rustdoc/short-docblock.rs @@ -2,8 +2,9 @@ // @has foo/index.html '//*[@class="desc docblock-short"]' 'fooo' // @!has foo/index.html '//*[@class="desc docblock-short"]/h1' 'fooo' -// @has foo/fn.foo.html '//h2[@id="fooo"]/a[@href="#fooo"]' 'fooo' +// @has foo/fn.foo.html '//h2[@id="fooo"]' 'fooo' +// @has foo/fn.foo.html '//h2[@id="fooo"]/a[@href="#fooo"]' '§' /// # fooo /// /// foo @@ -11,8 +12,9 @@ pub fn foo() {} // @has foo/index.html '//*[@class="desc docblock-short"]' 'mooood' // @!has foo/index.html '//*[@class="desc docblock-short"]/h2' 'mooood' -// @has foo/foo/index.html '//h3[@id="mooood"]/a[@href="#mooood"]' 'mooood' +// @has foo/foo/index.html '//h3[@id="mooood"]' 'mooood' +// @has foo/foo/index.html '//h3[@id="mooood"]/a[@href="#mooood"]' '§' /// ## mooood /// /// foo mod