markdownviewer

package module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

README

MarkDownViewer

A modern, embeddable Markdown viewer library for Go. Feed it Markdown, get back self-contained, themed, sanitized HTML — no CDN calls, no external assets, safe to render untrusted input by default. Built around a document model that sits between parsing and rendering, so the HTML renderer shipped today and native renderers planned for later both consume the same typed AST.

Status

v0.x — the API is not yet frozen. The document model is designed to become the stable contract for future renderers and bindings; it now carries block-level source spans, pinned Kind values, and a versioned JSON codec, but hasn't earned frozen-API status yet — expect further additive evolution before a v1.0 that commits to compatibility. See docs/Design.md for the architecture and roadmap, and CHANGELOG.md for release notes.

Features

Feature Notes
CommonMark + GFM 652/652 official CommonMark spec examples verified, plus GFM extras (tables, strikethrough, task lists, autolinks)
Footnotes [^1]-style references and definitions
Heading anchors Stable id attributes for deep linking
Front-matter YAML front-matter parsed as document metadata
Emoji :shortcode: support
Definition lists Term / : Description syntax
Admonitions / callouts > [!NOTE]-style blocks
Wiki-links [[Page]] / [[Page|Text]]
TeX math Inline and block math via KaTeX, rendered offline
Mermaid diagrams Flowcharts, sequence diagrams, etc., rendered offline
Syntax highlighting chroma, pure Go, 250+ languages
Themes Built-in light / dark / auto, with CSS custom-property overrides or a full stylesheet swap
Safe by default HTML sanitized via bluemonday, URL scheme allowlist, opt-in AllowRawHTML() escape hatch
Host-controlled resolution Pluggable Resolver for rewriting link/image/wiki-link targets
Offline output Every asset (KaTeX, mermaid, fonts) is embedded — rendered HTML has zero external dependencies
Source map Opt-in data-md-line attributes (WithSourceMap()) for editor↔preview scroll sync
JSON document tree document.MarshalJSON/UnmarshalJSON — versioned wire format with pinned Kind names
Native render tree render/tree — a resolved, layout-free semantic tree (version-1 JSON) for widget-native hosts; see "Native rendering" below

Which surface do I use?

One rendering core, five ways in — same output, same options:

