diff options
author | Vincent Ambo <tazjin@google.com> | 2020-02-09T12·07+0000 |
---|---|---|
committer | Vincent Ambo <tazjin@google.com> | 2020-02-09T12·07+0000 |
commit | 14b5447aac62c706fdef88055a849c51514c4482 (patch) | |
tree | 4252d99b9047dbf05cd33700965ce389dc74cba2 | |
parent | c3d930aa7f19456ac2d6bae59a252c963c8a97fe (diff) | |
parent | 2236d43ff7b496616c0072b0f02173e7022d81fa (diff) |
Merge branch 'feat/cheddar-extensions' r/511
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | tools/cheddar/src/main.rs | 150 |
2 files changed, 120 insertions, 36 deletions
diff --git a/README.md b/README.md index e4af99dee6f1..ab0c1ece54dd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ Twitter][]. challenges, before I ran out of interest * `tools/blog_cli` contains my tool for writing new blog posts and storing them in the DNS zone +* `tools/cheddar` contains a source code and Markdown rendering tool + that is integrated with my cgit instance to render files in various + views * `ops/kms_pass.nix` is a tiny tool that emulates the user-interface of `pass`, but actually uses Google Cloud KMS for secret decryption * `ops/kontemplate` contains my Kubernetes resource templating tool (with which @@ -34,6 +37,9 @@ Twitter][]. * `nix/buildGo` implements a Nix library that can build Go software in the style of Bazel's `rules_go`. Go programs in this repository are built using this library. +* `nix/buildLisp` implements a Nix library that can build Common Lisp + software. Currently only SBCL is supported. Lisp programs in this + repository are built using this library. * `tools/emacs-pkgs` contains various Emacs libraries that my Emacs setup uses, for example: * `dottime.el` provides [dottime][] in the Emacs modeline diff --git a/tools/cheddar/src/main.rs b/tools/cheddar/src/main.rs index 8968e4382310..52e518cd8201 100644 --- a/tools/cheddar/src/main.rs +++ b/tools/cheddar/src/main.rs @@ -1,6 +1,8 @@ -use comrak::nodes::{AstNode, NodeValue, NodeHtmlBlock}; +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; @@ -104,9 +106,94 @@ 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("</pre>"); + buf + }; + + let block = NodeHtmlBlock { + block_type: 1, // It's unclear what behaviour is toggled by this + literal: rendered.into_bytes(), + }; + + 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() { let document = { let mut buffer = String::new(); @@ -119,47 +206,38 @@ 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: "</p>".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(); - 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("</pre>"); - 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)), + NodeValue::Paragraph => if let Some(callout) = has_callout(node) { + node.insert_after(&p_close); + Some(format_callout_paragraph(callout)) + } else { + None }, - _ => (), + _ => None, }; + + if let Some(new_value) = new { + ast.value = new_value + } }); format_html(root, &MD_OPTS, &mut io::stdout()) |