Skip to content

Commit

Permalink
Improve the rendering pipeline (#73)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ISSOtm authored Dec 1, 2023
1 parent 3077cf7 commit bdd40d7
Show file tree
Hide file tree
Showing 30 changed files with 174 additions and 226 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[workspace]
members = [ "preproc", "renderer", "i18n-helpers" ]
resolver = "2"
4 changes: 2 additions & 2 deletions book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ before = ["links"]

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

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

[output.html]
additional-css = [
Expand Down
111 changes: 111 additions & 0 deletions preproc/src/admonitions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* This Source Code Form is subject to the
* terms of the Mozilla Public License, v.
* 2.0. If a copy of the MPL was not
* distributed with this file, You can
* obtain one at
* http://mozilla.org/MPL/2.0/.
*/

use std::{format, iter::Peekable, matches};

use anyhow::Error;
use mdbook::book::Chapter;
use pulldown_cmark::{Event, Options, Parser, Tag};

use crate::GbAsmTut;

impl GbAsmTut {
pub fn process_admonitions(&self, chapter: &mut Chapter) -> Result<(), Error> {
let mut buf = String::with_capacity(chapter.content.len());
let extensions =
Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH;

let events = AdmonitionsGenerator::new(Parser::new_ext(&chapter.content, extensions));

pulldown_cmark_to_cmark::cmark(events, &mut buf, None)
.map_err(|err| Error::from(err).context("Markdown serialization failed"))?;
chapter.content = buf;

Ok(())
}
}

struct AdmonitionsGenerator<'a, Iter: Iterator<Item = Event<'a>>> {
iter: Peekable<Iter>,
nesting_level: usize,
at_paragraph_start: bool,
}

impl<'a, Iter: Iterator<Item = Event<'a>>> AdmonitionsGenerator<'a, Iter> {
const KINDS: [&'static str; 3] = ["tip", "warning", "danger"];

fn new(iter: Iter) -> Self {
Self {
iter: iter.peekable(),
nesting_level: 0,
at_paragraph_start: false,
}
}
}

impl<'a, Iter: Iterator<Item = Event<'a>>> Iterator for AdmonitionsGenerator<'a, Iter> {
type Item = Event<'a>;

fn next(&mut self) -> Option<Self::Item> {
let mut evt = self.iter.next()?;

match evt {
Event::Text(ref text) if self.at_paragraph_start => {
if let Some(params) = text.strip_prefix(":::") {
// Check that there is no more text in the paragraph; if there isn't, we'll consume the entire paragraph.
// Note that this intentionally rejects any formatting within the paragraph—serialisation would be too complex.
if matches!(self.iter.peek(), Some(Event::End(Tag::Paragraph))) {
if params.is_empty() {
if self.nesting_level != 0 {
// Ending an admonition.
self.nesting_level -= 1;

evt = Event::Html("</div>".into());
}
} else {
let (kind, title) =
match params.split_once(|c: char| c.is_ascii_whitespace()) {
Some((kind, title)) => (kind, title.trim()),
None => (params, ""),
};
let (kind, decoration) = match kind.split_once(':') {
Some((kind, decoration)) => (kind, Some(decoration)),
None => (kind, None),
};
if Self::KINDS.contains(&kind) {
// Beginning an admonition.
self.nesting_level += 1;

evt = Event::Html(
if let Some(decoration) = decoration {
if title.is_empty() {
format!("<div class=\"box {kind} decorated\"><p>{decoration}</p>")
} else {
format!("<div class=\"box {kind} decorated\"><p>{decoration}</p><p class=\"box-title\">{title}</p>")
}
} else if title.is_empty() {
format!("<div class=\"box {kind}\">")
} else {
format!("<div class=\"box {kind}\"><p class=\"box-title\">{title}</p>")
}
.into(),
);
}
}
}
}
}
_ => {}
}

self.at_paragraph_start = matches!(evt, Event::Start(Tag::Paragraph));

Some(evt)
}
}
1 change: 1 addition & 0 deletions preproc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use std::io;
use std::process;

mod admonitions;
mod preproc;
use preproc::GbAsmTut;
mod links;
Expand Down
73 changes: 4 additions & 69 deletions preproc/src/preproc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Preprocessor for GbAsmTut {
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
let src_dir = ctx.root.join(&ctx.config.book.src);

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

ch.content = links::replace_all(&ch.content, base);
// match Self::process_content(&content) {
// Ok(content) => ch.content = content,
// Err(err) => res = Err(err),
// }
if let Err(err) = self.process_admonitions(ch) {
res = Err(err);
}
}
}
});

