diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-05-21 14:25:18 +1200 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-05-21 19:50:09 +1200 |
commit | 060c1f23a5585a5fdabc56eda808b4cfd90f9081 (patch) | |
tree | a8bd3d2aa0e5c87528a5b7c417cf4a63edb2dbef | |
parent | c749c1db30f2b757e63093ac39127b3e338cb704 (diff) | |
download | toaster-060c1f23a5585a5fdabc56eda808b4cfd90f9081.zip |
Implement syntax highlighting for syntax fragments
A new key 'highlighters' has been added to the toaster.conf file. The
value should be a line defining the languages to use that syntax for,
like [py/python]. The lines following are the template definitions, as
per the highlighter library.
-rw-r--r-- | Cargo.lock | 67 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/collect_files.rs | 47 | ||||
-rw-r--r-- | src/generate_html.rs | 19 |
4 files changed, 132 insertions, 2 deletions
@@ -3,6 +3,49 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "highlight" +version = "1.0.0" +source = "git+git://benbridle.com/highlight?tag=v1.0.0#0e9fe32e7439a597a49f9c36c56a26726100b434" +dependencies = [ + "fancy-regex", +] + +[[package]] name = "inked" version = "1.0.0" source = "git+git://benbridle.com/inked?tag=v1.0.0#2954d37b638fa2c1dd3d51ff53f08f475aea6ea3" @@ -29,6 +72,12 @@ version = "3.3.0" source = "git+git://benbridle.com/markdown?tag=v3.3.0#df45ffb3affb7cb1d53b567b70fef721353ccffe" [[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -40,6 +89,23 @@ version = "1.4.0" source = "git+git://benbridle.com/recipe?tag=v1.4.0#652aaee3130e2ee02742fdcc248ddd1bee285737" [[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] name = "switchboard" version = "1.0.0" source = "git+git://benbridle.com/switchboard?tag=v1.0.0#ea70fa89659e5cf1a9d4ca6ea31fb67f7a2cc633" @@ -61,6 +127,7 @@ dependencies = [ name = "toaster" version = "1.11.0" dependencies = [ + "highlight", "log 2.0.0", "markdown", "recipe", @@ -9,6 +9,7 @@ markdown = { git = "git://benbridle.com/markdown", tag = "v3.3.0" } recipe = { git = "git://benbridle.com/recipe", tag = "v1.4.0" } log = { git = "git://benbridle.com/log", tag = "v2.0.0" } switchboard = { git = "git://benbridle.com/switchboard", tag = "v1.0.0" } +highlight = { git = "git://benbridle.com/highlight", tag = "v1.0.0" } [profile.release] lto=true diff --git a/src/collect_files.rs b/src/collect_files.rs index 7a3c464..18d75f1 100644 --- a/src/collect_files.rs +++ b/src/collect_files.rs @@ -1,5 +1,6 @@ use crate::*; +use highlight::*; use vagabond::*; use std::collections::HashMap; @@ -8,6 +9,7 @@ use std::collections::HashMap; pub struct Website { pub name: String, pub config: HashMap<String, String>, + pub highlighters: Highlighters, pub pages: Vec<Page>, pub redirects: Vec<Redirect>, pub static_files: Vec<StaticItem>, // Redirects, !-prefixed-dir contents @@ -60,6 +62,10 @@ pub trait LinkFrom { } } +pub struct Highlighters { + pub languages: HashMap<String, usize>, + pub highlighters: Vec<Highlighter>, +} impl Page { pub fn root(&self) -> String { @@ -96,8 +102,13 @@ impl Website { Err(err) => fatal!("Couldn't open {:?}: {:?}", &path, err), }, config: HashMap::new(), + highlighters: Highlighters { + languages: HashMap::new(), + highlighters: Vec::new(), + }, }; new.collect_entry(path, path); + new.parse_highlighters(); return new; } @@ -149,7 +160,7 @@ impl Website { let mut key = None; let mut value = String::new(); for line in config.lines() { - if line.starts_with(" ") { + if line.starts_with(" ") || line.trim().is_empty() { value.push_str(line.trim()); value.push('\n'); } else { @@ -268,6 +279,40 @@ impl Website { } } + pub fn parse_highlighters(&mut self) { + let mut languages = Vec::new(); + let mut source = String::new(); + for line in self.get_config("highlighters").lines() { + if let Some(line) = line.trim().strip_prefix('[') { + if let Some(line) = line.strip_suffix(']') { + // Bank the current source. + if !languages.is_empty() { + let i = self.highlighters.highlighters.len(); + for language in languages { + self.highlighters.languages.insert(language, i); + } + let highlighter = Highlighter::from_str(&source); + self.highlighters.highlighters.push(highlighter); + } + languages = line.split('/').map(|s| s.trim().to_string()).collect(); + source.clear(); + continue; + } + } + source.push_str(line); + source.push('\n'); + } + // Bank the current source. + if !languages.is_empty() { + let i = self.highlighters.highlighters.len(); + for language in languages { + self.highlighters.languages.insert(language, i); + } + let highlighter = Highlighter::from_str(&source); + self.highlighters.highlighters.push(highlighter); + } + } + // Ext is extension without a dot. // Checks if a relative link to an internal page name can be reached from // the current page, and returns a resolved absolute link to the page with extension. diff --git a/src/generate_html.rs b/src/generate_html.rs index 0de42a5..5abddf2 100644 --- a/src/generate_html.rs +++ b/src/generate_html.rs @@ -236,7 +236,24 @@ pub fn document_to_html(document: &MarkdownDocument, page: &Page, website: &Webs warn!("Gallery-nav on page {from:?} has line without a '::' separator"); } }), - _ => wrap!("pre", format!("class='{language}'"), html!("{}", sanitize_text(content, false))), + _ => wrap!("pre", format!("class='{language}'"), { + if let Some(i) = website.highlighters.languages.get(language) { + let mut source = String::new(); + let highlighter = &website.highlighters.highlighters[*i]; + for span in highlighter.highlight(content) { + if span.tag.is_empty() { + source.push_str(&sanitize_text(&span.text, false)); + } else { + source.push_str(&format!("<span class='{}'>", span.tag.to_lowercase())); + source.push_str(&sanitize_text(&span.text, false)); + source.push_str("</span>"); + } + } + html!("{source}"); + } else { + html!("{}", sanitize_text(content, false)) + } + }) } } Block::Break => html!("<hr>"), |