Surface Artifact Render tree Use when
Go package go get github.com/sriannamalai/markdownviewer render/tree's tree.Build You're writing Go — the richest API (functional options, document AST, streaming RenderTo)
CLI cmd/mdview (go install) — (HTML only) One-off conversions, shell pipelines, editor "open preview" hooks
C ABI libmdviewer-<ver>-<os>-<arch>.zip (release asset) mdv_render_tree* Any language with a C FFI — desktop apps (Swift, C#, Rust, Python, ...)
WASM libmdviewer-<ver>-wasm.zip (npm-ready ESM) renderTree/renderTreeDoc Browsers and Node — no native binary allowed or wanted
Flutter plugin flutter/mdviewer (path/git dependency) renderTree + MdvDocumentView widgets Flutter mobile apps — typed Dart API over the C ABI, binaries fetched per release

Every surface renders the same sanitized HTML; since v0.10 all but the CLI can alternatively produce the native render tree for widget-native (non-webview) rendering — see "Native rendering" below.

Install

go get github.com/sriannamalai/markdownviewer

Requires Go 1.26+.

Quick start

package main

import (
	"fmt"

	markdownviewer "github.com/sriannamalai/markdownviewer"
)

func main() {
	html, err := markdownviewer.Render([]byte("# Hi\n"))
	if err != nil {
		panic(err)
	}
	fmt.Println(string(html))
}

Options compose as functional options on Render / RenderTo:

out, err := markdownviewer.Render(
	src,
	markdownviewer.WithTheme("dark"),
	markdownviewer.Fragment(),        // body-only HTML, no <html>/<head> wrapper
	markdownviewer.AllowRawHTML(),    // trust the input; disables sanitization
	markdownviewer.DisableMath(),     // skip KaTeX
	markdownviewer.DisableHeadingAnchors(), // headings without slug ids
)

Which Markdown syntax extensions the parser enables is a separate, composable axis — parser.Config (every field independently toggleable), selected via WithParserConfig for Render, or ParseWith when parsing directly:

import "github.com/sriannamalai/markdownviewer/parser"

cfg := parser.CommonMarkOnly() // zero Config: no extensions
cfg.Tables = true              // ... plus GFM tables
out, err := markdownviewer.Render(src, markdownviewer.WithParserConfig(cfg))

parser.Default() is everything on (tables, strikethrough, task lists, linkify, footnotes, definition lists, front matter, emoji, wiki-links, math, admonitions); start from either preset and flip individual fields. The same toggles ride the options JSON to every non-Go surface as the nested parser object, alongside headingAnchors — see ffi/README.md's Options JSON table.

If you only need the parsed document model — for example to build your own renderer — use Parse:

doc, err := markdownviewer.Parse(src)

document.Document and its node types are documented in the document package and are the intended long-term contract: renderers only ever depend on it, never on parser internals. It is not API-frozen yet — see Status above and the roadmap in docs/Design.md.

Hosts that need to resolve relative paths, rewrite wiki-links, or route images through an asset pipeline can supply a Resolver:

out, err := markdownviewer.Render(src, markdownviewer.WithResolver(
	func(kind markdownviewer.ResolveKind, target string) (url string, ok bool) {
		if kind == markdownviewer.ResolveWikiLink {
			return "/wiki/" + target, true
		}
		return "", false // fall back to default handling
	},
))

Trust contract: URLs a Resolver returns with ok=true are emitted as-is, without scheme filtering. The library assumes hosts fully control resolution and will not echo untrusted targets back unexamined — see SECURITY.md.

Editor integration

Live-preview hosts (an editor pane rendering Markdown as you type) tend to need two things: a way to map rendered DOM back to source lines, and a way to avoid re-parsing on every keystroke or theme flip.

Scroll sync via WithSourceMap(). Top-level block elements (and footnote <li>s) get a data-md-line="<n>" attribute pointing at the 1-based source line the block started on:

out, err := markdownviewer.Render(src, markdownviewer.WithSourceMap())
<h1 data-md-line="1">Title</h1>
<p data-md-line="3">Some text.</p>

An editor can scroll the preview to the block whose data-md-line is closest to the cursor line, or do the reverse on click. Two kinds of output are deliberately left unannotated because the renderer doesn't own the markup it emits for them: raw HTML blocks, and chroma-highlighted code blocks (chroma emits its own <pre>/<span> structure).

Parse once, render many with RenderDoc. Re-parsing on every theme switch is wasted work when the source hasn't changed — parse once with Parse, then render the same tree repeatedly with RenderDoc/RenderDocTo:

doc, err := markdownviewer.Parse(src)
if err != nil {
	panic(err)
}

light, err := markdownviewer.RenderDoc(doc, markdownviewer.WithTheme("light"))
dark, err := markdownviewer.RenderDoc(doc, markdownviewer.WithTheme("dark"))

doc must not be mutated while a RenderDoc/RenderDocTo call is reading it — see the Concurrency section below.

CLI usage

The mdview command renders a file or stdin to HTML. Flags come before the positional file argument:

mdview -o out.html README.md          # file to file
mdview -theme dark README.md          # force dark theme
cat notes.md | mdview -fragment       # stdin to stdout, body-only fragment
mdview -unsafe notes.md               # trust the input: raw HTML, all schemes
mdview -width 860px README.md         # constrain content width (default is fluid)

Full flag list: -o FILE (default stdout), -theme light|dark|auto (default auto), -fragment, -unsafe, -no-mermaid, -no-math, -no-highlight, -width STRING (any CSS length, e.g. 860px or 70ch; default is fluid — no max-width).

Running mdview with no file argument and no piped input (stdin is an interactive terminal) prints name/version, flag defaults, and a few example invocations instead of blocking on a read that would never complete. Piped or redirected stdin (cat notes.md | mdview) is unaffected — that still renders normally.

Concurrency

All top-level functions (Parse, ParseWith, Render, RenderTo, RenderDoc, RenderDocTo, ParseContext, RenderContext, RenderDocContext) are safe for concurrent use — they share no mutable state beyond two package-level values, constructed once and never mutated afterward: bluemonday's sanitizer Policy (documented safe to Sanitize concurrently once constructed) and chroma's HTML formatter (internally mutex-protected for concurrent Format calls). The one thing that isn't synchronized: a *document.Document returned by Parse/ParseWith must not be mutated while it's being read by a concurrent Render/RenderTo/RenderDoc/RenderDocTo call, or by another mutation — document.Document has no internal locking of its own.

The Context variants extend this contract rather than relaxing it: when ctx ends before the underlying work finishes, the function returns ctx.Err() immediately, but the abandoned goroutine may still be reading src (ParseContext/RenderContext) or doc (RenderDocContext) for an unbounded window afterward — there's no signal for when it actually stops. Getting ctx.Err() back is not a guarantee those inputs are safe to mutate or reuse; treat src/doc passed to a Context variant as immutable for the lifetime of the call, and don't assume that lifetime has ended just because the call returned. ParseContext/RenderContext/RenderDocContext bound caller-observed latency, not CPU spend — the Markdown engine has no cancellation hooks, so an abandoned parse/render keeps running to completion. See SECURITY.md for the resource-exhaustion background.

Theming

Fragment output (Fragment() / mdview -fragment) contains no CSS or JS — it's body-only markup. The host owns the page and must supply its own styling; theme.BaseCSS() and the theme CSS-variable sets below are there to reuse if useful. Diagram and math nodes still emit their markup in fragment mode (<pre class="mermaid">…</pre>, <span class="math …">…), but as inert placeholders: without mermaid.js/KaTeX loaded, a viewer sees the raw diagram/math source until the host supplies those libraries itself.

Fragment mode emits no CSS/JS. To activate mermaid/math in your own page:

import "github.com/sriannamalai/markdownviewer/assets"
// once per page:
fmt.Fprintf(w, "<style>%s</style><script>%s</script><script>%s</script>",
    assets.KatexCSS(), assets.KatexJS(), assets.MermaidJS())

Fragment hosts also need the syntax-highlighting stylesheet for code blocks — htmlrender.HighlightCSS(theme.Light()) / (theme.Dark()) in Go, or the composed theme-light.css / theme-dark.css assets over the C ABI, which bundle the theme tokens and highlight CSS together.

Built-in themes are CSS custom-property sets layered over one base stylesheet: light, dark, and auto (light by default, with a prefers-color-scheme media query for dark). Override individual variables without replacing the whole stylesheet:

out, err := markdownviewer.Render(src, markdownviewer.WithThemeOverrides(map[string]string{
	"--md-bg":     "#f8f8f2",
	"--md-accent": "#ff79c6",
}))

Or replace the base stylesheet entirely:

out, err := markdownviewer.Render(src, markdownviewer.WithStylesheet(myCSS))

Or append CSS after whatever base is in effect — the built-in base+theme styling by default, or a WithStylesheet replacement — without replacing it (contrast with WithStylesheet, which swaps the base out entirely):

out, err := markdownviewer.Render(src, markdownviewer.WithExtraCSS(
	".md-code { border-radius: 8px; }"))

All three are emitted into the page's <style> element; </style sequences in supplied content are stripped defensively.

Code blocks can opt into a header row carrying the fence language and a Copy button:

out, err := markdownviewer.Render(src, markdownviewer.WithCodeHeader())

Full pages also get a small inline clipboard script wiring the buttons; fragment hosts receive the markup only and wire their own click handler. Note the inline script uses navigator.clipboard, which needs a secure context — webviews fed via loadHtmlString/srcdoc are not one, so embedded hosts should bridge the button to a native clipboard call instead: a capture-phase click listener on .md-code-copy posting the code text over a platform channel. See "codeHeader in webviews" in flutter/mdviewer/README.md for the worked recipe.

The default layout is fluid — no max-width constraint, so the page fills its container. Opt in to a constrained width with WithMaxWidth (any CSS length, e.g. "860px" or "70ch"), implemented as a --md-max-width theme override:

out, err := markdownviewer.Render(src, markdownviewer.WithMaxWidth("860px"))

An empty string (the default) stays fluid. Since the value flows into a CSS custom-property declaration, one containing ; or } is rejected defensively and the option no-ops rather than emitting a defanged value.

