Generate error index with mdbook instead of raw HTML pages
This commit is contained in:
parent
7a42ca942c
commit
117169799f
8 changed files with 220 additions and 147 deletions
|
@ -1268,7 +1268,7 @@ dependencies = [
|
|||
name = "error_index_generator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"rustdoc",
|
||||
"mdbook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -793,7 +793,7 @@ impl Step for ErrorIndex {
|
|||
t!(fs::create_dir_all(&out));
|
||||
let mut index = tool::ErrorIndex::command(builder);
|
||||
index.arg("html");
|
||||
index.arg(out.join("error-index.html"));
|
||||
index.arg(out);
|
||||
index.arg(&builder.version);
|
||||
|
||||
builder.run(&mut index);
|
||||
|
|
|
@ -4,7 +4,7 @@ version = "0.0.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rustdoc = { path = "../../librustdoc" }
|
||||
mdbook = { version = "0.4", default-features = false, features = ["search"] }
|
||||
|
||||
[[bin]]
|
||||
name = "error_index_generator"
|
||||
|
|
19
src/tools/error_index_generator/book_config.toml
Normal file
19
src/tools/error_index_generator/book_config.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[book]
|
||||
title = "Error codes index"
|
||||
description = "Book listing all Rust error codes"
|
||||
src = ""
|
||||
|
||||
[output.html]
|
||||
git-repository-url = "https://github.com/rust-lang/rust/"
|
||||
additional-css = ["error-index.css"]
|
||||
additional-js = ["error-index.js"]
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
limit-results = 20
|
||||
use-boolean-and = true
|
||||
boost-title = 2
|
||||
boost-hierarchy = 2
|
||||
boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 0
|
38
src/tools/error_index_generator/error-index.css
Normal file
38
src/tools/error_index_generator/error-index.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
code.compile_fail {
|
||||
border-left: 2px solid red;
|
||||
}
|
||||
|
||||
pre .tooltip {
|
||||
position: absolute;
|
||||
left: -25px;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
color: red;
|
||||
cursor: pointer;
|
||||
}
|
||||
pre .tooltip::after {
|
||||
display: none;
|
||||
content: "This example deliberately fails to compile";
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border-color: #000;
|
||||
text-align: center;
|
||||
padding: 5px 3px 3px 3px;
|
||||
border-radius: 6px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
pre .tooltip::before {
|
||||
display: none;
|
||||
border-color: transparent black transparent transparent;
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 16px;
|
||||
margin-top: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
pre .tooltip:hover::before, pre .tooltip:hover::after {
|
||||
display: inline;
|
||||
}
|
9
src/tools/error_index_generator/error-index.js
Normal file
9
src/tools/error_index_generator/error-index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
for (const elem of document.querySelectorAll("pre.playground")) {
|
||||
if (elem.querySelector(".compile_fail") === null) {
|
||||
continue;
|
||||
}
|
||||
const child = document.createElement("div");
|
||||
child.className = "tooltip";
|
||||
child.textContent = "ⓘ";
|
||||
elem.appendChild(child);
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
#![feature(rustc_private)]
|
||||
|
||||
extern crate rustc_driver;
|
||||
extern crate rustc_span;
|
||||
|
||||
// We use the function we generate from `register_diagnostics!`.
|
||||
use crate::error_codes::error_codes;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::fs::{self, create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rustc_span::edition::DEFAULT_EDITION;
|
||||
use std::str::FromStr;
|
||||
|
||||
use rustdoc::html::markdown::{ErrorCodes, HeadingOffset, IdMap, Markdown, Playground};
|
||||
use mdbook::book::{parse_summary, BookItem, Chapter};
|
||||
use mdbook::{Config, MDBook};
|
||||
|
||||
macro_rules! register_diagnostics {
|
||||
($($error_code:ident: $message:expr,)+ ; $($undocumented:ident,)* ) => {
|
||||
|
@ -33,104 +34,21 @@ macro_rules! register_diagnostics {
|
|||
mod error_codes;
|
||||
|
||||
enum OutputFormat {
|
||||
HTML(HTMLFormatter),
|
||||
HTML,
|
||||
Markdown,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
fn from(format: &str, resource_suffix: &str) -> OutputFormat {
|
||||
fn from(format: &str) -> OutputFormat {
|
||||
match &*format.to_lowercase() {
|
||||
"html" => OutputFormat::HTML(HTMLFormatter(resource_suffix.to_owned())),
|
||||
"html" => OutputFormat::HTML,
|
||||
"markdown" => OutputFormat::Markdown,
|
||||
s => OutputFormat::Unknown(s.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HTMLFormatter(String);
|
||||
|
||||
impl HTMLFormatter {
|
||||
fn create_error_code_file(
|
||||
&self,
|
||||
err_code: &str,
|
||||
explanation: &str,
|
||||
parent_dir: &Path,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut output_file = File::create(parent_dir.join(err_code).with_extension("html"))?;
|
||||
|
||||
self.header(&mut output_file, "../", "")?;
|
||||
self.title(&mut output_file, &format!("Error code {}", err_code))?;
|
||||
|
||||
let mut id_map = IdMap::new();
|
||||
let playground =
|
||||
Playground { crate_name: None, url: String::from("https://play.rust-lang.org/") };
|
||||
write!(
|
||||
output_file,
|
||||
"{}",
|
||||
Markdown {
|
||||
content: explanation,
|
||||
links: &[],
|
||||
ids: &mut id_map,
|
||||
error_codes: ErrorCodes::Yes,
|
||||
edition: DEFAULT_EDITION,
|
||||
playground: &Some(playground),
|
||||
heading_offset: HeadingOffset::H1,
|
||||
}
|
||||
.into_string()
|
||||
)?;
|
||||
write!(
|
||||
output_file,
|
||||
"<p>\
|
||||
<a style='text-align: center;display: block;width: 100%;' \
|
||||
href='../error-index.html'>Back to list of error codes</a>\
|
||||
</p>",
|
||||
)?;
|
||||
|
||||
self.footer(&mut output_file)
|
||||
}
|
||||
|
||||
fn header(
|
||||
&self,
|
||||
output: &mut dyn Write,
|
||||
extra_path: &str,
|
||||
extra: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
write!(
|
||||
output,
|
||||
r##"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Rust Compiler Error Index</title>
|
||||
<meta charset="utf-8">
|
||||
<!-- Include rust.css after light.css so its rules take priority. -->
|
||||
<link rel="stylesheet" type="text/css" href="{extra_path}rustdoc{suffix}.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="{extra_path}light{suffix}.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="{extra_path}rust.css"/>
|
||||
<style>
|
||||
.error-undescribed {{
|
||||
display: none;
|
||||
}}
|
||||
</style>{extra}
|
||||
</head>
|
||||
<body>
|
||||
"##,
|
||||
suffix = self.0,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn title(&self, output: &mut dyn Write, title: &str) -> Result<(), Box<dyn Error>> {
|
||||
write!(output, "<h1>{}</h1>\n", title)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn footer(&self, output: &mut dyn Write) -> Result<(), Box<dyn Error>> {
|
||||
write!(output, "</body></html>")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Output an HTML page for the errors in `err_map` to `output_path`.
|
||||
fn render_markdown(output_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let mut output_file = File::create(output_path)?;
|
||||
|
@ -147,61 +65,144 @@ fn render_markdown(output_path: &Path) -> Result<(), Box<dyn Error>> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_html(output_path: &Path, formatter: HTMLFormatter) -> Result<(), Box<dyn Error>> {
|
||||
let mut output_file = File::create(output_path)?;
|
||||
fn move_folder(source: &Path, target: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let entries =
|
||||
fs::read_dir(source)?.map(|res| res.map(|e| e.path())).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let error_codes_dir = "error_codes";
|
||||
|
||||
let parent = output_path.parent().expect("There should have a parent").join(error_codes_dir);
|
||||
|
||||
if !parent.exists() {
|
||||
create_dir_all(&parent)?;
|
||||
}
|
||||
|
||||
formatter.header(
|
||||
&mut output_file,
|
||||
"",
|
||||
&format!(
|
||||
r#"<script>(function() {{
|
||||
if (window.location.hash) {{
|
||||
let code = window.location.hash.replace(/^#/, '');
|
||||
// We have to make sure this pattern matches to avoid inadvertently creating an
|
||||
// open redirect.
|
||||
if (/^E[0-9]+$/.test(code)) {{
|
||||
window.location = './{error_codes_dir}/' + code + '.html';
|
||||
}}
|
||||
}}
|
||||
}})()</script>"#
|
||||
),
|
||||
)?;
|
||||
formatter.title(&mut output_file, "Rust Compiler Error Index")?;
|
||||
|
||||
write!(
|
||||
output_file,
|
||||
"<p>This page lists all the error codes emitted by the Rust compiler. If you want a full \
|
||||
explanation on an error code, click on it.</p>\
|
||||
<ul>",
|
||||
)?;
|
||||
for (err_code, explanation) in error_codes().iter() {
|
||||
if let Some(explanation) = explanation {
|
||||
write!(
|
||||
output_file,
|
||||
"<li><a href='./{0}/{1}.html'>{1}</a></li>",
|
||||
error_codes_dir, err_code
|
||||
)?;
|
||||
formatter.create_error_code_file(err_code, explanation, &parent)?;
|
||||
for entry in entries {
|
||||
let file_name = entry.file_name().expect("file_name() failed").to_os_string();
|
||||
let output = target.join(file_name);
|
||||
if entry.is_file() {
|
||||
fs::rename(entry, output)?;
|
||||
} else {
|
||||
write!(output_file, "<li>{}</li>", err_code)?;
|
||||
if !output.exists() {
|
||||
create_dir_all(&output)?;
|
||||
}
|
||||
move_folder(&entry, &output)?;
|
||||
}
|
||||
}
|
||||
write!(output_file, "</ul>")?;
|
||||
formatter.footer(&mut output_file)
|
||||
|
||||
fs::remove_dir(&source)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_html(output_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
// We need to render into a temporary folder to prevent `mdbook` from removing everything
|
||||
// in the output folder (including other completely unrelated things).
|
||||
let tmp_output = output_path.join("tmp");
|
||||
|
||||
if !tmp_output.exists() {
|
||||
create_dir_all(&tmp_output)?;
|
||||
}
|
||||
|
||||
render_html_inner(&tmp_output)?;
|
||||
|
||||
move_folder(&tmp_output, output_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// By default, mdbook doesn't consider code blocks as Rust ones contrary to rustdoc so we have
|
||||
// to manually add `rust` attribute whenever needed.
|
||||
fn add_rust_attribute_on_codeblock(explanation: &str) -> String {
|
||||
// Very hacky way to add the rust attribute on all code blocks.
|
||||
let mut skip = true;
|
||||
explanation.split("\n```").fold(String::new(), |mut acc, part| {
|
||||
if !acc.is_empty() {
|
||||
acc.push_str("\n```");
|
||||
}
|
||||
if !skip {
|
||||
if let Some(attrs) = part.split('\n').next() {
|
||||
if !attrs.contains("rust")
|
||||
&& (attrs.is_empty()
|
||||
|| attrs.contains("compile_fail")
|
||||
|| attrs.contains("ignore")
|
||||
|| attrs.contains("edition"))
|
||||
{
|
||||
if !attrs.is_empty() {
|
||||
acc.push_str("rust,");
|
||||
} else {
|
||||
acc.push_str("rust");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
skip = !skip;
|
||||
acc.push_str(part);
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
fn render_html_inner(output_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||
// We need to have a little difference between `summary` and `introduction` because the "draft"
|
||||
// chapters (the ones looking like `[a]()`) are not handled correctly when being put into a
|
||||
// `Chapter` directly: they generate a link whereas they shouldn't.
|
||||
let mut introduction = format!(
|
||||
"<script>{}</script>
|
||||
# Rust error codes index
|
||||
|
||||
This page lists all the error codes emitted by the Rust compiler.
|
||||
|
||||
",
|
||||
include_str!("redirect.js")
|
||||
);
|
||||
|
||||
let err_codes = error_codes();
|
||||
let mut chapters = Vec::with_capacity(err_codes.len());
|
||||
|
||||
for (err_code, explanation) in err_codes.iter() {
|
||||
if let Some(explanation) = explanation {
|
||||
introduction.push_str(&format!(" * [{0}](./error_codes/{0}.html)\n", err_code));
|
||||
|
||||
let content = add_rust_attribute_on_codeblock(explanation);
|
||||
chapters.push(BookItem::Chapter(Chapter {
|
||||
name: err_code.to_string(),
|
||||
content: format!("# Error code {}\n\n{}\n", err_code, content),
|
||||
number: None,
|
||||
sub_items: Vec::new(),
|
||||
// We generate it into the `error_codes` folder.
|
||||
path: Some(PathBuf::from(&format!("error_codes/{}.html", err_code))),
|
||||
source_path: None,
|
||||
parent_names: Vec::new(),
|
||||
}));
|
||||
} else {
|
||||
introduction.push_str(&format!(" * {}\n", err_code));
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = Config::from_str(include_str!("book_config.toml"))?;
|
||||
config.build.build_dir = output_path.to_path_buf();
|
||||
let mut book = MDBook::load_with_config_and_summary(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
config,
|
||||
parse_summary("")?,
|
||||
)?;
|
||||
let chapter = Chapter {
|
||||
name: "Rust error codes index".to_owned(),
|
||||
content: introduction,
|
||||
number: None,
|
||||
sub_items: chapters,
|
||||
// Very important: this file is named as `error-index.html` and not `index.html`!
|
||||
path: Some(PathBuf::from("error-index.html")),
|
||||
source_path: None,
|
||||
parent_names: Vec::new(),
|
||||
};
|
||||
book.book.sections.push(BookItem::Chapter(chapter));
|
||||
book.build()?;
|
||||
|
||||
// We don't need this file since it's handled by doc.rust-lang.org directly.
|
||||
let _ = fs::remove_file(output_path.join("404.html"));
|
||||
// We don't want this file either because it would overwrite the already existing `index.html`.
|
||||
let _ = fs::remove_file(output_path.join("index.html"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main_with_result(format: OutputFormat, dst: &Path) -> Result<(), Box<dyn Error>> {
|
||||
match format {
|
||||
OutputFormat::Unknown(s) => panic!("Unknown output format: {}", s),
|
||||
OutputFormat::HTML(h) => render_html(dst, h),
|
||||
OutputFormat::HTML => render_html(dst),
|
||||
OutputFormat::Markdown => render_markdown(dst),
|
||||
}
|
||||
}
|
||||
|
@ -210,12 +211,9 @@ fn parse_args() -> (OutputFormat, PathBuf) {
|
|||
let mut args = env::args().skip(1);
|
||||
let format = args.next();
|
||||
let dst = args.next();
|
||||
let resource_suffix = args.next().unwrap_or_else(String::new);
|
||||
let format = format
|
||||
.map(|a| OutputFormat::from(&a, &resource_suffix))
|
||||
.unwrap_or(OutputFormat::from("html", &resource_suffix));
|
||||
let format = format.map(|a| OutputFormat::from(&a)).unwrap_or(OutputFormat::from("html"));
|
||||
let dst = dst.map(PathBuf::from).unwrap_or_else(|| match format {
|
||||
OutputFormat::HTML(..) => PathBuf::from("doc/error-index.html"),
|
||||
OutputFormat::HTML => PathBuf::from("doc"),
|
||||
OutputFormat::Markdown => PathBuf::from("doc/error-index.md"),
|
||||
OutputFormat::Unknown(..) => PathBuf::from("<nul>"),
|
||||
});
|
||||
|
@ -225,9 +223,8 @@ fn parse_args() -> (OutputFormat, PathBuf) {
|
|||
fn main() {
|
||||
rustc_driver::init_env_logger("RUST_LOG");
|
||||
let (format, dst) = parse_args();
|
||||
let result =
|
||||
rustc_span::create_default_session_globals_then(move || main_with_result(format, &dst));
|
||||
let result = main_with_result(format, &dst);
|
||||
if let Err(e) = result {
|
||||
panic!("{}", e.to_string());
|
||||
panic!("{:?}", e);
|
||||
}
|
||||
}
|
||||
|
|
10
src/tools/error_index_generator/redirect.js
Normal file
10
src/tools/error_index_generator/redirect.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
(function() {{
|
||||
if (window.location.hash) {{
|
||||
let code = window.location.hash.replace(/^#/, '');
|
||||
// We have to make sure this pattern matches to avoid inadvertently creating an
|
||||
// open redirect.
|
||||
if (/^E[0-9]+$/.test(code)) {{
|
||||
window.location = './error_codes/' + code + '.html';
|
||||
}}
|
||||
}}
|
||||
}})()
|
Loading…
Add table
Reference in a new issue