AvalonMarkdown 10.0.0
dotnet add package AvalonMarkdown --version 10.0.0
NuGet\Install-Package AvalonMarkdown -Version 10.0.0
<PackageReference Include="AvalonMarkdown" Version="10.0.0" />
<PackageVersion Include="AvalonMarkdown" Version="10.0.0" />
<PackageReference Include="AvalonMarkdown" />
paket add AvalonMarkdown --version 10.0.0
#r "nuget: AvalonMarkdown, 10.0.0"
#:package AvalonMarkdown@10.0.0
#addin nuget:?package=AvalonMarkdown&version=10.0.0
#tool nuget:?package=AvalonMarkdown&version=10.0.0
AvalonMarkdown
A Markdown preview control for AvaloniaUI β renders Markdown with math (KaTeX), syntax highlighting (highlight.js), diagrams (Mermaid / PlantUML), function plots, and video embeds.
π δΈζζζ‘£
Installation
dotnet add package AvalonMarkdown
Everything you need lives on two types:
MarkdownViewβ show a Markdown document and react to the user (links, scroll, headings).MarkdownThemeViewModelβ style how that document looks.
MarkdownView β rendering & interaction
Add the control to a window and feed it Markdown content.
<Window xmlns="https://github.com/avaloniaui"
xmlns:md="clr-namespace:AvalonMarkdown.Views;assembly=AvalonMarkdown">
<md:MarkdownView x:Name="Preview" />
</Window>
Show content
Bind the Text property (this is the recommended way β it updates automatically):
<md:MarkdownView Text="{Binding MarkdownContent}" />
Or render explicitly after the control is ready:
Preview.OnReady += async (_, _) =>
{
await Preview.RenderMarkdownAsync("# Hello World\n\n**Bold** *Italic*");
};
When content is empty you can show a hint with Placeholder (blank by default):
<md:MarkdownView Text="{Binding MarkdownContent}" Placeholder="Type somethingβ¦" />
Function plots
A plot fence draws y = f(x). Its body is the function-plot options object as raw JSON β pass through anything the library documents:
```plot
{
"title": "sin(x)/x",
"grid": true,
"xAxis": { "domain": [-12, 12] },
"yAxis": { "domain": [-0.4, 1.1] },
"data": [
{ "fn": "sin(x)/x" },
{ "fn": "1/x", "color": "#e06c75" }
]
}
```
Only two things are ever defaulted, and your JSON always wins:
colorβ a series without one takes a colour from the theme palette, by index. Series colours are per-function (coloris a per-datum option), so they cannot come from CSS.disableZoomβ defaults totrue, keeping the graph static so the wheel keeps scrolling the page. Set"disableZoom": falseto enable wheel zoom and panning on that block. This default is deliberate: the library's zoom binding consumes every wheel event over the graph, which traps the scroll wheel in a reading pane.
width and height are the whole SVG including margins (left 40 / right 20 / bottom 20, top 20 β or 40 when there is a title), so "height": 350 yields a 310px plot area. If you omit width, it is fitted to the container.
A malformed body renders an inline error block instead of breaking the preview. The fence is matched on its info string exactly, so ordinary code fences β json included β are untouched.
Expression strings are checked against a character whitelist before they reach the library's evaluator (which compiles them to JavaScript). Comparison, ternary and modulo operators are allowed, so piecewise definitions work β "fn": "x > 0 ? x : -x" β but quotes, semicolons, braces, brackets and backslashes are rejected.
Two things to know about the expression syntax, both straight from the library:
- Constants are upper-case β
PIandE, notpi/e. Lower-case forms are undefined and the series silently fails to draw. - Available functions include
sincostanasinacosatansqrtabsexploglnmaxmin. Names outside that set (floor,sign, β¦) are undefined.
Handle link clicks
Every hyperlink click fires LinkClicked. Set Handled = true to take over navigation yourself; otherwise the link opens in the system browser.
Preview.LinkClicked += (_, e) =>
{
if (e.Url.StartsWith("app://"))
{
OpenInternalPage(e.Url); // your in-app routing
e.Handled = true; // suppress the default browser launch
}
};
Jump inside the document (anchors & TOC)
Markdown headings are linkable from within the same page, so [See Β§2](#2-text-formatting)-style links just work.
To build a clickable table of contents in your own UI:
var headings = await Preview.GetHeadingsAsync(); // [{ Text, Level }, β¦]
var ok = await Preview.NavigateToHeadingAsync("Chapter 2"); // scroll to a heading
Sync an editor with the preview (scroll progress)
Report/restore the vertical read position β handy for editor β preview split views:
double? progress = await Preview.GetScrollProgressAsync(); // 0β100
await Preview.ScrollToProgressAsync(50);
Export a diagram as an image
Right-click a Mermaid diagram, a function plot or a PlantUML image β or long-press it on touch β and choose Save as SVG or Save as PNG. The file is written through the platform's save dialog.
Only those three members are intercepted. Right-clicking anywhere else in the
document still shows the platform's own menu. Set EnableContextMenu="False" to
turn the feature off entirely and get the platform menu back everywhere:
<md:MarkdownView Text="{Binding MarkdownContent}" EnableContextMenu="False" />
Math is deliberately not exportable: KaTeX renders positioned HTML, not SVG, so there is no vector artefact to hand out β producing an image would mean screenshotting the DOM.
Three things worth knowing:
- An exported PlantUML image is black-on-white, even in dark theme. That is PlantUML's own output, deliberately unfiltered β the dark-theme inversion you see on screen is a display-only filter. It prints and pastes correctly, but it will look different from a dark-theme Mermaid export.
- Exported Mermaid and plot images carry an opaque background taken from the page, so a dark-theme diagram stays legible when pasted onto a white page.
- A very large diagram may refuse to save as PNG (reported in the preview) β the JSβC# bridge crosses Android's Binder with a ~1 MB limit. SVG has no such limit; use it, or export a smaller diagram.
Configuration & diagnostics
Content updates auto-scroll to the newest content, and pause while you operate the scrollbar. Behaviour can be tuned with ApplyConfigAsync:
await Preview.ApplyConfigAsync("setPreviewConfig({ autoScrollOnUpdate: true, autoScrollCooldown: 2000 })");
ErrorOccurred and ConsoleMessage events let you surface or log any rendering/script message.
MarkdownView reference
| Member | Type | What it does |
|---|---|---|
Text |
string? |
Markdown content (two-way bindable) |
Placeholder |
string? |
Hint shown when content is empty |
EnableContextMenu |
bool |
Right-click / long-press an image to save it (default true) |
OnReady |
event |
Fires when the preview is ready to render |
LinkClicked |
event |
Fires on hyperlink clicks; Handled = true takes over navigation |
ErrorOccurred |
event |
Fires on recoverable internal errors |
ConsoleMessage |
event |
Fires for each WebView console message (log/warn/error) |
RenderMarkdownAsync(text) |
Task |
Renders Markdown content |
GetHeadingsAsync() |
Task<IReadOnlyList<MarkdownHeading>> |
Lists all headings (Text, Level) |
NavigateToHeadingAsync(heading) |
Task<bool> |
Scrolls to the heading; returns whether it was found |
GetScrollProgressAsync() |
Task<double?> |
Current vertical progress (0β100) |
ScrollToProgressAsync(percent) |
Task |
Scrolls to a vertical progress position (0β100) |
ApplyConfigAsync(expression) |
Task |
Applies preview behaviour configuration |
ApplyCustomCssAsync(css) |
Task |
Applies a full custom stylesheet |
RestartPreviewAsync() |
Task |
Recreates the preview page |
MarkdownThemeViewModel β styling
Configures how the document looks: colors, typography, code highlighting, and diagram styling. Every property carries a [Description] (hover it in IntelliSense to see what it controls).
Option A β use the built-in theme editor
MarkdownThemeView is a ready-made editor panel for the properties below. Point it at your preview; any change is applied immediately.
<md:MarkdownView x:Name="Preview" />
<md:MarkdownThemeView Target="{Binding #Preview}" />
The panel is collapsed by default β click its header to expand it.
Option B β drive it programmatically
Create a MarkdownThemeViewModel, change the properties you care about, and let it push changes to one or more previews:
var theme = new MarkdownThemeViewModel
{
BgR = 30, BgG = 30, BgB = 30, // page background
TextR = 212, TextG = 212, TextB = 212, // body text
HljsKeyword = "#569cd6",
MermaidContainerPadding = 8,
};
theme.RegisterRenderer(myPreview);
theme.StartAutoApply(); // any further property change auto-applies
Prefer a one-shot apply instead?
await myPreview.ApplyCustomCssAsync(theme.GenerateCss());
The same styling applies whatever light/dark theme your app currently uses.
What you can configure
| Group | Covers |
|---|---|
| Core colors | Page background, text, links, headings, inline code, borders |
| Surface colors | Secondary background (e.g. code headers), secondary text, inline-code background, code-block background, table header |
| Typography | Code font size; code-block / pre corner radius |
| Code highlighting | Color of each syntax token (keyword, string, number, comment, β¦) |
| Diagrams | Mermaid & PlantUML container background / padding / margin / radius; PlantUML dark invert strength |
| Function plots | Plot container background / padding / margin / radius; axis, grid and title colour |
Supported style properties
Colors β each surface is three int channels R / G / B (0β255); each XxxHex is a read-only #RRGGBB preview.
| Surface | Channel properties | Preview |
|---|---|---|
| Background | BgR BgG BgB |
BgHex |
| Body text | TextR TextG TextB |
TextHex |
| Link | LinkR LinkG LinkB |
LinkHex |
| Heading | HeadingR HeadingG HeadingB |
HeadingHex |
| Inline code text | CodeR CodeG CodeB |
CodeHex |
| Border | BorderR BorderG BorderB |
BorderHex |
| Secondary background | BgSecR BgSecG BgSecB |
BgSecHex |
| Secondary text | TextSecR TextSecG TextSecB |
TextSecHex |
| Inline-code background | CodeBgR CodeBgG CodeBgB |
CodeBgHex |
| Code block background | PreBgR PreBgG PreBgB |
PreBgHex |
| Table header background | TableBgR TableBgG TableBgB |
TableBgHex |
Typography & layout (double / int, in px unless noted)
| Property | Meaning |
|---|---|
CodeFontSize |
Code font size |
BorderRadius |
Code-block / pre corner radius |
MermaidBgR MermaidBgG MermaidBgB |
Mermaid container background (0β255) |
MermaidContainerPadding Β· MermaidContainerMargin Β· MermaidBorderRadius |
Mermaid container padding / margin / radius |
PumlBgR PumlBgG PumlBgB |
PlantUML container background (0β255) |
PumlContainerPadding Β· PumlContainerMargin Β· PumlBorderRadius |
PlantUML container padding / margin / radius |
PumlDarkInvert |
PlantUML dark invert strength (0β1) |
PlotBgR PlotBgG PlotBgB |
Function-plot container background (0β255) |
PlotContainerPadding Β· PlotContainerMargin Β· PlotBorderRadius |
Function-plot container padding / margin / radius |
PlotAxisColor |
Function-plot axis / grid / title colour (CSS colour string, code-only) |
Code highlighting β highlight.js token colors (string, e.g. "#569cd6"):
HljsKeyword, HljsLiteral, HljsSymbol, HljsName, HljsBuiltIn, HljsType, HljsClass, HljsNumber, HljsString, HljsMetaString, HljsTitle, HljsTitleClass, HljsTitleClassInherited, HljsParams, HljsVariable, HljsTemplateVariable, HljsComment, HljsQuote, HljsAttr, HljsAttribute, HljsMeta, HljsTag, HljsSelectorAttr, HljsSelectorClass, HljsSelectorId, HljsSelectorPseudo, HljsSelectorTag, HljsBullet, HljsSection, HljsLink, HljsRegexp, HljsTemplateTag, HljsDoctag, HljsBackground, HljsForeground β plus diff markers HljsAdditionBg/HljsAdditionColor and HljsDeletionBg/HljsDeletionColor.
The built-in
MarkdownThemeViewpanel covers colors, typography, and diagrams; code-highlighting tokens (Hljs*) andPlotAxisColorare configured in code (or bound in your own UI).
Working on the renderer
There is no unit test project and no CI. Verification is three environment-gated drivers in the demo app, each of which exits the process β enable exactly one (a guard fails fast if more than one is set):
| Command | Checks |
|---|---|
AVALON_THEME_AUDIT=1 dotnet run --project AvalonMarkdown.Test.Desktop |
every MarkdownThemeViewModel property reaches the generated CSS, and every generated rule has a live consumer |
AVALON_CAPTURE_ERRORS=1 dotnet run --project AvalonMarkdown.Test.Desktop |
drives the app through its phases; asserts fence dispatch and that the showcase assets load |
AVALON_RENDER_CHECK=1 AVALON_RENDER_CHECK_FILE=<abs path> dotnet run --project AvalonMarkdown.Test.Desktop |
renders one file through the real pipeline and DOM-audits it; normally driven by render_check.py |
Run without any of them for an ordinary visual check.
Authoring reference & validator
skills/avalonmarkdown-syntax/ documents the exact
Markdown dialect this control renders β every member with worked good/bad examples,
including the delimiter rules for math and the expression grammar for function plots.
references/templates.md holds
copy-paste-ready source for each member. Those snippets are not written from memory:
scripts/check_templates.py extracts every one of them, renders the result through the
real pipeline, and fails if anything breaks.
Installing it as a Claude Code skill
The folder is a valid Claude Code skill, so it works as-is for your projects β if you are generating Markdown that this control will render, install it into the project where the generation happens:
mkdir -p .claude/skills
cp -r <path-to>/AvalonMarkdown/skills/avalonmarkdown-syntax .claude/skills/
Claude Code then loads it automatically and consults it before writing Markdown.
It is deliberately not installed into this repository's own .claude/ β a
skill for authoring documents is not something the repository itself needs, and
committing an agent instruction file here would push it on everyone who clones.
It ships a validator that answers a question the preview cannot: will this actually
render? Most failures here are silent β KaTeX draws its own error span without
logging, a $$ block left open deletes the rest of the document, and a PlantUML
syntax error comes back as a renderable error picture that displays at full size
as if it were fine.
python skills/avalonmarkdown-syntax/scripts/render_check.py doc.md
Two tiers. A static pass re-derives the renderer's own dispatch rules offline and instantly. With a build present it also renders the file through the real WebView pipeline and inspects the DOM, which is the only way to know that a formula compiled or a diagram parsed.
python skills/avalonmarkdown-syntax/scripts/render_check.py --self-test
--self-test asserts every check fires against the known-bad fixtures in
skills/avalonmarkdown-syntax/fixtures/ β a validator with no known-bad sample
cannot be trusted, so the fixtures and the assertions are part of the deliverable.
Demo
The repository contains runnable sample apps (AvalonMarkdown.Test.Desktop and Android/iOS/Browser variants) that exercise both levels.
Its toolbar carries three special-member showcases alongside the general tour β
Tables, Diagrams and Plots. Each isolates one kind of element and runs a large
set of cases at it, so a regression in that member is obvious rather than buried:
- Tables β alignment, escaped pipes inside code spans, math and footnotes in cells, CJK and emoji, an eight-column layout, plus three shapes that must not become tables.
- Diagrams β 15 Mermaid types and 11 PlantUML types, with CJK and HTML labels, alongside a deliberately broken diagram of each kind so the failure appearance is documented next to the successes.
- Plots β the full Penner/easings.net set (sine through bounce,
in/out/inOut) plus non-y = f(x)graph types: parametric, polar, implicit, scatter, vectors.
They live as real .md files under AvalonMarkdown.Test.Shared/Assets/Showcase/
rather than C# string literals, so the validator checks the exact file the demo
renders. The capture driver asserts all three load, since a wrong asset key fails
silently into a fallback error block.
License
| 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 was computed. 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
- Avalonia (>= 12.0.0)
- Avalonia.Controls.WebView (>= 12.0.0)
- Avalonia.Fonts.Inter (>= 12.0.0)
- Avalonia.Themes.Fluent (>= 12.0.0)
-
net8.0
- Avalonia (>= 12.0.0)
- Avalonia.Controls.WebView (>= 12.0.0)
- Avalonia.Fonts.Inter (>= 12.0.0)
- Avalonia.Themes.Fluent (>= 12.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.