Skip to content

Commit 021ebeb

Browse files
authored
Merge pull request #65 from ehuss/mdbook-spec-to-lib
Mdbook spec to lib
2 parents bb6b4f7 + 410d493 commit 021ebeb

File tree

4 files changed

+180
-178
lines changed

4 files changed

+180
-178
lines changed

mdbook-spec/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## mdbook-spec 0.1.1
4+
5+
- Moved code to a library to support upstream integration.
6+
37
## mdbook-spec 0.1.0
48

59
- Initial release

mdbook-spec/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mdbook-spec"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
edition = "2021"
55
license = "MIT OR Apache-2.0"
66
description = "An mdBook preprocessor to help with the Rust specification."

mdbook-spec/src/lib.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use mdbook::book::{Book, Chapter};
2+
use mdbook::errors::Error;
3+
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
4+
use mdbook::BookItem;
5+
use once_cell::sync::Lazy;
6+
use regex::{Captures, Regex};
7+
use semver::{Version, VersionReq};
8+
use std::collections::BTreeMap;
9+
use std::io;
10+
use std::path::PathBuf;
11+
12+
mod std_links;
13+
14+
/// The Regex for rules like `r[foo]`.
15+
static RULE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^r\[([^]]+)]$").unwrap());
16+
17+
/// The Regex for the syntax for blockquotes that have a specific CSS class,
18+
/// like `> [!WARNING]`.
19+
static ADMONITION_RE: Lazy<Regex> = Lazy::new(|| {
20+
Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *> .*\n)+)").unwrap()
21+
});
22+
23+
pub 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+
45+
pub struct Spec {
46+
/// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS
47+
/// environment variable).
48+
deny_warnings: bool,
49+
}
50+
51+
impl 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+
/// Converts lines that start with `r[…]` into a "rule" which has special
59+
/// styling and can be linked to.
60+
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+
/// Generates link references to all rules on all pages, so you can easily
94+
/// refer to rules anywhere in the book.
95+
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+
/// Converts blockquotes with special headers into admonitions.
116+
///
117+
/// The blockquote should look something like:
118+
///
119+
/// ```
120+
/// > [!WARNING]
121+
/// > ...
122+
/// ```
123+
///
124+
/// This will add a `<div class="warning">` around the blockquote so that
125+
/// it can be styled differently. Any text between the brackets that can
126+
/// be a CSS class is valid. The actual styling needs to be added in a CSS
127+
/// file.
128+
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+
141+
impl 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+
// This is a separate pass because it relies on the modifications of
160+
// the previous passes.
161+
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+
}

mdbook-spec/src/main.rs

