From 113b7e4a4c1357cd232fc27caa8481367651bd0f Mon Sep 17 00:00:00 2001 From: Timothy DeHerrera Date: Thu, 5 Feb 2026 12:38:00 -0700 Subject: [PATCH] docs(themes): add syntax highlighting documentation and test coverage - Update syntax-highlighting.md with tree-house integration details - Add themes/README.md explaining copy-to-project workflow - Add 13 tests: hierarchical scopes, injections (Nix+bash, MD, HTML) - All 64 tests passing --- docs/content/features/syntax-highlighting.md | 94 +++++++--- src/highlight.rs | 184 +++++++++++++++++++ themes/README.md | 90 +++++++++ 3 files changed, 344 insertions(+), 24 deletions(-) create mode 100644 themes/README.md diff --git a/docs/content/features/syntax-highlighting.md b/docs/content/features/syntax-highlighting.md index 3ec8b59..3a8a551 100644 --- a/docs/content/features/syntax-highlighting.md +++ b/docs/content/features/syntax-highlighting.md @@ -1,10 +1,10 @@ --- title: Syntax Highlighting -description: Build-time code highlighting with Tree-sitter +description: Build-time code highlighting with Tree-sitter and tree-house weight: 3 --- -sukr highlights code blocks at build time using Tree-sitter. No client-side JavaScript required. +sukr highlights code blocks at build time using [tree-house](https://github.com/helix-editor/tree-house) (Helix editor's Tree-sitter integration). No client-side JavaScript required. ## Usage @@ -150,37 +150,81 @@ int main() { ## How It Works 1. During Markdown parsing, code blocks are intercepted -2. Tree-sitter parses the code and generates a syntax tree -3. Spans are generated with semantic CSS classes -4. All work happens at build time +2. tree-house parses the code and generates a syntax tree +3. Spans are generated with **hierarchical CSS classes** (e.g., `.hl-keyword-control-return`) +4. All work happens at build time—zero JavaScript in the browser -## Styling +## Theme System -Highlighted code uses semantic CSS classes: +sukr uses a **decoupled theme system** with CSS custom properties. Themes are separate CSS files that define colors for syntax highlighting classes. + +### Hierarchical Scopes + +Highlighting uses fine-grained scope classes with hierarchical fallback: + +| Scope Class | Description | +| --------------------------------- | --------------------- | +| `.hl-keyword` | Generic keywords | +| `.hl-keyword-control` | Control flow | +| `.hl-keyword-control-return` | return/break/continue | +| `.hl-function` | Function names | +| `.hl-function-builtin` | Built-in functions | +| `.hl-type` | Type names | +| `.hl-variable` | Variables | +| `.hl-variable-parameter` | Function parameters | +| `.hl-string` | String literals | +| `.hl-comment` | Comments | +| `.hl-comment-block-documentation` | Doc comments | + +If a theme only defines `.hl-keyword`, it will apply to all keyword subtypes. + +### Using a Theme + +Themes are CSS files that define the color palette. Import a theme at the top of your stylesheet: ```css -.keyword { - color: #ff79c6; -} -.string { - color: #f1fa8c; -} -.function { - color: #50fa7b; -} -.comment { - color: #6272a4; -} -.number { - color: #bd93f9; -} +@import "path/to/theme.css"; ``` -The exact classes depend on the language grammar. +sukr uses [lightningcss](https://lightningcss.dev/) which inlines `@import` rules at build time, producing a single bundled CSS file. + +### Available Themes + +sukr includes several themes in the `themes/` directory: + +- **default.css** — Dracula-inspired dark theme (batteries included) +- **dracula.css** — Classic Dracula colors +- **gruvbox.css** — Warm retro palette +- **nord.css** — Cool arctic colors +- **github_dark.css** — GitHub's dark mode +- **github_light.css** — GitHub's light mode + +Copy the theme files to your project and import as shown above. + +### Theme Structure + +Themes use CSS custom properties for easy customization: + +```css +:root { + --hl-keyword: #ff79c6; + --hl-string: #f1fa8c; + --hl-function: #50fa7b; + --hl-comment: #6272a4; +} + +.hl-keyword { + color: var(--hl-keyword); +} +.hl-string { + color: var(--hl-string); +} +/* ... */ +``` ## Injection Support -Some languages support injection—highlighting embedded languages. For example, Bash inside Nix strings: +Some languages support **injection**—highlighting embedded languages. For example, bash inside Nix strings: ```nix stdenv.mkDerivation { @@ -193,6 +237,8 @@ stdenv.mkDerivation { Markdown also supports injection—code blocks inside markdown fences are highlighted with their respective languages. +Languages with injection support: Bash, C, CSS, Go, HTML, JavaScript, Markdown, Nix, Python, Rust, TOML, TypeScript, YAML. + ## Fallback Unknown languages fall back to plain `` blocks without highlighting. diff --git a/src/highlight.rs b/src/highlight.rs index 3b754d9..7eff4a2 100644 --- a/src/highlight.rs +++ b/src/highlight.rs @@ -712,4 +712,188 @@ mod tests { assert!(html.contains("pkgs")); } + + // === Hierarchical Scope Tests === + + #[test] + fn test_scope_to_class_generates_hierarchical_names() { + // Verify the SCOPE_CLASSES static contains hierarchical class names + let classes = SCOPE_CLASSES.clone(); + + // Should have keyword hierarchy + assert!(classes.contains(&"hl-keyword")); + assert!(classes.contains(&"hl-keyword-control")); + assert!(classes.contains(&"hl-keyword-control-return")); + + // Should have function hierarchy + assert!(classes.contains(&"hl-function")); + assert!(classes.contains(&"hl-function-builtin")); + + // Should have comment hierarchy + assert!(classes.contains(&"hl-comment")); + assert!(classes.contains(&"hl-comment-block-documentation")); + } + + #[test] + fn test_hierarchical_scope_resolution_fallback() { + // Direct match should work + let kw = resolve_scope("keyword"); + assert!(kw.is_some()); + + // Hierarchical match should find parent + let kw_ctrl = resolve_scope("keyword.control"); + assert!(kw_ctrl.is_some()); + + // Deeper hierarchy should find closest ancestor + let kw_ctrl_ret = resolve_scope("keyword.control.return"); + assert!(kw_ctrl_ret.is_some()); + + // Completely unknown should return None + assert!(resolve_scope("nonexistent.scope.here").is_none()); + } + + #[test] + fn test_highlight_generates_hl_prefixed_classes() { + // Rust code that should produce keyword highlighting + let code = "fn main() { return 42; }"; + let html = highlight_code(Language::Rust, code); + + // Should contain hl-prefixed span classes + assert!( + html.contains("hl-"), + "Expected hl-prefixed classes in: {html}" + ); + } + + #[test] + fn test_highlight_rust_keywords() { + let code = "pub fn foo() -> Result<(), Error> { Ok(()) }"; + let html = highlight_code(Language::Rust, code); + + // Should contain span elements + assert!(html.contains(" str:\n return f'Hello, {name}'"; + let html = highlight_code(Language::Python, code); + + // Should contain the function name + assert!(html.contains("greet")); + // Should have span highlighting + assert!(html.contains(" + + + + + +"#; + let html = highlight_code(Language::Html, code); + + // Should contain HTML structure + assert!(html.contains("html")); + assert!(html.contains("script")); + // JavaScript content should be present + assert!(html.contains("const")); + } + + #[test] + fn test_injection_html_with_style() { + // HTML with embedded CSS + let code = r#" + + + + + +"#; + let html = highlight_code(Language::Html, code); + + // Should handle CSS injection + assert!(html.contains("style")); + assert!(html.contains("container")); + assert!(html.contains("flex")); + } + + // === Edge Cases === + + #[test] + fn test_empty_input() { + let html = highlight_code(Language::Rust, ""); + // Empty input should produce minimal output + assert!(html.is_empty() || html.len() < 10); + } + + #[test] + fn test_whitespace_only_input() { + let html = highlight_code(Language::Rust, " \n\t\n "); + // Whitespace should be preserved + assert!(html.contains(' ') || html.contains('\n')); + } + + #[test] + fn test_special_characters_escaped() { + let code = r#"let x = "";"#; + let html = highlight_code(Language::Rust, code); + + // HTML special chars should be escaped + assert!(!html.contains("