blob: 6591e4161032016d7db6cdee8ac65356ef208154 [file] [log] [blame]
use mdbook::book::{Book, Chapter};
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use mdbook::BookItem;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use semver::{Version, VersionReq};
use std::collections::BTreeMap;
use std::io;
use std::path::PathBuf;
mod std_links;
/// The Regex for rules like `r[foo]`.
static RULE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^r\[([^]]+)]$").unwrap());
/// The Regex for the syntax for blockquotes that have a specific CSS class,
/// like `> [!WARNING]`.
static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)").unwrap()
});
pub fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
let book_version = Version::parse(&ctx.mdbook_version)?;
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
if !version_req.matches(&book_version) {
eprintln!(
"warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}",
pre.name(),
mdbook::MDBOOK_VERSION,
ctx.mdbook_version
);
}
let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;
Ok(())
}
pub struct Spec {
/// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS
/// environment variable).
deny_warnings: bool,
}
impl Spec {
pub fn new() -> Spec {
Spec {
deny_warnings: std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"),
}
}
/// Converts lines that start with `r[…]` into a "rule" which has special
/// styling and can be linked to.
fn rule_definitions(
&self,
chapter: &Chapter,
found_rules: &mut BTreeMap<String, (PathBuf, PathBuf)>,
) -> String {
let source_path = chapter.source_path.clone().unwrap_or_default();
let path = chapter.path.clone().unwrap_or_default();
RULE_RE
.replace_all(&chapter.content, |caps: &Captures<'_>| {
let rule_id = &caps[1];
if let Some((old, _)) =
found_rules.insert(rule_id.to_string(), (source_path.clone(), path.clone()))
{
let message = format!(
"rule `{rule_id}` defined multiple times\n\
First location: {old:?}\n\
Second location: {source_path:?}"
);
if self.deny_warnings {
panic!("error: {message}");
} else {
eprintln!("warning: {message}");
}
}
format!(
"<div class=\"rule\" id=\"{rule_id}\">\
<a class=\"rule-link\" href=\"#{rule_id}\">[{rule_id}]</a>\
</div>\n"
)
})
.to_string()
}
/// Generates link references to all rules on all pages, so you can easily
/// refer to rules anywhere in the book.
fn auto_link_references(
&self,
chapter: &Chapter,
found_rules: &BTreeMap<String, (PathBuf, PathBuf)>,
) -> String {
let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
let definitions: String = found_rules
.iter()
.map(|(rule_id, (_, path))| {
let relative = pathdiff::diff_paths(path, current_path).unwrap();
format!("[{rule_id}]: {}#{rule_id}\n", relative.display())
})
.collect();
format!(
"{}\n\
{definitions}",
chapter.content
)
}
/// Converts blockquotes with special headers into admonitions.
///
/// The blockquote should look something like:
///
/// ```markdown
/// > [!WARNING]
/// > ...
/// ```
///
/// This will add a `<div class="warning">` around the blockquote so that
/// it can be styled differently. Any text between the brackets that can
/// be a CSS class is valid. The actual styling needs to be added in a CSS
/// file.
fn admonitions(&self, chapter: &Chapter) -> String {
ADMONITION_RE
.replace_all(&chapter.content, |caps: &Captures<'_>| {
let lower = caps["admon"].to_lowercase();
format!(
"<div class=\"{lower}\">\n\n{}\n\n</div>\n",
&caps["blockquote"]
)
})
.to_string()
}
}
impl Preprocessor for Spec {
fn name(&self) -> &str {
"spec"
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
let mut found_rules = BTreeMap::new();
book.for_each_mut(|item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
ch.content = self.rule_definitions(&ch, &mut found_rules);
ch.content = self.admonitions(&ch);
ch.content = std_links::std_links(&ch);
});
// This is a separate pass because it relies on the modifications of
// the previous passes.
book.for_each_mut(|item| {
let BookItem::Chapter(ch) = item else {
return;
};
if ch.is_draft_chapter() {
return;
}
ch.content = self.auto_link_references(&ch, &found_rules);
});
Ok(book)
}
}