blob: 12528a1f2e762c456a5bf82b1a975f2e7cfb497f [file] [log] [blame] [edit]
use mdbook::{
book::Book,
errors::Result,
preprocess::{Preprocessor, PreprocessorContext},
BookItem,
};
use pulldown_cmark::{
Event::{self, *},
Tag, TagEnd,
};
use pulldown_cmark_to_cmark::cmark;
/// A simple preprocessor for semantic notes in _The Rust Programming Language_.
///
/// Takes in Markdown like this:
///
/// ```markdown
/// > Note: This is a note.
/// ```
///
/// Spits out Markdown like this:
///
/// ```markdown
/// <section class="note" aria-role="note">
///
/// This is a note.
///
/// </section>
/// ```
pub struct TrplNote;
impl Preprocessor for TrplNote {
fn name(&self) -> &str {
"simple-note-preprocessor"
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
book.for_each_mut(|item| {
if let BookItem::Chapter(ref mut chapter) = item {
chapter.content = rewrite(&chapter.content);
}
});
Ok(book)
}
fn supports_renderer(&self, renderer: &str) -> bool {
renderer == "html" || renderer == "markdown" || renderer == "test"
}
}
pub fn rewrite(text: &str) -> String {
let parser = crate::parser(text);
let mut events = Vec::new();
let mut state = Default;
for event in parser {
match (&mut state, event) {
(Default, Start(Tag::BlockQuote(_))) => {
state = StartingBlockquote(vec![Start(Tag::BlockQuote(None))]);
}
(StartingBlockquote(blockquote_events), Text(content)) => {
if content.starts_with("Note: ") {
// This needs the "extra" `SoftBreak`s so that when the final rendering pass
// happens, it does not end up treating the internal content as inline *or*
// treating the HTML tags as inline tags:
//
// - Content inside HTML blocks is only rendered as Markdown when it is
// separated from the block HTML elements: otherwise it gets treated as inline
// HTML and *not* rendered.
// - Along the same lines, an HTML tag that happens to be directly adjacent to
// the end of a previous Markdown block will end up being rendered as part of
// that block.
events.extend([
SoftBreak,
SoftBreak,
Html(
r#"<section class="note" aria-role="note">"#.into(),
),
SoftBreak,
SoftBreak,
Start(Tag::Paragraph),
Text(content),
]);
state = InNote;
} else {
events.append(blockquote_events);
events.push(Text(content));
state = Default;
}
}
(
StartingBlockquote(_blockquote_events),
heading @ Start(Tag::Heading { .. }),
) => {
events.extend([
SoftBreak,
SoftBreak,
Html(r#"<section class="note" aria-role="note">"#.into()),
SoftBreak,
SoftBreak,
heading,
]);
state = InNote;
}
(StartingBlockquote(ref mut events), Start(tag)) => {
events.push(Start(tag));
}
(InNote, End(TagEnd::BlockQuote(_))) => {
// As with the start of the block HTML, the closing HTML must be
// separated from the Markdown text by two newlines.
events.extend([
SoftBreak,
SoftBreak,
Html("</section>".into()),
]);
state = Default;
}
(_, event) => {
events.push(event);
}
}
}
let mut buf = String::new();
cmark(events.into_iter(), &mut buf).unwrap();
buf
}
use State::*;
#[derive(Debug)]
enum State<'e> {
Default,
StartingBlockquote(Vec<Event<'e>>),
InNote,
}
#[cfg(test)]
mod tests;