Skip to content

Commit 5a2e224

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 5a2e224

File tree

7 files changed

+110
-22
lines changed

7 files changed

+110
-22
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

+49-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,58 @@ 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+
const allCratesHeading = document.createElement("h3");
1008+
allCratesHeading.textContent = "Crates";
10151009

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

10271060
function expandAllDocs() {

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

+13-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ declare global {
1414
/** Make the current theme easy to find */
1515
currentTheme: HTMLLinkElement|null;
1616
/** List of all documented crates. */
17-
ALL_CRATES: string[]|undefined;
17+
ALL_CRATES: rustdoc.AllCratesEntry[]|undefined;
1818
/** Used by the popover tooltip code. */
1919
RUSTDOC_TOOLTIP_HOVER_MS: number;
2020
/** Used by the popover tooltip code. */
@@ -396,6 +396,18 @@ declare namespace rustdoc {
396396
bindings: Map<number, FingerprintableType[]>;
397397
};
398398

399+
/** Member of ALL_CRATES. */
400+
interface AllCratesEntry {
401+
/**
402+
* Heading under which the crate should be listed.
403+
*
404+
* May be empty to specify the first, primary heading.
405+
*/
406+
h: string,
407+
/** Crate name. */
408+
c: string,
409+
}
410+
399411
/**
400412
* The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f`
401413
* 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+
"PATH",
393+
),
386394
opt(
387395
Stable,
388396
Opt,

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)