Skip to content
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

rustdoc: Add support for local resources #107640

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions src/librustdoc/formats/cache.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::mem;
use std::path::PathBuf;

use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, DefIdSet};
Expand Down Expand Up @@ -121,6 +122,8 @@ pub(crate) struct Cache {
pub(crate) intra_doc_links: FxHashMap<ItemId, Vec<clean::ItemLink>>,
/// Cfg that have been hidden via #![doc(cfg_hide(...))]
pub(crate) hidden_cfg: FxHashSet<clean::cfg::Cfg>,
/// Local resources that are copied into the rustdoc output directory.
pub(crate) local_resources: LocalResources,
}

/// This struct is used to wrap the `cache` and `tcx` in order to run `DocFolder`.
Expand Down Expand Up @@ -516,6 +519,33 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> {
}
}

#[derive(Default)]
pub(crate) struct LocalResources {
/// The key is the original location of the resource. The value is the new name.
pub(crate) resources_to_copy: FxHashMap<PathBuf, String>,
/// This will be used when generating the HTML, once everything is generated, we copy these
/// files into the static folder.
Comment on lines +526 to +527
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't put per-crate files into static.files/, which is used for toolchain-specific static files. If we go this route, there should be a separate directory for invocation-specific files.

Copy link
Member Author

Choose a reason for hiding this comment

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

Absolutely. The RFC mentions that there won't be a crate folder. ;)

///
/// The key is the depth and the value is hashmap where the key is the path of the resource in
/// the markdown and the value is the new path to the resources in the rustdoc output folder.
pub(crate) resources_correspondance: FxHashMap<usize, FxHashMap<String, String>>,
pub(crate) total_entries: usize,
}

impl LocalResources {
pub(crate) fn add_entry_at_depth(&mut self, depth: usize, key: String, value: String) {
if self
.resources_correspondance
.entry(depth)
.or_insert_with(FxHashMap::default)
.insert(key, value)
.is_none()
{
self.total_entries += 1;
}
}
}

pub(crate) struct OrphanImplItem {
pub(crate) parent: DefId,
pub(crate) item: clean::Item,
Expand Down
2 changes: 2 additions & 0 deletions src/librustdoc/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ mod url_parts_builder;

#[cfg(test)]
mod tests;

pub(crate) const LOCAL_RESOURCES_FOLDER_NAME: &str = "local_resources";
6 changes: 5 additions & 1 deletion src/librustdoc/html/render/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ pub(crate) struct SharedContext<'tcx> {
pub(crate) call_locations: AllCallLocations,
}

pub(crate) fn root_path(depth: usize) -> String {
"../".repeat(depth)
}

