Add a custom syntax highlighter
Use this approach for fences tagged with a language token — a DSL, config format, or domain notation — that TextMateSharp does not cover, when styled output is the goal but authoring a full TextMate grammar is not. For line-level callouts on an already-supported language, see Highlight, diff, focus, or flag lines inside a code block. When the goal is transforming fence bodies rather than colouring tokens, see Add a custom fence syntax.
Assumptions
- An existing Pennington site rendering markdown fences (see the Getting Started tutorial if not).
- A target language not already served by
TextMateHighlighter(priority 50) orShellHighlighter(priority 75) — confirm by rendering a fence and inspecting the emitted HTML for the built-in token spans. - Comfort producing HTML for a fence body by hand —
ICodeHighlighter.Highlightreturns a raw HTML string, so the implementation owns escaping and the outer<pre><code>wrapper.
For a working setup, see examples/ExtensibilityLabExample — PipelineHighlighter.cs stakes out a fictional pipeline DSL and Program.cs registers it against a bare AddPennington host.
Steps
- 1
Implement
ICodeHighlighterThe contract requires three members:
SupportedLanguages,Priority, andHighlight(code, language). The next three steps fence each of those members separately from the examplePipelineHighlighteratexamples/ExtensibilityLabExample/PipelineHighlighter.cs, which wraps keywords, arrows, and string literals in classed<span>elements and HTML-encodes everything else. - 2
Declare
SupportedLanguagesEvery language token returned here maps to a fence language (for example,
```pipeline) that routes to the highlighter. UseStringComparer.OrdinalIgnoreCaseto matchPipelineandPIPELINEas well./// <summary>The languages this highlighter claims.</summary> public IReadOnlySet<string> SupportedLanguages { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "pipeline" }; - 3
Set
PriorityHigher priority wins when multiple highlighters claim the same language. The built-in chain places
PlainTextHighlighterat 0,TextMateHighlighterat 50, andShellHighlighterat 75. The example uses 100 so thepipelinefence routes here even if a future TextMate grammar also claims it, while leaving room below for any secondary fallbacks that ship alongside./// <summary>Priority for highlighter dispatch — higher wins.</summary> public int Priority => 100; - 4
Produce the fence HTML in
HighlightHighlightreceives the raw fence body and the language token and returns the full HTML for the block, including the outer<pre><code>wrapper — the same convention the built-in highlighters follow. HTML-encode every character not explicitly wrapped in a span; the pipeline example usesWebUtility.HtmlEncodeon every literal path to prevent injection. Full implementation:examples/ExtensibilityLabExample/PipelineHighlighter.cs. - 5
Register with
HighlightingOptions.AddHighlighterPenningtonOptions.Highlightingexposes anAddHighlighteroverload that inserts the instance into the priority-sorted chain resolved byHighlightingService. Call it inside theAddPenningtondelegate so the highlighter is active for bothdotnet runanddotnet run -- build output._highlighters.Add(highlighter) - 6
Author a fence that targets your language
Any markdown fence tagged with one of the strings from
SupportedLanguagesnow routes to the custom highlighter instead of the fallback chain.```pipeline source "orders" -> filter where=paid | transform total=sum | sink "warehouse"
Verify
- Run
dotnet run --project examples/ExtensibilityLabExampleand visit/pipeline-demo/. - Expect each keyword, arrow, and string literal inside the
pipelinefence to carry apipeline-*CSS class; the neighbouringtextfence should render with no spans (fallbackPlainTextHighlighter). - Static build:
dotnet run --project examples/ExtensibilityLabExample -- build output— grep the emitted HTML forclass="pipeline-keyword"to confirm the highlighter also runs during publish.
Related
- Reference: Highlighting interfaces
- Background: The syntax-highlighting cascade
- Related how-to: Register a code-block preprocessor