rs_web/lua/
highlight.rs

1//! Syntax highlighting module (rs.highlight)
2//!
3//! Provides server-side syntax highlighting using syntect.
4//! Outputs HTML with CSS class names for styling.
5
6use crate::lua::async_io::{AsyncIOTask, runtime};
7use mlua::{Lua, Result, Table};
8use once_cell::sync::Lazy;
9use syntect::highlighting::ThemeSet;
10use syntect::html::{ClassStyle, ClassedHTMLGenerator};
11use syntect::parsing::SyntaxSet;
12
13// Load syntax definitions and themes once
14static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
15static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
16
17/// Language name aliases (common names -> syntect names)
18fn get_syntax_name(lang: &str) -> &str {
19    match lang.to_lowercase().as_str() {
20        "py" | "python" | "python3" => "Python",
21        "rs" | "rust" => "Rust",
22        "js" | "javascript" => "JavaScript",
23        "ts" | "typescript" => "TypeScript",
24        "c" => "C",
25        "cpp" | "c++" | "cxx" => "C++",
26        "h" | "hpp" => "C++",
27        "hs" | "haskell" => "Haskell",
28        "rb" | "ruby" => "Ruby",
29        "go" | "golang" => "Go",
30        "java" => "Java",
31        "sh" | "bash" | "shell" => "Bourne Again Shell (bash)",
32        "zsh" => "Bourne Again Shell (bash)",
33        "json" => "JSON",
34        "yaml" | "yml" => "YAML",
35        "toml" => "TOML",
36        "xml" => "XML",
37        "html" | "htm" => "HTML",
38        "css" => "CSS",
39        "sql" => "SQL",
40        "md" | "markdown" => "Markdown",
41        "lua" => "Lua",
42        "vim" => "VimL",
43        "dockerfile" => "Dockerfile",
44        "make" | "makefile" => "Makefile",
45        "tex" | "latex" => "LaTeX",
46        "r" => "R",
47        "scala" => "Scala",
48        "kotlin" | "kt" => "Kotlin",
49        "swift" => "Swift",
50        "objc" | "objective-c" => "Objective-C",
51        "php" => "PHP",
52        "perl" | "pl" => "Perl",
53        "elixir" | "ex" => "Elixir",
54        "erlang" | "erl" => "Erlang",
55        "clojure" | "clj" => "Clojure",
56        "lisp" | "el" => "Lisp",
57        "scheme" | "scm" => "Lisp",
58        "ocaml" | "ml" => "OCaml",
59        "fsharp" | "fs" => "F#",
60        "csharp" | "cs" => "C#",
61        "diff" | "patch" => "Diff",
62        "ini" | "conf" => "INI",
63        "nginx" => "nginx",
64        "asm" | "assembly" => "Assembly (x86_64)",
65        _ => lang,
66    }
67}
68
69/// Highlight code and return HTML with CSS classes (sync version)
70/// This is also used by the Tera filter
71pub fn highlight_code_sync(code: &str, language: &str) -> String {
72    let syntax_name = get_syntax_name(language);
73
74    // Try to find syntax by name
75    let syntax = SYNTAX_SET
76        .find_syntax_by_name(syntax_name)
77        .or_else(|| SYNTAX_SET.find_syntax_by_extension(language))
78        .or_else(|| SYNTAX_SET.find_syntax_by_token(language))
79        .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
80
81    let mut html_generator =
82        ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAX_SET, ClassStyle::Spaced);
83
84    for line in syntect::util::LinesWithEndings::from(code) {
85        // Ignore errors for invalid UTF-8 or parsing issues
86        let _ = html_generator.parse_html_for_line_which_includes_newline(line);
87    }
88
89    html_generator.finalize()
90}
91
92/// Get list of available syntax names
93fn list_syntaxes() -> Vec<String> {
94    SYNTAX_SET
95        .syntaxes()
96        .iter()
97        .map(|s| s.name.clone())
98        .collect()
99}
100
101/// Get list of available theme names
102fn list_themes() -> Vec<String> {
103    THEME_SET.themes.keys().cloned().collect()
104}
105
106/// Generate CSS for a specific theme
107fn generate_theme_css(theme_name: &str) -> Option<String> {
108    let theme = THEME_SET.themes.get(theme_name)?;
109    syntect::html::css_for_theme_with_class_style(theme, ClassStyle::Spaced).ok()
110}
111
112/// Create the highlight module
113pub fn create_module(lua: &Lua) -> Result<Table> {
114    let module = lua.create_table()?;
115
116    // rs.highlight.highlight(code, language) -> AsyncIOTask that resolves to html
117    let highlight_fn = lua.create_function(|_, (code, language): (String, String)| {
118        let handle = runtime().spawn(async move {
119            // Use spawn_blocking for CPU-intensive work
120            let result = tokio::task::spawn_blocking(move || highlight_code_sync(&code, &language))
121                .await
122                .map_err(|e| format!("Highlight task panicked: {}", e))?;
123            Ok(result)
124        });
125
126        Ok(AsyncIOTask::from_string_handle(handle))
127    })?;
128    module.set("highlight", highlight_fn)?;
129
130    // Alias: rs.highlight.code(code, language) -> AsyncIOTask
131    module.set("code", module.get::<mlua::Function>("highlight")?)?;
132
133    // rs.highlight.highlight_sync(code, language) -> html (blocking version)
134    let highlight_sync_fn = lua.create_function(|_, (code, language): (String, String)| {
135        Ok(highlight_code_sync(&code, &language))
136    })?;
137    module.set("highlight_sync", highlight_sync_fn)?;
138
139    // rs.highlight.syntaxes() -> list of available syntax names
140    let syntaxes_fn = lua.create_function(|_, ()| Ok(list_syntaxes()))?;
141    module.set("syntaxes", syntaxes_fn)?;
142
143    // rs.highlight.themes() -> list of available theme names
144    let themes_fn = lua.create_function(|_, ()| Ok(list_themes()))?;
145    module.set("themes", themes_fn)?;
146
147    // rs.highlight.theme_css(theme_name) -> CSS string for the theme
148    let theme_css_fn =
149        lua.create_function(|_, theme_name: String| Ok(generate_theme_css(&theme_name)))?;
150    module.set("theme_css", theme_css_fn)?;
151
152    Ok(module)
153}