1use mdbook::book::{Book, Chapter};
2use mdbook::errors::Error;
3use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
4use mdbook::BookItem;
5use once_cell::sync::Lazy;
6use regex::{Captures, Regex};
7use semver::{Version, VersionReq};
8use std::collections::BTreeMap;
9use std::io;
10use std::path::PathBuf;
11
12mod std_links;
13
14static RULE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^r\[([^]]+)]$").unwrap());
16
17static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
20 Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)").unwrap()
21});
22
23pub fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
24 let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
25
26 let book_version = Version::parse(&ctx.mdbook_version)?;
27 let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
28
29 if !version_req.matches(&book_version) {
30 eprintln!(
31 "warning: The {} plugin was built against version {} of mdbook, \
32 but we're being called from version {}",
33 pre.name(),
34 mdbook::MDBOOK_VERSION,
35 ctx.mdbook_version
36 );
37 }
38
39 let processed_book = pre.run(&ctx, book)?;
40 serde_json::to_writer(io::stdout(), &processed_book)?;
41
42 Ok(())
43}
44
45pub struct Spec {
46 deny_warnings: bool,
49}
50
51impl Spec {
52 pub fn new() -> Spec {
53 Spec {
54 deny_warnings: std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"),
55 }
56 }
57
58 fn rule_definitions(
61 &self,
62 chapter: &Chapter,
63 found_rules: &mut BTreeMap<String, (PathBuf, PathBuf)>,
64 ) -> String {
65 let source_path = chapter.source_path.clone().unwrap_or_default();
66 let path = chapter.path.clone().unwrap_or_default();
67 RULE_RE
68 .replace_all(&chapter.content, |caps: &Captures| {
69 let rule_id = &caps[1];
70 if let Some((old, _)) =
71 found_rules.insert(rule_id.to_string(), (source_path.clone(), path.clone()))
72 {
73 let message = format!(
74 "rule `{rule_id}` defined multiple times\n\
75 First location: {old:?}\n\
76 Second location: {source_path:?}"
77 );
78 if self.deny_warnings {
79 panic!("error: {message}");
80 } else {
81 eprintln!("warning: {message}");
82 }
83 }
84 format!(
85 "<div class=\"rule\" id=\"{rule_id}\">\
86 <a class=\"rule-link\" href=\"#{rule_id}\">[{rule_id}]</a>\
87 </div>\n"
88 )
89 })
90 .to_string()
91 }
92
93 fn auto_link_references(
96 &self,
97 chapter: &Chapter,
98 found_rules: &BTreeMap<String, (PathBuf, PathBuf)>,
99 ) -> String {
100 let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
101 let definitions: String = found_rules
102 .iter()
103 .map(|(rule_id, (_, path))| {
104 let relative = pathdiff::diff_paths(path, current_path).unwrap();
105 format!("[{rule_id}]: {}#{rule_id}\n", relative.display())
106 })
107 .collect();
108 format!(
109 "{}\n\
110 {definitions}",
111 chapter.content
112 )
113 }
114
115 fn admonitions(&self, chapter: &Chapter) -> String {
129 ADMONITION_RE
130 .replace_all(&chapter.content, |caps: &Captures| {
131 let lower = caps["admon"].to_lowercase();
132 format!(
133 "<div class=\"{lower}\">\n\n{}\n\n</div>\n",
134 &caps["blockquote"]
135 )
136 })
137 .to_string()
138 }
139}
140
141impl Preprocessor for Spec {
142 fn name(&self) -> &str {
143 "nop-preprocessor"
144 }
145
146 fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
147 let mut found_rules = BTreeMap::new();
148 book.for_each_mut(|item| {
149 let BookItem::Chapter(ch) = item else {
150 return;
151 };
152 if ch.is_draft_chapter() {
153 return;
154 }
155 ch.content = self.rule_definitions(&ch, &mut found_rules);
156 ch.content = self.admonitions(&ch);
157 ch.content = std_links::std_links(&ch);
158 });
159 book.for_each_mut(|item| {
162 let BookItem::Chapter(ch) = item else {
163 return;
164 };
165 if ch.is_draft_chapter() {
166 return;
167 }
168 ch.content = self.auto_link_references(&ch, &found_rules);
169 });
170 Ok(book)
171 }
172}