Embedding from other languages

Every release ships prebuilt C-shared libraries (libmdviewer) for macOS (arm64/x86_64), Linux (amd64/arm64), and Windows (amd64) — see the release's libmdviewer-*.zip assets. Thirteen thread-safe symbols — this is unchanged by the resolver callback, which runs synchronously on the calling thread: mdv_render (Markdown → HTML), mdv_render_r (same, plus a host Resolver callback), mdv_parse (Markdown → versioned document-AST JSON), mdv_render_doc (AST JSON → HTML, for parse-once/render-many), mdv_render_doc_r (same, plus a Resolver callback), mdv_render_tree / mdv_render_tree_r / mdv_render_tree_doc / mdv_render_tree_doc_r (Markdown or AST JSON → the native render tree, see "Native rendering" below), mdv_asset (embedded assets: mermaid/KaTeX bundles, theme+highlight CSS, the theme/highlight JSON palettes), mdv_alloc (library-heap allocator, for the resolver's returned URL), mdv_free, and mdv_version. Options cross the boundary as a small JSON object mirroring this package's functional options. Fragment-mode hosts can pull the embedded mermaid/KaTeX bundles and per-theme highlight CSS over the boundary via mdv_asset (v0.5); Go fragment hosts get the same via the assets package and htmlrender.HighlightCSS.

char *html = NULL, *err = NULL; size_t n = 0;
if (mdv_render(md, strlen(md), "{\"theme\": \"dark\"}", &html, &n, &err) == 0) {
    fwrite(html, 1, n, stdout);
    mdv_free(html);
} else {
    fprintf(stderr, "%s\n", err);
    mdv_free(err);
}

