Skip to content

[rustdoc] Add new --book-location option to add a link to associated guide and generate it if local #139769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
623 changes: 591 additions & 32 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/doc/rustdoc/src/unstable-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -776,3 +776,9 @@ will be split as follows:
"you today?",
]
```

## Bundling or linking to a guide

You can generate a link to a guide or bundle an `mdbook` guide using the `--book-location`
command line argument. It accepts either a URL or a path. If a path is provided, the book will
be built with `mdbook`.
1 change: 1 addition & 0 deletions src/librustdoc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ askama = { version = "0.13", default-features = false, features = ["alloc", "con
base64 = "0.21.7"
itertools = "0.12"
indexmap = "2"
mdbook = "0.4.48"
minifier = { version = "0.3.5", default-features = false }
pulldown-cmark-escape = { version = "0.11.0", features = ["simd"] }
regex = "1"
Expand Down
24 changes: 24 additions & 0 deletions src/librustdoc/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,22 @@ impl fmt::Debug for Options {
}
}

#[derive(Clone, Debug)]
pub(crate) enum PathOrUrl {
Path(PathBuf),
Url(String),
}

impl PathOrUrl {
fn new(s: String) -> Self {
if s.starts_with("https://") || s.starts_with("http://") {
Self::Url(s)
} else {
Self::Path(s.into())
}
}
}

/// Configuration options for the HTML page-creation process.
#[derive(Clone, Debug)]
pub(crate) struct RenderOptions {
Expand Down Expand Up @@ -305,6 +321,10 @@ pub(crate) struct RenderOptions {
pub(crate) parts_out_dir: Option<PathToParts>,
/// disable minification of CSS/JS
pub(crate) disable_minification: bool,
/// Location where the associated book source is located.
pub(crate) book_source: Option<PathOrUrl>,
/// Location where the associated book is generated. None if it's not local.
pub(crate) book_target: Option<PathOrUrl>,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -808,6 +828,7 @@ impl Options {
rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref());

let disable_minification = matches.opt_present("disable-minification");
let book_source = matches.opt_str("book-location").map(PathOrUrl::new);

let options = Options {
bin_crate,
Expand Down Expand Up @@ -886,6 +907,9 @@ impl Options {
include_parts_dir,
parts_out_dir,
disable_minification,
book_source,
// This field is updated in `generate_book`.
book_target: None,
};
Some((input, options, render_options))
}
Expand Down
1 change: 1 addition & 0 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,7 @@ fn is_default_id(id: &str) -> bool {
| "copy-path"
| "rustdoc-toc"
| "rustdoc-modnav"
| "book-loc"
// This is the list of IDs used by rustdoc sections (but still generated by
// rustdoc).
| "fields"
Expand Down
4 changes: 4 additions & 0 deletions src/librustdoc/html/render/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub(crate) struct SharedContext<'tcx> {
/// Controls whether we read / write to cci files in the doc root. Defaults read=true,
/// write=true
should_merge: ShouldMerge,
pub(crate) book_target: Option<crate::config::PathOrUrl>,
}

impl SharedContext<'_> {
Expand Down Expand Up @@ -489,6 +490,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
call_locations,
no_emit_shared,
html_no_source,
book_target,
..
} = options;

Expand Down Expand Up @@ -575,6 +577,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
cache,
call_locations,
should_merge: options.should_merge,
book_target,
};

let dst = output;
Expand Down Expand Up @@ -646,6 +649,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
parent_is_crate: false,
blocks: vec![blocks],
path: String::new(),
book_location: shared.book_target.as_ref(),
};

bar.render_into(&mut sidebar).unwrap();
Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/html/render/sidebar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub(super) struct Sidebar<'a> {
pub(super) is_mod: bool,
pub(super) blocks: Vec<LinkBlock<'a>>,
pub(super) path: String,
pub(super) book_location: Option<&'a crate::config::PathOrUrl>,
}

impl Sidebar<'_> {
Expand Down Expand Up @@ -194,6 +195,7 @@ pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Str
parent_is_crate: sidebar_path.len() == 1,
blocks,
path,
book_location: cx.shared.book_target.as_ref(),
};
sidebar.render_into(buffer).unwrap();
}
Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/html/static/css/noscript.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ nav.sub {
--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
--sidebar-resizer-hover: hsl(207, 90%, 66%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--book-img-filter: invert(0%);
}
/* End theme: light */

Expand Down Expand Up @@ -244,6 +245,7 @@ nav.sub {
--scrape-example-code-wrapper-background-end: rgba(53, 53, 53, 0);
--sidebar-resizer-hover: hsl(207, 30%, 54%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--book-img-filter: invert(80%);
}
/* End theme: dark */
}
25 changes: 25 additions & 0 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* ignore-tidy-filelength */
/* When static files are updated, their suffixes need to be updated.
1. In the top directory run:
./x.py doc --stage 1 library/core
Expand Down Expand Up @@ -836,6 +837,27 @@ ul.block, .block li, .block ul {
margin-bottom: 1rem;
}

.sidebar .book {
display: flex;
}

.book::before {
width: 1em;
height: 1em;
/* Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License -
https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.*/
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">\
<path d="M96 0C43 0 0 43 0 96L0 416c0 53 43 96 96 96l288 0 32 0c17.7 0 32-14.3 \
32-32s-14.3-32-32-32l0-64c17.7 0 32-14.3 32-32l0-320c0-17.7-14.3-32-32-32L384 0 96 0zm0 \
384l256 0 0 64L96 448c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16l192 \
0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16zm16 48l192 0c8.8 0 16 7.2 16 \
16s-7.2 16-16 16l-192 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/></svg>');
display: block;
margin-right: 6px;
padding-top: 3px;
filter: var(--book-img-filter);
}

.mobile-topbar {
display: none;
}
Expand Down Expand Up @@ -2989,6 +3011,7 @@ by default.
--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
--sidebar-resizer-hover: hsl(207, 90%, 66%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--book-img-filter: invert(0%);
}
/* End theme: light */

Expand Down Expand Up @@ -3097,6 +3120,7 @@ by default.
--scrape-example-code-wrapper-background-end: rgba(53, 53, 53, 0);
--sidebar-resizer-hover: hsl(207, 30%, 54%);
--sidebar-resizer-active: hsl(207, 90%, 54%);
--book-img-filter: invert(80%);
}
/* End theme: dark */

Expand Down Expand Up @@ -3209,6 +3233,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
--scrape-example-code-wrapper-background-end: rgba(15, 20, 25, 0);
--sidebar-resizer-hover: hsl(34, 50%, 33%);
--sidebar-resizer-active: hsl(34, 100%, 66%);
--book-img-filter: invert(100%);
}

:root[data-theme="ayu"] h1,
Expand Down
14 changes: 14 additions & 0 deletions src/librustdoc/html/templates/sidebar.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
<div class="sidebar-elems">
{% if is_crate %}
{% if let Some(book_location) = book_location %}
<ul class="block"> {# #}
<li> {# #}
<h3> {# #}
<a id="book-loc" class="book" href="
{% match book_location %}
{% when crate::config::PathOrUrl::Path(s) %}{{s.display()}}
{% when crate::config::PathOrUrl::Url(s) %}{{s}}
{% endmatch %}
">Book</a> {# #}
</h3> {# #}
</li> {# #}
</ul>
{% endif %}
<ul class="block"> {# #}
<li><a id="all-types" href="all.html">All Items</a></li> {# #}
</ul>
Expand Down
45 changes: 44 additions & 1 deletion src/librustdoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,14 @@ fn opts() -> Vec<RustcOptGroup> {
"disable the minification of CSS/JS files (perma-unstable, do not use with cached files)",
"",
),
opt(
Unstable,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camelid: Just checked and the option is indeed unstable. 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I was just confused because you said it'd need an FCP, which I thought we only required for stabilizations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, well it's a big change in rustdoc (as is, scope expansion), so better be sure everyone agrees with this direction.

Opt,
"",
"book-location",
"URL where the book is hosted or the folder where the mdBook source is located",
"PATH or URL",
),
// deprecated / removed options
opt(
Stable,
Expand Down Expand Up @@ -751,6 +759,37 @@ fn run_renderer<'tcx, T: formats::FormatRenderer<'tcx>>(
}
}

fn generate_book(render_options: &mut config::RenderOptions) -> Result<(), String> {
let book_dir = match &render_options.book_source {
Some(config::PathOrUrl::Path(book_dir)) => book_dir,
other => {
render_options.book_target = other.clone();
return Ok(());
}
};
if !book_dir.is_dir() {
return Err(format!(
"`{}` is not a folder, expected a folder or a URL for `--book-location` argument",
book_dir.display(),
));
}
let mut book = match mdbook::MDBook::load(&book_dir) {
Ok(book) => book,
Err(error) => return Err(format!("failed to load book: {error:?}")),
};
let output_dir = render_options.output.join("doc-book");
let book_target = output_dir.join("index.html");
book.config.build.build_dir = output_dir;
if let Err(error) = book.build() {
return Err(format!(
"failed to generate book into `{}`: {error:?}",
book.config.build.build_dir.display()
));
}
render_options.book_target = Some(config::PathOrUrl::Path(book_target));
Ok(())
}

/// Renders and writes cross-crate info files, like the search index. This function exists so that
/// we can run rustdoc without a crate root in the `--merge=finalize` mode. Cross-crate info files
/// discovered via `--include-parts-dir` are combined and written to the doc root.
Expand Down Expand Up @@ -803,7 +842,7 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {

// Note that we discard any distinction between different non-zero exit
// codes from `from_matches` here.
let (input, options, render_options) =
let (input, options, mut render_options) =
match config::Options::from_matches(early_dcx, &matches, args) {
Some(opts) => opts,
None => return,
Expand Down Expand Up @@ -867,6 +906,10 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {
let scrape_examples_options = options.scrape_examples_options.clone();
let bin_crate = options.bin_crate;

if let Err(error) = generate_book(&mut render_options) {
early_dcx.early_fatal(error);
}

let config = core::create_config(input, options, &render_options);

let registered_lints = config.register_lints.is_some();
Expand Down
2 changes: 2 additions & 0 deletions src/tools/tidy/src/deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const LICENSES: &[&str] = &[
"Apache-2.0",
"Apache-2.0/MIT",
"BSD-2-Clause OR Apache-2.0 OR MIT", // zerocopy
"CC0-1.0", // notify
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who's responsible for this license exceptions list? Feels like it'd be good to check that these exceptions are OK with someone who's more experienced in this area.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the right person for this. cc @wesleywiser :)

"ISC",
"MIT / Apache-2.0",
"MIT AND (MIT OR Apache-2.0)",
Expand All @@ -38,6 +39,7 @@ const LICENSES: &[&str] = &[
"MIT OR Zlib OR Apache-2.0", // miniz_oxide
"MIT",
"MIT/Apache-2.0",
"MPL-2.0", // mdbook
"Unicode-3.0", // icu4x
"Unicode-DFS-2016", // tinystr
"Unlicense OR MIT",
Expand Down
3 changes: 3 additions & 0 deletions tests/run-make/rustdoc-default-output/output-default.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ Options:
--disable-minification
disable the minification of CSS/JS files
(perma-unstable, do not use with cached files)
--book-location PATH or URL
URL where the book is hosted or the folder where the
mdBook source is located
--plugin-path DIR
removed, see issue #44136
<https://github.com/rust-lang/rust/issues/44136> for
Expand Down
7 changes: 7 additions & 0 deletions tests/rustdoc/book-location.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//@ compile-flags: -Zunstable-options --book-location https://somewhere.world
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have another (run-make) test that tests the mdbook integration, especially since that's where most of the complexity of this PR is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you want to test exactly?


#![crate_name = "foo"]

//@ has 'foo/index.html'
//@ has - '//*[@id="book-loc"]' 'Book'
//@ has - '//*[@href="https://somewhere.world"]' 'Book'
Loading