Lines changed: 3 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,3 @@
1-
use mdbook::book::{Book, Chapter};
2-
use mdbook::errors::Error;
3-
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
4-
use mdbook::BookItem;
5-
use once_cell::sync::Lazy;
6-
use regex::{Captures, Regex};
7-
use semver::{Version, VersionReq};
8-
use std::collections::BTreeMap;
9-
use std::io;
10-
use std::path::PathBuf;
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-
});
23-
241
fn main() {
252
let mut args = std::env::args().skip(1);
263
match args.next().as_deref() {
@@ -35,161 +12,10 @@ fn main() {
3512
None => {}
3613
}
3714

38-
let preprocessor = Spec::new();
15+
let preprocessor = mdbook_spec::Spec::new();
3916

40-
if let Err(e) = handle_preprocessing(&preprocessor) {
17+
if let Err(e) = mdbook_spec::handle_preprocessing(&preprocessor) {
4118
eprintln!("{}", e);
42-
process::exit(1);
43-
}
44-
}
45-
46-
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
47-
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
48-
49-
let book_version = Version::parse(&ctx.mdbook_version)?;
50-
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
51-
52-
if !version_req.matches(&book_version) {
53-
eprintln!(
54-
"warning: The {} plugin was built against version {} of mdbook, \
55-
but we're being called from version {}",
56-
pre.name(),
57-
mdbook::MDBOOK_VERSION,
58-
ctx.mdbook_version
59-
);
60-
}
61-
62-
let processed_book = pre.run(&ctx, book)?;
63-
serde_json::to_writer(io::stdout(), &processed_book)?;
64-
65-
Ok(())
66-
}
67-
68-
struct Spec {
69-
/// Whether or not warnings should be errors (set by SPEC_DENY_WARNINGS
70-
/// environment variable).
71-
deny_warnings: bool,
72-
}
73-
74-
impl Spec {
75-
pub fn new() -> Spec {
76-
Spec {
77-
deny_warnings: std::env::var("SPEC_DENY_WARNINGS").as_deref() == Ok("1"),
78-
}
79-
}
80-
81-
/// Converts lines that start with `r[…]` into a "rule" which has special
82-
/// styling and can be linked to.
83-
fn rule_definitions(
84-
&self,
85-
chapter: &Chapter,
86-
found_rules: &mut BTreeMap<String, (PathBuf, PathBuf)>,
87-
) -> String {
88-
let source_path = chapter.source_path.clone().unwrap_or_default();
89-
let path = chapter.path.clone().unwrap_or_default();
90-
RULE_RE
91-
.replace_all(&chapter.content, |caps: &Captures| {
92-
let rule_id = &caps[1];
93-
if let Some((old, _)) =
94-
found_rules.insert(rule_id.to_string(), (source_path.clone(), path.clone()))
95-
{
96-
let message = format!(
97-
"rule `{rule_id}` defined multiple times\n\
98-
First location: {old:?}\n\
99-
Second location: {source_path:?}"
100-
);
101-
if self.deny_warnings {
102-
panic!("error: {message}");
103-
} else {
104-
eprintln!("warning: {message}");
105-
}
106-
}
107-
format!(
108-
"<div class=\"rule\" id=\"{rule_id}\">\
109-
<a class=\"rule-link\" href=\"#{rule_id}\">[{rule_id}]</a>\
110-
</div>\n"
111-
)
112-
})
113-
.to_string()
114-
}
115-
116-
/// Generates link references to all rules on all pages, so you can easily
117-
/// refer to rules anywhere in the book.
118-
fn auto_link_references(
119-
&self,
120-
chapter: &Chapter,
121-
found_rules: &BTreeMap<String, (PathBuf, PathBuf)>,
122-
) -> String {
123-
let current_path = chapter.path.as_ref().unwrap().parent().unwrap();
124-
let definitions: String = found_rules
125-
.iter()
126-
.map(|(rule_id, (_, path))| {
127-
let relative = pathdiff::diff_paths(path, current_path).unwrap();
128-
format!("[{rule_id}]: {}#{rule_id}\n", relative.display())
129-
})
130-
.collect();
131-
format!(
132-
"{}\n\
133-
{definitions}",
134-
chapter.content
135-
)
136-
}
137-
138-
/// Converts blockquotes with special headers into admonitions.
139-
///
140-
/// The blockquote should look something like:
141-
///
142-
/// ```
143-
/// > [!WARNING]
144-
/// > ...
145-
/// ```
146-
///
147-
/// This will add a `<div class="warning">` around the blockquote so that
148-
/// it can be styled differently. Any text between the brackets that can
149-
/// be a CSS class is valid. The actual styling needs to be added in a CSS
150-
/// file.
151-
fn admonitions(&self, chapter: &Chapter) -> String {
152-
ADMONITION_RE
153-
.replace_all(&chapter.content, |caps: &Captures| {
154-
let lower = caps["admon"].to_lowercase();
155-
format!(
156-
"<div class=\"{lower}\">\n\n{}\n\n</div>\n",
157-
&caps["blockquote"]
158-
)
159-
})
160-
.to_string()
161-
}
162-
}
163-
164-
impl Preprocessor for Spec {
165-
fn name(&self) -> &str {
166-
"nop-preprocessor"
167-
}
168-
169-
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
170-
let mut found_rules = BTreeMap::new();
171-
book.for_each_mut(|item| {
172-
let BookItem::Chapter(ch) = item else {
173-
return;
174-
};
175-
if ch.is_draft_chapter() {
176-
return;
177-
}
178-
ch.content = self.rule_definitions(&ch, &mut found_rules);
179-
ch.content = self.admonitions(&ch);
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;
187-
};
188-
if ch.is_draft_chapter() {
189-
return;
190-
}
191-
ch.content = self.auto_link_references(&ch, &found_rules);
192-
});
193-
Ok(book)
19+
std::process::exit(1);
19420
}
19521
}

0 commit comments

Comments
 (0)