Each release zip bundles ffi/README.md as README.md alongside the library and header — that's the API reference (ownership: every returned buffer is freed with mdv_free). Working consumers: examples/c/ (the CI-gated harness) and examples/dart/ (dart:ffi, the pattern a Flutter host uses). To build locally: ./scripts/build-ffi.sh.

Browser and Node hosts get the same rendering over WebAssembly instead of a C ABI: every release also ships libmdviewer-<version>-wasm.zip, an npm-ready ESM package (import { loadMdviewer } from 'libmdviewer') with no bundler or native dependency required. See wasm/npm/README.md for the JS API, and ./scripts/build-wasm.sh to build locally.

Flutter/mobile hosts get a plugin, flutter/mdviewer, over the same thirteen-symbol C ABI instead of a new binding layer — static on iOS, c-shared on Android, consumed via dart:ffi with NativeCallable for the Resolver callback. Every release also ships the two mobile artifacts the plugin's tool/fetch_binaries.sh pulls down: libmdviewer-<version>-ios.xcframework.zip and libmdviewer-<version>-android.zip. See flutter/mdviewer/README.md for the Dart API, and ./scripts/build-mobile.sh to build locally.

Native rendering: the render tree

HTML into a webview is one way to display Markdown; since v0.10 the library also renders to a native render tree — a layout-free, fully resolved semantic tree (strict version-1 JSON) that widget-native hosts (Flutter, SwiftUI, Compose, ...) walk and render as platform widgets, no webview involved. "Resolved" is the point: URL policy and resolver rewriting, raw-HTML sanitizing, admonition titles, footnote pairing, and math/mermaid fallbacks are all applied library-side — through the same shared code as the HTML renderer, differential-tested for text parity — so a host styles nodes and never re-implements policy. Code blocks carry chroma token runs ([{text, tokenType}]), and the highlight-light.json / highlight-dark.json assets map token types to colors, generated from the same chroma styles as the CSS. Every block has a content-hash id for host-side diffing (byte-identical blocks share an id by design — key by (id, occurrenceIndex)).

In Go:

import "github.com/sriannamalai/markdownviewer/render/tree"

doc, _ := markdownviewer.Parse(src)
t, err := tree.Build(doc, tree.Options{
	HeadingAnchors: true,
	Highlighting:   true,
	Math:           true,
	Mermaid:        true,
	Source:         src, // enables content-hash block ids
})
if err != nil {
	log.Fatal(err)
}
wire, _ := json.Marshal(t) // the version-1 wire JSON

In Flutter, the plugin pairs the typed model with a ready-made widget renderer (MdvDocumentView — selectable text, token-run code with a native copy button, native KaTeX math via flutter_math_fork, async image resolution, tap callbacks):

final tree = Mdviewer.instance.renderTree(markdown);

MdvDocumentView(
  tree,
  palette: palette, // MdvPalette.load(dark: ...) or the baked defaults
  onLinkTap: (url, blocked, source) => openUrl(url),
  imageProvider: resolveImage, // Future<ImageProvider?> Function(url, alt)
)