impl SharedContext<'_> {
pub(crate) fn ensure_dir(&self, dst: &Path) -> Result<(), Error> {
let mut dirs = self.created_dirs.borrow_mut();
Expand Down Expand Up @@ -165,7 +169,7 @@ impl<'tcx> Context<'tcx> {
/// String representation of how to get back to the root path of the 'doc/'
/// folder in terms of a relative URL.
pub(super) fn root_path(&self) -> String {
"../".repeat(self.current.len())
root_path(self.current.len())
}

fn render_item(&mut self, it: &clean::Item, is_module: bool) -> String {
Expand Down
137 changes: 137 additions & 0 deletions src/librustdoc/passes/collect_local_resources.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! This file will go through all doc comments and retrieve local resources to then store them
//! in the rustdoc output directory.

use pulldown_cmark::{Event, Parser, Tag};

use rustc_span::def_id::LOCAL_CRATE;
use rustc_span::FileName;

use crate::clean::{Crate, Item};
use crate::core::DocContext;
use crate::html::markdown::main_body_opts;
use crate::html::render::root_path;
use crate::html::LOCAL_RESOURCES_FOLDER_NAME;
use crate::passes::Pass;
use crate::visit::DocVisitor;

use std::path::{Path, PathBuf};

pub(crate) const COLLECT_LOCAL_RESOURCES: Pass = Pass {
name: "collect-local-resources",
run: collect_local_resources,
description: "resolves intra-doc links",
};

fn span_file_path(cx: &DocContext<'_>, item: &Item) -> Option<PathBuf> {
item.span(cx.tcx).and_then(|span| match span.filename(cx.sess()) {
FileName::Real(ref path) => Some(path.local_path_if_available().into()),
_ => None,
})
}

struct ResourcesCollector<'a, 'tcx> {
cx: &'a mut DocContext<'tcx>,
/// The depth is used to know how many "../" needs to be generated to get the original file
/// path.
depth: usize,
}

fn collect_local_resources(krate: Crate, cx: &mut DocContext<'_>) -> Crate {
let mut collector = ResourcesCollector { cx, depth: 1 };
collector.visit_crate(&krate);
krate
}

impl<'a, 'tcx> ResourcesCollector<'a, 'tcx> {
pub fn handle_event(
&mut self,
event: Event<'_>,
current_path: &mut Option<PathBuf>,
item: &Item,
) {
if let Event::Start(Tag::Image(_, ref ori_path, _)) = event &&
!ori_path.starts_with("http://") &&
!ori_path.starts_with("https://")
{
let ori_path = ori_path.to_string();
if self.cx.cache.local_resources.resources_correspondance
.get(&self.depth)
.and_then(|entry| entry.get(&ori_path))
.is_some()
{
// We already have this entry so nothing to be done!
return;
}
if current_path.is_none() {
*current_path = span_file_path(self.cx, item);
}
let Some(current_path) = current_path else { return };

let path = match current_path.parent()
.unwrap_or_else(|| Path::new("."))
.join(&ori_path)
.canonicalize()
{
Ok(p) => p,
Err(_) => {
self.cx.tcx.sess.struct_span_err(
item.attr_span(self.cx.tcx),
&format!("`{ori_path}`: No such file"),
).emit();
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably emit a warning, at least for awhile, because someone could've made a home-grown build system that already did the resource copying.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just a warning forever should be enough indeed.

return;
}
};

if !path.is_file() {
self.cx.tcx.sess.struct_span_err(
item.attr_span(self.cx.tcx),
&format!("`{ori_path}`: No such file (expanded into `{}`)", path.display()),
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably emit a warning, at least for awhile, because someone could've made a home-grown build system that already did the resource copying.

).emit();
return;
}

// We now enter the file into the `resources_to_copy` in case it's not already in
// and then generate a path the file that we store into `resources_correspondance`
// with the `add_entry_at_depth` method.
let current_nb = self.cx.cache.local_resources.resources_to_copy.len();
let file_name = self.cx.cache.local_resources.resources_to_copy
.entry(path.clone())
.or_insert_with(|| {
let extension = path.extension();
let (extension, dot) = match extension.and_then(|e| e.to_str()) {
Some(e) => (e, "."),
None => ("", ""),
};
format!(
"{current_nb}{}{dot}{extension}",
self.cx.render_options.resource_suffix,
)
});
let file = format!(
"{}{LOCAL_RESOURCES_FOLDER_NAME}/{}/{file_name}",
root_path(self.depth),
self.cx.tcx.crate_name(LOCAL_CRATE).as_str(),
);
self.cx.cache.local_resources.add_entry_at_depth(self.depth, ori_path, file);
}
}
}

impl<'a, 'tcx> DocVisitor for ResourcesCollector<'a, 'tcx> {
fn visit_item(&mut self, item: &Item) {
if let Some(md) = item.collapsed_doc_value() {
let mut current_path = None;
for event in Parser::new_ext(&md, main_body_opts()).into_iter() {
self.handle_event(event, &mut current_path, item);
}
}

if item.is_mod() && !item.is_crate() {
self.depth += 1;
self.visit_item_recur(item);
self.depth -= 1;
} else {
self.visit_item_recur(item)
}
}
}
5 changes: 5 additions & 0 deletions src/librustdoc/passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub(crate) use self::propagate_doc_cfg::PROPAGATE_DOC_CFG;
pub(crate) mod collect_intra_doc_links;
pub(crate) use self::collect_intra_doc_links::COLLECT_INTRA_DOC_LINKS;

pub(crate) mod collect_local_resources;
pub(crate) use self::collect_local_resources::COLLECT_LOCAL_RESOURCES;

mod check_doc_test_visibility;
pub(crate) use self::check_doc_test_visibility::CHECK_DOC_TEST_VISIBILITY;

Expand Down Expand Up @@ -77,6 +80,7 @@ pub(crate) const PASSES: &[Pass] = &[
PROPAGATE_DOC_CFG,
COLLECT_INTRA_DOC_LINKS,
COLLECT_TRAIT_IMPLS,
COLLECT_LOCAL_RESOURCES,
CALCULATE_DOC_COVERAGE,
RUN_LINTS,
];
Expand All @@ -89,6 +93,7 @@ pub(crate) const DEFAULT_PASSES: &[ConditionalPass] = &[
ConditionalPass::new(STRIP_PRIVATE, WhenNotDocumentPrivate),
ConditionalPass::new(STRIP_PRIV_IMPORTS, WhenDocumentPrivate),
ConditionalPass::always(COLLECT_INTRA_DOC_LINKS),
ConditionalPass::always(COLLECT_LOCAL_RESOURCES),
ConditionalPass::always(PROPAGATE_DOC_CFG),
ConditionalPass::always(RUN_LINTS),
];
Expand Down