feat(cheddar): Implement callout paragraphs

Implements support for tagging paragraphs that begin with a callout
word (TODO, WARNING, QUESTION, TIP) with an additional `cheddar-*`
class that makes it possible to render these callouts specially.

This is currently not the nicest implementation, but it works.
This commit is contained in:
Vincent Ambo 2020-01-11 05:06:36 +00:00
parent d4e469508f
commit 2236d43ff7

View file

@ -1,6 +1,8 @@
use comrak::arena_tree::Node;
use comrak::nodes::{Ast, AstNode, NodeValue, NodeCodeBlock, NodeHtmlBlock}; use comrak::nodes::{Ast, AstNode, NodeValue, NodeCodeBlock, NodeHtmlBlock};
use comrak::{Arena, parse_document, format_html, ComrakOptions}; use comrak::{Arena, parse_document, format_html, ComrakOptions};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::cell::RefCell;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::BufRead; use std::io::BufRead;
@ -122,6 +124,52 @@ fn highlight_code_block(code_block: &NodeCodeBlock) -> NodeValue {
NodeValue::HtmlBlock(block) NodeValue::HtmlBlock(block)
} }
// Supported callout elements (which each have their own distinct rendering):
enum Callout {
Todo,
Warning,
Question,
Tip,
}
// Determine whether the first child of the supplied node contains a text that
// should cause a callout section to be rendered.
fn has_callout<'a>(node: &Node<'a, RefCell<Ast>>) -> Option<Callout> {
match node.first_child().map(|c| c.data.borrow()) {
Some(child) => match &child.value {
NodeValue::Text(text) => {
if text.starts_with("TODO".as_bytes()) {
return Some(Callout::Todo)
} else if text.starts_with("WARNING".as_bytes()) {
return Some(Callout::Warning)
} else if text.starts_with("QUESTION".as_bytes()) {
return Some(Callout::Question)
} else if text.starts_with("TIP".as_bytes()) {
return Some(Callout::Tip)
}
return None
},
_ => return None,
},
_ => return None,
}
}
fn format_callout_paragraph(callout: Callout) -> NodeValue {
let class = match callout {
Callout::Todo => "cheddar-todo",
Callout::Warning => "cheddar-warning",
Callout::Question => "cheddar-question",
Callout::Tip => "cheddar-tip",
};
NodeValue::HtmlBlock(NodeHtmlBlock {
block_type: 1,
literal: format!("<p class=\"cheddar-callout {}\">", class).into_bytes(),
})
}
fn format_markdown() { fn format_markdown() {
let document = { let document = {
let mut buffer = String::new(); let mut buffer = String::new();
@ -134,12 +182,32 @@ fn format_markdown() {
let arena = Arena::new(); let arena = Arena::new();
let root = parse_document(&arena, &document, &MD_OPTS); let root = parse_document(&arena, &document, &MD_OPTS);
// This node must exist with a lifetime greater than that of the parsed AST
// in case that callouts are encountered (otherwise insertion into the tree
// is not possible).
let p_close = Node::new(RefCell::new(Ast {
start_line: 0, // TODO(tazjin): hrmm
content: vec![],
open: false,
last_line_blank: false,
value: NodeValue::HtmlBlock(NodeHtmlBlock {
block_type: 1,
literal: "</p>".as_bytes().to_vec(),
}),
}));
// Syntax highlighting is implemented by traversing the arena and // Syntax highlighting is implemented by traversing the arena and
// replacing all code blocks with HTML blocks rendered by syntect. // replacing all code blocks with HTML blocks rendered by syntect.
iter_nodes(root, &|node| { iter_nodes(root, &|node| {
let mut ast = node.data.borrow_mut(); let mut ast = node.data.borrow_mut();
let new = match &ast.value { let new = match &ast.value {
NodeValue::CodeBlock(code) => Some(highlight_code_block(code)), NodeValue::CodeBlock(code) => Some(highlight_code_block(code)),
NodeValue::Paragraph => if let Some(callout) = has_callout(node) {
node.insert_after(&p_close);
Some(format_callout_paragraph(callout))
} else {
None
},
_ => None, _ => None,
}; };