diff --git a/tools/cheddar/src/main.rs b/tools/cheddar/src/main.rs index 3eb6fc605..0912e29ec 100644 --- a/tools/cheddar/src/main.rs +++ b/tools/cheddar/src/main.rs @@ -1,6 +1,8 @@ +use comrak::arena_tree::Node; use comrak::nodes::{Ast, AstNode, NodeValue, NodeCodeBlock, NodeHtmlBlock}; use comrak::{Arena, parse_document, format_html, ComrakOptions}; use lazy_static::lazy_static; +use std::cell::RefCell; use std::env; use std::ffi::OsStr; use std::io::BufRead; @@ -122,6 +124,52 @@ fn highlight_code_block(code_block: &NodeCodeBlock) -> NodeValue { 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>) -> Option { + 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!("

", class).into_bytes(), + }) +} + fn format_markdown() { let document = { let mut buffer = String::new(); @@ -134,12 +182,32 @@ fn format_markdown() { let arena = Arena::new(); 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: "

".as_bytes().to_vec(), + }), + })); + // Syntax highlighting is implemented by traversing the arena and // replacing all code blocks with HTML blocks rendered by syntect. iter_nodes(root, &|node| { let mut ast = node.data.borrow_mut(); let new = match &ast.value { 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, };