aboutsummaryrefslogtreecommitdiff
path: root/htmlua-parser
diff options
context:
space:
mode:
authorGrace Yoder <70408179+gyoder@users.noreply.github.com>2025-07-07 12:46:48 -0600
committerGitHub <noreply@github.com>2025-07-07 12:46:48 -0600
commit23885aba3f680e0ff764c4e94306125636532234 (patch)
treee8eedd8f47c8ed2a20a34b3017f9d992eefef403 /htmlua-parser
parent6c3f257942d2b1d88aa471f52d5b0241492730ae (diff)
parent262a2e23d89d35f78d8972f5b560c7e5ee7225ee (diff)
Merge pull request #8 from trypsynth/syntect
Add basic syntax highlighting with the syntect crate. Closes #5
Diffstat (limited to 'htmlua-parser')
-rw-r--r--htmlua-parser/Cargo.toml1
-rw-r--r--htmlua-parser/src/render.rs90
-rw-r--r--htmlua-parser/src/serve.rs5
3 files changed, 91 insertions, 5 deletions
diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml
index 436844d..28416b7 100644
--- a/htmlua-parser/Cargo.toml
+++ b/htmlua-parser/Cargo.toml
@@ -10,6 +10,7 @@ kuchikiki = "0.8.2"
markup5ever = "0.11.0"
mlua = { version = "0.10.5", features = ["lua54", "vendored", "serialize"] }
pulldown-cmark = "0.13.0"
+syntect = "5.2.0"
tendril = "0.4.3"
diff --git a/htmlua-parser/src/render.rs b/htmlua-parser/src/render.rs
index 927eb35..35c37f4 100644
--- a/htmlua-parser/src/render.rs
+++ b/htmlua-parser/src/render.rs
@@ -4,6 +4,13 @@ use anyhow::{Result, anyhow};
use kuchikiki::{NodeRef, traits::TendrilSink};
use mlua::{Lua, Table};
use pulldown_cmark::{Options, Parser, html};
+use syntect::{
+ easy::HighlightLines,
+ highlighting::{Style, ThemeSet},
+ html::{IncludeBackground, styled_line_to_highlighted_html},
+ parsing::SyntaxSet,
+ util::LinesWithEndings,
+};
use crate::helpers::read_doc_from_file;
@@ -129,6 +136,54 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from
Ok(document)
}
+pub fn process_syntax_highlighting(document: NodeRef) -> Result<NodeRef> {
+ let ps = SyntaxSet::load_defaults_newlines();
+ let mut ts = ThemeSet::load_defaults();
+ let syntax_elements: Vec<_> = match document.select("syntaxhighlight") {
+ Ok(e) => e.collect(),
+ Err(()) => return Err(anyhow!("Unable to find syntaxhighlight elements")),
+ };
+ if !syntax_elements.is_empty() {
+ // TODO: read from config
+ let themes = PathBuf::from("/var/www/htmlua/themes");
+ ts.add_from_folder(themes)?;
+ }
+ for node in syntax_elements {
+ let attrs = match node.as_node().as_element() {
+ Some(e) => e.attributes.borrow(),
+ None => continue,
+ };
+ let language = attrs.get("lang").unwrap_or("text");
+ let theme_name = attrs.get("theme").unwrap_or("base16-ocean.dark");
+ if let Some(text_node) = node.as_node().first_child() {
+ if let Some(code_text) = text_node.as_text() {
+ let syntax = ps
+ .find_syntax_by_extension(language)
+ .or_else(|| ps.find_syntax_by_name(language))
+ .unwrap_or_else(|| ps.find_syntax_plain_text());
+ let theme = &ts.themes[theme_name];
+ let mut h = HighlightLines::new(syntax, theme);
+ let mut html_output = String::new();
+ write!(html_output, r#"<pre class="syntax-highlight" data-lang="{language}">"#)?;
+ html_output.push_str("<code>");
+ for line in LinesWithEndings::from(&code_text.borrow()) {
+ let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps)?;
+ let escaped = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No)?;
+ html_output.push_str(&escaped);
+ }
+ html_output.push_str("</code></pre>");
+ let html_fragment = kuchikiki::parse_html().one(html_output);
+ for child in html_fragment.children() {
+ node.as_node().insert_before(child);
+ }
+ // Remove the original syntaxhighlight node.
+ node.as_node().detach();
+ }
+ }
+ }
+ Ok(document)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -238,7 +293,6 @@ mod tests {
let text = d.select_first("#inc1").unwrap().as_node().text_contents();
assert_eq!(text, "included1");
assert!(d.select_first("include").is_err());
-
}
#[test]
@@ -293,7 +347,7 @@ mod tests {
#[test]
fn basic_markdown() {
- let page = r#"
+ let page = r"
<!DOCTYPE html>
<html>
<head>
@@ -312,11 +366,41 @@ This is a paragraph with **bold text** and *italic text*.
</markdown>
</div>
</body>
- </html>"#;
+ </html>";
let document = kuchikiki::parse_html().one(page);
let d = process_markdown(document).unwrap();
assert!(d.select_first("markdown").is_err());
assert!(d.select_first("p").is_ok());
assert!(d.select_first("ul").is_ok());
}
+
+ #[test]
+ fn basic_syntax_highlighting() {
+ let page = r#"
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Syntax Highlighting Test</title>
+ </head>
+ <body>
+ <h1>Code Example</h1>
+ <syntaxhighlight lang="rust">
+fn main() {
+ println!("Hello, world!");
+ let x = 42;
+ let y = "test";
+}
+ </syntaxhighlight>
+ </body>
+ </html>"#;
+ let document = kuchikiki::parse_html().one(page);
+ let d = process_syntax_highlighting(document).unwrap();
+ assert!(d.select_first("syntaxhighlight").is_err());
+ assert!(d.select_first("pre").is_ok());
+ assert!(d.select_first("code").is_ok());
+ let pre = d.select_first("pre").unwrap();
+ let attrs = pre.attributes.borrow();
+ assert_eq!(attrs.get("data-lang"), Some("rust"));
+ assert!(attrs.get("class").unwrap().contains("syntax-highlight"));
+ }
}
diff --git a/htmlua-parser/src/serve.rs b/htmlua-parser/src/serve.rs
index 981cdf6..06de0ea 100644
--- a/htmlua-parser/src/serve.rs
+++ b/htmlua-parser/src/serve.rs
@@ -1,6 +1,6 @@
use crate::helpers::read_doc_from_file;
-use super::render::{execute_lua, expand_template, process_markdown};
+use super::render::{execute_lua, expand_template, process_markdown, process_syntax_highlighting};
use anyhow::Result;
use std::path::{Path, PathBuf}
;
@@ -16,6 +16,7 @@ pub fn serve_content(request_uri: &str) -> Result<String> {
let full_doc = expand_template(doc, &components, None)?;
let markdown_doc = process_markdown(full_doc)?;
- let executed_doc = execute_lua(markdown_doc)?;
+ let highlighted_doc = process_syntax_highlighting(markdown_doc)?;
+ let executed_doc = execute_lua(highlighted_doc)?;
Ok(executed_doc.to_string())
}