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:
commit
68d3cfac47
@ -1259,7 +1259,7 @@ dependencies = [
|
||||
name = "error_index_generator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"rustdoc",
|
||||
"mdbook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -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);
|
||||
|
@ -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, 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);
|
||||
}
|
||||
}
|
||||
|
16
src/tools/error_index_generator/redirect.js
Normal file
16
src/tools/error_index_generator/redirect.js
Normal 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';
|
||||
}
|
||||
}
|
||||
})()
|
Loading…
Reference in New Issue
Block a user