2 use crate::docfs::PathError;
3 use crate::error::Error;
4 use crate::html::format::Buffer;
5 use crate::html::highlight;
6 use crate::html::layout;
7 use crate::html::render::{Context, BASIC_KEYWORDS};
8 use crate::visit::DocVisitor;
10 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
11 use rustc_hir::def_id::LOCAL_CRATE;
12 use rustc_middle::ty::TyCtxt;
13 use rustc_session::Session;
14 use rustc_span::source_map::FileName;
16 use std::cell::RefCell;
19 use std::path::{Component, Path, PathBuf};
22 pub(crate) fn render(cx: &mut Context<'_>, krate: &clean::Crate) -> Result<(), Error> {
23 info!("emitting source files");
25 let dst = cx.dst.join("src").join(krate.name(cx.tcx()).as_str());
26 cx.shared.ensure_dir(&dst)?;
28 let mut collector = SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default() };
29 collector.visit_crate(krate);
33 pub(crate) fn collect_local_sources<'tcx>(
37 ) -> FxHashMap<PathBuf, String> {
38 let mut lsc = LocalSourcesCollector { tcx, local_sources: FxHashMap::default(), src_root };
39 lsc.visit_crate(krate);
43 struct LocalSourcesCollector<'a, 'tcx> {
45 local_sources: FxHashMap<PathBuf, String>,
49 fn is_real_and_local(span: clean::Span, sess: &Session) -> bool {
50 span.cnum(sess) == LOCAL_CRATE && span.filename(sess).is_real()
53 impl LocalSourcesCollector<'_, '_> {
54 fn add_local_source(&mut self, item: &clean::Item) {
55 let sess = self.tcx.sess;
56 let span = item.span(self.tcx);
57 let Some(span) = span else { return };
58 // skip all synthetic "files"
59 if !is_real_and_local(span, sess) {
62 let filename = span.filename(sess);
63 let p = if let FileName::Real(file) = filename {
64 match file.into_local_path() {
71 if self.local_sources.contains_key(&*p) {
72 // We've already emitted this source
76 let href = RefCell::new(PathBuf::new());
81 href.borrow_mut().push(component);
84 href.borrow_mut().pop();
88 let mut href = href.into_inner().to_string_lossy().to_string();
89 if let Some(c) = href.as_bytes().last() && *c != b'/' {
92 let mut src_fname = p.file_name().expect("source has no filename").to_os_string();
93 src_fname.push(".html");
94 href.push_str(&src_fname.to_string_lossy());
95 self.local_sources.insert(p, href);
99 impl DocVisitor for LocalSourcesCollector<'_, '_> {
100 fn visit_item(&mut self, item: &clean::Item) {
101 self.add_local_source(item);
103 self.visit_item_recur(item)
107 /// Helper struct to render all source code to HTML pages
108 struct SourceCollector<'a, 'tcx> {
109 cx: &'a mut Context<'tcx>,
111 /// Root destination to place all HTML output into
113 emitted_local_sources: FxHashSet<PathBuf>,
116 impl DocVisitor for SourceCollector<'_, '_> {
117 fn visit_item(&mut self, item: &clean::Item) {
118 if !self.cx.include_sources {
122 let tcx = self.cx.tcx();
123 let span = item.span(tcx);
124 let Some(span) = span else { return };
127 // If we're not rendering sources, there's nothing to do.
128 // If we're including source files, and we haven't seen this file yet,
129 // then we need to render it out to the filesystem.
130 if is_real_and_local(span, sess) {
131 let filename = span.filename(sess);
132 let span = span.inner();
133 let pos = sess.source_map().lookup_source_file(span.lo());
134 let file_span = span.with_lo(pos.start_pos).with_hi(pos.end_pos);
135 // If it turns out that we couldn't read this file, then we probably
136 // can't read any of the files (generating html output from json or
137 // something like that), so just don't include sources for the
138 // entire crate. The other option is maintaining this mapping on a
139 // per-file basis, but that's probably not worth it...
140 self.cx.include_sources = match self.emit_source(&filename, file_span) {
143 self.cx.shared.tcx.sess.span_err(
146 "failed to render source code for `{}`: {}",
147 filename.prefer_local(),
156 self.visit_item_recur(item)
160 impl SourceCollector<'_, '_> {
161 /// Renders the given filename into its corresponding HTML source file.
165 file_span: rustc_span::Span,
166 ) -> Result<(), Error> {
167 let p = match *filename {
168 FileName::Real(ref file) => {
169 if let Some(local_path) = file.local_path() {
170 local_path.to_path_buf()
172 unreachable!("only the current crate should have sources emitted");
177 if self.emitted_local_sources.contains(&*p) {
178 // We've already emitted this source
182 let contents = match fs::read_to_string(&p) {
183 Ok(contents) => contents,
185 return Err(Error::new(e, &p));
189 // Remove the utf-8 BOM if any
190 let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
192 let shared = Rc::clone(&self.cx.shared);
193 // Create the intermediate directories
194 let cur = RefCell::new(PathBuf::new());
195 let root_path = RefCell::new(PathBuf::new());
201 cur.borrow_mut().push(component);
202 root_path.borrow_mut().push("..");
205 cur.borrow_mut().pop();
206 root_path.borrow_mut().pop();
210 let root_path = PathBuf::from("../../").join(root_path.into_inner());
211 let mut root_path = root_path.to_string_lossy();
212 if let Some(c) = root_path.as_bytes().last() && *c != b'/' {
215 let mut cur = self.dst.join(cur.into_inner());
216 shared.ensure_dir(&cur)?;
218 let src_fname = p.file_name().expect("source has no filename").to_os_string();
219 let mut fname = src_fname.clone();
223 let title = format!("{} - source", src_fname.to_string_lossy());
224 let desc = format!("Source of the Rust file `{}`.", filename.prefer_remapped());
225 let page = layout::Page {
228 root_path: &root_path,
229 static_root_path: shared.static_root_path.as_deref(),
231 keywords: BASIC_KEYWORDS,
232 resource_suffix: &shared.resource_suffix,
234 let v = layout::render(
239 let cx = &mut self.cx;
246 highlight::DecorationInfo::default(),
247 SourceContext::Standalone,
252 shared.fs.write(cur, v)?;
253 self.emitted_local_sources.insert(p);
258 /// Takes a path to a source file and cleans the path to it. This canonicalizes
259 /// things like ".." to components which preserve the "top down" hierarchy of a
260 /// static HTML tree. Each component in the cleaned path will be passed as an
261 /// argument to `f`. The very last component of the path (ie the file name) is ignored.
262 /// If a `..` is encountered, the `parent` closure will be called to allow the callee to
264 pub(crate) fn clean_path<F, P>(src_root: &Path, p: &Path, mut f: F, mut parent: P)
269 // make it relative, if possible
270 let p = p.strip_prefix(src_root).unwrap_or(p);
272 let mut iter = p.components().peekable();
274 while let Some(c) = iter.next() {
275 if iter.peek().is_none() {
280 Component::ParentDir => parent(),
281 Component::Normal(c) => f(c),
287 pub(crate) enum SourceContext {
289 Embedded { offset: usize, needs_expansion: bool },
292 /// Wrapper struct to render the source code of a file. This will do things like
293 /// adding line numbers to the left-hand side.
294 pub(crate) fn print_src(
297 file_span: rustc_span::Span,
298 context: &Context<'_>,
300 decoration_info: highlight::DecorationInfo,
301 source_context: SourceContext,
303 let lines = s.lines().count();
304 let mut line_numbers = Buffer::empty_from(buf);
306 line_numbers.write_str("<pre class=\"src-line-numbers\">");
307 let current_href = context
308 .href_from_span(clean::Span::new(file_span), false)
309 .expect("only local crates should have sources emitted");
310 match source_context {
311 SourceContext::Standalone => {
313 for line in 1..=lines {
314 writeln!(line_numbers, "<a href=\"#{line}\" id=\"{line}\">{line}</a>")
317 SourceContext::Embedded { offset, needs_expansion } => {
318 extra = if needs_expansion {
319 Some(r#"<button class="expand">↕</button>"#)
323 for line_number in 1..=lines {
324 let line = line_number + offset;
325 writeln!(line_numbers, "<span>{line}</span>")
329 line_numbers.write_str("</pre>");
330 highlight::render_source_with_highlighting(
334 highlight::HrefContext { context, file_span, root_path, current_href },