From 0a25ca7c23db32d0e57522de1111ad9c1b279b81 Mon Sep 17 00:00:00 2001 From: binarycat Date: Sat, 28 Jun 2025 15:20:56 -0500 Subject: [PATCH 1/2] rustdoc: add doc_link_canonical feature --- compiler/rustc_ast_passes/src/feature_gate.rs | 1 + compiler/rustc_feature/src/unstable.rs | 2 + compiler/rustc_passes/src/check_attr.rs | 1 + compiler/rustc_span/src/symbol.rs | 2 + src/librustdoc/html/layout.rs | 5 ++- src/librustdoc/html/render/context.rs | 41 ++++++++++++++++--- src/librustdoc/html/render/write_shared.rs | 1 + src/librustdoc/html/sources.rs | 1 + src/librustdoc/html/templates/page.html | 3 ++ tests/rustdoc/link-canonical.rs | 10 +++++ 10 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 tests/rustdoc/link-canonical.rs diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index 1ec56868f378f..082955d71a36f 100644 --- a/compiler/rustc_ast_passes/src/feature_gate.rs +++ b/compiler/rustc_ast_passes/src/feature_gate.rs @@ -182,6 +182,7 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> { gate_doc!( "experimental" { + html_link_canonical => doc_link_canonical cfg => doc_cfg cfg_hide => doc_cfg_hide masked => doc_masked diff --git a/compiler/rustc_feature/src/unstable.rs b/compiler/rustc_feature/src/unstable.rs index 719ba597da190..7d8794ebedcad 100644 --- a/compiler/rustc_feature/src/unstable.rs +++ b/compiler/rustc_feature/src/unstable.rs @@ -479,6 +479,8 @@ declare_features! ( (unstable, doc_cfg, "1.21.0", Some(43781)), /// Allows `#[doc(cfg_hide(...))]`. (unstable, doc_cfg_hide, "1.57.0", Some(43781)), + /// Allows `#![doc(html_link_canonical]` + (unstable, doc_link_canonical, "1.88.0", Some(143139)), /// Allows `#[doc(masked)]`. (unstable, doc_masked, "1.21.0", Some(44027)), /// Allows `dyn* Trait` objects. diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index 877bb9be28961..a25d932718ecd 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -1329,6 +1329,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> { Some( sym::html_favicon_url + | sym::html_link_canonical | sym::html_logo_url | sym::html_playground_url | sym::issue_tracker_base_url diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 0045801c9f6d8..2e20381635754 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -864,6 +864,7 @@ symbols! { doc_cfg, doc_cfg_hide, doc_keyword, + doc_link_canonical, doc_masked, doc_notable_trait, doc_primitive, @@ -1134,6 +1135,7 @@ symbols! { homogeneous_aggregate, host, html_favicon_url, + html_link_canonical, html_logo_url, html_no_source, html_playground_url, diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index 50320cb231d2f..51bcfa558c4c0 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -20,9 +20,13 @@ pub(crate) struct Layout { pub(crate) css_file_extension: Option, /// If true, then scrape-examples.js will be included in the output HTML file pub(crate) scrape_examples_extension: bool, + /// if present, insert a rel="canonical" link with this prefix. + pub(crate) link_canonical: Option, } pub(crate) struct Page<'a> { + /// url relative to documentation bundle root. + pub(crate) relative_url: Option, pub(crate) title: &'a str, pub(crate) css_class: &'a str, pub(crate) root_path: &'a str, @@ -47,7 +51,6 @@ struct PageLayout<'a> { static_root_path: String, page: &'a Page<'a>, layout: &'a Layout, - files: &'static StaticFiles, themes: Vec, diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 3b4dae841ee7f..64b603d7fed96 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -49,6 +49,10 @@ pub(crate) struct Context<'tcx> { /// The current destination folder of where HTML artifacts should be placed. /// This changes as the context descends into the module hierarchy. pub(crate) dst: PathBuf, + /// Initial length of `dst`. + /// + /// Used to split `dst` into (doc bundle path, relative path) + dst_prefix_doc_bundle: usize, /// Tracks section IDs for `Deref` targets so they match in both the main /// body and the sidebar. pub(super) deref_id_map: RefCell>, @@ -180,13 +184,24 @@ impl<'tcx> Context<'tcx> { self.id_map.borrow_mut().derive(id) } + pub(crate) fn dst_relative_to_doc_bundle_root(&self) -> &str { + str::from_utf8(&self.dst.as_os_str().as_encoded_bytes()[self.dst_prefix_doc_bundle..]) + .expect("non-utf8 in name generated by rustdoc") + .trim_start_matches('/') + } + /// String representation of how to get back to the root path of the 'doc/' /// folder in terms of a relative URL. pub(super) fn root_path(&self) -> String { "../".repeat(self.current.len()) } - fn render_item(&mut self, it: &clean::Item, is_module: bool) -> String { + fn render_item( + &mut self, + it: &clean::Item, + is_module: bool, + relative_url: Option, + ) -> String { let mut render_redirect_pages = self.info.render_redirect_pages; // If the item is stripped but inlined, links won't point to the item so no need to generate // a file for it. @@ -238,6 +253,7 @@ impl<'tcx> Context<'tcx> { if !render_redirect_pages { let content = print_item(self, it); let page = layout::Page { + relative_url, css_class: tyname_s, root_path: &self.root_path(), static_root_path: self.shared.static_root_path.as_deref(), @@ -511,6 +527,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { krate_version: krate_version.to_string(), css_file_extension: extension_css, scrape_examples_extension: !call_locations.is_empty(), + link_canonical: None, }; let mut issue_tracker_base_url = None; let mut include_sources = !html_no_source; @@ -537,6 +554,14 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { (Some(sym::html_no_source), None) if attr.is_word() => { include_sources = false; } + (Some(sym::html_link_canonical), Some(s)) => { + let mut s = s.to_string(); + // ensure trailing slash + if !s.ends_with('/') { + s.push('/'); + } + layout.link_canonical = Some(s); + } _ => {} } } @@ -579,6 +604,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { let mut cx = Context { current: Vec::new(), + dst_prefix_doc_bundle: dst.as_os_str().len(), dst, id_map: RefCell::new(id_map), deref_id_map: Default::default(), @@ -626,6 +652,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { css_class: "mod sys", root_path: "../", static_root_path: shared.static_root_path.as_deref(), + relative_url: None, description: "List of all items in this crate", resource_suffix: &shared.resource_suffix, rust_logo: has_doc_flag(self.tcx(), LOCAL_CRATE.as_def_id(), sym::rust_logo), @@ -787,7 +814,8 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { info!("Recursing into {}", self.dst.display()); if !item.is_stripped() { - let buf = self.render_item(item, true); + let rel_path = format!("{}/index.html", self.dst_relative_to_doc_bundle_root()); + let buf = self.render_item(item, true, Some(rel_path)); // buf will be empty if the module is stripped and there is no redirect for it if !buf.is_empty() { self.shared.ensure_dir(&self.dst)?; @@ -842,12 +870,13 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { self.info.render_redirect_pages = item.is_stripped(); } - let buf = self.render_item(&item, false); + let name = item.name.as_ref().unwrap(); + let item_type = item.type_(); + let file_name = print_item_path(item_type, name.as_str()).to_string(); + let rel_path = format!("{}/{file_name}", self.dst_relative_to_doc_bundle_root()); + let buf = self.render_item(&item, false, Some(rel_path)); // buf will be empty if the item is stripped and there is no redirect for it if !buf.is_empty() { - let name = item.name.as_ref().unwrap(); - let item_type = item.type_(); - let file_name = print_item_path(item_type, name.as_str()).to_string(); self.shared.ensure_dir(&self.dst)?; let joint_dst = self.dst.join(&file_name); self.shared.fs.write(joint_dst, buf)?; diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index 606a911390870..b1ba115cb7191 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -428,6 +428,7 @@ impl CratesIndexPart { title: "Index of crates", css_class: "mod sys", root_path: "./", + relative_url: None, static_root_path: cx.shared.static_root_path.as_deref(), description: "List of crates", resource_suffix: &cx.shared.resource_suffix, diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index c34b31542697d..c62ae5572d003 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -232,6 +232,7 @@ impl SourceCollector<'_, '_> { title: &title, css_class: "src", root_path: &root_path, + relative_url: None, static_root_path: shared.static_root_path.as_deref(), description: &desc, resource_suffix: &shared.resource_suffix, diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html index 7af99e7097c37..4c06c1422bab3 100644 --- a/src/librustdoc/html/templates/page.html +++ b/src/librustdoc/html/templates/page.html @@ -62,6 +62,9 @@ {% endif %} + {% if layout.link_canonical.is_some() && page.relative_url.is_some() %} + + {% endif %} {{ layout.external_html.in_header|safe }} {# #} {# #} diff --git a/tests/rustdoc/link-canonical.rs b/tests/rustdoc/link-canonical.rs new file mode 100644 index 0000000000000..2244413f8d0fe --- /dev/null +++ b/tests/rustdoc/link-canonical.rs @@ -0,0 +1,10 @@ +#![crate_name = "foo"] +#![feature(doc_link_canonical)] +#![doc(html_link_canonical = "https://foo.example/")] + +//@ has 'foo/index.html' +//@ has - '//head/link[@rel="canonical"][@href="https://foo.example/foo/index.html"]' '' + +//@ has 'foo/struct.FooBaz.html' +//@ has - '//head/link[@rel="canonical"][@href="https://foo.example/foo/struct.FooBaz.html"]' '' +pub struct FooBaz; From 6ee58eb0a102e57f806c3960d5cb41c3e475ec21 Mon Sep 17 00:00:00 2001 From: binarycat Date: Sat, 28 Jun 2025 15:45:46 -0500 Subject: [PATCH 2/2] add feature gate test for doc(html_link_canonical) --- .../feature-gate-doc-link-canonical.rs | 4 ++++ .../feature-gate-doc-link-canonical.stderr | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/ui/feature-gates/feature-gate-doc-link-canonical.rs create mode 100644 tests/ui/feature-gates/feature-gate-doc-link-canonical.stderr diff --git a/tests/ui/feature-gates/feature-gate-doc-link-canonical.rs b/tests/ui/feature-gates/feature-gate-doc-link-canonical.rs new file mode 100644 index 0000000000000..e5305a853c748 --- /dev/null +++ b/tests/ui/feature-gates/feature-gate-doc-link-canonical.rs @@ -0,0 +1,4 @@ +#![doc(html_link_canonical = "http://example.com/")] +//~^ ERROR `#[doc(html_link_canonical)]` is experimental + +fn main() {} diff --git a/tests/ui/feature-gates/feature-gate-doc-link-canonical.stderr b/tests/ui/feature-gates/feature-gate-doc-link-canonical.stderr new file mode 100644 index 0000000000000..ac555b4825140 --- /dev/null +++ b/tests/ui/feature-gates/feature-gate-doc-link-canonical.stderr @@ -0,0 +1,13 @@ +error[E0658]: `#[doc(html_link_canonical)]` is experimental + --> $DIR/feature-gate-doc-link-canonical.rs:1:1 + | +LL | #![doc(html_link_canonical = "http://example.com/")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: see issue #143139 for more information + = help: add `#![feature(doc_link_canonical)]` to the crate attributes to enable + = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0658`.