diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index e67fe6c88ea41..11a89d520e5c1 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -243,6 +243,10 @@ pub(crate) struct RenderOptions { /// What sorting mode to use for module pages. /// `ModuleSorting::Alphabetical` by default. pub(crate) module_sorting: ModuleSorting, + /// In the sidebar list of all crates, put the current crate(s) under this heading. + /// To put a crate under the unnamed primary heading, which is always listed first, + /// make this the empty string. + pub(crate) crate_list_heading: String, /// List of themes to extend the docs with. Original argument name is included to assist in /// displaying errors if it fails a theme check. pub(crate) themes: Vec, @@ -767,6 +771,7 @@ impl Options { } else { ModuleSorting::Alphabetical }; + let crate_list_heading = matches.opt_str("crate-list-heading").unwrap_or_default(); let resource_suffix = matches.opt_str("resource-suffix").unwrap_or_default(); let markdown_no_toc = matches.opt_present("markdown-no-toc"); let markdown_css = matches.opt_strs("markdown-css"); @@ -867,6 +872,7 @@ impl Options { id_map, playground_url, module_sorting, + crate_list_heading, themes, extension_css, extern_html_root_urls, diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index b2bbf4614bf45..20e25cc05af05 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -13,6 +13,7 @@ //! --resource-suffix flag and are emitted when --emit-type is empty (default) //! or contains "invocation-specific". +use std::borrow::Cow; use std::cell::RefCell; use std::ffi::OsString; use std::fs::File; @@ -69,8 +70,13 @@ pub(crate) fn write_shared( write_search_desc(cx, krate, &desc)?; // does not need to be merged let crate_name = krate.name(cx.tcx()); - let crate_name = crate_name.as_str(); // rand - let crate_name_json = OrderedJson::serialize(crate_name).unwrap(); // "rand" + let crate_name = crate_name.as_str(); // e.g. rand + let crate_name_json = OrderedJson::serialize(AllCratesEntry { + heading: Cow::Borrowed(&opt.crate_list_heading), + crate_name: Cow::Borrowed(crate_name), + }) + .unwrap(); // e.g. {"c":"rand","h":""} + let external_crates = hack_get_external_crate_names(&cx.dst, &cx.shared.resource_suffix)?; let info = CrateInfo { version: CrateInfoVersion::V1, @@ -387,6 +393,19 @@ impl AllCratesPart { } } +/// Type of a serialized entry in the [`AllCratesPart`] JSON. +#[derive(Serialize, Deserialize, Clone, Default, Debug)] +struct AllCratesEntry<'a> { + #[serde(rename = "c")] + crate_name: Cow<'a, str>, + + /// If non-absent and non-empty, is the name of a section heading in the crate sidebar. + #[serde(rename = "h")] + #[serde(default)] + #[serde(skip_serializing_if = "str::is_empty")] + heading: Cow<'a, str>, +} + /// Reads `crates.js`, which seems like the best /// place to obtain the list of externally documented crates if the index /// page was disabled when documenting the deps. @@ -408,8 +427,12 @@ fn hack_get_external_crate_names( let Some(content) = regex.find(&content) else { return Err(Error::new("could not find crates list in crates.js", path)); }; - let content: Vec = try_err!(serde_json::from_str(content.as_str()), &path); - Ok(content) + let crate_names: Vec = + try_err!(serde_json::from_str::>>(content.as_str()), &path) + .into_iter() + .map(|entry| entry.crate_name.into_owned()) + .collect(); + Ok(crate_names) } #[derive(Serialize, Deserialize, Clone, Default, Debug)] diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs index a235f1d37243a..96b6d13ece36a 100644 --- a/src/librustdoc/html/render/write_shared/tests.rs +++ b/src/librustdoc/html/render/write_shared/tests.rs @@ -9,7 +9,8 @@ fn hack_external_crate_names() { let path = path.path(); let crates = hack_get_external_crate_names(&path, "").unwrap(); assert!(crates.is_empty()); - fs::write(path.join("crates.js"), r#"window.ALL_CRATES = ["a","b","c"];"#).unwrap(); + fs::write(path.join("crates.js"), r#"window.ALL_CRATES = [{"c":"a"},{"c":"b"},{"c":"c"}];"#) + .unwrap(); let crates = hack_get_external_crate_names(&path, "").unwrap(); assert_eq!(crates, ["a".to_string(), "b".to_string(), "c".to_string()]); } diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index edfcc1291b976..de7068f73c867 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1003,8 +1003,8 @@ function preLoadCss(cssUrl) { window.register_type_impls(window.pending_type_impls); } + // Draw a convenient sidebar of known crates if we have a listing function addSidebarCrates() { - // @ts-expect-error if (!window.ALL_CRATES) { return; } @@ -1012,27 +1012,59 @@ function preLoadCss(cssUrl) { if (!sidebarElems) { return; } - // Draw a convenient sidebar of known crates if we have a listing - const h3 = document.createElement("h3"); - h3.innerHTML = "Crates"; - const ul = document.createElement("ul"); - ul.className = "block crate"; - // @ts-expect-error - for (const crate of window.ALL_CRATES) { - const link = document.createElement("a"); - link.href = window.rootPath + crate + "/index.html"; - link.textContent = crate; - - const li = document.createElement("li"); - if (window.rootPath !== "./" && crate === window.currentCrate) { - li.className = "current"; + // h2 puts this on the same level as the current crate name heading at the top + const allCratesHeading = document.createElement("h2"); + allCratesHeading.textContent = "Crates"; + + const allCratesSection = document.createElement("section"); + + // window.ALL_CRATES is in unsorted array-of-structs format; reorganize so crates with the + // same heading are grouped. + const cratesGroupedByHeading = new Map(); + for (const entry of window.ALL_CRATES) { + const heading = entry.h || ""; + const crateName = entry.c; + let group = cratesGroupedByHeading.get(heading); + if (group === undefined) { + group = []; + cratesGroupedByHeading.set(heading, group); } - li.appendChild(link); - ul.appendChild(li); + group.push(crateName); + } + const headings = Array.from(cratesGroupedByHeading.keys()); + headings.sort(); + + // Generate HTML for grouped crates. + for (const headingText of headings) { + // Empty string denotes a group with no named heading. + if (headingText !== "") { + const crateCategoryHeading = document.createElement("h3"); + crateCategoryHeading.textContent = headingText; + allCratesSection.appendChild(crateCategoryHeading); + } + + const cratesUl = document.createElement("ul"); + cratesUl.className = "block crate"; + + for (const crateName of cratesGroupedByHeading.get(headingText)) { + const link = document.createElement("a"); + link.href = window.rootPath + crateName + "/index.html"; + link.textContent = crateName; + + const li = document.createElement("li"); + if (window.rootPath !== "./" && crateName === window.currentCrate) { + li.className = "current"; + } + li.appendChild(link); + cratesUl.appendChild(li); + } + + allCratesSection.appendChild(cratesUl); } - sidebarElems.appendChild(h3); - sidebarElems.appendChild(ul); + + sidebarElems.appendChild(allCratesHeading); + sidebarElems.appendChild(allCratesSection); } function expandAllDocs() { diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts index 1554c045a3271..52a6986cc4d8c 100644 --- a/src/librustdoc/html/static/js/rustdoc.d.ts +++ b/src/librustdoc/html/static/js/rustdoc.d.ts @@ -7,6 +7,8 @@ declare global { interface Window { /** Make the current theme easy to find */ currentTheme: HTMLLinkElement|null; + /** List of all documented crates. */ + ALL_CRATES: rustdoc.AllCratesEntry[]|undefined; /** Used by the popover tooltip code. */ RUSTDOC_TOOLTIP_HOVER_MS: number; /** Used by the popover tooltip code. */ @@ -348,6 +350,18 @@ declare namespace rustdoc { bindings: Map; }; + /** Member of ALL_CRATES. */ + interface AllCratesEntry { + /** + * Heading under which the crate should be listed. + * + * May be empty to specify the first, primary heading. + */ + h?: string, + /** Crate name. */ + c: string, + } + /** * The raw search data for a given crate. `n`, `t`, `d`, `i`, and `f` * are arrays with the same length. `q`, `a`, and `c` use a sparse diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 4fe5e13c3afe0..ff9c9957b9fe7 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -383,6 +383,14 @@ fn opts() -> Vec { "sort modules by where they appear in the program, rather than alphabetically", "", ), + opt( + Unstable, + Opt, + "", + "crate-list-heading", + "heading under which to list this crate in the sidebar crate list", + "TEXT", + ), opt( Stable, Opt, diff --git a/tests/rustdoc-gui/sidebar.goml b/tests/rustdoc-gui/sidebar.goml index 38160cc49d085..896b503ed5f96 100644 --- a/tests/rustdoc-gui/sidebar.goml +++ b/tests/rustdoc-gui/sidebar.goml @@ -48,7 +48,9 @@ call-function: ("switch-theme", {"theme": "light"}) assert-text: (".sidebar > .sidebar-crate > h2 > a", "test_docs") // Crate root has no "location" element assert-count: (".sidebar .location", 0) -assert-count: (".sidebar h2", 1) +// Crate root has two h2s, the second of which is the the crate list +assert-count: (".sidebar h2", 2) +assert-text: (".sidebar #rustdoc-modnav h2", "Crates") assert-text: ("#all-types", "All Items") assert-css: ("#all-types", {"color": "#356da4"}) // We check that we have the crates list and that the "current" on is "test_docs". diff --git a/tests/rustdoc/crate-list-heading.rs b/tests/rustdoc/crate-list-heading.rs new file mode 100644 index 0000000000000..8b7e372c42dce --- /dev/null +++ b/tests/rustdoc/crate-list-heading.rs @@ -0,0 +1,5 @@ +//@ compile-flags: -Zunstable-options --crate-list-heading=helloworld +#![crate_name = "foo"] + +//@ hasraw crates.js '"h":"helloworld"' +//@ hasraw crates.js '"c":"foo"'