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, FileNameDisplayPreference, RealFileName, sym}; 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 { 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, src_root: &'a Path, } fn filename_real_and_local(span: clean::Span, sess: &Session) -> Option { 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, 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.to_string_lossy(FileNameDisplayPreference::Local), ), ); 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.strip_prefix('\u{feff}').unwrap_or(&contents); 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 `{}`.", file.to_string_lossy(FileNameDisplayPreference::Remapped) ); 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(), sym::rust_logo), }; 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(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_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| { let current_href = 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(()) }