MdlibPure.Blazor #
Markdown editor with live preview and Markdown viewer for Blazor (.NET 10), in every render mode: Static SSR, Interactive Server, Interactive WebAssembly and Interactive Auto. Plus a C# service for the AST, the table of contents and HTML. Since version 0.1.1 it also includes a dedicated PodcastEditor with custom tags, participant completion, supporting documents and planned duration.
<MarkdownEditor @bind-Value="Body" Plugins="MarkdownPlugins.All" Style="block-size: 30rem" />
<Markdown Value="@Body" Plugins="MarkdownPlugins.All" />
Built on mdlib-pure: CommonMark 0.31.2 (652/652 spec examples), GitHub Flavored Markdown, math (KaTeX), Mermaid diagrams, containers, definition lists, highlights, front matter. Safe by default.
- The preview never waits for the server. Markdown is rendered in the browser on every keystroke; the C# value follows after a short typing pause (200 ms by default). Under Interactive Server, typing is never slowed down or overwritten by round trips.
- Same components in every render mode, Static SSR included: there the editor still works in the browser and posts its value with your form.
- No setup in the page: no
<script>, no<link>. Blazor loads the package's JavaScript initializer, which also adds the stylesheets. - KaTeX and Mermaid included in the package, loaded only by pages that contain math or diagrams.
- Nothing is rendered on the server: no SEO prerendering of Markdown, by design.
Contents: Install · Setup · Editor · Viewer · Podcast editor · Plugins · Render modes · AST service · Named configs · Styling · Security · Troubleshooting · Building from source
How it works #
The standard components wrap two Web Components of mdlib-pure, <mdlib-markdown> and
<mdlib-editor>. The Razor components render the element empty and only
ever set its attributes (source, value, plugins…); the element renders
the Markdown into itself, in the browser, with DOM APIs (no HTML parsing). The
browser upgrades custom elements wherever they appear: in static HTML, after
an enhanced navigation, in an interactive render. That is why one mechanism
works in all four render modes.
@bind-Value listens to the element's own input event, which the element
sends after a typing pause (Delay) with the whole text in
event.target.value. When Blazor sends the value back as the value
attribute while the user keeps typing, the element recognizes it as an echo
and ignores it.
Install #
Not on nuget.org yet: dotnet add package MdlibPure.Blazor alone finds
nothing. Until the first release there, use a local package feed. (The package
bundles mdlib-pure, which is not on npm either; you need no npm.)
-
Download
MdlibPure.Blazor.0.2.0.nupkgfrom the GitHub release (or build it, see Building from source) and put it in a folder, for examplepackages/next to your solution. -
Add a
nuget.confignext to your solution:<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="local" value="packages" /> </packageSources> </configuration> -
Reference the package from the project that contains your components:
dotnet add package MdlibPure.Blazor --version 0.2.0App Reference it from Blazor Web App, Static SSR / Interactive Server only the server project Blazor Web App with WebAssembly or Auto the .Clientproject (the server project gets it through its reference to.Client)Standalone Blazor WebAssembly the app project
The package depends only on Microsoft.AspNetCore.Components.Web.
PodcastEditor needs 0.1.1 or later: the older blazor-v0.1.0 release does not
contain it. The package includes the editor, plugin and styles. The consuming app
needs no Node build and no manually registered JavaScript.
Setup #
_Imports.razor
@using MdlibPure.Blazor
That is enough for the components. For the AST service,
register it in Program.cs, in the server project and, with WebAssembly or
Auto, in the .Client project too:
builder.Services.AddMdlibPure();
Interactive Server: the editor sends the whole document in each input
event, and SignalR refuses incoming messages over 32 KB by default (the
circuit disconnects). Raise the limit for documents of more than a few pages:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(o => o.MaximumReceiveMessageSize = 1024 * 1024); // 1 MB
Nothing to add to App.razor: Blazor loads
_content/MdlibPure.Blazor/MdlibPure.Blazor.lib.module.js by itself (the
"JS initializer" mechanism of Razor class libraries). It implements the hooks
of Blazor Web Apps (beforeWebStart/afterWebStarted, tested end to end) and
the classic ones of standalone WebAssembly apps and _Host.cshtml Blazor
Server apps (beforeStart/afterStarted, not covered by the tests).
<MarkdownEditor> #
A toolbar (bold, italic, heading, link, lists, quote, code; Ctrl+B / Ctrl+I /
Ctrl+K), the Markdown source and the live preview, side by side ("split"),
source only ("write") or preview only ("preview"). It derives from
InputBase<string?>, like InputText.
@* Interactive (Server, WebAssembly, Auto) *@
<MarkdownEditor @bind-Value="Body" Plugins="MarkdownPlugins.All" Style="block-size: 30rem" />
@code {
private string Body { get; set; } = "# Release notes";
}
Save while the user writes, after each typing pause:
<MarkdownEditor @bind-Value="Body" @bind-Value:after="SaveDraftAsync" Delay="1000" />
In an EditForm, with validation (the editor gets the usual modified,
valid/invalid classes and aria-invalid):
<EditForm Model="Article" OnValidSubmit="SaveAsync">
<DataAnnotationsValidator />
<MarkdownEditor @bind-Value="Article.Body" Plugins="MarkdownPlugins.Gfm" Label="Article body" />
<ValidationMessage For="() => Article.Body" />
<button type="submit">Save</button>
</EditForm>
| Parameter | Type | Default | |
|---|---|---|---|
Value / ValueChanged / ValueExpression |
string? |
Use @bind-Value. Value alone also works (no ValueExpression needed, unlike InputText). |
|
Plugins |
MarkdownPlugins |
None |
Preview syntax: see Plugins. |
Config |
string? |
Name of a named config. | |
View |
MarkdownEditorView |
Split |
Split, Write or Preview. The toolbar's buttons switch it in the browser. |
Delay |
int? |
200 | Milliseconds of typing pause before ValueChanged. On blur the value is sent at once. |
Label |
string? |
"Markdown" | Accessible name of the text area. |
Name |
string? |
Form field name. Inside an EditForm it defaults to the bound expression (Article.Body), like InputText. |
|
Class, Style |
string? |
On the element. Give the editor a height (block-size: 30rem): the panes fill it and scroll. |
|
InertUntilInteractive |
bool |
true |
See Prerendering. |
OnValueCommit |
EventCallback<string?> |
The user left the editor (blur), with the text at that moment; a pending ValueChanged is raised first. Interactive render modes. Like Angular's valueCommit. Since 0.2.0. |
|
| any other attribute | Passed to the element (id, aria-*, data-*…). |
Element (ElementReference?) is the <mdlib-editor> element, for your own JS interop.
Static SSR forms #
Without interactivity no event reaches .NET, but the element is a
form-associated custom element: its value is submitted with the form under
Name. With [SupplyParameterFromForm] and an EditForm, the name is
generated for you:
@page "/articles/edit"
<EditForm Model="Input" FormName="article" OnValidSubmit="Save" Enhance>
<MarkdownEditor @bind-Value="Input!.Body" Plugins="MarkdownPlugins.All" />
<button type="submit">Save</button>
</EditForm>
@code {
[SupplyParameterFromForm(FormName = "article")]
private ArticleInput? Input { get; set; }
protected override void OnInitialized() => Input ??= new() { Body = "# Draft" };
private void Save() { /* Input.Body holds the posted Markdown */ }
public sealed class ArticleInput { public string Body { get; set; } = ""; }
}
Enhanced (Enhance) and full-page form posts both work. In a plain <form>
use <MarkdownEditor Value="@Body" Name="body" />. Note: like a <textarea>,
a full-page (non-enhanced) post sends line breaks as CRLF.
<Markdown> #
A read-only viewer.
<Markdown Value="@Article.Body" Plugins="MarkdownPlugins.Gfm | MarkdownPlugins.Math" class="article" />
| Parameter | Type | Default | |
|---|---|---|---|
Value |
string? |
The Markdown. | |
Plugins |
MarkdownPlugins |
None |
See Plugins. |
Config |
string? |
Name of a named config. | |
Class |
string? |
Extra classes; the element always has md, the class the theme styles. |
|
| any other attribute | Passed to the element. |
Plugins #
MarkdownPlugins is a [Flags] enum: combine values with |, or use All.
None (the default) is plain CommonMark.
| Flag | Syntax |
|---|---|
Gfm |
GitHub Flavored Markdown: tables, ~~strikethrough~~, autolinks (www.example.com), task lists (- [x]), footnotes ([^1]), alerts (> [!NOTE]) |
Headings |
GitHub-style heading ids (## Next steps → id="next-steps"), for anchors and the TOC |
Math |
$inline$, $$display$$, typeset by KaTeX |
Mermaid |
```mermaid code blocks, drawn by Mermaid, colored from the theme (light and dark) |
Container |
::: warning Title … ::: (note, tip, important, warning, caution, or any word) |
DefinitionList |
Term then : Definition |
Mark |
==highlight== |
FrontMatter |
YAML (---) or TOML (+++) at the top, kept out of the output |
All |
all of the above |
KaTeX (~540 KB) and Mermaid (~5 MB, split in chunks per diagram type) ship inside the package and are downloaded only by pages whose Markdown contains math or a diagram.
The podcast sample is
available through the dedicated PodcastEditor below. It is not a flag in
MarkdownPlugins.All; named configs still do not register additional syntax plugins.
<PodcastEditor> #
Available since 0.1.1, with the same editor and parser as the
public sample. The full episode
is bound as a typed PodcastEpisode, including separate supporting documents:
<PodcastEditor @bind-Value="Episode" Participants="People" Delay="300" />
<p>Planned time: @Episode.PlannedDuration</p>
@code {
private string[] People = ["Nora", "Elio", "Vera"];
private PodcastEpisode Episode { get; set; } = new()
{
Source = "# An imaginary garden\n\n[ ] @Nora Introduction [](time:5:00:00)\n\n[scheda](Notes)",
Documents = new() { ["Notes"] = "@Elio Ideas for the garden." }
};
}
The toolbar inserts @Name, [](time:M:SS:CC), [scheda](name) and checkboxes,
alongside ordinary formatting. _text_ underlines. Links open new browser tabs;
document buttons switch the editor's document. Names are configurable, with no
application-specific names embedded in the package. The default sample names
are fictional. Participant names contain 1–32 letters, digits, underscores or
hyphens; duplicates are ignored without case sensitivity (up to 100 names).
| Parameter | Behavior |
|---|---|
Value / ValueChanged / ValueExpression |
PodcastEpisode; use @bind-Value, also inside an EditForm. |
Participants |
string[]; updates both badges and completion, independently for each instance. |
View |
MarkdownEditorView.Split by default; also Write and Preview. |
Delay |
Typing pause before C# updates, 200 ms by default; pending changes flush on blur. |
Name |
Form field for the entire episode serialized as JSON. |
Label, Class, Style, InertUntilInteractive |
As for MarkdownEditor. Style controls the host layout; see the theme example below. |
| Additional attributes | Passed to the host; disabled disables editing and form submission. |
Grammar |
PodcastGrammar, read when the element connects: Durations (Centiseconds, the sample's M:SS:CC, or Clock: m:ss, h:mm:ss, 5m, 90s, with titles [intro](time:04:30)), Tags (Mentions: Participants or Any; Hashtags), LineBlocks (one source line = one block), LooseTasks ([], [ x ], -[]). |
TagColors |
Fixed tag colours by name, compared ignoring case (e.g. the participants' profile colours); otherwise #name(color:green) in the text, otherwise a hue hashed from the name. Live. |
Tasks |
PodcastTaskMode.Edit (default: a click rewrites the source) or Notify: the source is untouched and OnTaskToggle receives PodcastTaskEventArgs (Document, Id, Checked, Line, Box, lines and boxes 0-based); the host changes Value. |
Documents |
PodcastDocumentMode.Local (default: tabs over PodcastEpisode.Documents) or Host: no tabs, a document button only raises OnOpenDocument (PodcastDocumentEventArgs: Name, Title) and the toolbar inserts [name](doc:name). |
ReadOnly |
Preview only: toolbar and source hidden, checkboxes and document buttons still live. Live. |
Compact |
No header, legend, status bar or pane labels: the editor fits a column. Read when the element connects. |
CurrentLine |
0-based source line of the current point, highlighted in the preview (.is-current). Live. |
RunningTimer |
(Line, Index): the timer chip to show as running (.is-running). Live. The element attribute running-timer also takes a third field, line:index:text, shown in place of the planned value while the timer runs (a countdown written by the host's own script; .is-over when it starts with a minus). |
OnTaskToggle, OnOpenDocument, OnActiveDocumentChanged |
Callbacks of the three modes above; OnActiveDocumentChanged follows the tabs in Local mode. |
OnValueCommit |
EventCallback<PodcastEpisode>: focus left the editor (moves inside it, such as text area → toolbar, do not count); a pending ValueChanged is raised first. Since 0.2.0. |
Hosted use, for an application with its own rundown server (the demo's "Hosted editor" section does the same):
<PodcastEditor @bind-Value="Episode" Participants="Members" Grammar="Grammar" TagColors="Colors"
Tasks="PodcastTaskMode.Notify" Documents="PodcastDocumentMode.Host" Compact="true"
CurrentLine="CurrentLine" RunningTimer="(CurrentLine, 0)"
OnTaskToggle="ToggleAsync" OnOpenDocument="e => OpenPrompterAsync(e.Name)"
Style="--podcast-height: 60vh" />
@code {
private static readonly PodcastGrammar Grammar = new() { Durations = PodcastDurations.Clock, LineBlocks = true, LooseTasks = true, Tags = new() { Mentions = PodcastMentions.Any, Hashtags = true } };
private Task ToggleAsync(PodcastTaskEventArgs e) => Api.PostAsync("rundown/toggle", new { e.Line, e.Box, e.Checked });
}
The custom DOM events behind the callbacks (podcast-task, podcast-document,
podcast-active) are registered by the package's JS initializer, so nothing is
needed in the app.
PodcastEpisode has Source (main rundown), Documents (a dictionary of
supporting documents), ActiveDocument ("Scaletta" for the main rundown),
DurationCentiseconds and the computed PlannedDuration (TimeSpan). The browser
calculates the initial duration and updates it after edits; only main-rundown
time tags count. Documents use the sample's name rules.
Use Episode.ToJson() and PodcastEpisode.FromJson(json) to save and restore
the complete state. JSON uses generated serialization metadata (trimming/AOT
safe). The component has no implicit localStorage persistence: your application
owns saving, for example with @bind-Value:after="SaveAsync". The browser preview
updates immediately, independently of Interactive Server round trips.
In Static SSR, post JSON into a string form field, then call
PodcastEpisode.FromJson. Do not bind a JSON field directly to a complex
[SupplyParameterFromForm] property:
<PodcastEditor Value="Episode" Name="Input.Json" />
The demo's /podcast/ssr page contains a complete enhanced EditForm example.
The /podcast/server, /podcast/wasm and /podcast/auto pages demonstrate typed
binding, participant changes, replacement from C#, mounting/unmounting and two
independent instances. All four render modes are tested in Chromium.
The package-local <mdlib-podcast-editor> uses Shadow DOM to isolate IDs and
styles between instances. Customize its semantic tokens through the exposed part:
mdlib-podcast-editor::part(editor) {
--podcast-accent: #b8e986;
--podcast-speaker-first: #a5efbd;
--podcast-height: 60vh; /* panes height (default 650px) */
--podcast-font-size: 18px; /* preview text (default 15px) */
}
Its connected/disconnected lifecycle mounts and cleans
up the shared editor; styles load automatically when a podcast editor appears.
The standard MarkdownEditor, Markdown and IMarkdownService retain their
existing grammar. Use PodcastEditor for these custom tags.
Render modes #
| Static SSR | Interactive Server | Interactive WebAssembly | Interactive Auto | |
|---|---|---|---|---|
<Markdown> viewer |
✓ | ✓ | ✓ | ✓ |
| Editor preview while typing | ✓ live, in the browser | ✓ live | ✓ live | ✓ live |
C# Value updated |
on form submit (Name) |
after the typing pause, over the circuit | after the typing pause, in the browser | as Server, then WebAssembly |
@bind-Value, EditForm validation |
form post + server validation | ✓ | ✓ | ✓ |
IMarkdownService |
✗ throws InvalidOperationException |
✓ | ✓ | ✓ |
| Enhanced navigation / enhanced forms | ✓ | ✓ | ✓ | ✓ |
All of the above is covered by end-to-end tests in Chromium
(test/blazor/e2e.blazor.ts in the repository).
Enhanced navigation. Blazor patches the page in place after an enhanced
navigation or form post, which would delete what the elements rendered into
themselves. The components mark the elements data-permanent (Blazor's
documented opt-out): their content is kept, their attributes are still
updated, so a new Value re-renders.
Prerendering #
Interactive components are prerendered by default: the page shows the editor
before it is interactive, and Blazor replaces that markup when the
component becomes interactive (after the circuit connects, or after the
WebAssembly download). Anything typed in between would be lost, so while
prerendered the editor is inert: visible, rendered, but not focusable. Set
InertUntilInteractive="false" to turn that off, or disable prerendering on
the page (@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))).
AST service: IMarkdownService #
public interface IMarkdownService
{
Task<MarkdownNode> ParseAsync(string markdown, MarkdownPlugins plugins = None, string? config = null, CancellationToken ct = default);
Task<IReadOnlyList<TocEntry>> GetTocAsync(string markdown, MarkdownPlugins plugins = None, string? config = null, CancellationToken ct = default);
Task<string> RenderHtmlAsync(string markdown, MarkdownPlugins plugins = None, string? config = null, CancellationToken ct = default);
}
The work is done by mdlib-pure in the browser, through JavaScript interop:
the service needs an interactive render mode, and must be called once the
component is interactive (an event handler, or OnAfterRenderAsync). In
Static SSR or during prerendering it throws an InvalidOperationException
that says so. Results travel as a stream, so large documents are fine under
Interactive Server too (limit: AddMdlibPure(o => o.MaxResultSize = …),
16 MB by default).
@inject IMarkdownService MarkdownService
<button @onclick="BuildTocAsync">Table of contents</button>
<ol>
@foreach (var h in _toc)
{
<li style="margin-inline-start: @(h.Depth - 1)em"><a href="#@h.Id">@h.Text</a></li>
}
</ol>
@code {
private IReadOnlyList<TocEntry> _toc = [];
private async Task BuildTocAsync() =>
_toc = await MarkdownService.GetTocAsync(Body, MarkdownPlugins.Gfm);
}
GetTocAsync always enables the Headings plugin, so the ids match a
<Markdown> viewer that has MarkdownPlugins.Headings.
MarkdownNode is an mdast-style tree:
var root = await MarkdownService.ParseAsync(Body, MarkdownPlugins.All);
var links = root.Descendants()
.Where(n => n.Type == "link")
.Select(n => (Url: n.GetString("url"), Text: n.TextContent()));
var images = root.Descendants().Count(n => n.Type == "image");
var openTasks = root.Descendants().Count(n => n.Type == "listItem" && n.GetBoolean("checked") == false);
var level2 = root.Descendants().Where(n => n.Type == "heading" && n.GetInt32("depth") == 2);
| Member | |
|---|---|
Type |
root, paragraph, heading, text, emphasis, strong, link, image, code, inlineCode, list, listItem, blockquote, table, math, inlineMath, mermaid, container, footnoteDefinition… |
Value |
literal content of leaves (text, code, inlineCode, math…), else null |
Children |
child nodes, else null |
Properties |
every other property as JsonElement (depth, url, title, lang, ordered, checked, id…) |
GetString, GetInt32, GetBoolean, TryGetProperty |
typed access to Properties |
Descendants(), DescendantsAndSelf() |
depth-first, document order (not recursive: deep documents are fine) |
TextContent() |
the text a reader sees |
Soft line breaks are \n inside text values. Node types and properties are
described in mdlib-pure's docs/api.md and in each plugin's source header.
RenderHtmlAsync returns the same HTML as the viewer, before KaTeX and
Mermaid run (math stays as \(…\), diagrams as <pre class="mermaid">).
Named configs (highlighting, URL policy) #
Some mdlib-pure options are JavaScript functions: syntax highlighting
(highlight), the URL policy (allowUrl), extra attributes per node
(attributes), CSS classes per node type (classes). They are registered in
JavaScript under a name and selected with Config="name" (components) or the
config argument (service). Put them in your app's own JS initializer,
wwwroot/<YourAssemblyName>.lib.module.js (in a Blazor Web App, the server
project's wwwroot); Blazor loads it by itself:
// wwwroot/MyApp.lib.module.js
export async function beforeWebStart() {
const { defineElements } = await import('./_content/MdlibPure.Blazor/dist/mdlib-element.js')
defineElements({
configs: {
docs: {
// External links open in a new tab.
attributes: (node) => (node.type === 'link' && /^https?:/i.test(node.url) ? { target: '_blank', rel: 'noopener noreferrer' } : null),
// Only links to our own domains (default: relative, http(s), mailto, tel, sms).
allowUrl: (url, kind) => url.startsWith('/') || /^https:\/\/([a-z0-9-]+\.)*example\.com\//i.test(url),
},
},
})
}
// Standalone WebAssembly / Blazor Server apps: name the function beforeStart instead.
<MarkdownEditor @bind-Value="Body" Config="docs" />
The demo app does exactly this (samples/…/wwwroot/MdlibPure.Blazor.Demo.lib.module.js).
Options reference: mdlib-pure's docs/api.md. The html option can only be
enabled here too, never from Razor attributes; see Security.
Styling #
The JS initializer adds mdlib-pure's theme.css (the Markdown typography) and
editor.css (toolbar and panes) as adopted stylesheets, unless the page
already has them. The viewer and the preview have the class md.
-
Customize with the
--md-*tokens in your own CSS (see mdlib-pure'sdocs/tokens.md)::root { --md-color-link: #0f6cbd; --md-font-heading: "Segoe UI", sans-serif; } -
Dark mode follows
color-scheme: add<meta name="color-scheme" content="light dark">or:root { color-scheme: light dark; }. Neutral colors derive from the text color. -
Your own copy (bundled, Tailwind via mdlib-pure's
tailwind.css…): the initializer detects it and adds nothing. Or link the package's files yourself:_content/MdlibPure.Blazor/dist/theme.cssand…/dist/editor.css. -
No stylesheets at all:
<meta name="mdlib-pure-styles" content="none">inApp.razor's<head>.
All rules are in @layer mdlib with zero specificity: any rule of yours wins.
PodcastEditor loads its own shadow-root styles, independently of the document
stylesheet opt-out above. Customize those with ::part(editor) as shown in its section.
Security #
- Raw HTML is off:
<script>,<iframe>,onerror=… in the Markdown are shown as text, never interpreted. It cannot be turned on from Razor; only a named config (html: true, ideally withsanitize) can, for trusted content. - URL allowlist: every
href/srcgoes through it. Allowed: relative URLs,http:,https:,mailto:,tel:,sms:, anddata:image/png|gif|jpeg|webpfor images.javascript:and other schemes lose their URL. - Markdown content is never parsed as HTML: the renderers build DOM nodes. PodcastEditor clones a source-controlled static UI template; episode contents, names and documents are assigned through safe DOM APIs.
- Mermaid runs with
securityLevel: 'strict'(no HTML labels, no click handlers). KaTeX runs with its defaults (trust: false: no\hreflinks, no\includegraphics). - Heading ids (
Headingsplugin) come from the content: a document can create an element withid="config", which browsers expose aswindow.config("DOM clobbering"). With untrusted authors, do not rely on global variables named like possible headings, or leaveHeadingsoff. - Interactive Server:
IMarkdownServiceresults come from the browser. Treat them like any client input. Never storeRenderHtmlAsyncoutput to serve it to other users: store the Markdown, and render it on display with<Markdown>(each reader's browser renders it safely). - Content-Security-Policy: everything is served from your origin (no
CDN, no
eval). Not tested under a strict policy: Mermaid inserts<style>elements into its SVGs, which astyle-srcwithout'unsafe-inline'blocks.
More: mdlib-pure's docs/security.md.
Troubleshooting #
| Symptom | Cause |
|---|---|
Build error MDLIB001: the browser assets are missing |
Building from the repository without the generated assets: run npm ci and npm run blazor:assets at the repository root. |
Elements stay empty, 404 for _content/MdlibPure.Blazor/… |
The package is not referenced by the project that serves static assets (WebAssembly: reference it from .Client), or app.MapStaticAssets() is missing. |
| Interactive Server disconnects with long documents | Raise MaximumReceiveMessageSize (see Setup). |
InvalidOperationException: IMarkdownService needs JavaScript interop… |
Called in Static SSR or during prerendering: call it from an event handler or OnAfterRenderAsync of an interactive component. |
| The editor cannot be focused for a moment after the page loads | It is inert until the component is interactive: see Prerendering. |
| Text typed in the editor reverts | Some code sets Value to an older value while the user is not typing in the editor (for example after a button click): the element applies values that are not echoes of its own. |
Building from source #
This folder lives in the mdlib-pure repository (bindings/blazor/):
bindings/blazor/
src/MdlibPure.Blazor/ the Razor class library (the NuGet package)
wwwroot/MdlibPure.Blazor.lib.module.js JS initializer
wwwroot/dist/ generated by `npm run blazor:assets` (gitignored)
Scripts/mdlib-service.js source of dist/mdlib-service.js (IMarkdownService)
samples/MdlibPure.Blazor.Demo/ Blazor Web App: one page per render mode
tests/MdlibPure.Blazor.Tests/ bUnit tests
From the repository root (Node ≥ 22.18 and the .NET 10 SDK):
npm ci
npm run blazor:assets # bundles src/ + styles/ + KaTeX + Mermaid into wwwroot/dist/
dotnet build bindings/blazor/MdlibPure.Blazor.slnx
(cd bindings/blazor && dotnet test) # bUnit; global.json there selects Microsoft.Testing.Platform
npx playwright install chromium # once
npm run test:blazor # end-to-end: builds and starts the demo, drives every render mode
dotnet pack bindings/blazor/src/MdlibPure.Blazor -c Release -o packages # → packages/MdlibPure.Blazor.0.2.0.nupkg
Run the demo: dotnet run --project bindings/blazor/samples/MdlibPure.Blazor.Demo/MdlibPure.Blazor.Demo,
then open http://localhost:5088.
Run npm run blazor:assets again after changing mdlib-pure's src/ or
styles/, templates/podcast/ or site/assets/podcast.css. The npm package itself stays zero-dependency: KaTeX and Mermaid are
dev dependencies of the repository, shipped only inside the NuGet package.
License #
MIT © Michele Polo. The package's wwwroot/dist/ bundles KaTeX (MIT), Mermaid
(MIT) and their dependencies: see wwwroot/dist/THIRD-PARTY-NOTICES.txt in
the package (_content/MdlibPure.Blazor/dist/THIRD-PARTY-NOTICES.txt in an app).