From d4e469508f9bb9fdd8c349c53ab4fd0ef5f3e87e Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Jan 2020 02:17:19 +0000 Subject: refactor(cheddar): Extract code block highlighting into function Since I am going down the path of adding additional Markdown extensions it makes sense to avoid letting `format_markdown` turn into a giant beast of a function. Therefore this commit extracts the logic for rendering code blocks via syntect and changes the innards of `format_markdown` to instead provide arbitrary AST value replacements. --- tools/cheddar/src/main.rs | 84 ++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 37 deletions(-) (limited to 'tools') diff --git a/tools/cheddar/src/main.rs b/tools/cheddar/src/main.rs index d0115d391c..3eb6fc605e 100644 --- a/tools/cheddar/src/main.rs +++ b/tools/cheddar/src/main.rs @@ -1,4 +1,4 @@ -use comrak::nodes::{AstNode, NodeValue, NodeHtmlBlock}; +use comrak::nodes::{Ast, AstNode, NodeValue, NodeCodeBlock, NodeHtmlBlock}; use comrak::{Arena, parse_document, format_html, ComrakOptions}; use lazy_static::lazy_static; use std::env; @@ -80,9 +80,48 @@ fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F) where F : Fn(&'a AstNode<'a>) // Instead, try finding a syntax match by comparing case insensitively (for // ASCII characters, anyways). fn find_syntax_case_insensitive(info: &str) -> Option<&'static SyntaxReference> { + // TODO(tazjin): memoize this lookup SYNTAXES.syntaxes().iter().rev().find(|&s| info.eq_ignore_ascii_case(&s.name)) } +// Replaces code-block inside of a Markdown AST with HTML blocks rendered by +// syntect. This enables static (i.e. no JavaScript) syntax highlighting, even +// of complex languages. +fn highlight_code_block(code_block: &NodeCodeBlock) -> NodeValue { + let theme = &THEMES.themes["InspiredGitHub"]; + let info = String::from_utf8_lossy(&code_block.info); + + let syntax = find_syntax_case_insensitive(&info) + .or_else(|| SYNTAXES.find_syntax_by_extension(&info)) + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); + + let code = String::from_utf8_lossy(&code_block.literal); + + let rendered = { + // Write the block preamble manually to get exactly the + // desired layout: + let mut hl = HighlightLines::new(syntax, theme); + let mut buf = BLOCK_PRE.to_string(); + + for line in LinesWithEndings::from(&code) { + let regions = hl.highlight(line, &SYNTAXES); + append_highlighted_html_for_styled_line( + ®ions[..], IncludeBackground::No, &mut buf, + ); + } + + buf.push_str(""); + buf + }; + + let block = NodeHtmlBlock { + block_type: 1, // It's unclear what behaviour is toggled by this + literal: rendered.into_bytes(), + }; + + NodeValue::HtmlBlock(block) +} + fn format_markdown() { let document = { let mut buffer = String::new(); @@ -99,43 +138,14 @@ fn format_markdown() { // replacing all code blocks with HTML blocks rendered by syntect. iter_nodes(root, &|node| { let mut ast = node.data.borrow_mut(); - match &ast.value { - NodeValue::CodeBlock(code_block) => { - let theme = &THEMES.themes["InspiredGitHub"]; - let info = String::from_utf8_lossy(&code_block.info); - - let syntax = find_syntax_case_insensitive(&info) - .or_else(|| SYNTAXES.find_syntax_by_extension(&info)) - .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); - - let code = String::from_utf8_lossy(&code_block.literal); - - let rendered = { - // Write the block preamble manually to get exactly the - // desired layout: - let mut hl = HighlightLines::new(syntax, theme); - let mut buf = BLOCK_PRE.to_string(); - - for line in LinesWithEndings::from(&code) { - let regions = hl.highlight(line, &SYNTAXES); - append_highlighted_html_for_styled_line( - ®ions[..], IncludeBackground::No, &mut buf, - ); - } - - buf.push_str(""); - buf - }; - - let block = NodeHtmlBlock { - block_type: 1, // It's unclear what behaviour is toggled by this - literal: rendered.into_bytes(), - }; - - ast.value = NodeValue::HtmlBlock(block); - }, - _ => (), + let new = match &ast.value { + NodeValue::CodeBlock(code) => Some(highlight_code_block(code)), + _ => None, }; + + if let Some(new_value) = new { + ast.value = new_value + } }); format_html(root, &MD_OPTS, &mut io::stdout()) -- cgit 1.4.1 From 2236d43ff7b496616c0072b0f02173e7022d81fa Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Jan 2020 05:06:36 +0000 Subject: 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. --- tools/cheddar/src/main.rs | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) (limited to 'tools') diff --git a/tools/cheddar/src/main.rs b/tools/cheddar/src/main.rs index 3eb6fc605e..0912e29ec0 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, }; -- cgit 1.4.1