The same trees come out of the C ABI (mdv_render_tree / mdv_render_tree_r / mdv_render_tree_doc / mdv_render_tree_doc_r) and WASM (renderTree / renderTreeDoc, fully typed in the package's index.d.ts) — C and WASM output is byte-identical for the same input. The *_doc variants build the tree from mdv_parse document JSON (parse-once/render-many); with no source bytes at hand their block ids use a deterministic positional fallback instead of content hashes.

Options relevance: the tree operations take the same strict options JSON as the HTML ones, but only the semantic fields apply (parser, headingAnchors, highlighting, math, mermaid, allowRawHTML). The HTML-only fields — theme, themeOverrides, fragment, maxWidth, sourceMap, stylesheet, extraCss, codeHeader — are decoded and ignored, and spans are always included. See ffi/README.md's options-relevance table.

Wire-schema reference: the render/tree package docs. Flutter widget-layer quickstart: flutter/mdviewer/README.md.

Security model

  • Sanitized by default. Raw HTML in Markdown input is passed through bluemonday's UGC policy unless AllowRawHTML() / -unsafe is set.
  • URL scheme allowlist. Only http, https, mailto, and tel are permitted in links/images by default; everything else (including javascript:, data:, and unknown schemes) is blocked. AllowRawHTML() lifts this restriction.
  • Resolver trust boundary. A host-supplied Resolver fully controls its own output — see the trust contract above.
  • Offline, self-contained output. Rendered HTML never fetches from a CDN, so there's no third-party request surface at render or view time.

See SECURITY.md for the vulnerability reporting process and what's in scope (sanitizer bypasses and URL scheme allowlist bypasses are explicitly in scope).

Roadmap

v0.2 adds block-level source spans, a versioned JSON codec for the document tree, RenderDoc/ParseWith/WithParserConfig, an exported assets package, and context-aware parse/render variants on top of v0.1's Go package API and mdview CLI. v0.4 adds the C-shared libmdviewer FFI described above, v0.6 adds the WASM build and npm package described above plus the Resolver callback over both the C ABI and WASM, and v0.7 adds the Flutter/mobile plugin described above. v0.8 closes host-integration gaps found embedding the library in real apps (extraCss, codeHeader, Flutter pre-resolve helpers), and v0.9 is the native-render enabling train: renderer-agnostic resolve policy, split + cached syntax highlighting, theme palettes as JSON data, inline source spans, parser config across every surface, and the Flutter version handshake. v0.10 ships what that enabled — the native render tree described above, across every surface, plus the Flutter widget layer (MdvDocumentView) with native math. See docs/Design.md for what remains ahead of an eventual v1.0 that commits to API stability — mobile native-reader validation, the mermaid offscreen-SVG fast-follow, the freeze discussion itself, and incremental rendering if profiling demands it.

License

Apache-2.0 — see LICENSE.

This project bundles third-party software (goldmark, chroma, bluemonday, mermaid, KaTeX). See NOTICE and third_party/README.md for attributions, versions, and license details.

Documentation

Overview

Package markdownviewer is an embeddable Markdown previewer: it parses Markdown (CommonMark + GFM + modern extensions) into a document model and renders themed, self-contained, sanitized HTML.

Quick start:

html, err := markdownviewer.Render(src)

See the document package for the AST and render/html for renderer options.

Concurrency

All top-level functions in this package (Parse, ParseWith, Render, RenderTo, RenderDoc, RenderDocTo, ParseContext, RenderContext, RenderDocContext) are safe for concurrent use: they share no mutable state across calls beyond two package-level values that are constructed once and never mutated afterward — bluemonday's sanitizer Policy (its README documents Sanitize as safe to call concurrently on a constructed Policy; only construction/editing is not) and chroma's HTML formatter (its style cache is internally mutex-protected for concurrent Format calls). A *document.Document returned by Parse/ParseWith must not be mutated concurrently with a Render/RenderTo/RenderDoc/RenderDocTo call that reads it, or with another mutation — document.Document itself has no internal synchronization.

ParseContext/RenderContext/RenderDocContext extend this contract: when ctx ends before the underlying work finishes, the function returns ctx.Err() immediately, but the abandoned goroutine may still be reading src (ParseContext, RenderContext) or doc (RenderDocContext) for an unbounded window afterward — there is no signal for when it actually stops. Getting ctx.Err() back is not a guarantee that those inputs are safe to mutate or reuse; callers must treat src/doc passed to a Context variant as immutable for the lifetime of the call, and must not assume that lifetime has ended just because the call returned.

Resource exhaustion

Parse/ParseWith/Render/RenderTo have no built-in wall-clock or work budget. A deeply nested list (thousands of levels) can take goldmark's parser tens of seconds even though the input itself is small, because the cost is super-quadratic in nesting depth rather than in input size. Hosts that process untrusted input should wrap these calls in their own wall-clock timeout. See SECURITY.md for measurements and the recommended pattern. RenderDoc/RenderDocTo receive an already-parsed tree and so are not subject to the parse-time cost, but rendering a pathologically deep tree built by other means can still be costly.

ParseContext/RenderContext/RenderDocContext implement exactly this timeout pattern for the caller: they honor ctx's deadline/cancellation and return promptly, but the abandoned goroutine keeps running to completion — its CPU is not reclaimed. See SECURITY.md.

Index

Examples

Constants

View Source
const (
	// ResolveLink signals resolution of a standard Markdown link target.
	ResolveLink = resolve.ResolveLink
	// ResolveImage signals resolution of a Markdown image target.
	ResolveImage = resolve.ResolveImage
	// ResolveWikiLink signals resolution of a wiki-link target.
	ResolveWikiLink = resolve.ResolveWikiLink
)

Variables

This section is empty.

Functions

func Parse

func Parse(src []byte) (*document.Document, error)

Parse returns the document model for src.

Example

Parse returns the document model directly, for callers that want to walk or otherwise process the tree themselves rather than render it to HTML.

package main

import (
	"fmt"

	markdownviewer "github.com/sriannamalai/markdownviewer"
	"github.com/sriannamalai/markdownviewer/document"
)

func main() {
	src := "# One\n\nSome text.\n\n## Two\n\nMore text.\n\n### Three\n"
	doc, err := markdownviewer.Parse([]byte(src))
	if err != nil {
		panic(err)
	}
	headings := 0
	document.Walk(doc, func(n document.Node, entering bool) document.WalkStatus {
		if entering {
			if _, ok := n.(*document.Heading); ok {
				headings++
			}
		}
		return document.Continue
	})
	fmt.Println(headings)
}
Output:
3

func ParseContext added in v0.2.0

func ParseContext(ctx context.Context, src []byte) (*document.Document, error)

ParseContext is Parse with caller-side deadline support. If ctx ends first, ParseContext returns ctx.Err() immediately — but the underlying parse continues on its goroutine until it finishes and cannot be stopped (the Markdown engine has no cancellation hooks). The guarantee is bounded caller latency, not reclaimed CPU; see SECURITY.md. The abandoned goroutine may still be reading src after this function returns, for an unbounded window — do not mutate or reuse src until you can otherwise guarantee that goroutine has finished (see the package's Concurrency section).

func ParseWith added in v0.2.0

func ParseWith(src []byte, cfg parser.Config) (*document.Document, error)

ParseWith parses src with an explicit extension configuration.

func Render

func Render(src []byte, opts ...Option) ([]byte, error)

Render parses src and renders HTML with the given options.

Example

Render parses and renders Markdown to HTML. Fragment() is used here to keep the example's output small and deterministic; without it Render produces a full HTML page (doctype, head, embedded theme CSS) which is too large to pin down with an // Output: comment.

package main

import (
	"fmt"

	markdownviewer "github.com/sriannamalai/markdownviewer"
)

func main() {
	out, err := markdownviewer.Render([]byte("# Hello\n\nWorld.\n"), markdownviewer.Fragment())
	if err != nil {
		panic(err)
	}
	fmt.Print(string(out))
}
Output:
<h1 id="hello">Hello</h1>
<p>World.</p>

func RenderContext added in v0.2.0

func RenderContext(ctx context.Context, src []byte, opts ...Option) ([]byte, error)

RenderContext is Render with caller-side deadline support (same abandonment semantics as ParseContext, including that src must not be mutated or reused after a cancelled return until the abandoned goroutine is otherwise known to have finished).

func RenderDoc added in v0.2.0

func RenderDoc(doc *document.Document, opts ...Option) ([]byte, error)

RenderDoc renders an already-parsed document, enabling parse-once / render-many workflows such as switching themes without re-parsing.

func RenderDocContext added in v0.2.0

func RenderDocContext(ctx context.Context, w io.Writer, doc *document.Document, opts ...Option) error

RenderDocContext is RenderDocTo with caller-side deadline support. The render happens into an internal buffer; w is written only on success, so an abandoned render never touches w after this function returns. As with ParseContext/RenderContext, the abandoned goroutine may still be reading doc for an unbounded window after a cancelled return — its CPU is not reclaimed, and doc must not be mutated or reused until you can otherwise guarantee that goroutine has finished; see SECURITY.md.

func RenderDocTo added in v0.2.0

func RenderDocTo(w io.Writer, doc *document.Document, opts ...Option) error

RenderDocTo renders an already-parsed document into w.

func RenderTo

func RenderTo(w io.Writer, src []byte, opts ...Option) error

RenderTo renders into w.

Types

type Option

type Option func(*config)

Option is a functional option that modifies render configuration.

func AllowRawHTML

func AllowRawHTML() Option

AllowRawHTML permits raw HTML and unsafe URL schemes in the output.

func DisableHeadingAnchors added in v0.9.0

func DisableHeadingAnchors() Option

DisableHeadingAnchors omits the slug id attributes from rendered headings (<h1 id="...">). Anchor ids are still computed at parse time (document.Heading.AnchorID is unaffected); this only stops the HTML renderer from emitting them, so intra-page #fragment links to headings stop resolving.

func DisableHighlighting

func DisableHighlighting() Option

DisableHighlighting disables syntax highlighting in code blocks.

func DisableMath

func DisableMath() Option

DisableMath disables KaTeX mathematical notation rendering.

func DisableMermaid

func DisableMermaid() Option

DisableMermaid disables mermaid diagram rendering.

func Fragment

func Fragment() Option

Fragment emits body-only HTML instead of a full page: no <html>/<head>, no theme <style>, no embedded mermaid/KaTeX <script> assets. The host owns the page and is responsible for supplying styling — see the Theming section of README.md for the CSS variables the markup expects.

Diagram and math nodes still render their markup (a <pre class="mermaid"> block, a <span>/<div class="math ...">), but as inert placeholders: with no mermaid.js/KaTeX loaded, the raw diagram/math source is what a viewer sees until the host provides those libraries itself. The same mermaid.js and KaTeX JS/CSS this package embeds for full-page output are available to fragment hosts via the assets package (assets.MermaidJS, assets.KatexJS, assets.KatexCSS) — see README.md's Theming section for the injection snippet.

Example

Fragment emits body-only HTML: no <html>/<head>/<style>, just the rendered markup. Hosts embedding output into an existing page use this instead of the default full-page output.

package main

import (
	"fmt"

	markdownviewer "github.com/sriannamalai/markdownviewer"
)

func main() {
	out, err := markdownviewer.Render([]byte("**bold**\n"), markdownviewer.Fragment())
	if err != nil {
		panic(err)
	}
	fmt.Print(string(out))
}
Output:
<p><strong>bold</strong></p>

func WithCodeHeader added in v0.8.0

func WithCodeHeader() Option

WithCodeHeader wraps each rendered code block in a header row carrying a language label (the fence language, or "code" when unlabeled) and a Copy button, using the md-code / md-code-header / md-code-lang / md-code-copy classes styled by the base stylesheet. Live mermaid diagrams and math are not wrapped; their engine-disabled plain-code fallbacks are. Full pages also get a small inline clipboard script wiring the buttons; fragment hosts receive the markup and wire their own handler.

func WithExtraCSS added in v0.8.0

func WithExtraCSS(css string) Option

WithExtraCSS appends css after the page's base styling — the base+theme stylesheets by default, or the WithStylesheet replacement when one is set. Full-page rendering only, like WithStylesheet; it has no effect in fragment mode. Content is emitted into the page's <style> element; </style sequences are stripped defensively.

func WithMaxWidth added in v0.3.0

func WithMaxWidth(width string) Option

WithMaxWidth constrains the rendered page's content width, via the --md-max-width CSS custom property (theme.BaseCSS's "max-width: var(--md-max-width, none)"). width accepts any CSS length, e.g. "860px" or "70ch". The default is fluid: an empty string (or never calling this option) leaves --md-max-width unset, so the page has no max-width constraint and fills its container.

width flows into a CSS custom-property value inside the page's <style> element. As a defense-in-depth measure on top of the usual </style stripping applied to all theme override values, a width containing ';' or '}' — either of which could break out of the single declaration this option controls — is rejected outright: the option no-ops, leaving whatever width was already configured (or the fluid default) unchanged, rather than emitting a truncated or defanged value.

