Skip to content

Commit bdd40d7

Browse files
authored
Improve the rendering pipeline (#73)
* Reduce noise generated by running the preproc and renderer * Delete dead code in preproc * Move admonition markup processing to preproc instead of renderer This lets them work with `mdbook serve` (which hardcodes the HTML renderer), and at the same time is more robust (no more running regexes against HTML output!). The syntax was slightly adjusted to be closer to established VuePress etc. * Enforce v2 resolver for the entire workspace
1 parent 3077cf7 commit bdd40d7

30 files changed

+174
-226
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[workspace]
22
members = [ "preproc", "renderer", "i18n-helpers" ]
3+
resolver = "2"

book.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ before = ["links"]
1818

1919
# Custom preprocessor for our custom markup
2020
[preprocessor.custom]
21-
command = "cargo run -p preproc --locked --release --"
21+
command = "cargo run -p preproc --locked -rq --"
2222

2323
# Custom back-end for our custom markup
2424
[output.custom]
25-
command = "cargo run -p renderer --locked --release --"
25+
command = "cargo run -p renderer --locked -rq --"
2626

2727
[output.html]
2828
additional-css = [

preproc/src/admonitions.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* This Source Code Form is subject to the
3+
* terms of the Mozilla Public License, v.
4+
* 2.0. If a copy of the MPL was not
5+
* distributed with this file, You can
6+
* obtain one at
7+
* http://mozilla.org/MPL/2.0/.
8+
*/
9+
10+
use std::{format, iter::Peekable, matches};
11+
12+
use anyhow::Error;
13+
use mdbook::book::Chapter;
14+
use pulldown_cmark::{Event, Options, Parser, Tag};
15+
16+
use crate::GbAsmTut;
17+
18+
impl GbAsmTut {
19+
pub fn process_admonitions(&self, chapter: &mut Chapter) -> Result<(), Error> {
20+
let mut buf = String::with_capacity(chapter.content.len());
21+
let extensions =
22+
Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH;
23+
24+
let events = AdmonitionsGenerator::new(Parser::new_ext(&chapter.content, extensions));
25+
26+
pulldown_cmark_to_cmark::cmark(events, &mut buf, None)
27+
.map_err(|err| Error::from(err).context("Markdown serialization failed"))?;
28+
chapter.content = buf;
29+
30+
Ok(())
31+
}
32+
}
33+
34+
struct AdmonitionsGenerator<'a, Iter: Iterator<Item = Event<'a>>> {
35+
iter: Peekable<Iter>,
36+
nesting_level: usize,
37+
at_paragraph_start: bool,
38+
}
39+
40+
impl<'a, Iter: Iterator<Item = Event<'a>>> AdmonitionsGenerator<'a, Iter> {
41+
const KINDS: [&'static str; 3] = ["tip", "warning", "danger"];
42+
43+
fn new(iter: Iter) -> Self {
44+
Self {
45+
iter: iter.peekable(),
46+
nesting_level: 0,
47+
at_paragraph_start: false,
48+
}
49+
}
50+
}
51+
52+
impl<'a, Iter: Iterator<Item = Event<'a>>> Iterator for AdmonitionsGenerator<'a, Iter> {
53+
type Item = Event<'a>;
54+
55+
fn next(&mut self) -> Option<Self::Item> {
56+
let mut evt = self.iter.next()?;
57+
58+
match evt {
59+
Event::Text(ref text) if self.at_paragraph_start => {
60+
if let Some(params) = text.strip_prefix(":::") {
61+
// Check that there is no more text in the paragraph; if there isn't, we'll consume the entire paragraph.
62+
// Note that this intentionally rejects any formatting within the paragraph—serialisation would be too complex.
63+
if matches!(self.iter.peek(), Some(Event::End(Tag::Paragraph))) {
64+
if params.is_empty() {
65+
if self.nesting_level != 0 {
66+
// Ending an admonition.
67+
self.nesting_level -= 1;
68+
69+
evt = Event::Html("</div>".into());
70+
}
71+
} else {
72+
let (kind, title) =
73+
match params.split_once(|c: char| c.is_ascii_whitespace()) {
74+
Some((kind, title)) => (kind, title.trim()),
75+
None => (params, ""),
76+
};
77+
let (kind, decoration) = match kind.split_once(':') {
78+
Some((kind, decoration)) => (kind, Some(decoration)),
79+
None => (kind, None),
80+
};
81+
if Self::KINDS.contains(&kind) {
82+
// Beginning an admonition.
83+
self.nesting_level += 1;
84+
85+
evt = Event::Html(
86+
if let Some(decoration) = decoration {
87+
if title.is_empty() {
88+
format!("<div class=\"box {kind} decorated\"><p>{decoration}</p>")
89+
} else {
90+
format!("<div class=\"box {kind} decorated\"><p>{decoration}</p><p class=\"box-title\">{title}</p>")
91+
}
92+
} else if title.is_empty() {
93+
format!("<div class=\"box {kind}\">")
94+
} else {
95+
format!("<div class=\"box {kind}\"><p class=\"box-title\">{title}</p>")
96+
}
97+
.into(),
98+
);
99+
}
100+
}
101+
}
102+
}
103+
}
104+
_ => {}
105+
}
106+
107+
self.at_paragraph_start = matches!(evt, Event::Start(Tag::Paragraph));
108+
109+
Some(evt)
110+
}
111+
}

preproc/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
1313
use std::io;
1414
use std::process;
1515

16+
mod admonitions;
1617
mod preproc;
1718
use preproc::GbAsmTut;
1819
mod links;

preproc/src/preproc.rs

Lines changed: 4 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ impl Preprocessor for GbAsmTut {
3333
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
3434
let src_dir = ctx.root.join(&ctx.config.book.src);
3535

36-
let res = Ok(());
36+
let mut res = Ok(());
3737
book.for_each_mut(|section: &mut BookItem| {
3838
if res.is_err() {
3939
return;
@@ -47,78 +47,13 @@ impl Preprocessor for GbAsmTut {
4747
.expect("All book items have a parent");
4848

4949
ch.content = links::replace_all(&ch.content, base);
50-
// match Self::process_content(&content) {
51-
// Ok(content) => ch.content = content,
52-
// Err(err) => res = Err(err),
53-
// }
50+
if let Err(err) = self.process_admonitions(ch) {
51+
res = Err(err);
52+
}
5453
}
5554
}
5655
});
5756

5857
res.map(|_| book)
5958
}
6059
}
61-
/*
62-
impl GbAsmTut {
63-
fn process_content(content: &str) -> Result<String, Error> {
64-
let mut buf = String::with_capacity(content.len());
65-
let mut state = None;
66-
67-
let mut serialize = |events: &[_]| -> Result<_, Error> {
68-
let state = &mut state;
69-
*state = Some(
70-
pulldown_cmark_to_cmark::cmark(events.iter(), &mut buf, state.clone())
71-
.map_err(|err| Error::from(err).context("Markdown serialization failed"))?,
72-
);
73-
Ok(())
74-
};
75-
76-
let mut events = Parser::new(&content);
77-
while let Some(event) = events.next() {
78-
match event {
79-
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang)))
80-
if lang.starts_with("linenos__") =>
81-
{
82-
let start = Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(
83-
lang.strip_prefix("linenos__").unwrap().to_string().into(),
84-
)));
85-
let code = events.next().expect("Code blocks must at least be closed");
86-
87-
if matches!(code, Event::End(_)) {
88-
serialize(&[start, code])?;
89-
} else if let Event::Text(code) = code {
90-
let end = events.next().expect("Code blocks must be closed");
91-
if !matches!(end, Event::End(_)) {
92-
return Err(Error::msg(format!(
93-
"Unexpected {:?} instead of code closing tag",
94-
end
95-
)));
96-
}
97-
98-
eprintln!("{:?}", code);
99-
let line_nos: String = code
100-
.lines()
101-
.enumerate()
102-
.map(|(n, _)| format!("{}\n", n))
103-
.collect();
104-
serialize(&[
105-
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced("linenos".into()))),
106-
Event::Text(line_nos.into()),
107-
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced("linenos".into()))),
108-
start,
109-
Event::Text(code),
110-
end,
111-
])?;
112-
} else {
113-
return Err(Error::msg(format!("Unexpected {:?} within code tag", code)));
114-
}
115-
}
116-
117-
_ => serialize(&[event])?,
118-
}
119-
}
120-
121-
Ok(buf)
122-
}
123-
}
124-
*/