res.map(|_| book)
}
}
/*
impl GbAsmTut {
fn process_content(content: &str) -> Result<String, Error> {
let mut buf = String::with_capacity(content.len());
let mut state = None;
let mut serialize = |events: &[_]| -> Result<_, Error> {
let state = &mut state;
*state = Some(
pulldown_cmark_to_cmark::cmark(events.iter(), &mut buf, state.clone())
.map_err(|err| Error::from(err).context("Markdown serialization failed"))?,
);
Ok(())
};
let mut events = Parser::new(&content);
while let Some(event) = events.next() {
match event {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang)))
if lang.starts_with("linenos__") =>
{
let start = Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(
lang.strip_prefix("linenos__").unwrap().to_string().into(),
)));
let code = events.next().expect("Code blocks must at least be closed");
if matches!(code, Event::End(_)) {
serialize(&[start, code])?;
} else if let Event::Text(code) = code {
let end = events.next().expect("Code blocks must be closed");
if !matches!(end, Event::End(_)) {
return Err(Error::msg(format!(
"Unexpected {:?} instead of code closing tag",
end
)));
}
eprintln!("{:?}", code);
let line_nos: String = code
.lines()
.enumerate()
.map(|(n, _)| format!("{}\n", n))
.collect();
serialize(&[
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced("linenos".into()))),
Event::Text(line_nos.into()),
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced("linenos".into()))),
start,
Event::Text(code),
end,
])?;
} else {
return Err(Error::msg(format!("Unexpected {:?} within code tag", code)));
}
}
_ => serialize(&[event])?,
}
}
Ok(buf)
}
}
*/
116 changes: 8 additions & 108 deletions renderer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use anyhow::Context;
use lazy_static::lazy_static;
use mdbook::book::BookItem;
use mdbook::errors::{Error, Result};
use mdbook::errors::Result;
use mdbook::renderer::{HtmlHandlebars, RenderContext, Renderer};
use regex::Regex;
use std::fs::{self, File};
Expand Down Expand Up @@ -63,20 +63,16 @@ impl Renderer for GbAsmTut {
BookItem::Chapter(chapter) if !chapter.is_draft_chapter() => {
let mut path = ctx.destination.join(chapter.path.as_ref().unwrap());
path.set_extension("html");
render(&mut path, &chapter.name, i)
post_process(&mut path, i)
.context(format!("Failed to render {}", &chapter.name))?;
}

_ => (),
}
}
// Post-process the print page as well
render(
&mut ctx.destination.join("print.html"),
"<print>",
usize::MAX,
)
.context("Failed to render print page")?;
post_process(&mut ctx.destination.join("print.html"), usize::MAX)
.context("Failed to render print page")?;

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

#[derive(Debug)]
enum BoxType {
Plain,
Decorated,
}

fn render(path: &mut PathBuf, name: &str, index: usize) -> Result<()> {
fn post_process(path: &mut PathBuf, index: usize) -> Result<()> {
// Since we are about to edit the file in-place, we must buffer it into memory
let html = fs::read_to_string(&path)?;
// Open the output file, and possibly the output "index.html" file
Expand All @@ -125,104 +115,18 @@ fn render(path: &mut PathBuf, name: &str, index: usize) -> Result<()> {
};
}

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

// Yes, this relies on how the HTML renderer outputs paragraphs, i.e.
// that tags are flush with the content.
// Yes, this relies on how the HTML renderer outputs HTML, i.e. that the above tags are flush with each-other.
// Yes, this sucks, and yes, I hate it.
// If you have a better idea, please tell us! x_x

if let Some(line) = line.strip_prefix("<p>:::") {
if let Some(line) = line.strip_suffix("</p>") {
let line = line.trim();

if let Some(box_type) = line.split_whitespace().next() {
// This is a box start marker
if cur_box.is_some() {
return Err(Error::msg(format!(
"{}:{}: Attempting to open box inside of one",
path.display(),
line_no
)));
}

let (box_type_name, decoration) = match box_type.find(':') {
Some(n) => (&box_type[..n], Some(&box_type[n + 1..])),
None => (box_type, None),
};

let box_type_name = if ["tip", "warning", "danger"].contains(&box_type_name) {
box_type_name
} else {
let mut stderr = StandardStream::stderr(ColorChoice::Auto);
stderr
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))
.unwrap();
write!(&mut stderr, "warning").unwrap();
stderr.reset().unwrap();
eprintln!(
" ({}): unknown box type \"{}\", defaulting to \"tip\"",
name, box_type_name
);
"tip"
};
output!(format!(
"<div class=\"box {}{}\">\n",
box_type_name,
decoration.map_or("", |_| " decorated")
));

cur_box = if let Some(decoration) = decoration {
output!(format!("<div><p>{}</p></div>\n<div>\n", decoration));
Some(BoxType::Decorated)
} else {
Some(BoxType::Plain)
};

let title = &line[box_type.len()..].trim_start();
if !title.is_empty() {
output!(format!("<p class=\"box-title\">{}</p>", title));
}
} else {
// This is a box ending marker
match cur_box {
None => {
return Err(Error::msg(format!(
"{}:{}: Attempting to close box outside of one",
path.display(),
line_no
)))
}
Some(BoxType::Decorated) => {
output!("</div>\n"); // Close the `box-inner
}
Some(BoxType::Plain) => (),
}
cur_box = None;

output!("</div>\n");
}

// Prevent normal output
continue;
} else {
let mut stderr = StandardStream::stderr(ColorChoice::Auto);
stderr
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))
.unwrap();
write!(&mut stderr, "warning").unwrap();
stderr.reset().unwrap();
eprintln!(" ({}): ignoring \":::{}\"; box start/end tags must be alone in their paragraph", name, line);
}
} else if let Some(match_info) = CONSOLE_CODE_RE.find(line) {
if let Some(match_info) = CONSOLE_CODE_RE.find(line) {
output!("<pre><code>"); // Disable the highlighting
in_console = true;
debug_assert_eq!(match_info.start(), 0);
Expand All @@ -248,9 +152,5 @@ fn render(path: &mut PathBuf, name: &str, index: usize) -> Result<()> {
output!("\n");
}

if cur_box.is_some() {
return Err(Error::msg(format!("{}: Unclosed box", path.display())));
}

Ok(())
}
Loading

0 comments on commit bdd40d7

Please sign in to comment.