Rollup merge of #101166 - GuillaumeGomez:error-index-mdbook, r=notriddle

Generate error index with mdbook instead of raw HTML pages

This is a follow-up of https://github.com/rust-lang/rust/pull/100922.

This comes from a remark from ````@estebank```` who said that the search was a nice thing on the previous version and that it wasn't possible anymore. An easy way to come around this limitation was to use `mdbook`, which is what I did here.

Now some explanations on the code. I'll explain how I developed this and why I reached this solution. First I did it very basically by simply setting the source directory and the output directory, generated a `SUMMARY.md` manually which listed all error codes and that was it. Two problems arose from this:
 1. A lot of new HTML files were generated at the top level
 2. An `index.html` file was generated at the top-level (the summary in short).

So for `1.`, it's not great to have too many files at the top-level as it could create file conflicts more easily. And for `2.`, this is actually a huge issue because <doc.rust-lang.org> generates an `index.html` file with a links to a few different resources, so it should never be overwritten. <s>Unfortunately, `mdbook` **always** generates an `index.html` file so the only solution I could see (except for sending them a contribution, I'll maybe do that later) was to temporaly move a potentially existing `index.html` file and then puts it back once done. For this last part, to ensure that we don't return *before* it has been put back, I wrapped the `mdbook` generation code inside `render_html_inner` which is called from `render_html` which in turn handle the "save" of `index.html`.</s>

EDIT: `mdbook` completely deletes ALL the content in the target directory so I instead generate into a sub directory and then I move the files to the real target directory.

To keep compatibility with the old version, I also put the `<script>` ````@notriddle```` nicely provided in the previous PR only into the `error-index.html` file to prevent unneeded repetition. I didn't use `mdbook` `additional-js` option because the JS is included at the end of all HTML files, which we don't want for two reasons:
 1. It's slow.
 2. We only want it to be run in `error-index.html` (even if we also ensure in the JS itself too!).

<s>Now the last part: why we generate the summary twice. We actually generate it once to tell `mdbook` what the book will look like and a second time because a create a new chapter content which will actually list all the error codes (with the updated paths).</s>

EDIT: I removed the need for two summaries.

You can test it [here](https://rustdoc.crud.net/imperio/error-index-mdbook/error-index.html).

r? ````@notriddle````
This commit is contained in:
Guillaume Gomez 2022-09-02 11:34:51 +02:00 committed by GitHub
commit 68d3cfac47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 196 additions and 142 deletions

View file

@ -1259,7 +1259,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, 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,119 @@ 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)?;
// 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
})
}
let error_codes_dir = "error_codes";
fn render_html(output_path: &Path) -> Result<(), Box<dyn Error>> {
let mut introduction = format!(
"<script src='redirect.js'></script>
# Rust error codes index
let parent = output_path.parent().expect("There should have a parent").join(error_codes_dir);
This page lists all the error codes emitted by the Rust compiler.
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")?;
let err_codes = error_codes();
let mut chapters = Vec::with_capacity(err_codes.len());
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() {
for (err_code, explanation) in err_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)?;
introduction.push_str(&format!(" * [{0}](./{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!("{}.html", err_code))),
source_path: None,
parent_names: Vec::new(),
}));
} else {
write!(output_file, "<li>{}</li>", err_code)?;
introduction.push_str(&format!(" * {}\n", err_code));
}
}
write!(output_file, "</ul>")?;
formatter.footer(&mut output_file)
let mut config = Config::from_str(include_str!("book_config.toml"))?;
config.build.build_dir = output_path.join("error_codes").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 can't put this content into another file, otherwise `mbdbook` will also put it into the
// output directory, making a duplicate.
fs::write(
output_path.join("error-index.html"),
r#"<!DOCTYPE html>
<html>
<head>
<title>Rust error codes index - Error codes index</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="Book listing all Rust error codes">
<script src="error_codes/redirect.js"></script>
</head>
<body>
<div>If you are not automatically redirected to the error code index, please <a id="index-link" href="./error_codes/error-index.html">here</a>.
<script>document.getElementById("index-link").click()</script>
</body>
</html>"#,
)?;
// No need for a 404 file, it's already handled by the server.
fs::remove_file(output_path.join("error_codes/404.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 +186,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 +198,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,16 @@
(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)) {
return;
}
if (window.location.pathname.indexOf("/error_codes/") !== -1) {
// We're not at the top level, so we don't prepend with "./error_codes/".
window.location = './' + code + '.html';
} else {
window.location = './error_codes/' + code + '.html';
}
}
})()