Syntax Highlighting in ndg-commonmark
The syntax highlighting system in ndg-commonmark is designed with extensibility in mind, allowing different highlighting backends to be plugged in through a trait-based architecture. This enables users to choose the best highlighting engine for their needs while maintaining a consistent API.
Tip
This document highlights1 using ndg-commonmark's syntax highlighting system for developers who intend to integrate ndg-commonmark into their project. If you are working with ndg, you may be more interested in its documentation and the syntax reference.
Quick Start
Basic Usage with MarkdownProcessor
use ndg_commonmark::{MarkdownOptions, MarkdownProcessor};
let mut options = MarkdownOptions::default();
options.highlight_code = true;
options.highlight_theme = Some("one-dark".to_string());
let processor = MarkdownProcessor::new(options);
let markdown = r#"
# Example
```rust
fn main() {
println!("Hello, world!");
}
```
"#;
let result = processor.render(markdown); println!("{}", result.html);
Direct Syntax Manager Usage
use ndg_commonmark::syntax::create_default_manager;
let manager = create_default_manager()?;
let highlighted = manager.highlight_code(
"fn main() { println!(\"Hello\"); }",
"rust",
Some("one-dark"),
)?;
println!("{}", highlighted);
Configuration
Feature Flags
The syntax highlighting system is controlled by feature flags:
syntastica: Enables Syntastica backend (enabled byndg-flavored)ndg-flavored: Enables all NDG-specific features including syntax highlightingsyntect: Enables the legacy highlighting system. More lightweight, but lacks language support.
[dependencies]
ndg-commonmark = { version = "1.0", features = ["syntastica"] }
MarkdownOptions
Configure syntax highlighting through MarkdownOptions:
let mut options = MarkdownOptions::default();
// Enable/disable syntax highlighting
options.highlight_code = true;
// Set default theme
options.highlight_theme = Some("gruvbox-dark".to_string());
SyntaxConfig
For advanced configuration, use SyntaxConfig:
use ndg_commonmark::syntax::{SyntaxConfig, SyntaxManager};
use std::collections::HashMap;
let mut config = SyntaxConfig::default();
// Set default theme
config.default_theme = Some("one-dark".to_string());
// Add custom language aliases
config.language_aliases.insert("ts".to_string(), "typescript".to_string());
// Configure fallback behavior
config.fallback_to_plain = true;
let manager = SyntaxManager::new(highlighter, config);
Supported Languages
The Syntastica crate supports a large variety of languages. We enable support for all languages by default.
Language Aliases
Common aliases are automatically resolved:
| Alias | Resolves To |
|---|---|
js |
javascript |
ts |
typescript |
py |
python |
rb |
ruby |
sh |
bash |
shell |
bash |
yml |
yaml |
nixos |
nix |
md |
markdown |
File Extension Detection
Languages can be automatically detected from file extensions:
let manager = create_default_manager()?;
if let Some(lang) = manager.highlighter().language_from_filename("main.rs") {
println!("Detected language: {}", lang); // "rust"
}
Themes
The number and the names of supported themes depend on the backend crate used.
Syntastica Backend
one-dark(default)one-lightgruvbox-darkgruvbox-lightgithub
Syntect Backend
InspiredGitHub(default)- Plus all themes from syntect's default theme set
Using Themes
// With MarkdownProcessor
let mut options = MarkdownOptions::default();
options.highlight_theme = Some("gruvbox-dark".to_string());
// With SyntaxManager
let highlighted = manager.highlight_code(code, "rust", Some("one-dark"))?;
Custom Themes
For Syntastica
use syntastica::theme;
use ndg_commonmark::syntax::SyntasticaHighlighter;
let theme = theme! {
"purple": "#c678dd",
"blue": "#61afef",
"keyword": "$purple",
"function": "$blue",
};
let mut highlighter = SyntasticaHighlighter::new()?;
highlighter.add_theme("my-theme".to_string(), theme);
Backends
Backend Selection
The backend is automatically selected based on feature flags:
- If
syntasticafeature is enabled, then the Syntastica crate will be used. - If
syntectfeature is enabled, then Syntect will be used for syntax highlighting. - If neither are enabled, then ndg-commonmark will not do any syntax highlighting at all.
Manual backend selection:
use ndg_commonmark::syntax::{SyntasticaHighlighter, SyntaxManager, SyntaxConfig};
// Explicitly use Syntastica
#[cfg(feature = "syntastica")]
let highlighter = Box::new(SyntasticaHighlighter::new()?);
// Explicitly use Syntect
let highlighter = Box::new(SyntectHighlighter::default());
let manager = SyntaxManager::with_highlighter(highlighter);
Error Handling
Example:
use ndg_commonmark::syntax::SyntaxError;
match manager.highlight_code(code, "unknown-lang", None) {
Ok(html) => println!("Success: {}", html),
Err(SyntaxError::UnsupportedLanguage(lang)) => {
eprintln!("Language '{}' not supported", lang);
}
Err(SyntaxError::ThemeNotFound(theme)) => {
eprintln!("Theme '{}' not found", theme);
}
Err(SyntaxError::HighlightingFailed(msg)) => {
eprintln!("Highlighting failed: {}", msg);
}
Err(e) => eprintln!("Other error: {}", e),
}
Performance Considerations
ndg-commonmark is fast, very fast. Though you will still want to keep somethings in mind to retain the performance benefits of ndg-commonmark. Here are some tips that you might find useful.
- Create syntax managers once and reuse them compatibility
- Limit available themes as theme changes can be expensive
- Avoid manual language specification when possible
Advanced Usage
Custom Highlighter Implementation
Implement the SyntaxHighlighter trait for custom backends:
use ndg_commonmark::syntax::{SyntaxHighlighter, SyntaxResult, SyntaxError};
struct CustomHighlighter {
// Your implementation
}
impl SyntaxHighlighter for CustomHighlighter {
fn name(&self) -> &'static str {
"Custom"
}
fn supported_languages(&self) -> Vec<String> {
vec!["custom".to_string()]
}
fn available_themes(&self) -> Vec<String> {
vec!["default".to_string()]
}
fn highlight(&self, code: &str, language: &str, theme: Option<&str>) -> SyntaxResult<String> {
// Your highlighting logic
Ok(format!("<pre><code>{}</code></pre>", html_escape::encode_text(code)))
}
fn language_from_extension(&self, extension: &str) -> Option<String> {
match extension {
"custom" => Some("custom".to_string()),
_ => None,
}
}
}
Tree-sitter Query Overrides
When syntax highlighting is enabled with the syntastica backend, you can
override Tree-sitter queries through configuration.
Set syntax_queries_path in your NDG config, then place query files in this
layout:
<syntax_queries_path>/
<language>/
highlights.scm
injections.scm
locals.scm
<language>must match a Syntastica language name (for example:markdown,rust,nix,html).- Any file you provide overrides that built-in query for the language.
- Missing files fall back to bundled Syntastica queries.
- User queries are processed with Neovim-like compatibility enabled, including common injection query syntax patterns.
Example override to force Markdown fenced code blocks to be injected as Bash:
((fenced_code_block
(code_fence_content) @injection.content)
(#set! injection.language "bash"))
-
Ha, get it? Highlights. Ha. ↩