about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--tools/cheddar/src/main.rs150
2 files changed, 120 insertions, 36 deletions
diff --git a/README.md b/README.md
index e4af99dee6..ab0c1ece54 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 8968e43823..52e518cd82 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(
+                &regions[..], 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(
-                            &regions[..], 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())