diff --git a/htmlua-parser/Cargo.toml b/htmlua-parser/Cargo.toml index c3cbe07..1e27f09 100644 --- a/htmlua-parser/Cargo.toml +++ b/htmlua-parser/Cargo.toml @@ -8,6 +8,7 @@ anyhow = "1.0.98" kuchikiki = "0.8.2" 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 eb50c2c..396df4f 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, +}; fn create_htmlua_stdlib(l: &Lua, stdout: &Rc>) -> mlua::Result { let t = l.create_table()?; @@ -119,7 +126,9 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from item_path.push(include_path); let component_text = read_to_string(item_path)?; let new_doc = kuchikiki::parse_html().one(component_text); - let new_node = new_doc.select_first("body").map_err(|()| anyhow!("Failed to get body"))?; + let new_node = new_doc + .select_first("body") + .map_err(|()| anyhow!("Failed to get body"))?; let replaced_node = expand_template(new_node.as_node().clone(), component_path, Some(i.as_node()))?; i.as_node().insert_before(replaced_node); i.as_node().detach(); @@ -128,6 +137,49 @@ pub fn expand_template(document: NodeRef, component_path: &PathBuf, include_from Ok(document) } +pub fn process_syntax_highlighting(document: NodeRef) -> Result { + let ps = SyntaxSet::load_defaults_newlines(); + let 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")), + }; + 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#"
"#)?;
+                html_output.push_str("");
+                for line in LinesWithEndings::from(&code_text.borrow()) {
+                    let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
+                    let escaped = styled_line_to_highlighted_html(&ranges[..], IncludeBackground::No).unwrap();
+                    html_output.push_str(&escaped);
+                }
+                html_output.push_str("
"); + 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::*; @@ -237,7 +289,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] @@ -292,7 +343,7 @@ mod tests { #[test] fn basic_markdown() { - let page = r#" + let page = r" @@ -311,11 +362,41 @@ This is a paragraph with **bold text** and *italic text*. - "#; + "; 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#" + + + + Syntax Highlighting Test + + +

Code Example

+ +fn main() { + println!("Hello, world!"); + let x = 42; + let y = "test"; +} + + + "#; + 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 eb9a043..5b63e3d 100644 --- a/htmlua-parser/src/serve.rs +++ b/htmlua-parser/src/serve.rs @@ -1,4 +1,4 @@ -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 kuchikiki::traits::TendrilSink; use std::{ @@ -16,6 +16,7 @@ pub fn serve_content(request_uri: &str) -> Result { let doc = kuchikiki::parse_html().one(page_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()) }