feat: add syntax highlighting for 9 additional languages

- Cargo.toml: Add tree-sitter grammars for Nix, Python, JavaScript,
  TypeScript, Go, C, CSS, HTML, YAML. Upgrade tree-sitter-highlight
  to 0.26 for language version 15 compatibility.

- src/highlight.rs: Add Language enum variants and get_config()
  match arms for all new languages. Update render() callback for
  0.26 API (writes attributes to buffer). Add tests for Nix and
  Python highlighting.

TOML excluded due to incompatible API (tree-sitter 0.20 vs 0.26).
This commit is contained in:
Timothy DeHerrera
2026-01-25 17:20:00 -07:00
parent a73359098e
commit acb0ff3e15
3 changed files with 234 additions and 45 deletions

View File

@@ -56,18 +56,36 @@ const HTML_ATTRS: &[&[u8]] = &[
/// Supported languages for syntax highlighting.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Language {
Rust,
Bash,
C,
Css,
Go,
Html,
JavaScript,
Json,
Nix,
Python,
Rust,
TypeScript,
Yaml,
}
impl Language {
/// Parse a language identifier from a code fence.
pub fn from_fence(lang: &str) -> Option<Self> {
match lang.to_lowercase().as_str() {
"rust" | "rs" => Some(Language::Rust),
"bash" | "sh" | "shell" | "zsh" => Some(Language::Bash),
"c" => Some(Language::C),
"css" => Some(Language::Css),
"go" | "golang" => Some(Language::Go),
"html" => Some(Language::Html),
"javascript" | "js" => Some(Language::JavaScript),
"json" => Some(Language::Json),
"nix" => Some(Language::Nix),
"python" | "py" => Some(Language::Python),
"rust" | "rs" => Some(Language::Rust),
"typescript" | "ts" | "tsx" => Some(Language::TypeScript),
"yaml" | "yml" => Some(Language::Yaml),
_ => None,
}
}
@@ -76,21 +94,66 @@ impl Language {
/// Get highlight configuration for a language.
fn get_config(lang: Language) -> HighlightConfiguration {
let (language, name, highlights) = match lang {
Language::Rust => (
tree_sitter_rust::LANGUAGE.into(),
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
),
Language::Bash => (
tree_sitter_bash::LANGUAGE.into(),
"bash",
tree_sitter_bash::HIGHLIGHT_QUERY,
),
Language::C => (
tree_sitter_c::LANGUAGE.into(),
"c",
tree_sitter_c::HIGHLIGHT_QUERY,
),
Language::Css => (
tree_sitter_css::LANGUAGE.into(),
"css",
tree_sitter_css::HIGHLIGHTS_QUERY,
),
Language::Go => (
tree_sitter_go::LANGUAGE.into(),
"go",
tree_sitter_go::HIGHLIGHTS_QUERY,
),
Language::Html => (
tree_sitter_html::LANGUAGE.into(),
"html",
tree_sitter_html::HIGHLIGHTS_QUERY,
),
Language::JavaScript => (
tree_sitter_javascript::LANGUAGE.into(),
"javascript",
tree_sitter_javascript::HIGHLIGHT_QUERY,
),
Language::Json => (
tree_sitter_json::LANGUAGE.into(),
"json",
tree_sitter_json::HIGHLIGHTS_QUERY,
),
Language::Nix => (
tree_sitter_nix::LANGUAGE.into(),
"nix",
tree_sitter_nix::HIGHLIGHTS_QUERY,
),
Language::Python => (
tree_sitter_python::LANGUAGE.into(),
"python",
tree_sitter_python::HIGHLIGHTS_QUERY,
),
Language::Rust => (
tree_sitter_rust::LANGUAGE.into(),
"rust",
tree_sitter_rust::HIGHLIGHTS_QUERY,
),
Language::TypeScript => (
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
"typescript",
tree_sitter_typescript::HIGHLIGHTS_QUERY,
),
Language::Yaml => (
tree_sitter_yaml::LANGUAGE.into(),
"yaml",
tree_sitter_yaml::HIGHLIGHTS_QUERY,
),
};
let mut config = HighlightConfiguration::new(language, name, highlights, "", "")
@@ -111,8 +174,9 @@ pub fn highlight_code(lang: Language, source: &str) -> String {
};
let mut renderer = HtmlRenderer::new();
let result = renderer.render(highlights, source.as_bytes(), &|highlight| {
HTML_ATTRS.get(highlight.0).copied().unwrap_or(b"<span>")
let result = renderer.render(highlights, source.as_bytes(), &|highlight, buf| {
let attrs = HTML_ATTRS.get(highlight.0).copied().unwrap_or(b"");
buf.extend_from_slice(attrs);
});
match result {
@@ -139,6 +203,27 @@ mod tests {
assert_eq!(Language::from_fence("bash"), Some(Language::Bash));
assert_eq!(Language::from_fence("sh"), Some(Language::Bash));
assert_eq!(Language::from_fence("json"), Some(Language::Json));
assert_eq!(Language::from_fence("nix"), Some(Language::Nix));
assert_eq!(Language::from_fence("python"), Some(Language::Python));
assert_eq!(Language::from_fence("py"), Some(Language::Python));
assert_eq!(
Language::from_fence("javascript"),
Some(Language::JavaScript)
);
assert_eq!(Language::from_fence("js"), Some(Language::JavaScript));
assert_eq!(
Language::from_fence("typescript"),
Some(Language::TypeScript)
);
assert_eq!(Language::from_fence("ts"), Some(Language::TypeScript));
assert_eq!(Language::from_fence("tsx"), Some(Language::TypeScript));
assert_eq!(Language::from_fence("go"), Some(Language::Go));
assert_eq!(Language::from_fence("golang"), Some(Language::Go));
assert_eq!(Language::from_fence("c"), Some(Language::C));
assert_eq!(Language::from_fence("yaml"), Some(Language::Yaml));
assert_eq!(Language::from_fence("yml"), Some(Language::Yaml));
assert_eq!(Language::from_fence("css"), Some(Language::Css));
assert_eq!(Language::from_fence("html"), Some(Language::Html));
assert_eq!(Language::from_fence("unknown"), None);
}
@@ -147,11 +232,8 @@ mod tests {
let code = "fn main() { println!(\"hello\"); }";
let html = highlight_code(Language::Rust, code);
// Should contain span elements with highlight classes
assert!(html.contains("class=\"hl-"));
// Should contain the keyword "fn"
assert!(html.contains("fn"));
// Should contain the string
assert!(html.contains("hello"));
}
@@ -164,6 +246,24 @@ mod tests {
assert!(html.contains("echo"));
}
#[test]
fn test_highlight_nix_code() {
let code = "{ pkgs, ... }: { environment.systemPackages = [ pkgs.vim ]; }";
let html = highlight_code(Language::Nix, code);
assert!(html.contains("class=\"hl-"));
assert!(html.contains("pkgs"));
}
#[test]
fn test_highlight_python_code() {
let code = "def hello():\n print(\"world\")";
let html = highlight_code(Language::Python, code);
assert!(html.contains("class=\"hl-"));
assert!(html.contains("def"));
}
#[test]
fn test_html_escape_fallback() {
let escaped = html_escape("<script>alert('xss')</script>");