Generate error index with mdbook instead of raw HTML pages

This commit is contained in:
Guillaume Gomez 2022-08-29 18:05:21 +02:00
parent 7a42ca942c
commit 117169799f
8 changed files with 220 additions and 147 deletions

View file

@ -1268,7 +1268,7 @@ dependencies = [
name = "error_index_generator"
version = "0.0.0"
dependencies = [
"rustdoc",
"mdbook",
]
[[package]]

View file

@ -793,7 +793,7 @@ fn run(self, builder: &Builder<'_>) {
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);

View file

@ -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"

View 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

View 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;
}

View 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);
}

View file

@ -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 @@ pub fn error_codes() -> Vec<(&'static str, Option<&'static str>)> {
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);
}
}

View 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';
}}
}}
}})()