Implemented via the same mechanism as WithThemeOverrides (it sets the "--md-max-width" key), so calling WithThemeOverrides after WithMaxWidth replaces the whole override map, including this key; call WithMaxWidth after WithThemeOverrides (or fold "--md-max-width" into that map directly) if you need both.

func WithParserConfig added in v0.2.0

func WithParserConfig(cfg parser.Config) Option

WithParserConfig selects which Markdown extensions the parser enables when Render/RenderTo (and their Context variants) parse the source. It has no effect on RenderDoc, which receives an already-parsed tree.

func WithResolver

func WithResolver(r Resolver) Option

WithResolver provides a custom link/image resolution callback.

Example

WithResolver installs a callback that rewrites link/image/wiki-link targets — here, routing an image through a CDN. Returning ok=false falls back to default resolution for anything the resolver doesn't handle.

package main

import (
	"fmt"

	markdownviewer "github.com/sriannamalai/markdownviewer"
)

func main() {
	resolver := func(kind markdownviewer.ResolveKind, target string) (string, bool) {
		if kind == markdownviewer.ResolveImage {
			return "https://cdn.example.com/" + target, true
		}
		return "", false
	}
	out, err := markdownviewer.Render([]byte("![a cat](cat.png)\n"),
		markdownviewer.Fragment(), markdownviewer.WithResolver(resolver))
	if err != nil {
		panic(err)
	}
	fmt.Print(string(out))
}
Output:
<p><img src="https://cdn.example.com/cat.png" alt="a cat" /></p>

