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
This commit is contained in:
Timothy DeHerrera
2026-02-05 12:38:00 -07:00
parent 3f218ed49c
commit 113b7e4a4c
3 changed files with 344 additions and 24 deletions

View File

@@ -1,10 +1,10 @@
--- ---
title: Syntax Highlighting title: Syntax Highlighting
description: Build-time code highlighting with Tree-sitter description: Build-time code highlighting with Tree-sitter and tree-house
weight: 3 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 ## Usage
@@ -150,37 +150,81 @@ int main() {
## How It Works ## How It Works
1. During Markdown parsing, code blocks are intercepted 1. During Markdown parsing, code blocks are intercepted
2. Tree-sitter parses the code and generates a syntax tree 2. tree-house parses the code and generates a syntax tree
3. Spans are generated with semantic CSS classes 3. Spans are generated with **hierarchical CSS classes** (e.g., `.hl-keyword-control-return`)
4. All work happens at build time 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 ```css
.keyword { @import "path/to/theme.css";
color: #ff79c6;
}
.string {
color: #f1fa8c;
}
.function {
color: #50fa7b;
}
.comment {
color: #6272a4;
}
.number {
color: #bd93f9;
}
``` ```
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 ## 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 ```nix
stdenv.mkDerivation { stdenv.mkDerivation {
@@ -193,6 +237,8 @@ stdenv.mkDerivation {
Markdown also supports injection—code blocks inside markdown fences are highlighted with their respective languages. 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 ## Fallback
Unknown languages fall back to plain `<code>` blocks without highlighting. Unknown languages fall back to plain `<code>` blocks without highlighting.

View File

@@ -712,4 +712,188 @@ mod tests {
assert!(html.contains("pkgs")); 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("<span"));
// Should have keyword classes (fn, pub, etc.)
assert!(html.contains("hl-keyword") || html.contains("class="));
}
#[test]
fn test_highlight_python_function_definition() {
let code = "def greet(name: str) -> 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("<span"));
}
// === Injection Tests ===
#[test]
fn test_injection_nix_with_bash() {
// Nix code with bash in multi-line strings (common pattern)
let code = r#"
stdenv.mkDerivation {
buildPhase = ''
echo "Building..."
make -j$NIX_BUILD_CORES
'';
}
"#;
let html = highlight_code(Language::Nix, code);
// Should produce HTML output with the nix content
assert!(html.contains("stdenv"));
assert!(html.contains("mkDerivation"));
// The injection should be handled (even if bash isn't fully highlighted, should not error)
assert!(html.contains("echo"));
}
#[test]
fn test_injection_markdown_with_fenced_code() {
// Markdown with a fenced code block
let code = r#"# Header
Here is some code:
```rust
fn main() {}
```
"#;
let html = highlight_code(Language::Markdown, code);
// Should handle the markdown content
assert!(html.contains("Header"));
// Fenced code block should be present (may have spans inside)
assert!(html.contains("fn") && html.contains("main"));
}
#[test]
fn test_injection_html_with_script() {
// HTML with embedded JavaScript
let code = r#"
<!DOCTYPE html>
<html>
<head>
<script>
const x = 42;
console.log(x);
</script>
</head>
</html>
"#;
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#"
<html>
<head>
<style>
.container { display: flex; }
</style>
</head>
</html>
"#;
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 = "<script>alert('xss')</script>";"#;
let html = highlight_code(Language::Rust, code);
// HTML special chars should be escaped
assert!(!html.contains("<script>"));
assert!(html.contains("&lt;") || html.contains("script"));
}
} }

90
themes/README.md Normal file
View File

@@ -0,0 +1,90 @@
# Syntax Highlighting Themes
This directory contains CSS themes for sukr's syntax highlighting system.
## Available Themes
| Theme | Description |
| ------------------ | --------------------------- |
| `default.css` | Dracula-inspired dark theme |
| `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 |
## Usage
1. **Copy a theme** to your project's static directory
2. **Import it** in your main CSS file:
```css
@import "themes/default.css";
```
sukr uses [lightningcss](https://lightningcss.dev/) which bundles `@import` rules at build time—your final CSS will be a single minified file.
## Customization
Themes use CSS custom properties for easy customization. Override any variable in your own CSS:
```css
@import "themes/default.css";
/* Override just the keyword color */
:root {
--hl-keyword: #e879f9;
}
```
### Core Variables
All themes define these variables in `:root`:
| Variable | Description |
| --------------- | ---------------------- |
| `--hl-keyword` | Keywords, control flow |
| `--hl-string` | String literals |
| `--hl-function` | Function names |
| `--hl-comment` | Comments |
| `--hl-type` | Type names |
| `--hl-number` | Numeric literals |
| `--hl-variable` | Variables |
| `--hl-operator` | Operators |
## Hierarchical Scopes
sukr generates **hierarchical CSS classes** for fine-grained styling:
```html
<span class="hl-keyword-control-return">return</span>
```
Themes can style at any level of specificity:
```css
/* All keywords */
.hl-keyword {
color: var(--hl-keyword);
}
/* Just control-flow keywords */
.hl-keyword-control {
color: #ff79c6;
}
/* Just return/break/continue */
.hl-keyword-control-return {
font-weight: bold;
}
```
If a specific class isn't styled, highlighting falls back up the hierarchy.
## Creating Custom Themes
Start with `default.css` and modify the `:root` variables to create your own color scheme. The class rules reference these variables, so changing values updates the entire theme.
## Note
These themes are **not bundled into the sukr binary**—they're provided as starting points. Copy what you need to your project and customize to match your site's design.