ktsu.SyntaxHighlighting
3.76.0
Prefix Reserved
See the version list below for details.
dotnet add package ktsu.SyntaxHighlighting --version 3.76.0
NuGet\Install-Package ktsu.SyntaxHighlighting -Version 3.76.0
<PackageReference Include="ktsu.SyntaxHighlighting" Version="3.76.0" />
<PackageVersion Include="ktsu.SyntaxHighlighting" Version="3.76.0" />
<PackageReference Include="ktsu.SyntaxHighlighting" />
paket add ktsu.SyntaxHighlighting --version 3.76.0
#r "nuget: ktsu.SyntaxHighlighting, 3.76.0"
#:package ktsu.SyntaxHighlighting@3.76.0
#addin nuget:?package=ktsu.SyntaxHighlighting&version=3.76.0
#tool nuget:?package=ktsu.SyntaxHighlighting&version=3.76.0
ktsu.SyntaxHighlighting
SyntaxHighlighting turns source text into classified token runs. It is UI-agnostic: nothing in the package draws, references a graphics API, or knows that Dear ImGui exists. ktsu.ImGui.SyntaxHighlighting is one renderer over it; any host that can draw colored text can be another.
Features
- Fifteen built-in languages: C#, C, C++, JavaScript, TypeScript, Python, JSON, YAML, XML, HTML, CSS, SQL, shell, Lua, and plain text, each reachable by name or alias (
cs,c#,js,py,bash,yml, …) - Data-driven definitions: a language is a
LanguageDefinitionrecord — comment, string, keyword, operator and embedded-language rules — so an application can register its own, or derive a variant of a built-in with awithexpression - Embedded languages: XML in a doc comment, JSON in a fixture string and SQL in a query string are found inside the host language and highlighted in place, escapes and all
- Two tokenizers: a general lexer driven by the definition, and a structural one for angle-bracket markup, which has no keywords
- Colors without a renderer:
SyntaxThemeholds onektsu.Semantics.Color.ColorperTokenKind, withDarkandLightbuilt in, and leaves background, plain text and gutter unset so a host can fill them from its own theme - Cached tokenization:
SyntaxHighlighter.HighlightCachedkeys a bounded cache by source, language and tab width, andHighlightedCodetokenizes once for hot render paths
Installation
dotnet add package ktsu.SyntaxHighlighting
Quick Start
using ktsu.SyntaxHighlighting;
foreach (HighlightedLine line in SyntaxHighlighter.Highlight(source, "csharp"))
{
foreach (HighlightedToken token in line.Tokens)
{
Draw(token.Text, SyntaxTheme.Dark.ColorFor(token.Kind));
}
}
Tokens tile the source: concatenating every token's text, with a newline between lines, reproduces the input exactly (with tabs expanded). Unknown language names never throw — they fall back to plain text, since the name usually comes from a markdown fence or a user-selected file.
For a render loop, tokenize once:
private static readonly HighlightedCode Snippet = new("SELECT * FROM users;", "sql");
Embedded languages
XML, JSON and SQL are written inside other languages' comments and strings constantly, and a lexer that stops at the quote leaves them a single flat color. Each LanguageDefinition carries EmbeddedLanguages: rules saying which language to look for, and where.
/// <summary>Posts an order.</summary> // tags are XML, the prose stays a comment
string body = @"{""id"": 7, ""paid"": true}"; // keys, numbers and constants are JSON
string query = "SELECT id FROM receipts"; // keywords are SQL
Three things make this safe to leave on:
- Recognition is strict. JSON is parsed, not pattern-matched, so a fragment or a brace-heavy sentence is rejected. Markup must open with a tag and close with
>. SQL must open with a statement keyword and go on to use a second one, which is what keeps "Update the cache" from turning into a query — and SQL is only looked for in strings, never in comments. - Unclassified text keeps its host's color. Prose between doc comment tags still reads as a comment, so a false positive costs a few punctuation glyphs rather than a paragraph.
- Escapes are resolved for the tokenizer, not for the output. The inner tokenizer sees
{"id": 7}where the source holds{\"id\": 7}, but every token is re-sliced from the original text, so the backslashes are still drawn.
When recognition cannot work — a fragment, a heavily interpolated string — name the language outright with a hint comment, which applies to the next string literal:
// lang=sql
string tail = "ORDER BY total DESC";
/* language=sql */
string other = "ORDER BY total DESC";
Post(/* lang=json */ "{\"id\": 7}");
The hint is read from the comment's body, so either comment style works, lang and language are interchangeable, and an inline block comment hints the string beside it. The name runs to the next whitespace, so write lang=json rather than lang=json,.
Embedding is one level deep, and applies to LanguageDefinition.EmbeddedLanguages on the host language only. Turn it off, or change it, by deriving a definition:
LanguageRegistry.Register(BuiltInLanguages.CSharp with { EmbeddedLanguages = [] });
LanguageRegistry.Register(BuiltInLanguages.Python with
{
EmbeddedLanguages = [.. BuiltInEmbeddedRules.Default, new EmbeddedLanguageRule
{
Language = "graphql",
Hosts = EmbeddedHosts.StringLiteral,
Matches = body => body.TrimStart().StartsWith("query ", StringComparison.Ordinal),
}],
});
Registering a language
A language is plain data, so nothing needs to be subclassed:
LanguageRegistry.Register(new LanguageDefinition
{
Name = "ini",
Aliases = ["conf", "cfg"],
LineComments = [new LineCommentRule { Prefix = ";" }],
Strings = [new StringRule { Open = "\"", Close = "\"" }],
Constants = ["true", "false", "yes", "no"],
HighlightPropertyNames = true,
HighlightFunctionCalls = false,
});
Two rule options cover delimiters that a plain prefix cannot describe. LineCommentRule.RequiresWhitespaceBefore opens a comment only at the start of a line or after whitespace, which is how shell and YAML read # (so $#, ${#name} and http://host/#frag are not comments). StringRule.CloseMatchesOpeningRun lets the opener's last character repeat and closes only on a run as long as the one that opened it, which is how C# raw strings work ("""" is closed only by """").
A block comment whose close overlaps its own opener at the match position, as */ does inside the doc opener /** in /**/, gives way to a shorter rule that matches there, so /**/ is one empty comment rather than a doc comment running to the next */.
Limitations
- Highlighting is lexical, not semantic: user-defined type names are not distinguished from other identifiers, and a call is recognized by the
(that follows it - Embedding is one level deep and is not applied by the markup tokenizer, so
<script>and<style>bodies inside HTML are still treated as markup text - Doc comments written one
///line at a time are tokenized a line at a time, so an XML construct split across lines is classified per line - Lines are not wrapped and nothing is drawn; this classifies code, a renderer decides what to do with it
Contributing
Contributions are welcome! Feel free to open issues or submit pull requests.
License
This project is licensed under the MIT License. See the LICENSE.md file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ktsu.Semantics.Color (>= 5.12.1)
-
net8.0
- ktsu.Semantics.Color (>= 5.12.1)
-
net9.0
- ktsu.Semantics.Color (>= 5.12.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ktsu.SyntaxHighlighting:
| Package | Downloads |
|---|---|
|
ktsu.ImGui.SyntaxHighlighting
Renders syntax-highlighted source code inside Dear ImGui. Ships a data-driven tokenizer with definitions for fifteen languages, theme-aware palettes that follow the host application's light or dark theme, an optional line-number gutter, and cached tokenization for immediate-mode render loops. Layered on ktsu.ImGui.Color only, with no dependency on ktsu.ImGui.App, so it drops into any Hexa.NET.ImGui application — including as the code-block renderer for ktsu.ImGui.Markdown. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.77.2 | 0 | 10/21/2026 |
| 3.77.1 | 0 | 10/21/2026 |
| 3.77.0 | 0 | 10/20/2026 |
| 3.76.1 | 0 | 10/20/2026 |
| 3.76.0 | 0 | 10/20/2026 |
| 3.75.4 | 230 | 10/19/2026 |
| 3.75.3 | 101 | 10/18/2026 |
| 3.75.2 | 107 | 10/17/2026 |
| 3.75.2-pre.1 | 58 | 10/16/2026 |
| 3.75.1 | 111 | 10/14/2026 |
| 3.75.0 | 116 | 10/12/2026 |
| 3.74.0 | 110 | 10/12/2026 |
| 3.73.0 | 107 | 10/11/2026 |
| 3.72.1 | 111 | 10/11/2026 |
| 3.72.0 | 104 | 10/11/2026 |
| 3.71.0 | 115 | 10/11/2026 |
| 3.70.0 | 137 | 10/11/2026 |
| 3.69.1 | 111 | 10/11/2026 |
| 3.69.0 | 112 | 10/10/2026 |
| 3.68.0 | 106 | 10/10/2026 |
## v3.76.0 (minor)
Changes since v3.75.0:
- Give ImGui.Rendering the package validation suppressions the others carry [patch] (#717) (matt-edmondson)
- Clear main's quality gate: tolerances instead of exact float comparisons (#716) (matt-edmondson)
- [minor] FilesystemBrowser: breadcrumbs, back/forward, places, search, sortable columns and thumbnails (#715) (matt-edmondson)
- [minor] Two-finger touch scrolling, extension docs, and trim analysis for Widgets/Popups/Styler (#703) (matt-edmondson)
- Merge main into claude/project-thread-m8gl9z (matt-edmondson)
- [minor] Add the phase 4 mobile polish widgets (#710) (matt-edmondson)
- [minor] Add the phase 3 mobile overlay and navigation widgets (#709) (matt-edmondson)
- Pick a curve point's fill in its own method rather than a nested conditional (Claude)
- [minor] Add the phase 2 mobile gesture widgets (#708) (matt-edmondson)
- Fix Escape in nested modals, path text in the save box, and Enter on a search with no match [patch] (#693) (matt-edmondson)
- [minor] Add LogView, a virtualized log with level filters and search (#706) (matt-edmondson)
- Keep the active frame rate while an animation runs [minor] (#692) (matt-edmondson)
- [minor] Mark every widget for UI probes (#707) (matt-edmondson)
- Add ImGuiWidgets.Viewport3D and move the 3D contract into ktsu.ImGui.Rendering [minor] (#705) (matt-edmondson)
- Merge remote-tracking branch 'origin/main' into fix699 (matt-edmondson)
- [patch] Fix GradientEditor selection on reorder and two TransformBox bugs (#690) (matt-edmondson)
- [patch] Fix three SyntaxHighlighting tokenizer bugs that recolored the rest of a document (#691) (matt-edmondson)
- Compare curve range spans against epsilon rather than zero (Claude)
- Reach long lines in code blocks and diff hunks, draw only visible code lines, keep rebuilt hunks collapsed [patch] (#704) (matt-edmondson)
- Dim GradientEditor, SwatchPalette and themed code inside a disabled scope [patch] (#701) (matt-edmondson)
- Draw the curve and bezier editors with ImGui and drop Hexa Extras [minor] (Claude)
- Stop a hidden DataTable column from taking edits or filtering rows (#700) (matt-edmondson)
- Node editor: re-measure rebuilt graphs, edit long pins, pin nodes from the UI [minor] (Claude)
- Fix markdown fonts, link IDs, linked images, image placeholders and code spans [patch] (#698) (matt-edmondson)
- ReorderableTree: let the last child leave its group, and leave slider drags to the slider (#697) (matt-edmondson)
- Save the last frame of a failing UI test, and cover CapturedFrame and the frame wrapper order (#696) (matt-edmondson)
- Fix the extra step after a held repeat button and snap marks to frames [patch] (#695) (matt-edmondson)
- Honour markdown table column alignment and scroll long code lines [patch] (#694) (matt-edmondson)
- Read TGA colour map entries with the descriptor's attribute bits and treat an all-zero 32-bit map as opaque [patch] (#689) (matt-edmondson)
- Merge main into fix/680-knob-visible-title (matt-edmondson)
- Show a Knob's title without its ## ID suffix (Claude)
- Bound a JPEG's coefficient allocation by its scan data instead of its frame header [patch] (Claude)
- Refuse a TGA header with no pixel data before allocating the image [patch] (Claude)
- Drive the theme menu and browser with a theme applied (Claude)
- Suppress S6640 on FormatValue, as the Knob's other native interop is (Claude)
- Draw caller and theme text as it is instead of as a printf format [patch] (Claude)
- Move the new Knob tests to the end of the class so they don't touch the lines #673 adds to (Claude)
- Show the Knob's value in its tooltip, and drop the stray "f" from int knobs [patch] (Claude)
- Round a Knob drag without throwing on a format of more than 6 decimals [patch] (Claude)
- Read the Knob's format precision without throwing on "%.f" or a truncated format [patch] (Claude)
- Merge main into the TGA first-entry branch (Claude)
- Merge main into the hard line break branch (Claude)
- End the line at a Markdown hard line break [patch] (Claude)
- Pick only clipboard image types the decoder can read on Linux [patch] (Claude)
- Name Tab as Tab in KeyChordMatcher, not its NamedKeyBegin alias [patch] (Claude)
- Honour the TGA colour map's first entry index [patch] (Claude)
- Move the JPEG coefficient bound into a helper (Claude)
- Reject oversized image headers as InvalidImageDataException [patch] (Claude)
- Decode 16-bit greyscale TGA as grey plus alpha instead of RGB555 [patch] (Claude)
- Write a true minus sign on a value slider's face (#645) [patch] (matt-edmondson)
- Split the glued-word grouping into tokenizing and grouping (matt-edmondson)
- Stop Markdown inserting a space where inline runs touch [patch] (matt-edmondson)
- Let the syntax highlighting packages pass package validation (#638) [patch] (matt-edmondson)
- Let the syntax highlighting packages pass package validation [patch] (Claude)
- Report node size changes made while the node editor is zoomed (#637) (matt-edmondson)
- Report node size changes made while the node editor is zoomed (Claude)
- Keep a DiffView hunk's collapse with that hunk, not its position [patch] (Claude)