Skip to content

Commit 50577b1

Browse files
committed
rustdoc: add doc_link_canonical feature
1 parent 7ba34c7 commit 50577b1

File tree

11 files changed

+55
-7
lines changed

11 files changed

+55
-7
lines changed

compiler/rustc_ast_passes/src/feature_gate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ impl<'a> Visitor<'a> for PostExpansionVisitor<'a> {
182182

183183
gate_doc!(
184184
"experimental" {
185+
html_link_canonical => doc_link_canonical
185186
cfg => doc_cfg
186187
cfg_hide => doc_cfg_hide
187188
masked => doc_masked

compiler/rustc_feature/src/unstable.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ declare_features! (
479479
(unstable, doc_cfg, "1.21.0", Some(43781)),
480480
/// Allows `#[doc(cfg_hide(...))]`.
481481
(unstable, doc_cfg_hide, "1.57.0", Some(43781)),
482+
/// Allows `#![doc(html_link_canonical]`
483+
(unstable, doc_link_canonical, "1.88.0", Some(143139)),
482484
/// Allows `#[doc(masked)]`.
483485
(unstable, doc_masked, "1.21.0", Some(44027)),
484486
/// Allows `dyn* Trait` objects.

compiler/rustc_passes/src/check_attr.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,7 @@ impl<'tcx> CheckAttrVisitor<'tcx> {
13291329

13301330
Some(
13311331
sym::html_favicon_url
1332+
| sym::html_link_canonical
13321333
| sym::html_logo_url
13331334
| sym::html_playground_url
13341335
| sym::issue_tracker_base_url

compiler/rustc_span/src/symbol.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,7 @@ symbols! {
864864
doc_cfg,
865865
doc_cfg_hide,
866866
doc_keyword,
867+
doc_link_canonical,
867868
doc_masked,
868869
doc_notable_trait,
869870
doc_primitive,
@@ -1134,6 +1135,7 @@ symbols! {
11341135
homogeneous_aggregate,
11351136
host,
11361137
html_favicon_url,
1138+
html_link_canonical,
11371139
html_logo_url,
11381140
html_no_source,
11391141
html_playground_url,

src/librustdoc/html/layout.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ pub(crate) struct Layout {
2020
pub(crate) css_file_extension: Option<PathBuf>,
2121
/// If true, then scrape-examples.js will be included in the output HTML file
2222
pub(crate) scrape_examples_extension: bool,
23+
/// if present, insert a rel="canonical" link with this prefix.
24+
pub(crate) link_canonical: Option<String>,
2325
}
2426

2527
pub(crate) struct Page<'a> {
28+
/// url relative to documentation bundle root.
29+
pub(crate) relative_url: Option<String>,
2630
pub(crate) title: &'a str,
2731
pub(crate) css_class: &'a str,
2832
pub(crate) root_path: &'a str,
@@ -47,7 +51,6 @@ struct PageLayout<'a> {
4751
static_root_path: String,
4852
page: &'a Page<'a>,
4953
layout: &'a Layout,
50-
5154
files: &'static StaticFiles,
5255

5356
themes: Vec<String>,

src/librustdoc/html/render/context.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub(crate) struct Context<'tcx> {
4949
/// The current destination folder of where HTML artifacts should be placed.
5050
/// This changes as the context descends into the module hierarchy.
5151
pub(crate) dst: PathBuf,
52+
/// Initial length of `dst`.
53+
///
54+
/// Used to split `dst` into (doc bundle path, relative path)
55+
dst_prefix_doc_bundle: usize,
5256
/// Tracks section IDs for `Deref` targets so they match in both the main
5357
/// body and the sidebar.
5458
pub(super) deref_id_map: RefCell<DefIdMap<String>>,
@@ -180,13 +184,18 @@ impl<'tcx> Context<'tcx> {
180184
self.id_map.borrow_mut().derive(id)
181185
}
182186

187+
pub(crate) fn dst_relative_to_doc_bundle_root(&self) -> &str {
188+
str::from_utf8(&self.dst.as_os_str().as_encoded_bytes()[self.dst_prefix_doc_bundle..])
189+
.expect("non-utf8 in name generated by rustdoc").trim_start_matches('/')
190+
}
191+
183192
/// String representation of how to get back to the root path of the 'doc/'
184193
/// folder in terms of a relative URL.
185194
pub(super) fn root_path(&self) -> String {
186195
"../".repeat(self.current.len())
187196
}
188197

189-
fn render_item(&mut self, it: &clean::Item, is_module: bool) -> String {
198+
fn render_item(&mut self, it: &clean::Item, is_module: bool, relative_url: Option<String>) -> String {
190199
let mut render_redirect_pages = self.info.render_redirect_pages;
191200
// If the item is stripped but inlined, links won't point to the item so no need to generate
192201
// a file for it.
@@ -238,6 +247,7 @@ impl<'tcx> Context<'tcx> {
238247
if !render_redirect_pages {
239248
let content = print_item(self, it);
240249
let page = layout::Page {
250+
relative_url,
241251
css_class: tyname_s,
242252
root_path: &self.root_path(),
243253
static_root_path: self.shared.static_root_path.as_deref(),
@@ -511,6 +521,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
511521
krate_version: krate_version.to_string(),
512522
css_file_extension: extension_css,
513523
scrape_examples_extension: !call_locations.is_empty(),
524+
link_canonical: None,
514525
};
515526
let mut issue_tracker_base_url = None;
516527
let mut include_sources = !html_no_source;
@@ -537,6 +548,14 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
537548
(Some(sym::html_no_source), None) if attr.is_word() => {
538549
include_sources = false;
539550
}
551+
(Some(sym::html_link_canonical), Some(s)) => {
552+
let mut s = s.to_string();
553+
// ensure trailing slash
554+
if !s.ends_with('/') {
555+
s.push('/');
556+
}
557+
layout.link_canonical = Some(s);
558+
}
540559
_ => {}
541560
}
542561
}
@@ -579,6 +598,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
579598

580599
let mut cx = Context {
581600
current: Vec::new(),
601+
dst_prefix_doc_bundle: dst.as_os_str().len(),
582602
dst,
583603
id_map: RefCell::new(id_map),
584604
deref_id_map: Default::default(),
@@ -626,6 +646,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
626646
css_class: "mod sys",
627647
root_path: "../",
628648
static_root_path: shared.static_root_path.as_deref(),
649+
relative_url: None,
629650
description: "List of all items in this crate",
630651
resource_suffix: &shared.resource_suffix,
631652
rust_logo: has_doc_flag(self.tcx(), LOCAL_CRATE.as_def_id(), sym::rust_logo),
@@ -787,7 +808,8 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
787808
info!("Recursing into {}", self.dst.display());
788809

789810
if !item.is_stripped() {
790-
let buf = self.render_item(item, true);
811+
let rel_path = format!("{}/index.html", self.dst_relative_to_doc_bundle_root());
812+
let buf = self.render_item(item, true, Some(rel_path));
791813
// buf will be empty if the module is stripped and there is no redirect for it
792814
if !buf.is_empty() {
793815
self.shared.ensure_dir(&self.dst)?;
@@ -842,12 +864,13 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
842864
self.info.render_redirect_pages = item.is_stripped();
843865
}
844866

845-
let buf = self.render_item(&item, false);
867+
let name = item.name.as_ref().unwrap();
868+
let item_type = item.type_();
869+
let file_name = print_item_path(item_type, name.as_str()).to_string();
870+
let rel_path = format!("{}/{file_name}", self.dst_relative_to_doc_bundle_root());
871+
let buf = self.render_item(&item, false, Some(rel_path));
846872
// buf will be empty if the item is stripped and there is no redirect for it
847873
if !buf.is_empty() {
848-
let name = item.name.as_ref().unwrap();
849-
let item_type = item.type_();
850-
let file_name = print_item_path(item_type, name.as_str()).to_string();
851874
self.shared.ensure_dir(&self.dst)?;
852875
let joint_dst = self.dst.join(&file_name);
853876
self.shared.fs.write(joint_dst, buf)?;

src/librustdoc/html/render/write_shared.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ impl CratesIndexPart {
428428
title: "Index of crates",
429429
css_class: "mod sys",
430430
root_path: "./",
431+
relative_url: None,
431432
static_root_path: cx.shared.static_root_path.as_deref(),
432433
description: "List of crates",
433434
resource_suffix: &cx.shared.resource_suffix,

src/librustdoc/html/sources.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ impl SourceCollector<'_, '_> {
232232
title: &title,
233233
css_class: "src",
234234
root_path: &root_path,
235+
relative_url: None,
235236
static_root_path: shared.static_root_path.as_deref(),
236237
description: &desc,
237238
resource_suffix: &shared.resource_suffix,

src/librustdoc/html/templates/page.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@
6262
<link rel="icon" type="image/svg+xml" {#+ #}
6363
href="{{static_root_path|safe}}{{files.rust_favicon_svg}}">
6464
{% endif %}
65+
{% if layout.link_canonical.is_some() && page.relative_url.is_some() %}
66+
<link rel="canonical" href="{{layout.link_canonical.as_ref().unwrap()|safe}}{{page.relative_url.as_ref().unwrap()|safe}}">
67+
{% endif %}
6568
{{ layout.external_html.in_header|safe }}
6669
</head> {# #}
6770
<body class="rustdoc {{+page.css_class}}"> {# #}

src/librustdoc/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ fn opts() -> Vec<RustcOptGroup> {
711711
"removed, see issue #44136 <https://github.com/rust-lang/rust/issues/44136> for more information",
712712
"[rust]",
713713
),
714+
714715
]
715716
}
716717

tests/rustdoc/link-canonical.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#![crate_name = "foo"]
2+
#![feature(doc_link_canonical)]
3+
#![doc(html_link_canonical = "https://foo.example/")]
4+
5+
//@ has 'foo/index.html'
6+
//@ has - '//head/link[@rel="canonical"][@href="https://foo.example/foo/index.html"]' ''
7+
8+
//@ has 'foo/struct.FooBaz.html'
9+
//@ has - '//head/link[@rel="canonical"][@href="https://foo.example/foo/struct.FooBaz.html"]' ''
10+
pub struct FooBaz;

0 commit comments

Comments
 (0)