Skip to content

Commit e7b5a0c

Browse files
committed
Add unstable rustdoc option --crate-list-heading.
When used, this option creates a subheading within the sidebar “Crates” list for this crate and all crates which pass the same value. These headings are displayed in alphabetical order; crates without the option, or with it set to the empty string, always appear first. The intended use of this option is to allow Cargo to automatically separate a workspace’s crate list into the workspace’s own crates and their external dependencies. Authors could also use RUSTDOCFLAGS or other means to create custom headings for their documentation builds.
1 parent c27af6b commit e7b5a0c

File tree

8 files changed

+116
-23
lines changed

8 files changed

+116
-23
lines changed

src/librustdoc/config.rs

+6
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ pub(crate) struct RenderOptions {
241241
/// What sorting mode to use for module pages.
242242
/// `ModuleSorting::Alphabetical` by default.
243243
pub(crate) module_sorting: ModuleSorting,
244+
/// In the sidebar list of all crates, put the current crate(s) under this heading.
245+
/// To put a crate under the unnamed primary heading, which is always listed first,
246+
/// make this the empty string.
247+
pub(crate) crate_list_heading: String,
244248
/// List of themes to extend the docs with. Original argument name is included to assist in
245249
/// displaying errors if it fails a theme check.
246250
pub(crate) themes: Vec<StylePath>,
@@ -760,6 +764,7 @@ impl Options {
760764
} else {
761765
ModuleSorting::Alphabetical
762766
};
767+
let crate_list_heading = matches.opt_str("crate-list-heading").unwrap_or_default();
763768
let resource_suffix = matches.opt_str("resource-suffix").unwrap_or_default();
764769
let markdown_no_toc = matches.opt_present("markdown-no-toc");
765770
let markdown_css = matches.opt_strs("markdown-css");
@@ -859,6 +864,7 @@ impl Options {
859864
id_map,
860865
playground_url,
861866
module_sorting,
867+
crate_list_heading,
862868
themes,
863869
extension_css,
864870
extern_html_root_urls,

src/librustdoc/html/render/write_shared.rs

+27-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//! --resource-suffix flag and are emitted when --emit-type is empty (default)
1414
//! or contains "invocation-specific".
1515
16+
use std::borrow::Cow;
1617
use std::cell::RefCell;
1718
use std::ffi::OsString;
1819
use std::fs::File;
@@ -69,8 +70,13 @@ pub(crate) fn write_shared(
6970
write_search_desc(cx, krate, &desc)?; // does not need to be merged
7071

7172
let crate_name = krate.name(cx.tcx());
72-
let crate_name = crate_name.as_str(); // rand
73-
let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand"
73+
let crate_name = crate_name.as_str(); // e.g. rand
74+
let crate_name_json = OrderedJson::serialize(AllCratesEntry {
75+
heading: Cow::Borrowed(&opt.crate_list_heading),
76+
crate_name: Cow::Borrowed(crate_name),
77+
})
78+
.unwrap(); // e.g. {"c":"rand","h":""}
79+
7480
let external_crates = hack_get_external_crate_names(&cx.dst, &cx.shared.resource_suffix)?;
7581
let info = CrateInfo {
7682
version: CrateInfoVersion::V1,
@@ -387,6 +393,19 @@ impl AllCratesPart {
387393
}
388394
}
389395

396+
/// Type of a serialized entry in the [`AllCratesPart`] JSON.
397+
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
398+
struct AllCratesEntry<'a> {
399+
#[serde(rename = "c")]
400+
crate_name: Cow<'a, str>,
401+
402+
/// If non-absent and non-empty, is the name of a section heading in the crate sidebar.
403+
#[serde(rename = "h")]
404+
#[serde(default)]
405+
#[serde(skip_serializing_if = "str::is_empty")]
406+
heading: Cow<'a, str>,
407+
}
408+
390409
/// Reads `crates.js`, which seems like the best
391410
/// place to obtain the list of externally documented crates if the index
392411
/// page was disabled when documenting the deps.
@@ -408,8 +427,12 @@ fn hack_get_external_crate_names(
408427
let Some(content) = regex.find(&content) else {
409428
return Err(Error::new("could not find crates list in crates.js", path));
410429
};
411-
let content: Vec<String> = try_err!(serde_json::from_str(content.as_str()), &path);
412-
Ok(content)
430+
let crate_names: Vec<String> =
431+
try_err!(serde_json::from_str::<Vec<AllCratesEntry<'static>>>(content.as_str()), &path)
432+
.into_iter()
433+
.map(|entry| entry.crate_name.into_owned())
434+
.collect();
435+
Ok(crate_names)
413436
}
414437

415438
#[derive(Serialize, Deserialize, Clone, Default, Debug)]

src/librustdoc/html/render/write_shared/tests.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ fn hack_external_crate_names() {
99
let path = path.path();
1010
let crates = hack_get_external_crate_names(&path, "").unwrap();
1111
assert!(crates.is_empty());
12-
fs::write(path.join("crates.js"), r#"window.ALL_CRATES = ["a","b","c"];"#).unwrap();
12+
fs::write(path.join("crates.js"), r#"window.ALL_CRATES = [{"c":"a"},{"c":"b"},{"c":"c"}];"#)
13+
.unwrap();
1314
let crates = hack_get_external_crate_names(&path, "").unwrap();
1415
assert_eq!(crates, ["a".to_string(), "b".to_string(), "c".to_string()]);
1516
}

src/librustdoc/html/static/js/main.js

+50-16
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,7 @@ function preLoadCss(cssUrl) {
994994
window.register_type_impls(window.pending_type_impls);
995995
}
996996

997+
// Draw a convenient sidebar of known crates if we have a listing
997998
function addSidebarCrates() {
998999
if (!window.ALL_CRATES) {
9991000
return;
@@ -1002,26 +1003,59 @@ function preLoadCss(cssUrl) {
10021003
if (!sidebarElems) {
10031004
return;
10041005
}
1005-
// Draw a convenient sidebar of known crates if we have a listing
1006-
const h3 = document.createElement("h3");
1007-
h3.innerHTML = "Crates";
1008-
const ul = document.createElement("ul");
1009-
ul.className = "block crate";
10101006

1011-
for (const crate of window.ALL_CRATES) {
1012-
const link = document.createElement("a");
1013-
link.href = window.rootPath + crate + "/index.html";
1014-
link.textContent = crate;
1007+
// h2 puts this on the same level as the current crate name heading at the top
1008+
const allCratesHeading = document.createElement("h2");
1009+
allCratesHeading.textContent = "Crates";
10151010

1016-
const li = document.createElement("li");
1017-
if (window.rootPath !== "./" && crate === window.currentCrate) {
1018-
li.className = "current";
1011+
const allCratesSection = document.createElement("section");
1012+
1013+
// window.ALL_CRATES is in unsorted array-of-structs format; reorganize so crates with the
1014+
// same heading are grouped.
1015+
const cratesGroupedByHeading = new Map();
1016+
for (const entry of window.ALL_CRATES) {
1017+
const heading = entry.h || "";
1018+
const crateName = entry.c;
1019+
let group = cratesGroupedByHeading.get(heading);
1020+
if (group === undefined) {
1021+
group = [];
1022+
cratesGroupedByHeading.set(heading, group);
1023+
}
1024+
group.push(crateName);
1025+
}
1026+
const headings = Array.from(cratesGroupedByHeading.keys());
1027+
headings.sort();
1028+
1029+
// Generate HTML for grouped crates.
1030+
for (const headingText of headings) {
1031+
// Empty string denotes a group with no named heading.
1032+
if (headingText !== "") {
1033+
const crateCategoryHeading = document.createElement("h3");
1034+
crateCategoryHeading.textContent = headingText;
1035+
allCratesSection.appendChild(crateCategoryHeading);
1036+
}
1037+
1038+
const cratesUl = document.createElement("ul");
1039+
cratesUl.className = "block crate";
1040+
1041+
for (const crateName of cratesGroupedByHeading.get(headingText)) {
1042+
const link = document.createElement("a");
1043+
link.href = window.rootPath + crateName + "/index.html";
1044+
link.textContent = crateName;
1045+
1046+
const li = document.createElement("li");
1047+
if (window.rootPath !== "./" && crateName === window.currentCrate) {
1048+
li.className = "current";
1049+
}
1050+
li.appendChild(link);
1051+
cratesUl.appendChild(li);
10191052
}
1020-
li.appendChild(link);
1021-
ul.appendChild(li);
1053+
1054+
allCratesSection.appendChild(cratesUl);
10221055
}
1023-
sidebarElems.appendChild(h3);
1024-
sidebarElems.appendChild(ul);
1056+
1057+
sidebarElems.appendChild(allCratesHeading);
1058+
sidebarElems.appendChild(allCratesSection);
10251059
}
10261060

10271061
function expandAllDocs() {

src/librustdoc/html/static/js/rustdoc.d.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ declare global {
1313
interface Window {
1414
/** Make the current theme easy to find */
1515
currentTheme: HTMLLinkElement|null;
16+
/** Generated in `render/context.rs` */
17+
SIDEBAR_ITEMS?: { [key: string]: string[] };
1618
/** List of all documented crates. */
17-
ALL_CRATES: string[]|undefined;
19+
ALL_CRATES: rustdoc.AllCratesEntry[]|undefined;
1820
/** Used by the popover tooltip code. */
1921
RUSTDOC_TOOLTIP_HOVER_MS: number;
2022
/** Used by the popover tooltip code. */
@@ -396,6 +398,18 @@ declare namespace rustdoc {
396398
bindings: Map<number, FingerprintableType[]>;
397399
};
398400

401+
/** Member of ALL_CRATES. */
402+
interface AllCratesEntry {
403+
/**
404+
* Heading under which the crate should be listed.
405+
*
406+
* May be empty to specify the first, primary heading.
407+
*/
408+
h?: string,
409+
/** Crate name. */
410+
c: string,
411+
}
412+
399413
/**
400414
* The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f`
401415
* are arrays with the same length. `q`, `a`, and `c` use a sparse

src/librustdoc/lib.rs

+8
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,14 @@ fn opts() -> Vec<RustcOptGroup> {
383383
"sort modules by where they appear in the program, rather than alphabetically",
384384
"",
385385
),
386+
opt(
387+
Unstable,
388+
Opt,
389+
"",
390+
"crate-list-heading",
391+
"heading under which to list this crate in the sidebar crate list",
392+
"TEXT",
393+
),
386394
opt(
387395
Stable,
388396
Opt,

tests/rustdoc-gui/sidebar.goml

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ call-function: ("switch-theme", {"theme": "light"})
4848
assert-text: (".sidebar > .sidebar-crate > h2 > a", "test_docs")
4949
// Crate root has no "location" element
5050
assert-count: (".sidebar .location", 0)
51-
assert-count: (".sidebar h2", 1)
51+
// Crate root has two h2s, the second of which is the the crate list
52+
assert-count: (".sidebar h2", 2)
53+
assert-text: (".sidebar #rustdoc-modnav h2", "Crates")
5254
assert-text: ("#all-types", "All Items")
5355
assert-css: ("#all-types", {"color": "#356da4"})
5456
// We check that we have the crates list and that the "current" on is "test_docs".

tests/rustdoc/crate-list-heading.rs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
//@ compile-flags: -Zunstable-options --crate-list-heading=helloworld
2+
#![crate_name = "foo"]
3+
4+
//@ hasraw crates.js '"h":"helloworld"'
5+
//@ hasraw crates.js '"c":"foo"'

0 commit comments

Comments
 (0)