Skip to content

Commit

Permalink
Move admonition markup processing to preproc instead of renderer
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ISSOtm committed Dec 1, 2023
1 parent e47f46a commit 6884a88
Show file tree
Hide file tree
Showing 24 changed files with 171 additions and 156 deletions.
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
5 changes: 4 additions & 1 deletion 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,6 +47,9 @@ impl Preprocessor for GbAsmTut {
.expect("All book items have a parent");

ch.content = links::replace_all(&ch.content, base);
if let Err(err) = self.process_admonitions(ch) {
res = Err(err);
}
}
}
});
Expand Down
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(())
}
2 changes: 1 addition & 1 deletion src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
👋 Welcome to gb-asm-tutorial!
This tutorial will teach you how to make games for the Game Boy and Game Boy Color.

::: warning:⚠️
:::warning:⚠️

While the Game Boy and Game Boy Color are almost the same console, **the Game Boy Advance is entirely different**.
However, the GBA is able to run GB and GBC games!
Expand Down
6 changes: 3 additions & 3 deletions src/part1/assembly.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ Instruction | Mnemonic | Effect
------------|----------|----------------------
Load | `ld` | Copies values around

::: tip:ℹ️
:::tip:ℹ️

Due to CPU limitations, not all operand combinations are valid for `ld` and many other instructions; we will talk about this when writing our own code later.

:::

::: tip:🤔
:::tip:🤔

RGBDS has an [instruction reference](https://rgbds.gbdev.io/docs/gbz80.7) worth bookmarking, and you can also consult it locally with `man 7 gbz80` if RGBDS is installed on your machine (except Windows...).
The descriptions there are more succinct, since they're intended as reminders, not as tutorials.
Expand Down Expand Up @@ -153,7 +153,7 @@ It is worth mentioning that this first argument here is an *expression*.
RGBDS (thankfully!) supports arbitrary expressions essentially anywhere.
This expression is a simple subtraction: $150 minus `@`, which is a special symbol that stands for "the current memory address".

::: tip
:::tip

A symbol is essentially "a name attached to a value", usually a number.
We will explore the different types of symbols throughout the tutorial, starting with labels in the next section.
Expand Down
4 changes: 2 additions & 2 deletions src/part1/bin_and_hex.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Here's how digits work:
And here we can see the digits that make up the number!
```

::: tip:ℹ️
:::tip:ℹ️

`^` here means "to the power of", where `X^N` is equal to multiplying `X` with itself `N` times, and `X ^ 0 = 1`.

Expand Down Expand Up @@ -102,7 +102,7 @@ And, don't worry, decimal can still be used 😜

(Side note: one could point that octal, i.e. base 8, would also work for this; however, we will primarily deal with units of 8 bits, for which hexadecimal works much better than octal. RGBDS supports octal via the `&` prefix, but I have yet to see it used.)

::: tip:💡
:::tip:💡

If you're having trouble converting between decimal and binary/hexadecimal, check if your favorite calculator program doesn't have a "programmer" mode, or a way to convert between bases.

Expand Down
8 changes: 4 additions & 4 deletions src/part1/header.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ It's the region of memory from $0104 to $014F (inclusive).
It contains metadata about the ROM, such as its title, Game Boy Color compatibility, size,
two checksums, and interestingly, the Nintendo logo that is displayed during the power-on animation.

::: tip
:::tip

You can find this information and more [in the Pan Docs](https://gbdev.io/pandocs/The_Cartridge_Header).

Expand All @@ -42,7 +42,7 @@ However, at that time, a small program called the *boot ROM*, burned within the
The boot ROM is responsible for the startup animation, but it also checks the ROM's header!
Specifically, it verifies that the Nintendo logo and header checksums are correct; if either check fails, the boot ROM intentionally *locks up*, and our game never gets to run :(

::: tip For the curious
:::tip For the curious

You can find a more detailed description of what the boot ROM does [in the Pan Docs](https://gbdev.io/pandocs/Power_Up_Sequence), as well as an explanation of the logo check.
Beware that it is quite advanced, though.
Expand Down Expand Up @@ -85,7 +85,7 @@ $ rgblink -o hello-world.gb -n hello-world.sym hello-world.o

(I am intentionally not running RGBFIX; we will see why in a minute.)

::: danger
:::danger

Make sure the boot ROMs are not enabled for this!
If they are, make sure to disable them (untick their box in the options, click `OK` or `Apply`, and reset the emulator).
Expand Down Expand Up @@ -132,7 +132,7 @@ But why is `EntryPoint` there?
Well, as you may have figured out from the warnings RGBFIX printed, it *overwrites* the header area in the ROM.
However, RGBLINK is **not** aware of the header (because RGBLINK is not only used to generate ROMs!), so you must explicitly reserve space for the header area.

::: danger:🥴
:::danger:🥴

Forgetting to reserve this space, and having a piece of code or data ending up there then overwritten, is a common beginner mistake that can be quite puzzling.
Fortunately, RGBFIX since version 0.5.1 warns when it detects this mistake, as shown above.
Expand Down
Loading

0 comments on commit 6884a88

Please sign in to comment.