Skip to content

Commit 0a25ca7

Browse files
committed
rustdoc: add doc_link_canonical feature
1 parent 7ba34c7 commit 0a25ca7

File tree

10 files changed

+60
-7
lines changed

10 files changed

+60
-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: 35 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,24 @@ 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")
190+
.trim_start_matches('/')
191+
}
192+
183193
/// String representation of how to get back to the root path of the 'doc/'
184194
/// folder in terms of a relative URL.
185195
pub(super) fn root_path(&self) -> String {
186196
"../".repeat(self.current.len())
187197
}
188198

189-
fn render_item(&mut self, it: &clean::Item, is_module: bool) -> String {
199+
fn render_item(
200+
&mut self,
201+
it: &clean::Item,
202+
is_module: bool,
203+
relative_url: Option<String>,
204+
) -> String {
190205
let mut render_redirect_pages = self.info.render_redirect_pages;
191206
// If the item is stripped but inlined, links won't point to the item so no need to generate
192207
// a file for it.
@@ -238,6 +253,7 @@ impl<'tcx> Context<'tcx> {
238253
if !render_redirect_pages {
239254
let content = print_item(self, it);
240255
let page = layout::Page {
256+
relative_url,
241257
css_class: tyname_s,
242258
root_path: &self.root_path(),
243259
static_root_path: self.shared.static_root_path.as_deref(),
@@ -511,6 +527,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
511527
krate_version: krate_version.to_string(),
512528
css_file_extension: extension_css,
513529
scrape_examples_extension: !call_locations.is_empty(),
530+
link_canonical: None,
514531
};
515532
let mut issue_tracker_base_url = None;
516533
let mut include_sources = !html_no_source;
@@ -537,6 +554,14 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
537554
(Some(sym::html_no_source), None) if attr.is_word() => {
538555
include_sources = false;
539556
}
557+
(Some(sym::html_link_canonical), Some(s)) => {
558+
let mut s = s.to_string();
559+
// ensure trailing slash
560+
if !s.ends_with('/') {
561+
s.push('/');
562+
}
563+
layout.link_canonical = Some(s);
564+
}
540565
_ => {}
541566
}
542567
}
@@ -579,6 +604,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
579604

580605
let mut cx = Context {
581606
current: Vec::new(),
607+
dst_prefix_doc_bundle: dst.as_os_str().len(),
582608
dst,
583609
id_map: RefCell::new(id_map),
584610
deref_id_map: Default::default(),
@@ -626,6 +652,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
626652
css_class: "mod sys",
627653
root_path: "../",
628654
static_root_path: shared.static_root_path.as_deref(),
655+
relative_url: None,
629656
description: "List of all items in this crate",
630657
resource_suffix: &shared.resource_suffix,
631658
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> {
787814
info!("Recursing into {}", self.dst.display());
788815

789816
if !item.is_stripped() {
790-
let buf = self.render_item(item, true);
817+
let rel_path = format!("{}/index.html", self.dst_relative_to_doc_bundle_root());
818+
let buf = self.render_item(item, true, Some(rel_path));
791819
// buf will be empty if the module is stripped and there is no redirect for it
792820
if !buf.is_empty() {
793821
self.shared.ensure_dir(&self.dst)?;
@@ -842,12 +870,13 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
842870
self.info.render_redirect_pages = item.is_stripped();
843871
}
844872

845-
let buf = self.render_item(&item, false);
873+
let name = item.name.as_ref().unwrap();
874+
let item_type = item.type_();
875+
let file_name = print_item_path(item_type, name.as_str()).to_string();
876+
let rel_path = format!("{}/{file_name}", self.dst_relative_to_doc_bundle_root());
877+
let buf = self.render_item(&item, false, Some(rel_path));
846878
// buf will be empty if the item is stripped and there is no redirect for it
847879
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();
851880
self.shared.ensure_dir(&self.dst)?;
852881
let joint_dst = self.dst.join(&file_name);
853882
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}}"> {# #}

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)