Skip to content

Commit f1a2cac

Browse files
authored
Merge pull request #61 from ehuss/mdbook-spec-fixes
Several fixes for the mdbook-spec preprocessor.
2 parents 46b05e8 + 59586c0 commit f1a2cac

File tree

4 files changed

+247
-162
lines changed

4 files changed

+247
-162
lines changed

mdbook-spec/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdbook-spec/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0"
88
[dependencies]
99
anyhow = "1.0.79"
1010
mdbook = { version = "0.4.36", default-features = false }
11+
once_cell = "1.19.0"
1112
pathdiff = "0.2.1"
1213
regex = "1.10.3"
1314
semver = "1.0.21"

mdbook-spec/src/main.rs

Lines changed: 31 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ use mdbook::book::{Book, Chapter};
22
use mdbook::errors::Error;
33
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
44
use mdbook::BookItem;
5+
use once_cell::sync::Lazy;
56
use regex::{Captures, Regex};
67
use semver::{Version, VersionReq};
78
use std::collections::BTreeMap;
8-
use std::fmt::Write as _;
9-
use std::fs;
10-
use std::io::{self, Write as _};
9+
use std::io;
1110
use std::path::PathBuf;
12-
use std::process::{self, Command};
11+
use std::process;
12+
13+
mod std_links;
14+
15+
/// The Regex for rules like `r[foo]`.
16+
static RULE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^r\[([^]]+)]$").unwrap());
17+
18+
/// The Regex for the syntax for blockquotes that have a specific CSS class,
19+
/// like `> [!WARNING]`.
20+
static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
21+
Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)").unwrap()
22+
});
1323

1424
fn main() {
1525
let mut args = std::env::args().skip(1);
@@ -56,41 +66,15 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
5666
}
5767

5868
struct Spec {
69+
/// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS
70+
/// environment variable).
5971
deny_warnings: bool,
60-
rule_re: Regex,
61-
admonition_re: Regex,
62-
std_link_re: Regex,
63-
std_link_extract_re: Regex,
6472
}
6573

