diff --git a/src/librustdoc/externalfiles.rs b/src/librustdoc/externalfiles.rs index 8b5a3a2ba6131..6ac04a1fcbb95 100644 --- a/src/librustdoc/externalfiles.rs +++ b/src/librustdoc/externalfiles.rs @@ -30,6 +30,7 @@ impl ExternalHtml { edition: Edition, playground: &Option, ) -> Option { + let mut images_to_copy = Vec::new(); let codes = ErrorCodes::from(UnstableFeatures::from_environment().is_nightly_build()); let ih = load_external_files(in_header, diag)?; let bc = load_external_files(before_content, diag)?; @@ -37,14 +38,16 @@ impl ExternalHtml { let bc = format!( "{}{}", bc, - Markdown(&m_bc, &[], id_map, codes, edition, playground).to_string() + Markdown(&m_bc, &[], id_map, codes, edition, playground, &mut images_to_copy, &None) + .to_string() ); let ac = load_external_files(after_content, diag)?; let m_ac = load_external_files(md_after_content, diag)?; let ac = format!( "{}{}", ac, - Markdown(&m_ac, &[], id_map, codes, edition, playground).to_string() + Markdown(&m_ac, &[], id_map, codes, edition, playground, &mut images_to_copy, &None) + .to_string() ); Some(ExternalHtml { in_header: ih, before_content: bc, after_content: ac }) } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index c87964af0200c..74295811e1dd9 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -23,10 +23,13 @@ use rustc_data_structures::fx::FxHashMap; use rustc_span::edition::Edition; use std::borrow::Cow; use std::cell::RefCell; +use std::collections::hash_map::DefaultHasher; use std::collections::VecDeque; use std::default::Default; use std::fmt::Write; +use std::hash::{Hash, Hasher}; use std::ops::Range; +use std::path::{Path, PathBuf}; use std::str; use crate::html::highlight; @@ -44,7 +47,7 @@ fn opts() -> Options { /// When `to_string` is called, this struct will emit the HTML corresponding to /// the rendered version of the contained markdown string. -pub struct Markdown<'a>( +pub struct Markdown<'a, 'b>( pub &'a str, /// A list of link replacements. pub &'a [(String, String)], @@ -52,17 +55,25 @@ pub struct Markdown<'a>( pub &'a mut IdMap, /// Whether to allow the use of explicit error codes in doctest lang strings. pub ErrorCodes, - /// Default edition to use when parsing doctests (to add a `fn main`). + /// Default edition to use when parsing dcotests (to add a `fn main`). pub Edition, pub &'a Option, + /// images_to_copy + pub &'b mut Vec<(String, PathBuf)>, + /// static_root_path + pub &'b Option, ); /// A tuple struct like `Markdown` that renders the markdown with a table of contents. -pub struct MarkdownWithToc<'a>( +pub struct MarkdownWithToc<'a, 'b>( pub &'a str, pub &'a mut IdMap, pub ErrorCodes, pub Edition, pub &'a Option, + /// images_to_copy + pub &'b mut Vec<(String, PathBuf)>, + /// static_root_path + pub &'b Option, ); /// A tuple struct like `Markdown` that renders the markdown escaping HTML tags. pub struct MarkdownHtml<'a>( @@ -550,6 +561,56 @@ impl<'a, I: Iterator>> Iterator for Footnotes<'a, I> { } } +struct LocalImages<'a, 'b, I: Iterator>> { + inner: I, + images_to_copy: &'b mut Vec<(String, PathBuf)>, + static_root_path: &'b Option, +} + +impl<'a, 'b, I: Iterator>> LocalImages<'a, 'b, I> { + fn new( + iter: I, + images_to_copy: &'b mut Vec<(String, PathBuf)>, + static_root_path: &'b Option, + ) -> Self { + LocalImages { inner: iter, images_to_copy, static_root_path } + } +} + +impl<'a, 'b, I: Iterator>> Iterator for LocalImages<'a, 'b, I> { + type Item = Event<'a>; + + fn next(&mut self) -> Option { + let event = self.inner.next(); + if let Some(Event::Start(Tag::Image(type_, ref url, ref title))) = event { + if url.starts_with("http://") || url.starts_with("https://") { + // Not a local image, move on! + } + if let Ok(url) = Path::new(&url.clone().into_string()).canonicalize() { + let mut hasher = DefaultHasher::new(); + url.hash(&mut hasher); + let hash = format!("{:x}", hasher.finish()); + let static_folder_path = format!("static/{}", hash); + if self.images_to_copy.iter().find(|(h, _)| *h == hash).is_none() { + self.images_to_copy.push((hash, url)); + } + return Some(match self.static_root_path { + Some(p) => { + let s = format!("../{}", Path::new(p).join(&static_folder_path).display()); + Event::Start(Tag::Image(type_, CowStr::Boxed(s.into()), title.clone())) + } + None => Event::Start(Tag::Image( + type_, + CowStr::Boxed(format!("../{}", static_folder_path).into()), + title.clone(), + )), + }); + } + } + event + } +} + pub fn find_testable_code( doc: &str, tests: &mut T, @@ -720,9 +781,18 @@ impl LangString { } } -impl Markdown<'_> { +impl Markdown<'_, '_> { pub fn to_string(self) -> String { - let Markdown(md, links, mut ids, codes, edition, playground) = self; + let Markdown( + md, + links, + mut ids, + codes, + edition, + playground, + images_to_copy, + static_root_path, + ) = self; // This is actually common enough to special-case if md.is_empty() { @@ -742,6 +812,7 @@ impl Markdown<'_> { let p = HeadingLinks::new(p, None, &mut ids); let p = LinkReplacer::new(p, links); + let p = LocalImages::new(p, images_to_copy, static_root_path); let p = CodeBlocks::new(p, codes, edition, playground); let p = Footnotes::new(p); html::push_html(&mut s, p); @@ -750,9 +821,17 @@ impl Markdown<'_> { } } -impl MarkdownWithToc<'_> { +impl MarkdownWithToc<'_, '_> { pub fn to_string(self) -> String { - let MarkdownWithToc(md, mut ids, codes, edition, playground) = self; + let MarkdownWithToc( + md, + mut ids, + codes, + edition, + playground, + images_to_copy, + static_root_path, + ) = self; let p = Parser::new_ext(md, opts()); @@ -762,6 +841,7 @@ impl MarkdownWithToc<'_> { { let p = HeadingLinks::new(p, Some(&mut toc), &mut ids); + let p = LocalImages::new(p, images_to_copy, static_root_path); let p = CodeBlocks::new(p, codes, edition, playground); let p = Footnotes::new(p); html::push_html(&mut s, p); diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs index c73960fe33b9c..f7994213fef72 100644 --- a/src/librustdoc/html/render.rs +++ b/src/librustdoc/html/render.rs @@ -72,6 +72,7 @@ use crate::html::item_type::ItemType; use crate::html::markdown::{self, ErrorCodes, IdMap, Markdown, MarkdownHtml, MarkdownSummaryLine}; use crate::html::sources; use crate::html::{highlight, layout, static_files}; +use crate::markdown::generate_static_images; use minifier; @@ -203,6 +204,10 @@ crate struct SharedContext { pub edition: Edition, pub codes: ErrorCodes, playground: Option, + /// Local images to move into the static folder. + /// + /// The tuple contains the hash as first argument and the image original path. + pub images_to_copy: RefCell>, } impl Context { @@ -482,9 +487,10 @@ pub fn run( edition, codes: ErrorCodes::from(UnstableFeatures::from_environment().is_nightly_build()), playground, + images_to_copy: RefCell::new(Vec::new()), }; - let dst = output; + let dst = output.clone(); scx.ensure_dir(&dst)?; krate = sources::render(&dst, &mut scx, krate)?; let (new_crate, index, cache) = @@ -1349,6 +1355,8 @@ impl Context { ); self.shared.fs.write(&settings_file, v.as_bytes())?; + generate_static_images(&self.dst, &*self.shared.images_to_copy.borrow()); + Ok(()) } @@ -1801,6 +1809,7 @@ fn render_markdown( is_hidden: bool, ) { let mut ids = cx.id_map.borrow_mut(); + let mut images_to_copy = cx.shared.images_to_copy.borrow_mut(); write!( w, "
{}{}
", @@ -1812,7 +1821,9 @@ fn render_markdown( &mut ids, cx.shared.codes, cx.shared.edition, - &cx.shared.playground + &cx.shared.playground, + &mut images_to_copy, + &cx.shared.static_root_path, ) .to_string() ) @@ -3660,6 +3671,7 @@ fn render_impl( write!(w, ""); if let Some(ref dox) = cx.shared.maybe_collapsed_doc_value(&i.impl_item) { let mut ids = cx.id_map.borrow_mut(); + let mut images_to_copy = cx.shared.images_to_copy.borrow_mut(); write!( w, "
{}
", @@ -3669,7 +3681,9 @@ fn render_impl( &mut ids, cx.shared.codes, cx.shared.edition, - &cx.shared.playground + &cx.shared.playground, + &mut images_to_copy, + &cx.shared.static_root_path, ) .to_string() ); diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 912a40722b8af..4f0fce3ba78c1 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -1,6 +1,6 @@ -use std::fs::File; +use std::fs::{self, File}; use std::io::prelude::*; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use rustc_feature::UnstableFeatures; use rustc_span::edition::Edition; @@ -76,10 +76,21 @@ pub fn render( let mut ids = IdMap::new(); let error_codes = ErrorCodes::from(UnstableFeatures::from_environment().is_nightly_build()); + let mut images_to_copy = Vec::new(); let text = if !options.markdown_no_toc { - MarkdownWithToc(text, &mut ids, error_codes, edition, &playground).to_string() + MarkdownWithToc( + text, + &mut ids, + error_codes, + edition, + &playground, + &mut images_to_copy, + &None, + ) + .to_string() } else { - Markdown(text, &[], &mut ids, error_codes, edition, &playground).to_string() + Markdown(text, &[], &mut ids, error_codes, edition, &playground, &mut images_to_copy, &None) + .to_string() }; let err = write!( @@ -122,7 +133,27 @@ pub fn render( diag.struct_err(&format!("cannot write to `{}`: {}", output.display(), e)).emit(); 6 } - Ok(_) => 0, + Ok(_) => { + generate_static_images(&output, &images_to_copy); + 0 + } + } +} + +pub fn generate_static_images>(target_dir: P, images_to_copy: &[(String, PathBuf)]) { + if images_to_copy.is_empty() { + return; + } + let target_dir = target_dir.as_ref().join("static"); + let _ = fs::create_dir(&target_dir); + for (hash, image_to_copy) in images_to_copy { + if fs::copy(image_to_copy, target_dir.join(hash)).is_err() { + eprintln!( + "Couldn't copy `{}` into `{}`...", + image_to_copy.display(), + target_dir.display() + ); + } } } diff --git a/src/test/rustdoc/copy-local-img.rs b/src/test/rustdoc/copy-local-img.rs new file mode 100644 index 0000000000000..1e0f49121e23b --- /dev/null +++ b/src/test/rustdoc/copy-local-img.rs @@ -0,0 +1,10 @@ +#![crate_name = "foo"] + +// @has static/8a40d4987fbb905 +// @has foo/struct.Enum.html +// @has - '//img[@src="../static/8a40d4987fbb905"]' '' + +/// Image test! +/// +/// ![osef](src/test/rustdoc/copy-local-img.rs) +pub struct Enum;