func WithSourceMap added in v0.2.0

func WithSourceMap() Option

WithSourceMap annotates top-level block elements with data-md-line attributes for editor↔preview scroll synchronization.

func WithStylesheet

func WithStylesheet(css string) Option

WithStylesheet replaces the base stylesheet entirely with the provided CSS. When non-empty, it overrides theme.BaseCSS() and takes precedence over theme-based styling. Content is emitted into the page's <style> element; </style sequences are stripped defensively.

func WithTheme

func WithTheme(name string) Option

WithTheme sets the theme ("light", "dark", or "auto").

func WithThemeOverrides

func WithThemeOverrides(vars map[string]string) Option

WithThemeOverrides applies CSS custom-property overrides to the rendered theme. Keys must match the pattern --[a-zA-Z0-9_-]+; non-conforming keys are silently dropped. Overrides are emitted after the base theme in sorted key order, ensuring they win in both light and dark variants.

Security: override VALUES are host-trusted CSS, emitted into the page's <style> element essentially verbatim — only the </style sequence is stripped (so a value cannot terminate the style element itself). A value CAN still close the :root{} declaration block and inject arbitrary CSS rules, including url() fetches to attacker-controlled origins. Hosts must never echo untrusted data into override values.

type ResolveKind

type ResolveKind = resolve.ResolveKind