6674
impl Spec {
6775
pub fn new() -> Spec {
68-
// This is roughly a rustdoc intra-doc link definition.
69-
let std_link = r"(?: [a-z]+@ )?
70-
(?: std|core|alloc|proc_macro|test )
71-
(?: ::[A-Za-z_!:<>{}()\[\]]+ )?";
7276
Spec {
7377
deny_warnings: std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"),
74-
rule_re: Regex::new(r"(?m)^r\[([^]]+)]$").unwrap(),
75-
admonition_re: Regex::new(
76-
r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)",
77-
)
78-
.unwrap(),
79-
std_link_re: Regex::new(&format!(
80-
r"(?x)
81-
(?:
82-
( \[`[^`]+`\] ) \( ({std_link}) \)
83-
)
84-
| (?:
85-
( \[`{std_link}`\] )
86-
)
87-
"
88-
))
89-
.unwrap(),
90-
std_link_extract_re: Regex::new(
91-
r#"<li><a [^>]*href="(https://doc.rust-lang.org/[^"]+)""#,
92-
)
93-
.unwrap(),
9478
}
9579
}
9680

@@ -103,7 +87,7 @@ impl Spec {
10387
) -> String {
10488
let source_path = chapter.source_path.clone().unwrap_or_default();
10589
let path = chapter.path.clone().unwrap_or_default();
106-
self.rule_re
90+
RULE_RE
10791
.replace_all(&chapter.content, |caps: &Captures| {
10892
let rule_id = &caps[1];
10993
if let Some((old, _)) =
@@ -165,7 +149,7 @@ impl Spec {
165149
/// be a CSS class is valid. The actual styling needs to be added in a CSS
166150
/// file.
167151
fn admonitions(&self, chapter: &Chapter) -> String {
168-
self.admonition_re
152+
ADMONITION_RE
169153
.replace_all(&chapter.content, |caps: &Captures| {
170154
let lower = caps["admon"].to_lowercase();
171155
format!(
@@ -175,122 +159,6 @@ impl Spec {
175159
})
176160
.to_string()
177161
}
178-
179-
/// Converts links to the standard library to the online documentation in
180-
/// a fashion similar to rustdoc intra-doc links.
181-
fn std_links(&self, chapter: &Chapter) -> String {
182-
// This is very hacky, but should work well enough.
183-
//
184-
// Collect all standard library links.
185-
//
186-
// links are tuples of ("[`std::foo`]", None) for links without dest,
187-
// or ("[`foo`]", "std::foo") with a dest.
188-
let mut links: Vec<_> = self
189-
.std_link_re
190-
.captures_iter(&chapter.content)
191-
.map(|cap| {
192-
if let Some(no_dest) = cap.get(3) {
193-
(no_dest.as_str(), None)
194-
} else {
195-
(
196-
cap.get(1).unwrap().as_str(),
197-
Some(cap.get(2).unwrap().as_str()),
198-
)
199-
}
200-
})
201-
.collect();
202-
if links.is_empty() {
203-
return chapter.content.clone();
204-
}
205-
links.sort();
206-
links.dedup();
207-
208-
// Write a Rust source file to use with rustdoc to generate intra-doc links.
209-
let tmp = tempfile::TempDir::with_prefix("mdbook-spec-").unwrap();
210-
let src_path = tmp.path().join("a.rs");
211-
// Allow redundant since there could some in-scope things that are
212-
// technically not necessary, but we don't care about (like
213-
// [`Option`](std::option::Option)).
214-
let mut src = format!(
215-
"#![deny(rustdoc::broken_intra_doc_links)]\n\
216-
#![allow(rustdoc::redundant_explicit_links)]\n"
217-
);
218-
for (link, dest) in &links {
219-
write!(src, "//! - {link}").unwrap();
220-
if let Some(dest) = dest {
221-
write!(src, "({})", dest).unwrap();
222-
}
223-
src.push('\n');
224-
}
225-
writeln!(
226-
src,
227-
"extern crate alloc;\n\
228-
extern crate proc_macro;\n\
229-
extern crate test;\n"
230-
)
231-
.unwrap();
232-
fs::write(&src_path, &src).unwrap();
233-
let output = Command::new("rustdoc")
234-
.arg("--edition=2021")
235-
.arg(&src_path)
236-
.current_dir(tmp.path())
237-
.output()
238-
.expect("rustdoc installed");
239-
if !output.status.success() {
240-
eprintln!(
241-
"error: failed to extract std links ({:?}) in chapter {} ({:?})\n",
242-
output.status,
243-
chapter.name,
244-
chapter.source_path.as_ref().unwrap()
245-
);
246-
io::stderr().write_all(&output.stderr).unwrap();
247-
process::exit(1);
248-
}
249-
250-
// Extract the links from the generated html.
251-
let generated =
252-
fs::read_to_string(tmp.path().join("doc/a/index.html")).expect("index.html generated");
253-
let urls: Vec<_> = self
254-
.std_link_extract_re
255-
.captures_iter(&generated)
256-
.map(|cap| cap.get(1).unwrap().as_str())
257-
.collect();
258-
if urls.len() != links.len() {
259-
eprintln!(
260-
"error: expected rustdoc to generate {} links, but found {} in chapter {} ({:?})",
261-
links.len(),
262-
urls.len(),
263-
chapter.name,
264-
chapter.source_path.as_ref().unwrap()
265-
);
266-
process::exit(1);
267-
}
268-
269-
// Replace any disambiguated links with just the disambiguation.
270-
let mut output = self
271-
.std_link_re
272-
.replace_all(&chapter.content, |caps: &Captures| {
273-
if let Some(dest) = caps.get(2) {
274-
// Replace destination parenthesis with a link definition (square brackets).
275-
format!("{}[{}]", &caps[1], dest.as_str())
276-
} else {
277-
caps[0].to_string()
278-
}
279-
})
280-
.to_string();
281-
282-
// Append the link definitions to the bottom of the chapter.
283-
write!(output, "\n").unwrap();
284-
for ((link, dest), url) in links.iter().zip(urls) {
285-
if let Some(dest) = dest {
286-
write!(output, "[{dest}]: {url}\n").unwrap();
287-
} else {
288-
write!(output, "{link}: {url}\n").unwrap();
289-
}
290-
}
291-
292-
output
293-
}
294162
}
295163

296164
impl Preprocessor for Spec {
@@ -300,27 +168,28 @@ impl Preprocessor for Spec {
300168

301169
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
302170
let mut found_rules = BTreeMap::new();
303-
for section in &mut book.sections {
304-
let BookItem::Chapter(ch) = section else {
305-
continue;
171+
book.for_each_mut(|item| {
172+
let BookItem::Chapter(ch) = item else {
173+
return;
306174
};
307175
if ch.is_draft_chapter() {
308-
continue;
176+
return;
309177
}
310178
ch.content = self.rule_definitions(&ch, &mut found_rules);
311179
ch.content = self.admonitions(&ch);
312-
ch.content = self.std_links(&ch);
313-
}
314-
for section in &mut book.sections {
315-
let BookItem::Chapter(ch) = section else {
316-
continue;
180+
ch.content = std_links::std_links(&ch);
181+
});
182+
// This is a separate pass because it relies on the modifications of
183+
// the previous passes.
184+
book.for_each_mut(|item| {
185+
let BookItem::Chapter(ch) = item else {
186+
return;
317187
};
318188
if ch.is_draft_chapter() {
319-
continue;
189+
return;
320190
}
321191
ch.content = self.auto_link_references(&ch, &found_rules);
322-
}
323-
192+
});
324193
Ok(book)
325194
}
326195
}

0 commit comments

Comments
 (0)