diff options
| author | Grace Yoder <70408179+gyoder@users.noreply.github.com> | 2025-07-07 12:46:48 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-07 12:46:48 -0600 |
| commit | 23885aba3f680e0ff764c4e94306125636532234 (patch) | |
| tree | e8eedd8f47c8ed2a20a34b3017f9d992eefef403 /htmlua-parser/src | |
| parent | 6c3f257942d2b1d88aa471f52d5b0241492730ae (diff) | |
| parent | 262a2e23d89d35f78d8972f5b560c7e5ee7225ee (diff) | |
Merge pull request #8 from trypsynth/syntect
Add basic syntax highlighting with the syntect crate. Closes #5
Diffstat (limited to 'htmlua-parser/src')
| -rw-r--r-- | htmlua-parser/src/render.rs | 90 | ||||
| -rw-r--r-- | htmlua-parser/src/serve.rs | 5 |
2 files changed, 90 insertions, 5 deletions
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()) } |
