]> git.proxmox.com Git - rustc.git/blob - src/librustdoc/html/render/write_shared.rs
New upstream version 1.55.0+dfsg1
[rustc.git] / src / librustdoc / html / render / write_shared.rs
1 use std::ffi::OsStr;
2 use std::fmt::Write;
3 use std::fs::{self, File};
4 use std::io::prelude::*;
5 use std::io::{self, BufReader};
6 use std::lazy::SyncLazy as Lazy;
7 use std::path::{Component, Path, PathBuf};
8
9 use itertools::Itertools;
10 use rustc_data_structures::flock;
11 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
12 use serde::Serialize;
13
14 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
15 use crate::clean::Crate;
16 use crate::config::{EmitType, RenderOptions};
17 use crate::docfs::PathError;
18 use crate::error::Error;
19 use crate::html::{layout, static_files};
20
21 static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
22 map! {
23 "FiraSans-Regular.woff2" => static_files::fira_sans::REGULAR2,
24 "FiraSans-Medium.woff2" => static_files::fira_sans::MEDIUM2,
25 "FiraSans-Regular.woff" => static_files::fira_sans::REGULAR,
26 "FiraSans-Medium.woff" => static_files::fira_sans::MEDIUM,
27 "FiraSans-LICENSE.txt" => static_files::fira_sans::LICENSE,
28 "SourceSerif4-Regular.ttf.woff2" => static_files::source_serif_4::REGULAR2,
29 "SourceSerif4-Bold.ttf.woff2" => static_files::source_serif_4::BOLD2,
30 "SourceSerif4-It.ttf.woff2" => static_files::source_serif_4::ITALIC2,
31 "SourceSerif4-Regular.ttf.woff" => static_files::source_serif_4::REGULAR,
32 "SourceSerif4-Bold.ttf.woff" => static_files::source_serif_4::BOLD,
33 "SourceSerif4-It.ttf.woff" => static_files::source_serif_4::ITALIC,
34 "SourceSerif4-LICENSE.md" => static_files::source_serif_4::LICENSE,
35 "SourceCodePro-Regular.ttf.woff2" => static_files::source_code_pro::REGULAR2,
36 "SourceCodePro-Semibold.ttf.woff2" => static_files::source_code_pro::SEMIBOLD2,
37 "SourceCodePro-It.ttf.woff2" => static_files::source_code_pro::ITALIC2,
38 "SourceCodePro-Regular.ttf.woff" => static_files::source_code_pro::REGULAR,
39 "SourceCodePro-Semibold.ttf.woff" => static_files::source_code_pro::SEMIBOLD,
40 "SourceCodePro-It.ttf.woff" => static_files::source_code_pro::ITALIC,
41 "SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
42 "noto-sans-kr-v13-korean-regular.woff" => static_files::noto_sans_kr::REGULAR,
43 "noto-sans-kr-v13-korean-regular-LICENSE.txt" => static_files::noto_sans_kr::LICENSE,
44 "LICENSE-MIT.txt" => static_files::LICENSE_MIT,
45 "LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
46 "COPYRIGHT.txt" => static_files::COPYRIGHT,
47 }
48 });
49
50 enum SharedResource<'a> {
51 /// This file will never change, no matter what toolchain is used to build it.
52 ///
53 /// It does not have a resource suffix.
54 Unversioned { name: &'static str },
55 /// This file may change depending on the toolchain.
56 ///
57 /// It has a resource suffix.
58 ToolchainSpecific { basename: &'static str },
59 /// This file may change for any crate within a build, or based on the CLI arguments.
60 ///
61 /// This differs from normal invocation-specific files because it has a resource suffix.
62 InvocationSpecific { basename: &'a str },
63 }
64
65 impl SharedResource<'_> {
66 fn extension(&self) -> Option<&OsStr> {
67 use SharedResource::*;
68 match self {
69 Unversioned { name }
70 | ToolchainSpecific { basename: name }
71 | InvocationSpecific { basename: name } => Path::new(name).extension(),
72 }
73 }
74
75 fn path(&self, cx: &Context<'_>) -> PathBuf {
76 match self {
77 SharedResource::Unversioned { name } => cx.dst.join(name),
78 SharedResource::ToolchainSpecific { basename } => cx.suffix_path(basename),
79 SharedResource::InvocationSpecific { basename } => cx.suffix_path(basename),
80 }
81 }
82
83 fn should_emit(&self, emit: &[EmitType]) -> bool {
84 if emit.is_empty() {
85 return true;
86 }
87 let kind = match self {
88 SharedResource::Unversioned { .. } => EmitType::Unversioned,
89 SharedResource::ToolchainSpecific { .. } => EmitType::Toolchain,
90 SharedResource::InvocationSpecific { .. } => EmitType::InvocationSpecific,
91 };
92 emit.contains(&kind)
93 }
94 }
95
96 impl Context<'_> {
97 fn suffix_path(&self, filename: &str) -> PathBuf {
98 // We use splitn vs Path::extension here because we might get a filename
99 // like `style.min.css` and we want to process that into
100 // `style-suffix.min.css`. Path::extension would just return `css`
101 // which would result in `style.min-suffix.css` which isn't what we
102 // want.
103 let (base, ext) = filename.split_once('.').unwrap();
104 let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
105 self.dst.join(&filename)
106 }
107
108 fn write_shared<C: AsRef<[u8]>>(
109 &self,
110 resource: SharedResource<'_>,
111 contents: C,
112 emit: &[EmitType],
113 ) -> Result<(), Error> {
114 if resource.should_emit(emit) {
115 self.shared.fs.write(resource.path(self), contents)
116 } else {
117 Ok(())
118 }
119 }
120
121 fn write_minify(
122 &self,
123 resource: SharedResource<'_>,
124 contents: &str,
125 minify: bool,
126 emit: &[EmitType],
127 ) -> Result<(), Error> {
128 let tmp;
129 let contents = if minify {
130 tmp = if resource.extension() == Some(&OsStr::new("css")) {
131 minifier::css::minify(contents).map_err(|e| {
132 Error::new(format!("failed to minify CSS file: {}", e), resource.path(self))
133 })?
134 } else {
135 minifier::js::minify(contents)
136 };
137 tmp.as_bytes()
138 } else {
139 contents.as_bytes()
140 };
141
142 self.write_shared(resource, contents, emit)
143 }
144 }
145
146 pub(super) fn write_shared(
147 cx: &Context<'_>,
148 krate: &Crate,
149 search_index: String,
150 options: &RenderOptions,
151 ) -> Result<(), Error> {
152 // Write out the shared files. Note that these are shared among all rustdoc
153 // docs placed in the output directory, so this needs to be a synchronized
154 // operation with respect to all other rustdocs running around.
155 let lock_file = cx.dst.join(".lock");
156 let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
157
158 // The weird `: &_` is to work around a borrowck bug: https://github.com/rust-lang/rust/issues/41078#issuecomment-293646723
159 let write_minify = |p, c: &_| {
160 cx.write_minify(
161 SharedResource::ToolchainSpecific { basename: p },
162 c,
163 options.enable_minification,
164 &options.emit,
165 )
166 };
167 // Toolchain resources should never be dynamic.
168 let write_toolchain = |p: &'static _, c: &'static _| {
169 cx.write_shared(SharedResource::ToolchainSpecific { basename: p }, c, &options.emit)
170 };
171
172 // Crate resources should always be dynamic.
173 let write_crate = |p: &_, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
174 let content = make_content()?;
175 cx.write_shared(SharedResource::InvocationSpecific { basename: p }, content, &options.emit)
176 };
177
178 // Add all the static files. These may already exist, but we just
179 // overwrite them anyway to make sure that they're fresh and up-to-date.
180 write_minify("rustdoc.css", static_files::RUSTDOC_CSS)?;
181 write_minify("settings.css", static_files::SETTINGS_CSS)?;
182 write_minify("noscript.css", static_files::NOSCRIPT_CSS)?;
183
184 // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
185 // then we'll run over the "official" styles.
186 let mut themes: FxHashSet<String> = FxHashSet::default();
187
188 for entry in &cx.shared.style_files {
189 let theme = try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path);
190 let extension =
191 try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
192
193 // Handle the official themes
194 match theme {
195 "light" => write_minify("light.css", static_files::themes::LIGHT)?,
196 "dark" => write_minify("dark.css", static_files::themes::DARK)?,
197 "ayu" => write_minify("ayu.css", static_files::themes::AYU)?,
198 _ => {
199 // Handle added third-party themes
200 let filename = format!("{}.{}", theme, extension);
201 write_crate(&filename, &|| Ok(try_err!(fs::read(&entry.path), &entry.path)))?;
202 }
203 };
204
205 themes.insert(theme.to_owned());
206 }
207
208 if (*cx.shared).layout.logo.is_empty() {
209 write_toolchain("rust-logo.png", static_files::RUST_LOGO)?;
210 }
211 if (*cx.shared).layout.favicon.is_empty() {
212 write_toolchain("favicon.svg", static_files::RUST_FAVICON_SVG)?;
213 write_toolchain("favicon-16x16.png", static_files::RUST_FAVICON_PNG_16)?;
214 write_toolchain("favicon-32x32.png", static_files::RUST_FAVICON_PNG_32)?;
215 }
216 write_toolchain("brush.svg", static_files::BRUSH_SVG)?;
217 write_toolchain("wheel.svg", static_files::WHEEL_SVG)?;
218 write_toolchain("clipboard.svg", static_files::CLIPBOARD_SVG)?;
219 write_toolchain("down-arrow.svg", static_files::DOWN_ARROW_SVG)?;
220
221 let mut themes: Vec<&String> = themes.iter().collect();
222 themes.sort();
223
224 // FIXME: this should probably not be a toolchain file since it depends on `--theme`.
225 // But it seems a shame to copy it over and over when it's almost always the same.
226 // Maybe we can change the representation to move this out of main.js?
227 write_minify(
228 "main.js",
229 &static_files::MAIN_JS.replace(
230 "/* INSERT THEMES HERE */",
231 &format!(" = {}", serde_json::to_string(&themes).unwrap()),
232 ),
233 )?;
234 write_minify("search.js", static_files::SEARCH_JS)?;
235 write_minify("settings.js", static_files::SETTINGS_JS)?;
236
237 if cx.shared.include_sources {
238 write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT)?;
239 }
240
241 {
242 write_minify(
243 "storage.js",
244 &format!(
245 "var resourcesSuffix = \"{}\";{}",
246 cx.shared.resource_suffix,
247 static_files::STORAGE_JS
248 ),
249 )?;
250 }
251
252 if let Some(ref css) = cx.shared.layout.css_file_extension {
253 let buffer = try_err!(fs::read_to_string(css), css);
254 // This varies based on the invocation, so it can't go through the write_minify wrapper.
255 cx.write_minify(
256 SharedResource::InvocationSpecific { basename: "theme.css" },
257 &buffer,
258 options.enable_minification,
259 &options.emit,
260 )?;
261 }
262 write_minify("normalize.css", static_files::NORMALIZE_CSS)?;
263 for (name, contents) in &*FILES_UNVERSIONED {
264 cx.write_shared(SharedResource::Unversioned { name }, contents, &options.emit)?;
265 }
266
267 fn collect(path: &Path, krate: &str, key: &str) -> io::Result<(Vec<String>, Vec<String>)> {
268 let mut ret = Vec::new();
269 let mut krates = Vec::new();
270
271 if path.exists() {
272 let prefix = format!(r#"{}["{}"]"#, key, krate);
273 for line in BufReader::new(File::open(path)?).lines() {
274 let line = line?;
275 if !line.starts_with(key) {
276 continue;
277 }
278 if line.starts_with(&prefix) {
279 continue;
280 }
281 ret.push(line.to_string());
282 krates.push(
283 line[key.len() + 2..]
284 .split('"')
285 .next()
286 .map(|s| s.to_owned())
287 .unwrap_or_else(String::new),
288 );
289 }
290 }
291 Ok((ret, krates))
292 }
293
294 fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
295 let mut ret = Vec::new();
296 let mut krates = Vec::new();
297
298 if path.exists() {
299 let prefix = format!("\"{}\"", krate);
300 for line in BufReader::new(File::open(path)?).lines() {
301 let line = line?;
302 if !line.starts_with('"') {
303 continue;
304 }
305 if line.starts_with(&prefix) {
306 continue;
307 }
308 if line.ends_with(",\\") {
309 ret.push(line[..line.len() - 2].to_string());
310 } else {
311 // Ends with "\\" (it's the case for the last added crate line)
312 ret.push(line[..line.len() - 1].to_string());
313 }
314 krates.push(
315 line.split('"')
316 .find(|s| !s.is_empty())
317 .map(|s| s.to_owned())
318 .unwrap_or_else(String::new),
319 );
320 }
321 }
322 Ok((ret, krates))
323 }
324
325 use std::ffi::OsString;
326
327 #[derive(Debug)]
328 struct Hierarchy {
329 elem: OsString,
330 children: FxHashMap<OsString, Hierarchy>,
331 elems: FxHashSet<OsString>,
332 }
333
334 impl Hierarchy {
335 fn new(elem: OsString) -> Hierarchy {
336 Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
337 }
338
339 fn to_json_string(&self) -> String {
340 let mut subs: Vec<&Hierarchy> = self.children.values().collect();
341 subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
342 let mut files = self
343 .elems
344 .iter()
345 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
346 .collect::<Vec<_>>();
347 files.sort_unstable();
348 let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
349 let dirs =
350 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
351 let files = files.join(",");
352 let files =
353 if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
354 format!(
355 "{{\"name\":\"{name}\"{dirs}{files}}}",
356 name = self.elem.to_str().expect("invalid osstring conversion"),
357 dirs = dirs,
358 files = files
359 )
360 }
361 }
362
363 if cx.shared.include_sources {
364 let mut hierarchy = Hierarchy::new(OsString::new());
365 for source in cx
366 .shared
367 .local_sources
368 .iter()
369 .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
370 {
371 let mut h = &mut hierarchy;
372 let mut elems = source
373 .components()
374 .filter_map(|s| match s {
375 Component::Normal(s) => Some(s.to_owned()),
376 _ => None,
377 })
378 .peekable();
379 loop {
380 let cur_elem = elems.next().expect("empty file path");
381 if elems.peek().is_none() {
382 h.elems.insert(cur_elem);
383 break;
384 } else {
385 let e = cur_elem.clone();
386 h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
387 }
388 }
389 }
390
391 let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
392 let make_sources = || {
393 let (mut all_sources, _krates) =
394 try_err!(collect(&dst, &krate.name.as_str(), "sourcesIndex"), &dst);
395 all_sources.push(format!(
396 "sourcesIndex[\"{}\"] = {};",
397 &krate.name,
398 hierarchy.to_json_string()
399 ));
400 all_sources.sort();
401 Ok(format!(
402 "var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
403 all_sources.join("\n")
404 )
405 .into_bytes())
406 };
407 write_crate("source-files.js", &make_sources)?;
408 }
409
410 // Update the search index and crate list.
411 let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
412 let (mut all_indexes, mut krates) = try_err!(collect_json(&dst, &krate.name.as_str()), &dst);
413 all_indexes.push(search_index);
414 krates.push(krate.name.to_string());
415 krates.sort();
416
417 // Sort the indexes by crate so the file will be generated identically even
418 // with rustdoc running in parallel.
419 all_indexes.sort();
420 write_crate("search-index.js", &|| {
421 let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
422 v.push_str(&all_indexes.join(",\\\n"));
423 v.push_str("\\\n}');\nif (window.initSearch) {window.initSearch(searchIndex)};");
424 Ok(v.into_bytes())
425 })?;
426
427 write_crate("crates.js", &|| {
428 let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
429 Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
430 })?;
431
432 if options.enable_index_page {
433 if let Some(index_page) = options.index_page.clone() {
434 let mut md_opts = options.clone();
435 md_opts.output = cx.dst.clone();
436 md_opts.external_html = (*cx.shared).layout.external_html.clone();
437
438 crate::markdown::render(&index_page, md_opts, cx.shared.edition())
439 .map_err(|e| Error::new(e, &index_page))?;
440 } else {
441 let dst = cx.dst.join("index.html");
442 let page = layout::Page {
443 title: "Index of crates",
444 css_class: "mod",
445 root_path: "./",
446 static_root_path: cx.shared.static_root_path.as_deref(),
447 description: "List of crates",
448 keywords: BASIC_KEYWORDS,
449 resource_suffix: &cx.shared.resource_suffix,
450 extra_scripts: &[],
451 static_extra_scripts: &[],
452 };
453
454 let content = format!(
455 "<h1 class=\"fqn\">\
456 <span class=\"in-band\">List of all crates</span>\
457 </h1><ul class=\"crate mod\">{}</ul>",
458 krates
459 .iter()
460 .map(|s| {
461 format!(
462 "<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
463 ensure_trailing_slash(s),
464 s
465 )
466 })
467 .collect::<String>()
468 );
469 let v = layout::render(
470 &cx.shared.templates,
471 &cx.shared.layout,
472 &page,
473 "",
474 content,
475 &cx.shared.style_files,
476 );
477 cx.shared.fs.write(&dst, v.as_bytes())?;
478 }
479 }
480
481 // Update the list of all implementors for traits
482 let dst = cx.dst.join("implementors");
483 for (&did, imps) in &cx.cache.implementors {
484 // Private modules can leak through to this phase of rustdoc, which
485 // could contain implementations for otherwise private types. In some
486 // rare cases we could find an implementation for an item which wasn't
487 // indexed, so we just skip this step in that case.
488 //
489 // FIXME: this is a vague explanation for why this can't be a `get`, in
490 // theory it should be...
491 let &(ref remote_path, remote_item_type) = match cx.cache.paths.get(&did) {
492 Some(p) => p,
493 None => match cx.cache.external_paths.get(&did) {
494 Some(p) => p,
495 None => continue,
496 },
497 };
498
499 #[derive(Serialize)]
500 struct Implementor {
501 text: String,
502 synthetic: bool,
503 types: Vec<String>,
504 }
505
506 let implementors = imps
507 .iter()
508 .filter_map(|imp| {
509 // If the trait and implementation are in the same crate, then
510 // there's no need to emit information about it (there's inlining
511 // going on). If they're in different crates then the crate defining
512 // the trait will be interested in our implementation.
513 //
514 // If the implementation is from another crate then that crate
515 // should add it.
516 if imp.impl_item.def_id.krate() == did.krate || !imp.impl_item.def_id.is_local() {
517 None
518 } else {
519 Some(Implementor {
520 text: imp.inner_impl().print(false, cx).to_string(),
521 synthetic: imp.inner_impl().synthetic,
522 types: collect_paths_for_type(imp.inner_impl().for_.clone(), cx.cache()),
523 })
524 }
525 })
526 .collect::<Vec<_>>();
527
528 // Only create a js file if we have impls to add to it. If the trait is
529 // documented locally though we always create the file to avoid dead
530 // links.
531 if implementors.is_empty() && !cx.cache.paths.contains_key(&did) {
532 continue;
533 }
534
535 let implementors = format!(
536 r#"implementors["{}"] = {};"#,
537 krate.name,
538 serde_json::to_string(&implementors).unwrap()
539 );
540
541 let mut mydst = dst.clone();
542 for part in &remote_path[..remote_path.len() - 1] {
543 mydst.push(part);
544 }
545 cx.shared.ensure_dir(&mydst)?;
546 mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
547
548 let (mut all_implementors, _) =
549 try_err!(collect(&mydst, &krate.name.as_str(), "implementors"), &mydst);
550 all_implementors.push(implementors);
551 // Sort the implementors by crate so the file will be generated
552 // identically even with rustdoc running in parallel.
553 all_implementors.sort();
554
555 let mut v = String::from("(function() {var implementors = {};\n");
556 for implementor in &all_implementors {
557 writeln!(v, "{}", *implementor).unwrap();
558 }
559 v.push_str(
560 "if (window.register_implementors) {\
561 window.register_implementors(implementors);\
562 } else {\
563 window.pending_implementors = implementors;\
564 }",
565 );
566 v.push_str("})()");
567 cx.shared.fs.write(&mydst, &v)?;
568 }
569 Ok(())
570 }