summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Bridle <ben@derelict.engineering>2025-05-21 14:25:18 +1200
committerBen Bridle <ben@derelict.engineering>2025-05-21 19:50:09 +1200
commit060c1f23a5585a5fdabc56eda808b4cfd90f9081 (patch)
treea8bd3d2aa0e5c87528a5b7c417cf4a63edb2dbef
parentc749c1db30f2b757e63093ac39127b3e338cb704 (diff)
downloadtoaster-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.lock67
-rw-r--r--Cargo.toml1
-rw-r--r--src/collect_files.rs47
-rw-r--r--src/generate_html.rs19
4 files changed, 132 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a46e8db..ec76ce2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 6479d3f..b0faa4f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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>"),