diff --git a/src/librustdoc/externalfiles.rs b/src/librustdoc/externalfiles.rs index 37fd909c93342..1b10af25bcbf8 100644 --- a/src/librustdoc/externalfiles.rs +++ b/src/librustdoc/externalfiles.rs @@ -47,6 +47,8 @@ impl ExternalHtml { edition, playground, heading_offset: HeadingOffset::H2, + depth: 0, + local_resources: None, } .into_string() ); @@ -63,6 +65,8 @@ impl ExternalHtml { edition, playground, heading_offset: HeadingOffset::H2, + depth: 0, + local_resources: None, } .into_string() ); diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs index 24752cddb337c..27379478b30e1 100644 --- a/src/librustdoc/formats/cache.rs +++ b/src/librustdoc/formats/cache.rs @@ -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}; @@ -121,6 +122,8 @@ pub(crate) struct Cache { pub(crate) intra_doc_links: FxHashMap>, /// Cfg that have been hidden via #![doc(cfg_hide(...))] pub(crate) hidden_cfg: FxHashSet, + /// 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`. @@ -516,6 +519,31 @@ impl<'a, 'tcx> DocFolder for CacheBuilder<'a, 'tcx> { } } +#[derive(Default)] +pub struct LocalResources { + /// The key is the original location of the resource. The value is the new name. + pub(crate) resources_to_copy: FxHashMap, + /// This will be used when generating the HTML, once everything is generated, we copy these + /// files into the static 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>, +} + +impl LocalResources { + pub(crate) fn add_entry_at_depth(&mut self, depth: usize, key: String, value: String) { + self.resources_correspondance + .entry(depth) + .or_insert_with(FxHashMap::default) + .insert(key, value); + } + + pub(crate) fn get_at_depth(&self, depth: usize, key: &str) -> Option<&String> { + self.resources_correspondance.get(&depth).and_then(|e| e.get(key)) + } +} + pub(crate) struct OrphanImplItem { pub(crate) parent: DefId, pub(crate) item: clean::Item, diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 00e3f859bfcb3..20693aa12a07b 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -20,6 +20,8 @@ //! edition: Edition::Edition2015, //! playground: &None, //! heading_offset: HeadingOffset::H2, +//! depth: 0, +//! local_resources: None, //! }; //! let html = md.into_string(); //! // ... something using html @@ -42,6 +44,7 @@ use std::str; use crate::clean::RenderedLink; use crate::doctest; +use crate::formats::cache::LocalResources; use crate::html::escape::Escape; use crate::html::format::Buffer; use crate::html::highlight; @@ -101,21 +104,60 @@ pub struct Markdown<'a> { /// Offset at which we render headings. /// E.g. if `heading_offset: HeadingOffset::H2`, then `# something` renders an `

`. pub heading_offset: HeadingOffset, + pub depth: usize, + pub local_resources: Option<&'a LocalResources>, } /// A tuple struct like `Markdown` that renders the markdown with a table of contents. -pub(crate) struct MarkdownWithToc<'a>( - pub(crate) &'a str, - pub(crate) &'a mut IdMap, - pub(crate) ErrorCodes, - pub(crate) Edition, - pub(crate) &'a Option, -); +pub(crate) struct MarkdownWithToc<'a> { + pub(crate) content: &'a str, + pub(crate) ids: &'a mut IdMap, + pub(crate) error_codes: ErrorCodes, + pub(crate) edition: Edition, + pub(crate) playground: &'a Option, + pub(crate) depth: usize, + pub(crate) local_resources: Option<&'a LocalResources>, +} /// A tuple struct like `Markdown` that renders the markdown escaping HTML tags /// and includes no paragraph tags. pub(crate) struct MarkdownItemInfo<'a>(pub(crate) &'a str, pub(crate) &'a mut IdMap); /// A tuple struct like `Markdown` that renders only the first paragraph. pub(crate) struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [RenderedLink]); +struct LocalResourcesReplacer<'b, I> { + inner: I, + local_resources: Option<&'b LocalResources>, + depth: usize, +} + +impl<'b, I> LocalResourcesReplacer<'b, I> { + fn new(iter: I, local_resources: Option<&'b LocalResources>, depth: usize) -> Self { + Self { inner: iter, local_resources, depth } + } +} + +impl<'a, 'b, I: Iterator>> Iterator for LocalResourcesReplacer<'b, I> { + type Item = Event<'a>; + + fn next(&mut self) -> Option { + let event = self.inner.next()?; + // We only modify + if let Event::Start(Tag::Image(type_, ref path, ref title)) = event && + !path.starts_with("http://") && + !path.starts_with("https://") && + let Some(local_resources) = &self.local_resources && + let Some(correspondance) = local_resources.get_at_depth(self.depth, &*path) + { + Some(Event::Start(Tag::Image( + type_, + CowStr::Boxed(correspondance.clone().into_boxed_str()), + title.clone(), + ))) + } else { + Some(event) + } + } +} + #[derive(Copy, Clone, PartialEq, Debug)] pub enum ErrorCodes { Yes, @@ -1017,6 +1059,8 @@ impl Markdown<'_> { edition, playground, heading_offset, + depth, + local_resources, } = self; // This is actually common enough to special-case @@ -1038,6 +1082,7 @@ impl Markdown<'_> { let p = HeadingLinks::new(p, None, ids, heading_offset); let p = Footnotes::new(p); let p = LinkReplacer::new(p.map(|(ev, _)| ev), links); + let p = LocalResourcesReplacer::new(p, local_resources, depth); let p = TableWrapper::new(p); let p = CodeBlocks::new(p, codes, edition, playground); html::push_html(&mut s, p); @@ -1048,7 +1093,15 @@ impl Markdown<'_> { impl MarkdownWithToc<'_> { pub(crate) fn into_string(self) -> String { - let MarkdownWithToc(md, ids, codes, edition, playground) = self; + let MarkdownWithToc { + content: md, + ids, + error_codes: codes, + edition, + playground, + depth, + local_resources, + } = self; let p = Parser::new_ext(md, main_body_opts()).into_offset_iter(); @@ -1059,7 +1112,8 @@ impl MarkdownWithToc<'_> { { let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1); let p = Footnotes::new(p); - let p = TableWrapper::new(p.map(|(ev, _)| ev)); + let p = LocalResourcesReplacer::new(p.map(|(ev, _)| ev), local_resources, depth); + let p = TableWrapper::new(p); let p = CodeBlocks::new(p, codes, edition, playground); html::push_html(&mut s, p); } diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index 5878c58264ec3..fe574047c3cc7 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -154,6 +154,8 @@ fn test_header() { edition: DEFAULT_EDITION, playground: &None, heading_offset: HeadingOffset::H2, + depth: 0, + local_resources: None, } .into_string(); assert_eq!(output, expect, "original: {}", input); @@ -193,6 +195,8 @@ fn test_header_ids_multiple_blocks() { edition: DEFAULT_EDITION, playground: &None, heading_offset: HeadingOffset::H2, + depth: 0, + local_resources: None, } .into_string(); assert_eq!(output, expect, "original: {}", input); @@ -322,6 +326,8 @@ fn test_ascii_with_prepending_hashtag() { edition: DEFAULT_EDITION, playground: &None, heading_offset: HeadingOffset::H2, + depth: 0, + local_resources: None, } .into_string(); assert_eq!(output, expect, "original: {}", input); diff --git a/src/librustdoc/html/mod.rs b/src/librustdoc/html/mod.rs index 481ed16c05f7e..b5d38b9984e1d 100644 --- a/src/librustdoc/html/mod.rs +++ b/src/librustdoc/html/mod.rs @@ -13,3 +13,5 @@ mod url_parts_builder; #[cfg(test)] mod tests; + +pub(crate) const LOCAL_RESOURCES_FOLDER_NAME: &str = "local_resources"; diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index b59645ec2e2d5..3aec74974f514 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -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(); @@ -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 { @@ -715,6 +719,22 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { } } + { + // Copying the local resources to the destination folder. + let resources = &shared.cache.local_resources.resources_to_copy; + if !resources.is_empty() { + let dst = self + .dst + .join(crate::html::LOCAL_RESOURCES_FOLDER_NAME) + .join(crate_name.as_str()); + shared.ensure_dir(&dst)?; + for (original_path, dest_name) in resources.iter() { + let dst = dst.join(dest_name); + try_err!(std::fs::copy(original_path, &dst), &dst); + } + } + } + // No need for it anymore. drop(shared); diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 816a8f4e274ce..e92da957ea164 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -404,7 +404,9 @@ fn scrape_examples_help(shared: &SharedContext<'_>) -> String { error_codes: shared.codes, edition: shared.edition(), playground: &shared.playground, - heading_offset: HeadingOffset::H1 + heading_offset: HeadingOffset::H1, + depth: 0, + local_resources: None, } .into_string() ) @@ -447,6 +449,8 @@ fn render_markdown( edition: cx.shared.edition(), playground: &cx.shared.playground, heading_offset, + depth: cx.current.len(), + local_resources: Some(&cx.shared.cache.local_resources), } .into_string() ) @@ -1755,7 +1759,9 @@ fn render_impl( error_codes: cx.shared.codes, edition: cx.shared.edition(), playground: &cx.shared.playground, - heading_offset: HeadingOffset::H4 + heading_offset: HeadingOffset::H4, + depth: cx.current.len(), + local_resources: Some(&cx.shared.cache.local_resources), } .into_string() ); diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index 6b71ecc24bde6..1155e5899184a 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -329,7 +329,7 @@ if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex}; md_opts.output = cx.dst.clone(); md_opts.external_html = (*cx.shared).layout.external_html.clone(); - crate::markdown::render(&index_page, md_opts, cx.shared.edition()) + crate::markdown::render(&index_page, md_opts, cx.shared.edition(), Some(&cx)) .map_err(|e| Error::new(e, &index_page))?; } else { let shared = Rc::clone(&cx.shared); diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 64108c8828518..ad4790ab8498c 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -749,7 +749,7 @@ fn main_args(at_args: &[String]) -> MainResult { return wrap_return( &diag, interface::run_compiler(config, |_compiler| { - markdown::render(&input, render_options, edition) + markdown::render(&input, render_options, edition, None) }), ); } diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 5f4ad6d2aea34..d1ad48fb7cb25 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -9,10 +9,10 @@ use rustc_span::source_map::DUMMY_SP; use crate::config::{Options, RenderOptions}; use crate::doctest::{Collector, GlobalTestOptions}; use crate::html::escape::Escape; -use crate::html::markdown; use crate::html::markdown::{ - find_testable_code, ErrorCodes, HeadingOffset, IdMap, Markdown, MarkdownWithToc, + self, find_testable_code, ErrorCodes, HeadingOffset, IdMap, Markdown, MarkdownWithToc, }; +use crate::html::render::Context; /// Separate any lines at the start of the file that begin with `# ` or `%`. fn extract_leading_metadata(s: &str) -> (Vec<&str>, &str) { @@ -41,6 +41,7 @@ pub(crate) fn render>( input: P, options: RenderOptions, edition: Edition, + cx: Option<&Context<'_>>, ) -> Result<(), String> { if let Err(e) = create_dir_all(&options.output) { return Err(format!("{}: {}", options.output.display(), e)); @@ -71,8 +72,21 @@ pub(crate) fn render>( let mut ids = IdMap::new(); let error_codes = ErrorCodes::from(options.unstable_features.is_nightly_build()); + let (local_resources, depth) = match cx { + Some(cx) => (Some(&cx.shared.cache.local_resources), cx.current.len()), + None => (None, 0), + }; let text = if !options.markdown_no_toc { - MarkdownWithToc(text, &mut ids, error_codes, edition, &playground).into_string() + MarkdownWithToc { + content: text, + ids: &mut ids, + error_codes, + edition, + playground: &playground, + local_resources, + depth, + } + .into_string() } else { Markdown { content: text, @@ -82,6 +96,8 @@ pub(crate) fn render>( edition, playground: &playground, heading_offset: HeadingOffset::H1, + local_resources, + depth, } .into_string() }; diff --git a/src/librustdoc/passes/collect_local_resources.rs b/src/librustdoc/passes/collect_local_resources.rs new file mode 100644 index 0000000000000..2ffd619360c8e --- /dev/null +++ b/src/librustdoc/passes/collect_local_resources.rs @@ -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 { + 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, + 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(); + 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()), + ).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) + } + } +} diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs index 634e70ec97a0d..c633e4cbe7bab 100644 --- a/src/librustdoc/passes/mod.rs +++ b/src/librustdoc/passes/mod.rs @@ -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; @@ -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, ]; @@ -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), ]; diff --git a/tests/rustdoc-ui/issue-91713.stdout b/tests/rustdoc-ui/issue-91713.stdout index 1678352436315..738a6877a0155 100644 --- a/tests/rustdoc-ui/issue-91713.stdout +++ b/tests/rustdoc-ui/issue-91713.stdout @@ -6,6 +6,7 @@ check_doc_test_visibility - run various visibility-related lints on doctests propagate-doc-cfg - propagates `#[doc(cfg(...))]` to child items collect-intra-doc-links - resolves intra-doc links collect-trait-impls - retrieves trait impls for items in the crate +collect-local-resources - resolves intra-doc links calculate-doc-coverage - counts the number of items with and without documentation run-lints - runs some of rustdoc's lints @@ -16,6 +17,7 @@ check_doc_test_visibility strip-private (when not --document-private-items) strip-priv-imports (when --document-private-items) collect-intra-doc-links +collect-local-resources propagate-doc-cfg run-lints diff --git a/tests/rustdoc-ui/local-resources.rs b/tests/rustdoc-ui/local-resources.rs new file mode 100644 index 0000000000000..36678418cdb04 --- /dev/null +++ b/tests/rustdoc-ui/local-resources.rs @@ -0,0 +1,3 @@ +/// ![yep](../somewhere.whatever) +//~^ ERROR +pub struct Foo; diff --git a/tests/rustdoc-ui/local-resources.stderr b/tests/rustdoc-ui/local-resources.stderr new file mode 100644 index 0000000000000..66d9e89c15ea2 --- /dev/null +++ b/tests/rustdoc-ui/local-resources.stderr @@ -0,0 +1,8 @@ +error: `../somewhere.whatever`: No such file + --> $DIR/local-resources.rs:1:1 + | +LL | /// ![yep](../somewhere.whatever) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to previous error + diff --git a/tests/rustdoc/local-resources.rs b/tests/rustdoc/local-resources.rs new file mode 100644 index 0000000000000..c29c15fec650c --- /dev/null +++ b/tests/rustdoc/local-resources.rs @@ -0,0 +1,24 @@ +// Test ensuring that local resources copy is working as expected. +// Original issue: + +#![crate_name = "foo"] +#![feature(no_core)] +#![no_std] +#![no_core] + +// @has local_resources/foo/0.svg +// @has foo/struct.Enum.html +// @has - '//img[@src="../local_resources/foo/0.svg"]' '' +/// test! +/// +/// ![yep](../../src/librustdoc/html/static/images/rust-logo.svg) +pub struct Enum; + +pub mod sub { + // @has foo/sub/struct.Enum.html + // @has - '//img[@src="../../local_resources/foo/0.svg"]' '' + /// test! + /// + /// ![yep](../../src/librustdoc/html/static/images/rust-logo.svg) + pub struct Enum; +}