ResolveKind identifies which kind of target a Resolver is being asked to resolve: a standard link, an image, or a wiki-link. It is an alias for resolve.ResolveKind, the renderer-agnostic home of the resolution policy shared by all renderers.

type Resolver

type Resolver = resolve.Resolver

Resolver is a function that rewrites link and image targets. It accepts the resolution kind (link, image, or wiki-link) and target URL, returning the rewritten URL and true if resolution succeeded, or false to fall back to default handling. It is an alias for resolve.Resolver.

Trust contract: URLs returned with ok=true are emitted as-is without scheme filtering. Hosts fully control resolution and must not echo untrusted targets back unexamined.

Directories

Path Synopsis
Package assets exposes the vendored mermaid and KaTeX payloads that full-page rendering embeds automatically.
Package assets exposes the vendored mermaid and KaTeX payloads that full-page rendering embeds automatically.
cmd
mdview command
Command mdview renders Markdown to self-contained HTML.
Command mdview renders Markdown to self-contained HTML.
Package document defines the stable, renderer-agnostic Markdown document model.
Package document defines the stable, renderer-agnostic Markdown document model.
Command ffi builds libmdviewer, the C-shared FFI boundary for the markdownviewer library.
Command ffi builds libmdviewer, the C-shared FFI boundary for the markdownviewer library.
internal
boundary
Package boundary implements the shared JSON boundary consumed by both exported entry points — the cgo FFI (ffi/) and the wasm build (wasm/): strict version-1 options decoding plus markdown/document/HTML conversions and the embedded static asset registry.
Package boundary implements the shared JSON boundary consumed by both exported entry points — the cgo FFI (ffi/) and the wasm build (wasm/): strict version-1 options decoding plus markdown/document/HTML conversions and the embedded static asset registry.
Package parser converts Markdown source into the document model.
Package parser converts Markdown source into the document model.
render
html
Package htmlrender renders the document model to themed HTML.
Package htmlrender renders the document model to themed HTML.
internal/derive
Package derive holds the tiny output-shaping derivations shared by every renderer: the admonition title rule, the code-block display label, the footnote def/ref pairing walk, the raw-HTML sanitize policy, and the destination-resolution pipeline (resolver → default resolution → SafeURL filter → percent-encoding).
Package derive holds the tiny output-shaping derivations shared by every renderer: the admonition title rule, the code-block display label, the footnote def/ref pairing walk, the raw-HTML sanitize policy, and the destination-resolution pipeline (resolver → default resolution → SafeURL filter → percent-encoding).
tree
Package tree builds the version-1 native render tree: a layout-free, fully RESOLVED semantic tree that native hosts (Flutter, SwiftUI, Compose, …) render as platform widgets, with everything policy-heavy — URL resolution and filtering, raw-HTML sanitizing, admonition titles, footnote pairing, math/mermaid fallbacks — already applied library-side by Build, through the same shared derivations the HTML renderer uses (render/internal/derive and the resolve package), so the two renderers cannot drift.
Package tree builds the version-1 native render tree: a layout-free, fully RESOLVED semantic tree that native hosts (Flutter, SwiftUI, Compose, …) render as platform widgets, with everything policy-heavy — URL resolution and filtering, raw-HTML sanitizing, admonition titles, footnote pairing, math/mermaid fallbacks — already applied library-side by Build, through the same shared derivations the HTML renderer uses (render/internal/derive and the resolve package), so the two renderers cannot drift.
Package resolve holds the renderer-agnostic destination-resolution policy: the Resolver hook and its ResolveKind discriminants, the safeURL scheme allowlist (SafeURL), and the built-in default resolution rule (DefaultResolution, the wiki-link ".md" fallback).
Package resolve holds the renderer-agnostic destination-resolution policy: the Resolver hook and its ResolveKind discriminants, the safeURL scheme allowlist (SafeURL), and the built-in default resolution rule (DefaultResolution, the wiki-link ".md" fallback).
scripts
inlinefonts command
Command inlinefonts rewrites KaTeX's CSS to carry its woff2 fonts as data: URIs so rendered pages are fully self-contained.
Command inlinefonts rewrites KaTeX's CSS to carry its woff2 fonts as data: URIs so rendered pages are fully self-contained.
Package theme provides the built-in visual themes as CSS custom-property sets over one base stylesheet.
Package theme provides the built-in visual themes as CSS custom-property sets over one base stylesheet.
Command wasm builds libmdviewer for GOOS=js GOARCH=wasm.
Command wasm builds libmdviewer for GOOS=js GOARCH=wasm.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL