This way, we can show the plus and minus buttons on screens, while voice control will read off actual words "Collapse" and "Expand" instead of reading "open brace minus close brace" and "open brace plus close brace". Part of #87059
573 lines
22 KiB
Rust
573 lines
22 KiB
Rust
use std::ffi::OsStr;
|
|
use std::fmt::Write;
|
|
use std::fs::{self, File};
|
|
use std::io::prelude::*;
|
|
use std::io::{self, BufReader};
|
|
use std::lazy::SyncLazy as Lazy;
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
use itertools::Itertools;
|
|
use rustc_data_structures::flock;
|
|
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
|
|
use serde::Serialize;
|
|
|
|
use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
|
|
use crate::clean::Crate;
|
|
use crate::config::{EmitType, RenderOptions};
|
|
use crate::docfs::PathError;
|
|
use crate::error::Error;
|
|
use crate::html::{layout, static_files};
|
|
|
|
static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
|
|
map! {
|
|
"FiraSans-Regular.woff2" => static_files::fira_sans::REGULAR2,
|
|
"FiraSans-Medium.woff2" => static_files::fira_sans::MEDIUM2,
|
|
"FiraSans-Regular.woff" => static_files::fira_sans::REGULAR,
|
|
"FiraSans-Medium.woff" => static_files::fira_sans::MEDIUM,
|
|
"FiraSans-LICENSE.txt" => static_files::fira_sans::LICENSE,
|
|
"SourceSerif4-Regular.ttf.woff2" => static_files::source_serif_4::REGULAR2,
|
|
"SourceSerif4-Bold.ttf.woff2" => static_files::source_serif_4::BOLD2,
|
|
"SourceSerif4-It.ttf.woff2" => static_files::source_serif_4::ITALIC2,
|
|
"SourceSerif4-Regular.ttf.woff" => static_files::source_serif_4::REGULAR,
|
|
"SourceSerif4-Bold.ttf.woff" => static_files::source_serif_4::BOLD,
|
|
"SourceSerif4-It.ttf.woff" => static_files::source_serif_4::ITALIC,
|
|
"SourceSerif4-LICENSE.md" => static_files::source_serif_4::LICENSE,
|
|
"SourceCodePro-Regular.ttf.woff2" => static_files::source_code_pro::REGULAR2,
|
|
"SourceCodePro-Semibold.ttf.woff2" => static_files::source_code_pro::SEMIBOLD2,
|
|
"SourceCodePro-It.ttf.woff2" => static_files::source_code_pro::ITALIC2,
|
|
"SourceCodePro-Regular.ttf.woff" => static_files::source_code_pro::REGULAR,
|
|
"SourceCodePro-Semibold.ttf.woff" => static_files::source_code_pro::SEMIBOLD,
|
|
"SourceCodePro-It.ttf.woff" => static_files::source_code_pro::ITALIC,
|
|
"SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
|
|
"noto-sans-kr-v13-korean-regular.woff" => static_files::noto_sans_kr::REGULAR,
|
|
"noto-sans-kr-v13-korean-regular-LICENSE.txt" => static_files::noto_sans_kr::LICENSE,
|
|
"LICENSE-MIT.txt" => static_files::LICENSE_MIT,
|
|
"LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
|
|
"COPYRIGHT.txt" => static_files::COPYRIGHT,
|
|
}
|
|
});
|
|
|
|
enum SharedResource<'a> {
|
|
/// This file will never change, no matter what toolchain is used to build it.
|
|
///
|
|
/// It does not have a resource suffix.
|
|
Unversioned { name: &'static str },
|
|
/// This file may change depending on the toolchain.
|
|
///
|
|
/// It has a resource suffix.
|
|
ToolchainSpecific { basename: &'static str },
|
|
/// This file may change for any crate within a build, or based on the CLI arguments.
|
|
///
|
|
/// This differs from normal invocation-specific files because it has a resource suffix.
|
|
InvocationSpecific { basename: &'a str },
|
|
}
|
|
|
|
impl SharedResource<'_> {
|
|
fn extension(&self) -> Option<&OsStr> {
|
|
use SharedResource::*;
|
|
match self {
|
|
Unversioned { name }
|
|
| ToolchainSpecific { basename: name }
|
|
| InvocationSpecific { basename: name } => Path::new(name).extension(),
|
|
}
|
|
}
|
|
|
|
fn path(&self, cx: &Context<'_>) -> PathBuf {
|
|
match self {
|
|
SharedResource::Unversioned { name } => cx.dst.join(name),
|
|
SharedResource::ToolchainSpecific { basename } => cx.suffix_path(basename),
|
|
SharedResource::InvocationSpecific { basename } => cx.suffix_path(basename),
|
|
}
|
|
}
|
|
|
|
fn should_emit(&self, emit: &[EmitType]) -> bool {
|
|
if emit.is_empty() {
|
|
return true;
|
|
}
|
|
let kind = match self {
|
|
SharedResource::Unversioned { .. } => EmitType::Unversioned,
|
|
SharedResource::ToolchainSpecific { .. } => EmitType::Toolchain,
|
|
SharedResource::InvocationSpecific { .. } => EmitType::InvocationSpecific,
|
|
};
|
|
emit.contains(&kind)
|
|
}
|
|
}
|
|
|
|
impl Context<'_> {
|
|
fn suffix_path(&self, filename: &str) -> PathBuf {
|
|
// We use splitn vs Path::extension here because we might get a filename
|
|
// like `style.min.css` and we want to process that into
|
|
// `style-suffix.min.css`. Path::extension would just return `css`
|
|
// which would result in `style.min-suffix.css` which isn't what we
|
|
// want.
|
|
let (base, ext) = filename.split_once('.').unwrap();
|
|
let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
|
|
self.dst.join(&filename)
|
|
}
|
|
|
|
fn write_shared<C: AsRef<[u8]>>(
|
|
&self,
|
|
resource: SharedResource<'_>,
|
|
contents: C,
|
|
emit: &[EmitType],
|
|
) -> Result<(), Error> {
|
|
if resource.should_emit(emit) {
|
|
self.shared.fs.write(resource.path(self), contents)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn write_minify(
|
|
&self,
|
|
resource: SharedResource<'_>,
|
|
contents: &str,
|
|
minify: bool,
|
|
emit: &[EmitType],
|
|
) -> Result<(), Error> {
|
|
let tmp;
|
|
let contents =
|
|
contents.replace("data-url(minus-17x17.png)", static_files::MINUS_PNG_DATAURL);
|
|
let contents = contents.replace("data-url(plus-17x17.png)", static_files::PLUS_PNG_DATAURL);
|
|
let contents = if minify {
|
|
tmp = if resource.extension() == Some(&OsStr::new("css")) {
|
|
minifier::css::minify(&contents).map_err(|e| {
|
|
Error::new(format!("failed to minify CSS file: {}", e), resource.path(self))
|
|
})?
|
|
} else {
|
|
minifier::js::minify(&contents)
|
|
};
|
|
tmp.as_bytes()
|
|
} else {
|
|
contents.as_bytes()
|
|
};
|
|
|
|
self.write_shared(resource, contents, emit)
|
|
}
|
|
}
|
|
|
|
pub(super) fn write_shared(
|
|
cx: &Context<'_>,
|
|
krate: &Crate,
|
|
search_index: String,
|
|
options: &RenderOptions,
|
|
) -> Result<(), Error> {
|
|
// Write out the shared files. Note that these are shared among all rustdoc
|
|
// docs placed in the output directory, so this needs to be a synchronized
|
|
// operation with respect to all other rustdocs running around.
|
|
let lock_file = cx.dst.join(".lock");
|
|
let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
|
|
|
|
// The weird `: &_` is to work around a borrowck bug: https://github.com/rust-lang/rust/issues/41078#issuecomment-293646723
|
|
let write_minify = |p, c: &_| {
|
|
cx.write_minify(
|
|
SharedResource::ToolchainSpecific { basename: p },
|
|
c,
|
|
options.enable_minification,
|
|
&options.emit,
|
|
)
|
|
};
|
|
// Toolchain resources should never be dynamic.
|
|
let write_toolchain = |p: &'static _, c: &'static _| {
|
|
cx.write_shared(SharedResource::ToolchainSpecific { basename: p }, c, &options.emit)
|
|
};
|
|
|
|
// Crate resources should always be dynamic.
|
|
let write_crate = |p: &_, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
|
|
let content = make_content()?;
|
|
cx.write_shared(SharedResource::InvocationSpecific { basename: p }, content, &options.emit)
|
|
};
|
|
|
|
// Add all the static files. These may already exist, but we just
|
|
// overwrite them anyway to make sure that they're fresh and up-to-date.
|
|
write_minify("rustdoc.css", static_files::RUSTDOC_CSS)?;
|
|
write_minify("settings.css", static_files::SETTINGS_CSS)?;
|
|
write_minify("noscript.css", static_files::NOSCRIPT_CSS)?;
|
|
|
|
// To avoid "light.css" to be overwritten, we'll first run over the received themes and only
|
|
// then we'll run over the "official" styles.
|
|
let mut themes: FxHashSet<String> = FxHashSet::default();
|
|
|
|
for entry in &cx.shared.style_files {
|
|
let theme = try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path);
|
|
let extension =
|
|
try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
|
|
|
|
// Handle the official themes
|
|
match theme {
|
|
"light" => write_minify("light.css", static_files::themes::LIGHT)?,
|
|
"dark" => write_minify("dark.css", static_files::themes::DARK)?,
|
|
"ayu" => write_minify("ayu.css", static_files::themes::AYU)?,
|
|
_ => {
|
|
// Handle added third-party themes
|
|
let filename = format!("{}.{}", theme, extension);
|
|
write_crate(&filename, &|| Ok(try_err!(fs::read(&entry.path), &entry.path)))?;
|
|
}
|
|
};
|
|
|
|
themes.insert(theme.to_owned());
|
|
}
|
|
|
|
if (*cx.shared).layout.logo.is_empty() {
|
|
write_toolchain("rust-logo.png", static_files::RUST_LOGO)?;
|
|
}
|
|
if (*cx.shared).layout.favicon.is_empty() {
|
|
write_toolchain("favicon.svg", static_files::RUST_FAVICON_SVG)?;
|
|
write_toolchain("favicon-16x16.png", static_files::RUST_FAVICON_PNG_16)?;
|
|
write_toolchain("favicon-32x32.png", static_files::RUST_FAVICON_PNG_32)?;
|
|
}
|
|
write_toolchain("brush.svg", static_files::BRUSH_SVG)?;
|
|
write_toolchain("wheel.svg", static_files::WHEEL_SVG)?;
|
|
write_toolchain("clipboard.svg", static_files::CLIPBOARD_SVG)?;
|
|
write_toolchain("down-arrow.svg", static_files::DOWN_ARROW_SVG)?;
|
|
|
|
let mut themes: Vec<&String> = themes.iter().collect();
|
|
themes.sort();
|
|
|
|
// FIXME: this should probably not be a toolchain file since it depends on `--theme`.
|
|
// But it seems a shame to copy it over and over when it's almost always the same.
|
|
// Maybe we can change the representation to move this out of main.js?
|
|
write_minify(
|
|
"main.js",
|
|
&static_files::MAIN_JS.replace(
|
|
"/* INSERT THEMES HERE */",
|
|
&format!(" = {}", serde_json::to_string(&themes).unwrap()),
|
|
),
|
|
)?;
|
|
write_minify("search.js", static_files::SEARCH_JS)?;
|
|
write_minify("settings.js", static_files::SETTINGS_JS)?;
|
|
|
|
if cx.shared.include_sources {
|
|
write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT)?;
|
|
}
|
|
|
|
{
|
|
write_minify(
|
|
"storage.js",
|
|
&format!(
|
|
"var resourcesSuffix = \"{}\";{}",
|
|
cx.shared.resource_suffix,
|
|
static_files::STORAGE_JS
|
|
),
|
|
)?;
|
|
}
|
|
|
|
if let Some(ref css) = cx.shared.layout.css_file_extension {
|
|
let buffer = try_err!(fs::read_to_string(css), css);
|
|
// This varies based on the invocation, so it can't go through the write_minify wrapper.
|
|
cx.write_minify(
|
|
SharedResource::InvocationSpecific { basename: "theme.css" },
|
|
&buffer,
|
|
options.enable_minification,
|
|
&options.emit,
|
|
)?;
|
|
}
|
|
write_minify("normalize.css", static_files::NORMALIZE_CSS)?;
|
|
for (name, contents) in &*FILES_UNVERSIONED {
|
|
cx.write_shared(SharedResource::Unversioned { name }, contents, &options.emit)?;
|
|
}
|
|
|
|
fn collect(path: &Path, krate: &str, key: &str) -> io::Result<(Vec<String>, Vec<String>)> {
|
|
let mut ret = Vec::new();
|
|
let mut krates = Vec::new();
|
|
|
|
if path.exists() {
|
|
let prefix = format!(r#"{}["{}"]"#, key, krate);
|
|
for line in BufReader::new(File::open(path)?).lines() {
|
|
let line = line?;
|
|
if !line.starts_with(key) {
|
|
continue;
|
|
}
|
|
if line.starts_with(&prefix) {
|
|
continue;
|
|
}
|
|
ret.push(line.to_string());
|
|
krates.push(
|
|
line[key.len() + 2..]
|
|
.split('"')
|
|
.next()
|
|
.map(|s| s.to_owned())
|
|
.unwrap_or_else(String::new),
|
|
);
|
|
}
|
|
}
|
|
Ok((ret, krates))
|
|
}
|
|
|
|
fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
|
|
let mut ret = Vec::new();
|
|
let mut krates = Vec::new();
|
|
|
|
if path.exists() {
|
|
let prefix = format!("\"{}\"", krate);
|
|
for line in BufReader::new(File::open(path)?).lines() {
|
|
let line = line?;
|
|
if !line.starts_with('"') {
|
|
continue;
|
|
}
|
|
if line.starts_with(&prefix) {
|
|
continue;
|
|
}
|
|
if line.ends_with(",\\") {
|
|
ret.push(line[..line.len() - 2].to_string());
|
|
} else {
|
|
// Ends with "\\" (it's the case for the last added crate line)
|
|
ret.push(line[..line.len() - 1].to_string());
|
|
}
|
|
krates.push(
|
|
line.split('"')
|
|
.find(|s| !s.is_empty())
|
|
.map(|s| s.to_owned())
|
|
.unwrap_or_else(String::new),
|
|
);
|
|
}
|
|
}
|
|
Ok((ret, krates))
|
|
}
|
|
|
|
use std::ffi::OsString;
|
|
|
|
#[derive(Debug)]
|
|
struct Hierarchy {
|
|
elem: OsString,
|
|
children: FxHashMap<OsString, Hierarchy>,
|
|
elems: FxHashSet<OsString>,
|
|
}
|
|
|
|
impl Hierarchy {
|
|
fn new(elem: OsString) -> Hierarchy {
|
|
Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
|
|
}
|
|
|
|
fn to_json_string(&self) -> String {
|
|
let mut subs: Vec<&Hierarchy> = self.children.values().collect();
|
|
subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
|
|
let mut files = self
|
|
.elems
|
|
.iter()
|
|
.map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
|
|
.collect::<Vec<_>>();
|
|
files.sort_unstable();
|
|
let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
|
|
let dirs =
|
|
if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs) };
|
|
let files = files.join(",");
|
|
let files =
|
|
if files.is_empty() { String::new() } else { format!(",\"files\":[{}]", files) };
|
|
format!(
|
|
"{{\"name\":\"{name}\"{dirs}{files}}}",
|
|
name = self.elem.to_str().expect("invalid osstring conversion"),
|
|
dirs = dirs,
|
|
files = files
|
|
)
|
|
}
|
|
}
|
|
|
|
if cx.shared.include_sources {
|
|
let mut hierarchy = Hierarchy::new(OsString::new());
|
|
for source in cx
|
|
.shared
|
|
.local_sources
|
|
.iter()
|
|
.filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
|
|
{
|
|
let mut h = &mut hierarchy;
|
|
let mut elems = source
|
|
.components()
|
|
.filter_map(|s| match s {
|
|
Component::Normal(s) => Some(s.to_owned()),
|
|
_ => None,
|
|
})
|
|
.peekable();
|
|
loop {
|
|
let cur_elem = elems.next().expect("empty file path");
|
|
if elems.peek().is_none() {
|
|
h.elems.insert(cur_elem);
|
|
break;
|
|
} else {
|
|
let e = cur_elem.clone();
|
|
h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
|
|
let make_sources = || {
|
|
let (mut all_sources, _krates) =
|
|
try_err!(collect(&dst, &krate.name.as_str(), "sourcesIndex"), &dst);
|
|
all_sources.push(format!(
|
|
"sourcesIndex[\"{}\"] = {};",
|
|
&krate.name,
|
|
hierarchy.to_json_string()
|
|
));
|
|
all_sources.sort();
|
|
Ok(format!(
|
|
"var N = null;var sourcesIndex = {{}};\n{}\ncreateSourceSidebar();\n",
|
|
all_sources.join("\n")
|
|
)
|
|
.into_bytes())
|
|
};
|
|
write_crate("source-files.js", &make_sources)?;
|
|
}
|
|
|
|
// Update the search index and crate list.
|
|
let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
|
|
let (mut all_indexes, mut krates) = try_err!(collect_json(&dst, &krate.name.as_str()), &dst);
|
|
all_indexes.push(search_index);
|
|
krates.push(krate.name.to_string());
|
|
krates.sort();
|
|
|
|
// Sort the indexes by crate so the file will be generated identically even
|
|
// with rustdoc running in parallel.
|
|
all_indexes.sort();
|
|
write_crate("search-index.js", &|| {
|
|
let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
|
|
v.push_str(&all_indexes.join(",\\\n"));
|
|
v.push_str("\\\n}');\nif (window.initSearch) {window.initSearch(searchIndex)};");
|
|
Ok(v.into_bytes())
|
|
})?;
|
|
|
|
write_crate("crates.js", &|| {
|
|
let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
|
|
Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
|
|
})?;
|
|
|
|
if options.enable_index_page {
|
|
if let Some(index_page) = options.index_page.clone() {
|
|
let mut md_opts = options.clone();
|
|
md_opts.output = cx.dst.clone();
|
|
md_opts.external_html = (*cx.shared).layout.external_html.clone();
|
|
|
|
crate::markdown::render(&index_page, md_opts, cx.shared.edition())
|
|
.map_err(|e| Error::new(e, &index_page))?;
|
|
} else {
|
|
let dst = cx.dst.join("index.html");
|
|
let page = layout::Page {
|
|
title: "Index of crates",
|
|
css_class: "mod",
|
|
root_path: "./",
|
|
static_root_path: cx.shared.static_root_path.as_deref(),
|
|
description: "List of crates",
|
|
keywords: BASIC_KEYWORDS,
|
|
resource_suffix: &cx.shared.resource_suffix,
|
|
extra_scripts: &[],
|
|
static_extra_scripts: &[],
|
|
};
|
|
|
|
let content = format!(
|
|
"<h1 class=\"fqn\">\
|
|
<span class=\"in-band\">List of all crates</span>\
|
|
</h1><ul class=\"crate mod\">{}</ul>",
|
|
krates
|
|
.iter()
|
|
.map(|s| {
|
|
format!(
|
|
"<li><a class=\"crate mod\" href=\"{}index.html\">{}</a></li>",
|
|
ensure_trailing_slash(s),
|
|
s
|
|
)
|
|
})
|
|
.collect::<String>()
|
|
);
|
|
let v = layout::render(
|
|
&cx.shared.templates,
|
|
&cx.shared.layout,
|
|
&page,
|
|
"",
|
|
content,
|
|
&cx.shared.style_files,
|
|
);
|
|
cx.shared.fs.write(&dst, v.as_bytes())?;
|
|
}
|
|
}
|
|
|
|
// Update the list of all implementors for traits
|
|
let dst = cx.dst.join("implementors");
|
|
for (&did, imps) in &cx.cache.implementors {
|
|
// Private modules can leak through to this phase of rustdoc, which
|
|
// could contain implementations for otherwise private types. In some
|
|
// rare cases we could find an implementation for an item which wasn't
|
|
// indexed, so we just skip this step in that case.
|
|
//
|
|
// FIXME: this is a vague explanation for why this can't be a `get`, in
|
|
// theory it should be...
|
|
let &(ref remote_path, remote_item_type) = match cx.cache.paths.get(&did) {
|
|
Some(p) => p,
|
|
None => match cx.cache.external_paths.get(&did) {
|
|
Some(p) => p,
|
|
None => continue,
|
|
},
|
|
};
|
|
|
|
#[derive(Serialize)]
|
|
struct Implementor {
|
|
text: String,
|
|
synthetic: bool,
|
|
types: Vec<String>,
|
|
}
|
|
|
|
let implementors = imps
|
|
.iter()
|
|
.filter_map(|imp| {
|
|
// If the trait and implementation are in the same crate, then
|
|
// there's no need to emit information about it (there's inlining
|
|
// going on). If they're in different crates then the crate defining
|
|
// the trait will be interested in our implementation.
|
|
//
|
|
// If the implementation is from another crate then that crate
|
|
// should add it.
|
|
if imp.impl_item.def_id.krate() == did.krate || !imp.impl_item.def_id.is_local() {
|
|
None
|
|
} else {
|
|
Some(Implementor {
|
|
text: imp.inner_impl().print(false, cx).to_string(),
|
|
synthetic: imp.inner_impl().synthetic,
|
|
types: collect_paths_for_type(imp.inner_impl().for_.clone(), cx.cache()),
|
|
})
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// Only create a js file if we have impls to add to it. If the trait is
|
|
// documented locally though we always create the file to avoid dead
|
|
// links.
|
|
if implementors.is_empty() && !cx.cache.paths.contains_key(&did) {
|
|
continue;
|
|
}
|
|
|
|
let implementors = format!(
|
|
r#"implementors["{}"] = {};"#,
|
|
krate.name,
|
|
serde_json::to_string(&implementors).unwrap()
|
|
);
|
|
|
|
let mut mydst = dst.clone();
|
|
for part in &remote_path[..remote_path.len() - 1] {
|
|
mydst.push(part);
|
|
}
|
|
cx.shared.ensure_dir(&mydst)?;
|
|
mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
|
|
|
|
let (mut all_implementors, _) =
|
|
try_err!(collect(&mydst, &krate.name.as_str(), "implementors"), &mydst);
|
|
all_implementors.push(implementors);
|
|
// Sort the implementors by crate so the file will be generated
|
|
// identically even with rustdoc running in parallel.
|
|
all_implementors.sort();
|
|
|
|
let mut v = String::from("(function() {var implementors = {};\n");
|
|
for implementor in &all_implementors {
|
|
writeln!(v, "{}", *implementor).unwrap();
|
|
}
|
|
v.push_str(
|
|
"if (window.register_implementors) {\
|
|
window.register_implementors(implementors);\
|
|
} else {\
|
|
window.pending_implementors = implementors;\
|
|
}",
|
|
);
|
|
v.push_str("})()");
|
|
cx.shared.fs.write(&mydst, &v)?;
|
|
}
|
|
Ok(())
|
|
}
|