renderer/src/main.rs

Lines changed: 8 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use anyhow::Context;
1111
use lazy_static::lazy_static;
1212
use mdbook::book::BookItem;
13-
use mdbook::errors::{Error, Result};
13+
use mdbook::errors::Result;
1414
use mdbook::renderer::{HtmlHandlebars, RenderContext, Renderer};
1515
use regex::Regex;
1616
use std::fs::{self, File};
@@ -63,20 +63,16 @@ impl Renderer for GbAsmTut {
6363
BookItem::Chapter(chapter) if !chapter.is_draft_chapter() => {
6464
let mut path = ctx.destination.join(chapter.path.as_ref().unwrap());
6565
path.set_extension("html");
66-
render(&mut path, &chapter.name, i)
66+
post_process(&mut path, i)
6767
.context(format!("Failed to render {}", &chapter.name))?;
6868
}
6969

7070
_ => (),
7171
}
7272
}
7373
// Post-process the print page as well
74-
render(
75-
&mut ctx.destination.join("print.html"),
76-
"<print>",
77-
usize::MAX,
78-
)
79-
.context("Failed to render print page")?;
74+
post_process(&mut ctx.destination.join("print.html"), usize::MAX)
75+
.context("Failed to render print page")?;
8076

8177
// Take the "ANCHOR" lines out of `hello_world.asm`
8278
let path = ctx.destination.join("assets").join("hello-world.asm");
@@ -94,13 +90,7 @@ impl Renderer for GbAsmTut {
9490
}
9591
}
9692

