README
¶
goldmark
A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured.
goldmark is compliant with CommonMark 0.31.2.
- goldmark playground : Try goldmark online. This playground is built with WASM(5-10MB).
There is also a Rust version of goldmark: rushdown
v2 status
v2 is currently in beta. The API is almost stable, but some parts may change. Please report any issues you find.
v2 Motivation
goldmark was originally created with a focus on my personal goals.
- Extensible. You can easily add your own syntax to Markdown.
- Performance. Prioritize performance over semantically perfect AST.
- Focus on the purpose of converting to HTML.
Unexpectedly, goldmark has been used by many people.
goldmark has become a major Markdown parser in Go ecosystem.
In such a situation, there have been many requests regarding use cases that were not emphasized at the time of initial creation.
- Semantic analysis of Markdown documents using AST
- Use cases that use more detailed position information, such as LSP servers
In particular, as Markdown documents have come to be used as Lingua franca for AI, there is an increasing need to analyze Markdown documents semantically.For the same reason, there are also increasing use cases for generating Markdown documents rather than parsing them. The use of CLI in AI agents is increasing, also a growing need to convert to formats other than HTML.
Breaking changes to an extensible library like goldmark have a huge impact, as third-party extensions will no longer work. Therefore, I have avoided making breaking changes for a long time.
It has been more than 7 years since goldmark was created, and technical debt has been accumulating. In the meantime, the Go language specification has changed significantly, including the introduction of generics. I believe that the changes in use cases, represented by AI, are a good opportunity to fundamentally review the design of goldmark, and I have decided to make breaking changes.
v2 and v1 differences
- v2 focuses on building a more semantic AST. For that reason, it is slightly less performant than v1.
- v2 uses generics.
- v2 clearly separates the parser and renderer. This makes it easier to implement rendering to formats other than HTML.
- v2 allows you to programmatically build an AST. And you can render the constructed AST to another format.
- v2 has all nodes hold the start position. In the future, third-party extensions that support v2 are also expected to hold the start position.
- The core parsing algorithm is the same as v1. Third-party extensions must support v2, but the most complex parsing part can be used almost as it is.
Maintenance policy
This project will maintain bug fixes, including security fixes, up to one major version prior to the latest major version.
Features
- Standards-compliant : goldmark is fully compliant with the latest CommonMark specification.
- Extensible : Do you want to add a
@usernamemention syntax to Markdown? You can easily do so in goldmark. You can add your AST nodes, parsers for block-level elements, parsers for inline-level elements, transformers for paragraphs, transformers for the whole AST structure, and renderers. - Performance : goldmark is one of the fastest CommonMark-compliant Markdown parsers in Go.
- Robust : goldmark is tested with
go test --fuzz. - Built-in extensions : goldmark ships with common extensions like tables, strikethrough, task lists, and definition lists.
- Semantically clean AST : goldmark builds a clean AST structure that is easy to analyze and transform.
- Depends only on standard libraries.
Installation
$ go get github.com/yuin/goldmark/v2
Usage
Convert Markdown documents with the CommonMark-compliant mode:
import (
"bytes"
"github.com/yuin/goldmark/v2/parser"
"github.com/yuin/goldmark/v2/renderer/html"
)
source := []byte("こんにちは、 **世界** 。")
var buf bytes.Buffer
p := parser.New()
r := html.New()
doc := p.Parse(source)
if err := r.Render(&buf, source, doc); err != nil {
panic(err)
}
if "<p>こんにちは、 <strong>世界</strong> 。</p>\n" != buf.String() {
panic("unexpected output:" + buf.String())
}
Build an AST and render it to HTML:
import (
"bytes"
"github.com/yuin/goldmark/v2/ast"
"github.com/yuin/goldmark/v2/text"
"github.com/yuin/goldmark/v2/renderer/html"
)
doc := ast.N(ast.NewDocument(),
ast.N(ast.NewParagraph(),
"こんにちは、",
ast.N(ast.NewEmphasis(),
"世界",
),
"。",
),
ast.N(func() ast.Node {
n := ast.NewParagraph()
n.SetAttributeString("class", text.NewMultilineValue("greeting"))
return n
}(), "Hello, world."),
)
var buf bytes.Buffer
r := html.New()
if err := r.Render(&buf, nil, doc); err != nil {
panic(err)
}
if "<p>こんにちは、<em>世界</em>。</p>\n<p class=\"greeting\">Hello, world.</p>\n" != buf.String() {
panic("unexpected output:" + buf.String())
}
Custom parser and renderer
import (
"bytes"
"github.com/yuin/goldmark/v2/extension"
"github.com/yuin/goldmark/v2/parser"
"github.com/yuin/goldmark/v2/renderer/html"
)
source := []byte("こんにちは、 ~~世界~~ 。")
p := parser.New(parser.WithAttribute(), parser.WithExtensions(extension.NewStrikethroughParser()))
r := html.New(html.WithXHTML(), html.WithUnsafe(), html.WithExtensions(extension.NewStrikethroughHTMLRenderer()))
var buf bytes.Buffer
doc := p.Parse(source)
if err := r.Render(&buf, source, doc); err != nil {
panic(err)
}
if "<p>こんにちは、 <del>世界</del> 。</p>\n" != buf.String() {
panic("unexpected output:" + buf.String())
}
Parser options
| Functional option | Type | Description |
|---|---|---|
parser.WithBlockParsers |
[]util.PrioritizedValue[parser.BlockParser] |
Parsers for parsing block level elements. |
parser.WithInlineParsers |
[]util.PrioritizedValue[parser.InlineParser] |
Parsers for parsing inline level elements. |
parser.WithParagraphTransformers |
[]util.PrioritizedValue[parser.ParagraphTransformer] |
Transformers for transforming paragraph nodes. |
parser.WithASTTransformers |
[]util.PrioritizedValue[parser.ASTTransformer] |
Transformers for transforming an AST. |
parser.WithAutoHeadingID |
- |
Enables auto heading ids. |
parser.WithAttribute |
- |
Enables custom attributes. Currently only headings supports attributes. |
parser.WithIDGenerator |
parser.IDGenerator |
Generator for heading ids. |
parser.WithDefaultParsers |
bool |
Enables default parsers. Default is true. |
parser.WithEscapedSpace |
- |
Enables escaped space. This is useful for CJK users. |
parser.WithExtensions |
[]parser.Extension |
Enables parser extensions. |
HTML Renderer options
| Functional option | Type | Description |
|---|---|---|
html.WithEscapedSpace |
- |
Enables escaped space. This is useful for CJK users. |
html.WithEastAsianLineBreaks |
html.EastAsianLineBreaks |
Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. |
html.WithHardWraps |
- |
Render newlines as <br>. |
html.WithHooks |
[]html.Hook |
Add hooks to the renderer. |
html.WithIsInTightBlock |
html.IsInTightBlockFunc |
Function that determines whether a node is in a tight block. |
html.WithNodeRenderer |
ast.NodeKind, html.NodeRenderer |
Add a node renderer for a specific node kind. |
html.WithNodeRenderers |
map[ast.NodeKind]html.NodeRenderer |
Add node renderers for specific node kinds. |
html.WithXHTML |
- |
Render as XHTML. |
html.WithUnsafe |
- |
By default, goldmark does not render raw HTML or potentially dangerous links. With this option, goldmark renders such content as written. |
html.WithExtensions |
[]html.Extension |
Enables parser extensions. |
East asian line breaks
| Style | Description |
|---|---|
EastAsianLineBreaksSimple |
Soft line breaks are ignored if both sides of the break are east asian wide character. This behavior is the same as east_asian_line_breaks in Pandoc. |
EastAsianLineBreaksCSS3Draft |
This option implements CSS text level3 Segment Break Transformation Rules with some enhancements. |
Example of EastAsianLineBreaksSimple
Input Markdown:
私はプログラマーです。
東京の会社に勤めています。
GoでWebアプリケーションを開発しています。
Output:
<p>私はプログラマーです。東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。</p>
Example of EastAsianLineBreaksCSS3Draft
Input Markdown:
私はプログラマーです。
東京の会社に勤めています。
GoでWebアプリケーションを開発しています。
Output:
<p>私はプログラマーです。東京の会社に勤めています。GoでWebアプリケーションを開発しています。</p>
Built-in extensions
extension.Tableextension.Strikethroughextension.Linkifyextension.TaskListextension.GFM- This extension enables Table, Strikethrough, Linkify and TaskList.
- This extension does not filter tags defined in 6.11: Disallowed Raw HTML (extension). If you need to filter HTML tags, see Security.
- If you need to parse github emojis, you can use goldmark-emoji extension.
extension.DefinitionListextension.Footnoteextension.Typographer- This extension substitutes punctuations with typographic entities like smartypants.
Attributes
The parser.WithAttribute option allows you to define attributes on some elements.
Currently only headings support attributes.
Attributes are being discussed in the CommonMark forum. This syntax may possibly change in the future.
Headings
## heading ## {#id .className attrName=attrValue class="class1 class2"}
## heading {#id .className attrName=attrValue class="class1 class2"}
heading {#id .className attrName=attrValue}
============
Table extension
The Table extension implements Table(extension), as defined in GitHub Flavored Markdown Spec.
Specs are defined for XHTML, so specs use some deprecated attributes for HTML5.
You can override alignment rendering method via options.
| Functional option | Type | Description |
|---|---|---|
extension.WithTableCellAlignMethod |
extension.TableCellAlignMethod |
Option indicates how are table cells aligned. |
Typographer extension
The Typographer extension translates plain ASCII punctuation characters into typographic-punctuation HTML entities.
Default substitutions are:
| Punctuation | Default entity |
|---|---|
' |
‘, ’ |
" |
“, ” |
-- |
– |
--- |
— |
... |
… |
<< |
« |
>> |
» |
You can override the default substitutions via extensions.WithTypographicSubstitutions.
import (
"github.com/yuin/goldmark/v2/extension"
"github.com/yuin/goldmark/v2/parser"
)
_ = parser.New(
parser.WithExtensions(extension.NewTypographerParser(
extension.WithTypographicSubstitutions(extension.TypographicSubstitutions{
extension.LeftSingleQuote: []byte("‚"),
extension.RightSingleQuote: nil, // nil disables a substitution
}),
)),
)
Linkify extension
The Linkify extension implements Autolinks(extension), as defined in GitHub Flavored Markdown Spec.
Since the spec does not define details about URLs, there are numerous ambiguous cases.
You can override autolinking patterns via options.
| Functional option | Type | Description |
|---|---|---|
extension.WithAllowedProtocols |
[][]byte | []string |
List of allowed protocols such as []string{ "http:" } |
extension.WithURLRegexp |
*regexp.Regexp |
Regexp that defines URLs, including protocols |
extension.WithWWWRegexp |
*regexp.Regexp |
Regexp that defines URL starting with www.. This pattern corresponds to the extended www autolink |
extension.WithEmailRegexp |
*regexp.Regexp |
Regexp that defines email addresses` |
Example, using xurls:
import (
"mvdan.cc/xurls/v2"
"github.com/yuin/goldmark/v2/extension"
"github.com/yuin/goldmark/v2/parser"
)
_ = parser.New(
parser.WithExtensions(
extension.NewLinkifyParser(
extension.WithAllowedProtocols([]string{
"http:",
"https:",
}),
extension.WithURLRegexp(
xurls.Strict(),
),
),
),
)
Footnotes extension
The Footnote extension implements PHP Markdown Extra: Footnotes.
This extension has some options:
| Functional option | Type | Description |
|---|---|---|
extension.WithIDPrefix |
[]byte | string |
a prefix for the id attributes. |
extension.WithIDPrefixFunction |
func(gast.Node) []byte |
a function that determines the id attribute for given Node. |
extension.WithLinkTitle |
[]byte | string |
an optional title attribute for footnote links. |
extension.WithBacklinkTitle |
[]byte | string |
an optional title attribute for footnote backlinks. |
extension.WithLinkClass |
[]byte | string |
a class for footnote links. This defaults to footnote-ref. |
extension.WithBacklinkClass |
[]byte | string |
a class for footnote backlinks. This defaults to footnote-backref. |
extension.WithBacklinkHTML |
[]byte | string |
a class for footnote backlinks. This defaults to ↩︎. |
Some options can have special substitutions. Occurrences of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurrences of “%%” will be replaced by a number for the reference (footnotes can have multiple references).
extension.WithIDPrefix and extension.WithIDPrefixFunction are useful if you have multiple Markdown documents displayed inside one HTML document to avoid footnote ids to clash each other.
extension.WithIDPrefix sets fixed id prefix, so you may write codes like the following:
import (
"github.com/yuin/goldmark/v2/extension"
"github.com/yuin/goldmark/v2/parser"
)
for _, path := range files {
source := readAll(path)
prefix := getPrefix(path)
p := parser.New(
parser.WithExtensions(
extension.NewFootnote(
extension.WithIDPrefix(path),
),
),
)
// convert source to HTML
}
extension.WithIDPrefixFunction determines an id prefix by calling given function, so you may write codes like the following:
import (
"github.com/yuin/goldmark/v2/extension"
"github.com/yuin/goldmark/v2/parser"
)
p := parser.New(
extension.NewFootnote(
extension.WithIDPrefixFunction(func(n gast.Node) []byte {
v, ok := n.OwnerDocument().Meta()["footnote-prefix"]
if ok {
return util.StringToReadOnlyBytes(v.(string))
}
return nil
}),
),
)
for _, path := range files {
source := readAll(path)
doc := p.Parse(source)
doc.Meta()["footnote-prefix"] = getPrefix(path)
// convert doc to HTML
}
You can use goldmark-meta to define a id prefix in the markdown document:
---
title: document title
slug: article1
footnote-prefix: article1
---
# My article
Security
By default, goldmark does not render raw HTML or potentially-dangerous URLs. If you need to gain more control over untrusted contents, it is recommended that you use an HTML sanitizer such as bluemonday.
Benchmark
You can run this benchmark in the _benchmark directory.
against other golang libraries
goos: linux
goarch: amd64
pkg: banchmark
cpu: AMD Ryzen 7 8840HS w/ Radeon 780M Graphics
BenchmarkMarkdown/goldmark/v2-16 176 6786133 ns/op 2587272 B/op 14460 allocs/op
BenchmarkMarkdown/goldmark/v1-16 170 6815477 ns/op 2538288 B/op 14471 allocs/op
BenchmarkMarkdown/CommonMark-16 146 8520952 ns/op 2699081 B/op 20129 allocs/op
BenchmarkMarkdown/Lute-16 12 87993431 ns/op 12112462 B/op 33447 allocs/op
BenchmarkMarkdown/GoMarkdown-16 10 115382667 ns/op 2272684 B/op 23832 allocs/op
Extensions
List of extensions
Note that not all extensions support v2.
- goldmark-meta: A YAML metadata extension for the goldmark Markdown parser.
- goldmark-highlighting: A syntax-highlighting extension for the goldmark markdown parser.
- goldmark-emoji: An emoji extension for the goldmark Markdown parser.
- goldmark-mathjax: Mathjax support for the goldmark markdown parser
- goldmark-pdf: A PDF renderer that can be passed to
goldmark.WithRenderer(). - goldmark-hashtag: Adds support for
#hashtag-based tagging to goldmark. - goldmark-wikilink: Adds support for
[[wiki]]-style links to goldmark. - goldmark-anchor: Adds anchors (permalinks) next to all headers in a document.
- goldmark-figure: Adds support for rendering paragraphs starting with an image to
<figure>elements. - goldmark-frontmatter: Adds support for YAML, TOML, and custom front matter to documents.
- goldmark-toc: Adds support for generating tables-of-contents for goldmark documents.
- goldmark-mermaid: Adds support for rendering Mermaid diagrams in goldmark documents.
- goldmark-pikchr: Adds support for rendering Pikchr diagrams in goldmark documents.
- goldmark-embed: Adds support for rendering embeds from YouTube links.
- goldmark-latex: A $\LaTeX$ renderer that can be passed to
goldmark.WithRenderer(). - goldmark-fences: Support for pandoc-style fenced divs in goldmark.
- goldmark-d2: Adds support for D2 diagrams.
- goldmark-katex: Adds support for KaTeX math and equations.
- goldmark-img64: Adds support for embedding images into the document as DataURL (base64 encoded).
- goldmark-enclave: Adds support for embedding youtube/bilibili video, X's oembed X, tradingview chart's chart, quaily widget, spotify embeds, dify embed and html audio into the document.
- goldmark-wiki-table: Adds support for embedding Wiki Tables.
- goldmark-tgmd: A Telegram markdown renderer that can be passed to
goldmark.WithRenderer(). - goldmark-treeblood: Renders $\LaTeX$ expressions as MathML (pure Go, no external dependencies).
- goldmark-subtext: Support for Discord-style markdown subtexts
- goldmark-customtag: Allows you to define custom block tags.
- goldmark-cjk-friendly: Port of npm package
remark-cjk-friendly/markdown-it-cjk-friendlyto goldmark. Similar to the CJK extension (WithEscapedSpace), but you do not need to explicitly add\around*and**. You can combine this with the CJK extension. - goldmark-chart: Generate static ChartJS charts using the simple Markvis format.
goldmark internal(for extension developers)
Overview
goldmark's Markdown processing pipeline is outlined in the diagram below.
<Markdown source ([]byte)>
|
V
+-------- parser.Parser ---------------------------+
| 1. Parse block elements into AST |
| For each paragraph, apply |
| ParagraphTransformers |
| 2. Traverse block AST; for each block node, |
| parse its Source() into inline nodes. |
| At the end of each block, process |
| the delimiter stack (emphasis, strong, etc.) |
| 3. Apply ASTTransformers to the whole AST |
+--------------------------------------------------+
|
V
<ast.Node tree>
|
V
+------- renderer.Renderer[W] --------------------+
| 1. Execute PreRender hooks |
| 2. Walk AST; for each node, call the |
| NodeRenderer[W] registered for its Kind |
| 3. Execute PostRender hooks |
+--------------------------------------------------+
|
V
<Output written to W>
An extension can hook into any of these stages by providing implementations of the interfaces described below. At a high level, building an extension requires four steps:
- Define AST nodes — structs embedding
ast.BaseBlockorast.BaseInline. - Write a parser — implementing
parser.BlockParser,parser.InlineParser,parser.ParagraphTransformer, orparser.ASTTransformer. - Write a renderer — a
renderer.NodeRenderer[W]for your output format (e.g.html.NodeRenderer=renderer.NodeRenderer[io.Writer]). - Package them as extensions — implementing
parser.Extensionand/orrenderer.Extension[C].
Step 1: Define AST nodes
Every custom node must embed either ast.BaseBlock (for block-level elements) or ast.BaseInline (for inline elements) and must:
- Implement
Kind() ast.NodeKindreturning a package-levelNodeKindvariable. - Implement
Dump(source []byte) *ast.NodeDumpfor debugging. - Call
n.Init(n)in its constructor.
package myext
import gast "github.com/yuin/goldmark/v2/ast"
// MyNode represents a custom inline element.
type MyNode struct {
gast.BaseInline
// Add fields for data that belongs to the node semantics.
// Do NOT store parser-internal state here.
MyField string
}
func (n *MyNode) Dump(_ []byte) *NodeDump {
return gast.NewNodeDump(n, map[string]any {
"MyField": n.MyField,
})
}
var KindMyNode = gast.NewNodeKind("MyNode")
func (n *MyNode) Kind() gast.NodeKind { return KindMyNode }
func NewMyNode(field string) *MyNode {
n := &MyNode{MyField: field}
n.Init(n) // always required
return n
}
For block nodes, embed ast.BaseBlock. The block's raw source text (used later for inline parsing) is stored via AppendSource / Source() rather than in a plain string field.
type MyBlock struct {
gast.BaseBlock
}
func (n *MyNode) Dump(_ []byte) *NodeDump {
return gast.NewNodeDump(n, nil)
}
var KindMyBlock = gast.NewNodeKind("MyBlock")
func (n *MyBlock) Kind() gast.NodeKind { return KindMyBlock }
func NewMyBlock() *MyBlock {
n := &MyBlock{}
n.Init(n)
return n
}
Step 2: Write a parser
Block parser (parser.BlockParser)
A BlockParser opens and continues a block-level element line by line.
type BlockParser interface {
// Trigger returns the set of first-column bytes that activate Open.
// Return nil to be called for every line.
Trigger() []byte
// Open is called when the trigger byte is seen at the start of a line.
// Return (node, HasChildren) if this line begins a new block, or (nil, NoChildren).
Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State)
// Continue is called for each subsequent line while the block is open.
// Return (Continue | HasChildren), (Continue | NoChildren), or Close.
Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State
// Close is called when the block is finalised.
Close(node ast.Node, reader text.Reader, pc parser.Context)
// CanInterruptParagraph returns true if this parser may interrupt a paragraph.
CanInterruptParagraph() bool
// CanAcceptIndentedLine returns true if this parser may open with an indented line.
CanAcceptIndentedLine() bool
}
Inside Open and Continue, use text.Reader to inspect and advance through the source:
| Method | Description |
|---|---|
reader.PeekLine() |
Returns (line []byte, segment text.Segment) without advancing |
reader.Advance(n) |
Advances the pointer by n bytes within the current line |
reader.AdvanceToEOL() |
Advances to the end of the current line |
reader.AdvanceLine() |
Moves to the start of the next line |
reader.LineOffset() |
Byte offset of the current position from the line start |
reader.Source() |
The full source byte slice |
pc.BlockOffset() |
Position of the first non-space byte on the current line (valid only in Open) |
pc.BlockIndent() |
Indentation width of the current line (valid only in Open) |
To store the source text that will later be parsed into inline nodes, call node.AppendSource(segment):
func (b *myBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
line, segment := reader.PeekLine()
if !bytes.HasPrefix(line, []byte(">>> ")) {
return nil, parser.NoChildren
}
node := NewMyBlock()
node.SetPos(segment.Start)
reader.Advance(4) // consume ">>> "
_, seg := reader.PeekLine()
node.AppendSource(seg.TrimRightSpace(reader.Source()))
reader.AdvanceToEOL()
return node, parser.HasChildren
}
Inline parser (parser.InlineParser)
An InlineParser is triggered by a specific byte within a line and returns an inline AST node.
type InlineParser interface {
// Trigger returns the bytes that activate this parser (must be punctuation or space).
Trigger() []byte
// Parse is called when the trigger byte is encountered.
// It may consume beyond the current line.
// Return nil if the trigger does not match.
Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node
}
Optionally implement parser.CloseBlocker to receive a callback when the enclosing block is closed:
type CloseBlocker interface {
CloseBlock(parent ast.Node, block text.Reader, pc parser.Context)
}
Delimiter-based inline elements
Elements like emphasis, strong, and strikethrough are based on a matching opener/closer delimiter pair. Use parser.ParseDelimiter together with a parser.DelimiterProcessor:
type DelimiterProcessor interface {
IsDelimiter(byte) bool
CanOpenCloser(opener, closer *parser.Delimiter) bool
OnMatch(consumes int) ast.Node
}
parser.ParseDelimiter(block, minimum, processor, pc) scans the run of delimiter characters, pushes a *Delimiter node onto the delimiter stack in pc, and returns it. The matching between openers and closers is resolved later by parser.ProcessDelimiters. Refer to the strikethrough extension (extension/strikethrough.go) for a complete example.
Paragraph transformer (parser.ParagraphTransformer)
A ParagraphTransformer is called on every *ast.Paragraph after block parsing, before inline parsing. It can replace the paragraph with a different node (e.g. table, definition list). The table and definition list extensions use this hook.
type ParagraphTransformer interface {
Transform(node *ast.Paragraph, reader text.Reader, pc parser.Context)
}
AST transformer (parser.ASTTransformer)
An ASTTransformer receives the fully-parsed *ast.Document and can make global changes.
type ASTTransformer interface {
Transform(node *ast.Document, reader text.Reader, pc parser.Context)
}
Parser context (parser.Context)
pc parser.Context is a key/value store scoped to a single parse invocation. Use it to pass state between Open, Continue, and Close calls, or between a block parser and an AST transformer.
var myKey = parser.NewContextKey()
// store
pc.Set(myKey, myValue)
// retrieve
val := pc.Get(myKey)
Step 3: Write a renderer
The renderer walks the AST and calls the NodeRenderer[W] registered for each node's Kind. The type parameter W is the writer type; for HTML output W is io.Writer.
// renderer.NodeRenderer[W] signature
type NodeRenderer[W any] interface {
Render(w W, source []byte, n ast.Node, entering bool, rc renderer.Context) (ast.WalkStatus, error)
}
Use renderer.NodeRendererFunc to create a NodeRenderer from a plain function:
html.NodeRendererFunc(func(w io.Writer, source []byte, n ast.Node, entering bool, rc renderer.Context) (ast.WalkStatus, error) {
bw := w.(util.BufWriter)
if entering {
_, _ = bw.WriteString("<my-element>")
} else {
_, _ = bw.WriteString("</my-element>")
}
return ast.WalkContinue, nil
})
For HTML output, cast io.Writer to util.BufWriter for efficient buffered writes:
w := writer.(util.BufWriter)
_, _ = w.WriteString("<tag>")
_ = w.WriteByte('\n')
To render HTML attributes attached to a node, use html.RenderAttributes:
if n.Attributes() != nil {
_, _ = w.WriteString("<del")
html.RenderAttributes(w, n, MyAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<del>")
}
MyAttributeFilter is a util.BytesFilter that controls which attribute names are allowed. Start from html.GlobalAttributeFilter and extend it as needed:
var MyAttributeFilter = html.GlobalAttributeFilter.ExtendString(`align,width`)
Writing text values safely: html.Writer
When writing content that comes from the parsed source (node fields, attribute values, etc.) you must go through the html.Writer obtained from the render context, not write directly to the BufWriter. The html.Writer applies all escaping required by CommonMark and HTML:
| Method | What it applies |
|---|---|
WriteText(w, src) |
HTML-escapes &, <, >, ", replaces NUL (\x00) with the replacement character (\uFFFD), and resolves backslash-escaped characters (\*, \., etc.) and entity references (&, A, etc.) |
RawWriteText(w, src) |
Same as WriteText but does not resolve backslash escapes or entity references — use for content that is already in its final logical form (e.g. a link destination stored as a text.Value) |
WriteHTML(w, src) |
Replaces NUL only — use when writing a value that is already valid, escaped HTML |
// Render display text that may contain backslash escapes or entity references.
writer.WriteText(w, n.Value.Bytes(source))
// Render a URL that should not have backslash interpretation applied.
writer.RawWriteText(w, n.Destination.Bytes(source))
Writing a constant string (a fixed HTML tag or literal punctuation that contains no characters needing escaping) directly to the BufWriter is fine. Writing a variable value — anything derived from node fields or the source byte slice — must always go through html.Writer.
bw := w.(util.BufWriter)
_, _ = bw.WriteString("<em>") // OK: constant tag, no escaping needed
writer.WriteText(bw, value) // required: variable content from source
_, _ = bw.WriteString("</em>") // OK: constant tag
Render hooks
renderer.Hook[W] lets you run code before and after the entire render pass:
type Hook[W any] interface {
PreRender(w W, source []byte, n ast.Node, rc renderer.Context) error
PostRender(w W, source []byte, n ast.Node, rc renderer.Context) error
}
Use html.WithHooks(hook) (or renderer.WithHooks) to register hooks via options.
Step 4: Package as extensions
In v2, parser and renderer extensions are separate types.
Parser extension implements parser.Extension:
type Extension interface {
ParserOptions(c *parser.Config) []parser.Option
}
Renderer extension implements renderer.Extension[C] (e.g. html.Extension = renderer.Extension[html.Config]):
type Extension[C any] interface {
RendererOptions(c *C) []renderer.Option[C]
}
Pass parsers and transformers with a priority using util.Prioritized. Lower numbers run first. Built-in parsers use priorities in the range 0–1000; use a value in the same range to interleave with them, or a larger value to run after them.
type myParserExtension struct{}
func NewMyParser() parser.Extension { return &myParserExtension{} }
func (e *myParserExtension) ParserOptions(_ *parser.Config) []parser.Option {
return []parser.Option{
parser.WithBlockParsers(
util.Prioritized(newMyBlockParser(), 600),
),
parser.WithInlineParsers(
util.Prioritized(newMyInlineParser(), 600),
),
}
}
type myHTMLRendererExtension struct{
writer html.Writer
}
func NewMyHTMLRenderer() html.Extension { return &myHTMLRendererExtension{} }
func (e *myHTMLRendererExtension) RendererOptions(cfg *html.Config) []html.Option {
e.writer = cfg.Writer()
return []html.Option{
html.WithNodeRenderers(map[ast.NodeKind]html.NodeRenderer{
KindMyNode: html.NodeRendererFunc(renderMyNode),
}),
}
}
Use both extensions together when building the parser and renderer:
p := parser.New(parser.WithExtensions(NewMyParser()))
r := html.New(html.WithExtensions(NewMyHTMLRenderer()))
doc := p.Parse(source)
if err := r.Render(&buf, source, doc); err != nil {
// ...
}
Recommended naming convention
- Use
myext.NewParser()andmyext.NewHTMLRenderer()for the extension constructors, andKindMyExtfor the node kind variable.- If your extension do not have options, you can use
myext.Parserandmyext.HTMLRendereras the extension values.
- If your extension do not have options, you can use
Setting Pos on nodes
Every AST node stores a Pos() int value that records the byte offset of the node's start in the source. goldmark uses this for features such as source mapping and LSP support.
Automatic setting: goldmark sets Pos automatically in most cases.
- After
BlockParser.Openreturns, the parser setsPosto the position of the first non-space character on the opening line (blockPos.Start + BlockOffset()). - After
InlineParser.Parsereturns, ifPosis still-1(the initial value set byInit), the parser sets it to the position of the trigger character.
Manual setting is only needed when the default is wrong. The most common case is when your parser advances past a fixed prefix before creating the node, and you want Pos to point to a position after that prefix — for example, the content start rather than the syntax character start:
func (s *myInlineParser) Parse(_ ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, segment := block.PeekLine()
if !bytes.HasPrefix(line, []byte("@")) {
return nil
}
block.Advance(1) // skip '@'
_, afterAt := block.Position()
node := NewMyMention()
node.SetPos(afterAt.Start) // point to the mention name, not the '@'
// ...
return node
}
If you do not call SetPos, the parser will fall back to the trigger-character position, which is correct for most simple inline elements.
ParagraphTransformer and ASTTransformer: When you replace or restructure nodes during transformation, the new node does not automatically inherit Pos or HasBlankPreviousLines from the original. You must copy both explicitly:
func (t *myTransformer) Transform(para *ast.Paragraph, reader text.Reader, pc parser.Context) {
newNode := NewMyBlock()
// Copy the position from the paragraph being replaced.
newNode.SetPos(para.Pos())
// Preserve blank-line information so that tight/loose list rendering
// and other spacing logic continues to work correctly.
newNode.SetBlankPreviousLines(para.HasBlankPreviousLines())
parent := para.Parent()
parent.ReplaceChild(para, newNode)
}
Forgetting either of these is a common source of subtle rendering bugs.
Choosing between text.Value, text.MultilineValue, and text.Lines
The text package provides three types for holding source content in AST nodes. Choose based on the CommonMark specification for the field, not on implementation convenience.
| Type | When to use | Examples |
|---|---|---|
text.Value |
The spec guarantees the value fits on a single line | Link destination ([text](url)), fenced code block info string |
text.MultilineValue |
The spec allows the value to span multiple lines | Link title, code span content, raw HTML |
text.Lines |
A special block element that holds raw, unparsed block content line-by-line | CodeBlock.Value, HTMLBlock.Value |
text.Value and text.MultilineValue both reference source positions via text.Index (a [Start, Stop) byte range) or hold a literal string, so they never copy the source unnecessarily. text.Lines is a slice of text.Segment, where each segment corresponds to one source line with optional padding.
Use the generic constructors to create values:
import "github.com/yuin/goldmark/v2/text"
// Value — always single-line
dest := text.NewIndexValue(text.NewIndex(start, stop)) // source position
dest := text.NewStringValue("https://example.com") // literal string
// MultilineValue — may span lines
title := text.NewIndexMultilineValue(text.NewIndex(start, stop)) // single span
title := text.NewIndicesMultilineValue([]text.Index{seg1, seg2}) // multiple spans
// Lines — raw block content
var lines text.Lines
lines.AppendSegment(segment) // add one source line at a time
Complete examples
- See extension directory for complete examples of custom extensions.
Breaking changes in v2
Module path
The module path has changed from github.com/yuin/goldmark to github.com/yuin/goldmark/v2.
Top-level goldmark package
The goldmark.Markdown interface, goldmark.New(), goldmark.Convert(), and the goldmark.Extender interface have been removed.
Use parser.New() and html.New() (or another renderer) directly.
// v1
import "github.com/yuin/goldmark"
md := goldmark.New(goldmark.WithExtensions(...))
md.Convert(source, &buf)
// v2
import (
"github.com/yuin/goldmark/v2/parser"
"github.com/yuin/goldmark/v2/renderer/html"
)
p := parser.New(parser.WithExtensions(...))
r := html.New(html.WithExtensions(...))
doc := p.Parse(source)
r.Render(&buf, source, doc)
Extension pattern
In v1, extensions implemented the goldmark.Extender interface with a single Extend(goldmark.Markdown) method that configured both the parser and renderer.
In v2, parser extensions implement parser.Extension (returns []parser.Option) and renderer extensions implement renderer.Extension[C] (returns []renderer.Option[C]). These are passed separately to parser.New() and html.New().
// v1
type MyExtension struct{}
func (e *MyExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(...)
m.Renderer().AddOptions(...)
}
// v2: split into parser extension and renderer extension
type MyParserExtension struct{}
func (e *MyParserExtension) ParserOptions(c *parser.Config) []parser.Option { ... }
type MyHTMLRendererExtension struct{}
func (e *MyHTMLRendererExtension) RendererOptions(c *html.Config) []html.Option { ... }
renderer package
The renderer is now generic over the writer type. The main interfaces are now renderer.Renderer[W any] and renderer.NodeRenderer[W any].
In v1, NodeRenderer implemented RegisterFuncs(NodeRendererFuncRegisterer) to register NodeRendererFunc callbacks. In v2, use renderer.WithNodeRenderer(kind, nodeRenderer) or renderer.WithNodeRenderers(map[ast.NodeKind]NodeRenderer) options directly.
The v1 signature NodeRendererFunc func(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) is replaced by a generic renderer.NodeRendererFunc[W any]. For HTML rendering, W is io.Writer.
renderer/html package
html.NewRenderer(opts ...Option) renderer.NodeRenderer has been replaced by html.New(opts ...Option) Renderer.
A renderer.Hook[W any] interface is new in v2, allowing pre/post render hooks.
ast.Node interface
Type() NodeTypeand theNodeTypetype (with constantsTypeBlock,TypeInline,TypeDocument) have been removed. Use type assertions toast.BlockNodeorast.InlineNodeinstead.Text(source []byte) []byte(was already deprecated in v1) has been removed.HasBlankPreviousLines(),SetBlankPreviousLines(),Lines(),SetLines(), andIsRaw()have been removed fromNodeand moved to theBlockNodeinterface.- Tree mutation methods (
AppendChild,RemoveChild,RemoveChildren,InsertBefore,InsertAfter,ReplaceChild) no longer take aself Nodeas their first argument. Dumpnow returns*NodeDump.Dump(source []byte) *NodeDumpis the new signature.
New BlockNode and InlineNode interfaces
ast.BlockNode extends Node with block-specific behaviour:
HasBlankPreviousLines() bool/SetBlankPreviousLines(bool)Source() []text.Segment/SetSource([]text.Segment)(replacesLines() *text.Segments)
ast.InlineNode extends Node as a marker interface for inline nodes.
ast.BaseNode.Init() must be called in every custom node constructor
BaseNode now stores a self reference to support argument-free tree mutation methods. Call n.Init(n) in every node constructor, including those of custom extension nodes.
Removed and merged AST nodes
| Removed (v1) | Replacement (v2) |
|---|---|
ast.TextBlock |
Removed (was only used internally by the parser) |
ast.FencedCodeBlock |
Merged into ast.CodeBlock; distinguish via CodeBlock.CodeBlockKind (CodeBlockKindIndented / CodeBlockKindFenced) |
extension/ast.TaskCheckBox |
Removed; task state is stored as an attribute on ListItem |
KindTextBlock and KindFencedCodeBlock no longer exist.
Changed AST node fields and constructors
ast.Text
Segment text.Segment→Value text.ValueNewTextSegment(v Segment)→NewSegmentText(v Segment)NewRawTextSegment(v Segment)removed; useNewSegmentText(v)thenn.SetRaw(true)- New:
NewStringText(s string) *Text
ast.String (inline node) — removed
Use ast.NewStringText(s string) *Text instead.
ast.Emphasis
Level intfield removed.*Emphasisalways represents single emphasis (*/_),*Strongalways represents strong emphasis (**/__). They are now separate types.NewEmphasis(level int)→NewEmphasis()
ast.CodeSpan
- No longer holds
Textchild nodes. Now hasValue text.MultilineValue. NewCodeSpan()→NewCodeSpan(value text.MultilineValue)
ast.RawHTML
- Now has
Value text.MultilineValue(no children). NewRawHTML()→NewRawHTML(value text.MultilineValue)
ast.Heading
- Added
HeadingKind HeadingKind(HeadingKindATXorHeadingKindSetext). NewHeading(level int)→NewHeading(level int, kind HeadingKind)
ast.CodeBlock (fenced)
FencedCodeBlockis merged;NewFencedCodeBlock(info *Text)is gone.Infois nowtext.Value(not*Text);Value text.Linesholds the code body.NewCodeBlock(kind CodeBlockKind)→NewCodeBlock(kind CodeBlockKind, value text.Lines, opts ...CodeBlockOption)- Optional info string:
ast.WithCodeBlockInfo(info)
ast.Link and ast.Image
Destination []byte→Destination text.ValueTitle []byte→Title text.MultilineValueNewLink()→NewLink(destination text.Value, opts ...LinkOption)NewImage(link *Link)→NewImage(destination text.Value, opts ...LinkOption)- Optional title:
ast.WithLinkTitle(title) - Optional reference:
ast.WithLinkReference(kind, value)
ast.AutoLink
AutoLinkType AutoLinkType,Protocol []byte, andvalue *Textremoved.URL(source []byte) []byteandLabel(source []byte) []bytemethods removed.- Now has three
text.Valuefields:Destination(full href, email includesmailto:),Label(display text),Text(raw source text). NewAutoLink(typ AutoLinkType, value *Text)→NewAutoLink(destination, label text.Value, opts ...AutoLinkOption)- Optional source text:
ast.WithAutoLinkText(text)
ast.ReferenceLink
Type ReferenceLinkType→ReferenceLinkKind ReferenceLinkKind- Constants renamed:
ReferenceLinkFull/Collapsed/Shortcut→ReferenceLinkKindFull/Collapsed/Shortcut Value []byte→Value text.MultilineValue
ast.HTMLBlock
HTMLBlockTypetype renamed toHTMLBlockKind.- Constants renamed:
HTMLBlockType1..HTMLBlockType7→HTMLBlockKind1..HTMLBlockKind7.
ast.ListItem
Offset int(public field) → unexported; access viaOffset() int/SetOffset(int).NewListItem(offset int)→NewListItem()
ast.LinkReferenceDefinition
Label/Destination/Title []byte→Label text.MultilineValue,Destination text.Value,Title text.MultilineValue
extension/ast.DefinitionList
Offset intandTemporaryParagraph *Paragraph(public fields) → unexported; access viaOffset()/SetOffset()/TemporaryParagraph()/SetTemporaryParagraph().NewDefinitionList(offset int, para *Paragraph)→NewDefinitionList()
extension/ast.TableCell
NewTableCell()→NewTableCell(alignment Alignment)(alignment is now a required argument)
extension/ast.TableHeader
NewTableHeader(row *TableRow)→NewTableHeader()(child nodes must be moved manually)
text package
The text.Segments type (*Segments holding []Segment) is no longer part of the public Node API.
New types for representing text values:
text.Value— a single contiguous source span or a literal stringtext.Index— a raw(Start, Stop)index pairtext.MultilineValue— a value that may span multiple source linestext.Lines— a list of sourceSegments for block-level content (e.g. code blocks)
text.Reader.FindClosure() and text.FindClosureOptions have been removed (they were moved to parser-internal use only).
parser package
parser.Parser.Parse(reader, opts ...ParseOption) has been simplified. The opts ...ParseOption parameter is removed. reader is now source []byte.
Task list extension
The extension/ast.TaskCheckBox inline node no longer exists. Task state is stored as a text.Value attribute on the ListItem node. Use extension.IsTask(node) and extension.TaskStatusOf(node) to inspect task items.
New in v2
ast.N(node Node, children ...any) Node— builder helper that appends child nodes (or strings) to a node, useful for programmatically constructing an AST.ast.BlockNode/ast.InlineNodeinterfaces for type-safe node categorization.parser.Parser.ParseStringSource()andrenderer.Renderer.RenderStringSource()convenience methods.renderer.Hook[W any]interface for pre/post render callbacks.
Donation
BTC: 1NEDSyUmo4SMTDP83JJQSWi1MvQUGGNMZB
License
MIT
Author
Yusuke Inuzuka
Documentation
¶
Overview ¶
Package goldmark implements functions to convert markdown text to a desired format.
Directories
¶
| Path | Synopsis |
|---|---|
|
_benchmark
|
|
|
cmark
command
|
|
|
Package ast defines AST nodes that represent markdown elements.
|
Package ast defines AST nodes that represent markdown elements. |
|
Package extension is a collection of builtin extensions.
|
Package extension is a collection of builtin extensions. |
|
ast
Package ast defines AST nodes that represents extension's elements
|
Package ast defines AST nodes that represents extension's elements |
|
Package parser contains stuff that are related to parsing a Markdown text.
|
Package parser contains stuff that are related to parsing a Markdown text. |
|
Package renderer renders the given AST to certain formats.
|
Package renderer renders the given AST to certain formats. |
|
html
Package html implements renderer that outputs HTMLs.
|
Package html implements renderer that outputs HTMLs. |
|
Package testutil provides utilities for unit tests.
|
Package testutil provides utilities for unit tests. |
|
Package text provides functionalities to manipulate texts.
|
Package text provides functionalities to manipulate texts. |
|
Code generated by _tools; DO NOT EDIT.
|
Code generated by _tools; DO NOT EDIT. |