| use std::cell::RefCell; | |
| use std::ffi::OsStr; | |
| use std::path::{Component, Path, PathBuf}; | |
| use std::{fmt, fs}; | |
| use askama::Template; | |
| use rustc_data_structures::fx::{FxHashSet, FxIndexMap}; | |
| use rustc_hir::def_id::LOCAL_CRATE; | |
| use rustc_middle::ty::TyCtxt; | |
| use rustc_session::Session; | |
| use rustc_span::{FileName, RealFileName, RemapPathScopeComponents}; | |
| use tracing::info; | |
| use super::render::Context; | |
| use super::{highlight, layout}; | |
| use crate::clean; | |
| use crate::clean::utils::has_doc_flag; | |
| use crate::docfs::PathError; | |
| use crate::error::Error; | |
| use crate::visit::DocVisitor; | |
| pub(crate) fn render(cx: &mut Context<'_>, krate: &clean::Crate) -> Result<(), Error> { | |
| info!("emitting source files"); | |
| let dst = cx.dst.join("src").join(krate.name(cx.tcx()).as_str()); | |
| cx.shared.ensure_dir(&dst)?; | |
| let crate_name = krate.name(cx.tcx()); | |
| let crate_name = crate_name.as_str(); | |
| let mut collector = | |
| SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default(), crate_name }; | |
| collector.visit_crate(krate); | |
| Ok(()) | |
| } | |
| pub(crate) fn collect_local_sources( | |
| tcx: TyCtxt<'_>, | |
| src_root: &Path, | |
| krate: &clean::Crate, | |
| ) -> FxIndexMap<PathBuf, String> { | |
| let mut lsc = LocalSourcesCollector { tcx, local_sources: FxIndexMap::default(), src_root }; | |
| lsc.visit_crate(krate); | |
| lsc.local_sources | |
| } | |
| struct LocalSourcesCollector<'a, 'tcx> { | |
| tcx: TyCtxt<'tcx>, | |
| local_sources: FxIndexMap<PathBuf, String>, | |
| src_root: &'a Path, | |
| } | |
| fn filename_real_and_local(span: clean::Span, sess: &Session) -> Option<RealFileName> { | |
| if span.cnum(sess) == LOCAL_CRATE | |
| && let FileName::Real(file) = span.filename(sess) | |
| { | |
| Some(file) | |
| } else { | |
| None | |
| } | |
| } | |
| impl LocalSourcesCollector<'_, '_> { | |
| fn add_local_source(&mut self, item: &clean::Item) { | |
| let sess = self.tcx.sess; | |
| let span = item.span(self.tcx); | |
| let Some(span) = span else { return }; | |
| // skip all synthetic "files" | |
| let Some(p) = filename_real_and_local(span, sess).and_then(|file| file.into_local_path()) | |
| else { | |
| return; | |
| }; | |
| if self.local_sources.contains_key(&*p) { | |
| // We've already emitted this source | |
| return; | |
| } | |
| let href = RefCell::new(PathBuf::new()); | |
| clean_path( | |
| self.src_root, | |
| &p, | |
| |component| { | |
| href.borrow_mut().push(component); | |
| }, | |
| || { | |
| href.borrow_mut().pop(); | |
| }, | |
| ); | |
| let mut href = href.into_inner().to_string_lossy().into_owned(); | |
| if let Some(c) = href.as_bytes().last() | |
| && *c != b'/' | |
| { | |
| href.push('/'); | |
| } | |
| let mut src_fname = p.file_name().expect("source has no filename").to_os_string(); | |
| src_fname.push(".html"); | |
| href.push_str(&src_fname.to_string_lossy()); | |
| self.local_sources.insert(p, href); | |
| } | |
| } | |
| impl DocVisitor<'_> for LocalSourcesCollector<'_, '_> { | |
| fn visit_item(&mut self, item: &clean::Item) { | |
| self.add_local_source(item); | |
| self.visit_item_recur(item) | |
| } | |
| } | |
| /// Helper struct to render all source code to HTML pages | |
| struct SourceCollector<'a, 'tcx> { | |
| cx: &'a mut Context<'tcx>, | |
| /// Root destination to place all HTML output into | |
| dst: PathBuf, | |
| emitted_local_sources: FxHashSet<PathBuf>, | |
| crate_name: &'a str, | |
| } | |
| impl DocVisitor<'_> for SourceCollector<'_, '_> { | |
| fn visit_item(&mut self, item: &clean::Item) { | |
| if !self.cx.info.include_sources { | |
| return; | |
| } | |
| let tcx = self.cx.tcx(); | |
| let span = item.span(tcx); | |
| let Some(span) = span else { return }; | |
| let sess = tcx.sess; | |
| // If we're not rendering sources, there's nothing to do. | |
| // If we're including source files, and we haven't seen this file yet, | |
| // then we need to render it out to the filesystem. | |
| if let Some(filename) = filename_real_and_local(span, sess) { | |
| let span = span.inner(); | |
| let pos = sess.source_map().lookup_source_file(span.lo()); | |
| let file_span = span.with_lo(pos.start_pos).with_hi(pos.end_position()); | |
| // If it turns out that we couldn't read this file, then we probably | |
| // can't read any of the files (generating html output from json or | |
| // something like that), so just don't include sources for the | |
| // entire crate. The other option is maintaining this mapping on a | |
| // per-file basis, but that's probably not worth it... | |
| self.cx.info.include_sources = match self.emit_source(&filename, file_span) { | |
| Ok(()) => true, | |
| Err(e) => { | |
| self.cx.shared.tcx.dcx().span_err( | |
| span, | |
| format!( | |
| "failed to render source code for `{filename}`: {e}", | |
| filename = filename | |
| .path(RemapPathScopeComponents::DIAGNOSTICS) | |
| .to_string_lossy() | |
| .into_owned(), | |
| ), | |
| ); | |
| false | |
| } | |
| }; | |
| } | |
| self.visit_item_recur(item) | |
| } | |
| } | |
| impl SourceCollector<'_, '_> { | |
| /// Renders the given filename into its corresponding HTML source file. | |
| fn emit_source( | |
| &mut self, | |
| file: &RealFileName, | |
| file_span: rustc_span::Span, | |
| ) -> Result<(), Error> { | |
| let p = if let Some(local_path) = file.local_path() { | |
| local_path.to_path_buf() | |
| } else { | |
| unreachable!("only the current crate should have sources emitted"); | |
| }; | |
| if self.emitted_local_sources.contains(&*p) { | |
| // We've already emitted this source | |
| return Ok(()); | |
| } | |
| let contents = match fs::read_to_string(&p) { | |
| Ok(contents) => contents, | |
| Err(e) => { | |
| return Err(Error::new(e, &p)); | |
| } | |
| }; | |
| // Remove the utf-8 BOM if any | |
| let contents = contents.trim_prefix('\u{feff}'); | |
| let shared = &self.cx.shared; | |
| // Create the intermediate directories | |
| let cur = RefCell::new(PathBuf::new()); | |
| let root_path = RefCell::new(PathBuf::new()); | |
| clean_path( | |
| &shared.src_root, | |
| &p, | |
| |component| { | |
| cur.borrow_mut().push(component); | |
| root_path.borrow_mut().push(".."); | |
| }, | |
| || { | |
| cur.borrow_mut().pop(); | |
| root_path.borrow_mut().pop(); | |
| }, | |
| ); | |
| let src_fname = p.file_name().expect("source has no filename").to_os_string(); | |
| let mut fname = src_fname.clone(); | |
| let root_path = PathBuf::from("../../").join(root_path.into_inner()); | |
| let mut root_path = root_path.to_string_lossy(); | |
| if let Some(c) = root_path.as_bytes().last() | |
| && *c != b'/' | |
| { | |
| root_path += "/"; | |
| } | |
| let mut file_path = Path::new(&self.crate_name).join(&*cur.borrow()); | |
| file_path.push(&fname); | |
| fname.push(".html"); | |
| let mut cur = self.dst.join(cur.into_inner()); | |
| shared.ensure_dir(&cur)?; | |
| cur.push(&fname); | |
| let title = format!("{} - source", src_fname.to_string_lossy()); | |
| let desc = format!("Source of the Rust file `{}`.", p.to_string_lossy()); | |
| let page = layout::Page { | |
| title: &title, | |
| short_title: &src_fname.to_string_lossy(), | |
| css_class: "src", | |
| root_path: &root_path, | |
| static_root_path: shared.static_root_path.as_deref(), | |
| description: &desc, | |
| resource_suffix: &shared.resource_suffix, | |
| rust_logo: has_doc_flag(self.cx.tcx(), LOCAL_CRATE.as_def_id(), |d| { | |
| d.rust_logo.is_some() | |
| }), | |
| }; | |
| let source_context = SourceContext::Standalone { file_path }; | |
| let v = layout::render( | |
| &shared.layout, | |
| &page, | |
| "", | |
| fmt::from_fn(|f| { | |
| print_src( | |
| f, | |
| contents, | |
| file_span, | |
| self.cx, | |
| &root_path, | |
| &highlight::DecorationInfo::default(), | |
| &source_context, | |
| ) | |
| }), | |
| &shared.style_files, | |
| ); | |
| shared.fs.write(cur, v)?; | |
| self.emitted_local_sources.insert(p); | |
| Ok(()) | |
| } | |
| } | |
| /// Takes a path to a source file and cleans the path to it. This canonicalizes | |
| /// things like ".." to components which preserve the "top down" hierarchy of a | |
| /// static HTML tree. Each component in the cleaned path will be passed as an | |
| /// argument to `f`. The very last component of the path (ie the file name) is ignored. | |
| /// If a `..` is encountered, the `parent` closure will be called to allow the callee to | |
| /// handle it. | |
| pub(crate) fn clean_path<F, P>(src_root: &Path, p: &Path, mut f: F, mut parent: P) | |
| where | |
| F: FnMut(&OsStr), | |
| P: FnMut(), | |
| { | |
| // make it relative, if possible | |
| let p = p.strip_prefix(src_root).unwrap_or(p); | |
| let mut iter = p.components().peekable(); | |
| while let Some(c) = iter.next() { | |
| if iter.peek().is_none() { | |
| break; | |
| } | |
| match c { | |
| Component::ParentDir => parent(), | |
| Component::Normal(c) => f(c), | |
| _ => continue, | |
| } | |
| } | |
| } | |
| pub(crate) struct ScrapedInfo<'a> { | |
| pub(crate) offset: usize, | |
| pub(crate) name: &'a str, | |
| pub(crate) url: &'a str, | |
| pub(crate) title: &'a str, | |
| pub(crate) locations: String, | |
| pub(crate) needs_expansion: bool, | |
| } | |
| #[derive(Template)] | |
| #[template(path = "scraped_source.html")] | |
| struct ScrapedSource<'a, Code: std::fmt::Display> { | |
| info: &'a ScrapedInfo<'a>, | |
| code_html: Code, | |
| max_nb_digits: u32, | |
| } | |
| #[derive(Template)] | |
| #[template(path = "source.html")] | |
| struct Source<Code: std::fmt::Display> { | |
| code_html: Code, | |
| file_path: Option<(String, String)>, | |
| max_nb_digits: u32, | |
| } | |
| pub(crate) enum SourceContext<'a> { | |
| Standalone { file_path: PathBuf }, | |
| Embedded(ScrapedInfo<'a>), | |
| } | |
| /// Wrapper struct to render the source code of a file. This will do things like | |
| /// adding line numbers to the left-hand side. | |
| pub(crate) fn print_src( | |
| mut writer: impl fmt::Write, | |
| s: &str, | |
| file_span: rustc_span::Span, | |
| context: &Context<'_>, | |
| root_path: &str, | |
| decoration_info: &highlight::DecorationInfo, | |
| source_context: &SourceContext<'_>, | |
| ) -> fmt::Result { | |
| let mut lines = s.lines().count(); | |
| let line_info = if let SourceContext::Embedded(info) = source_context { | |
| highlight::LineInfo::new_scraped(lines as u32, info.offset as u32) | |
| } else { | |
| highlight::LineInfo::new(lines as u32) | |
| }; | |
| if line_info.is_scraped_example { | |
| lines += line_info.start_line as usize; | |
| } | |
| let code = fmt::from_fn(move |fmt| { | |
| // For scraped examples, use the URL from ScrapedInfo directly. | |
| // For regular sources, derive it from the span. | |
| let current_href = if let SourceContext::Embedded(info) = source_context { | |
| info.url.to_string() | |
| } else { | |
| context | |
| .href_from_span(clean::Span::new(file_span), false) | |
| .expect("only local crates should have sources emitted") | |
| }; | |
| highlight::write_code( | |
| fmt, | |
| s, | |
| Some(highlight::HrefContext { | |
| context, | |
| file_span: file_span.into(), | |
| root_path, | |
| current_href, | |
| }), | |
| Some(decoration_info), | |
| Some(line_info), | |
| ); | |
| Ok(()) | |
| }); | |
| let max_nb_digits = if lines > 0 { lines.ilog10() + 1 } else { 1 }; | |
| match source_context { | |
| SourceContext::Standalone { file_path } => Source { | |
| code_html: code, | |
| file_path: if let Some(file_name) = file_path.file_name() | |
| && let Some(file_path) = file_path.parent() | |
| { | |
| Some((file_path.display().to_string(), file_name.display().to_string())) | |
| } else { | |
| None | |
| }, | |
| max_nb_digits, | |
| } | |
| .render_into(&mut writer), | |
| SourceContext::Embedded(info) => { | |
| ScrapedSource { info, code_html: code, max_nb_digits }.render_into(&mut writer) | |
| } | |
| }?; | |
| Ok(()) | |
| } |