97-
#[derive(Debug)]
98-
enum BoxType {
99-
Plain,
100-
Decorated,
101-
}
102-
103-
fn render(path: &mut PathBuf, name: &str, index: usize) -> Result<()> {
93+
fn post_process(path: &mut PathBuf, index: usize) -> Result<()> {
10494
// Since we are about to edit the file in-place, we must buffer it into memory
10595
let html = fs::read_to_string(&path)?;
10696
// Open the output file, and possibly the output "index.html" file
@@ -125,104 +115,18 @@ fn render(path: &mut PathBuf, name: &str, index: usize) -> Result<()> {
125115
};
126116
}
127117

128-
let mut cur_box = None;
129118
let mut in_console = false; // Are we in a "console" code block?
130-
for (i, mut line) in html.lines().enumerate() {
131-
let line_no = i + 1;
119+
for mut line in html.lines() {
132120
lazy_static! {
133121
static ref CONSOLE_CODE_RE: Regex =
134122
Regex::new(r#"^<pre><code class="(?:\S*\s+)*language-console(?:\s+\S*)*">"#)
135123
.unwrap();
136124
}
137125

138-
// Yes, this relies on how the HTML renderer outputs paragraphs, i.e.
139-
// that tags are flush with the content.
126+
// Yes, this relies on how the HTML renderer outputs HTML, i.e. that the above tags are flush with each-other.
140127
// Yes, this sucks, and yes, I hate it.
141128
// If you have a better idea, please tell us! x_x
142-
143-
if let Some(line) = line.strip_prefix("<p>:::") {
144-
if let Some(line) = line.strip_suffix("</p>") {
145-
let line = line.trim();
146-
147-
if let Some(box_type) = line.split_whitespace().next() {
148-
// This is a box start marker
149-
if cur_box.is_some() {
150-
return Err(Error::msg(format!(
151-
"{}:{}: Attempting to open box inside of one",
152-
path.display(),
153-
line_no
154-
)));
155-
}
156-
157-
let (box_type_name, decoration) = match box_type.find(':') {
158-
Some(n) => (&box_type[..n], Some(&box_type[n + 1..])),
159-
None => (box_type, None),
160-
};
161-
162-
let box_type_name = if ["tip", "warning", "danger"].contains(&box_type_name) {
163-
box_type_name
164-
} else {
165-
let mut stderr = StandardStream::stderr(ColorChoice::Auto);
166-
stderr
167-
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))
168-
.unwrap();
169-
write!(&mut stderr, "warning").unwrap();
170-
stderr.reset().unwrap();
171-
eprintln!(
172-
" ({}): unknown box type \"{}\", defaulting to \"tip\"",
173-
name, box_type_name
174-
);
175-
"tip"
176-
};
177-
output!(format!(
178-
"<div class=\"box {}{}\">\n",
179-
box_type_name,
180-
decoration.map_or("", |_| " decorated")
181-
));
182-
183-
cur_box = if let Some(decoration) = decoration {
184-
output!(format!("<div><p>{}</p></div>\n<div>\n", decoration));
185-
Some(BoxType::Decorated)
186-
} else {
187-
Some(BoxType::Plain)
188-
};
189-
190-
let title = &line[box_type.len()..].trim_start();
191-
if !title.is_empty() {
192-
output!(format!("<p class=\"box-title\">{}</p>", title));
193-
}
194-
} else {
195-
// This is a box ending marker
196-
match cur_box {
197-
None => {
198-
return Err(Error::msg(format!(
199-
"{}:{}: Attempting to close box outside of one",
200-
path.display(),
201-
line_no
202-
)))
203-
}
204-
Some(BoxType::Decorated) => {
205-
output!("</div>\n"); // Close the `box-inner
206-
}
207-
Some(BoxType::Plain) => (),
208-
}
209-
cur_box = None;
210-
211-
output!("</div>\n");
212-
}
213-
214-
// Prevent normal output
215-
continue;
216-
} else {
217-
let mut stderr = StandardStream::stderr(ColorChoice::Auto);
218-
stderr
219-
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))
220-
.unwrap();
221-
write!(&mut stderr, "warning").unwrap();
222-
stderr.reset().unwrap();
223-
eprintln!(" ({}): ignoring \":::{}\"; box start/end tags must be alone in their paragraph", name, line);
224-
}
225-
} else if let Some(match_info) = CONSOLE_CODE_RE.find(line) {
129+
if let Some(match_info) = CONSOLE_CODE_RE.find(line) {
226130
output!("<pre><code>"); // Disable the highlighting
227131
in_console = true;
228132
debug_assert_eq!(match_info.start(), 0);
@@ -248,9 +152,5 @@ fn render(path: &mut PathBuf, name: &str, index: usize) -> Result<()> {
248152
output!("\n");
249153
}
250154

251-
if cur_box.is_some() {
252-
return Err(Error::msg(format!("{}: Unclosed box", path.display())));
253-
}
254-
255155
Ok(())
256156
}

0 commit comments

Comments
 (0)