A goldmark extension that preserves Go template actions ({{...}}) in rendered Markdown, preventing HTML escaping and maintaining template syntax wherever it appears so that the HTML output can be used directly by the Go stdlib html/template
You want to use go html/template actions in your markdown. Markdown is not HTML so you can't execute the markdown as a template and you might not want to do that anyway. Instead you want to goldmark to simply "ignore" all the go template actions putting them verbatim into the correct places in the output HTML so that you can execute the output from goldmark using go's html/template package.
Warning
This makes your goldmark instance no longer commonmark compliant since go template actions are not valid URLs and you want to be able to use a template action in place of a URL potentially.
- Preserves template actions in inline code and code blocks
- Template-aware parsing for links, images, and autolinks
- Reference link support with template URLs and titles
- Standalone template actions as inline elements
- Full compatibility with other goldmark extensions (GFM, etc.)
- Smart parsing that handles quotes and nested braces correctly
- Comprehensive testing for 100% compatibility with the existing goldmark parsers and renderers
go get github.com/hermit-ink/goldmark-templatepackage main
import (
"bytes"
"fmt"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/renderer/html"
goldmarktemplate "github.com/hermit-ink/goldmark-template"
)
func main() {
md := goldmark.New(
goldmark.WithExtensions(
goldmarktemplate.New(),
),
goldmark.WithRendererOptions(
html.WithUnsafe(), // Required for action preservation in raw HTML
),
)
input := []byte("# {{ .Title }}\n\n[Link]({{ .URL }})")
var buf bytes.Buffer
if err := md.Convert(input, &buf); err != nil {
panic(err)
}
fmt.Println(buf.String())
// Output: <h1>{{ .Title }}</h1>\n<p><a href="{{ .URL }}">Link</a></p>
}Since goldmark-template replaces built-in parsers you need to use the goldmark-template
specific way of adding parser options. The API is identical to goldmark's WithParserOptions except it's an alternative constructor for the extensions. Use it just like
you would goldmark's WithParserOptions.
package main
import (
"bytes"
"fmt"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
goldmarktemplate "github.com/hermit-ink/goldmark-template"
)
func main() {
md := goldmark.New(
goldmark.WithExtensions(
goldmarktemplate.WithParserOptions(
parser.WithAutoHeadingID(),
parser.WithAttribute(),
),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)
input := []byte("# {{ .Title }}\n\n[Link]({{ .URL }})")
var buf bytes.Buffer
if err := md.Convert(input, &buf); err != nil {
panic(err)
}
fmt.Println(buf.String())
// Output: <h1 id="title">{{ .Title }}</h1>\n<p><a href="{{ .URL }}">Link</a></p>
}md := goldmark.New(
goldmark.WithExtensions(
goldmarktemplate.New(), // Must come FIRST
extension.GFM,
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)Inline: `{{ .Variable }}`
Block:
```go
func main() {
fmt.Println("{{ .Message }}")
}
```Output:
<p>Inline: <code>{{ .Variable }}</code></p>
<pre><code class="language-go">func main() {
fmt.Println("{{ .Message }}")
}
</code></pre>Input:
[{{ .LinkText }}]({{ .URL }})

<{{ .BaseURL }}/page>Output:
<p><a href="{{ .URL }}">{{ .LinkText }}</a></p>
<p><img src="{{ .ImagePath }}" alt="{{ .Alt }}"></p>
<p><a href="{{ .BaseURL }}/page">{{ .BaseURL }}/page</a></p>[Example][ref]
[ref]: {{ .URL }} "{{ .Title }}"Output:
<p><a href="{{ .URL }}" title="{{ .Title }}">Example</a></p>Welcome {{ .User.Name }}!
Today is {{ .Date }}.Output:
<p>Welcome {{ .User.Name }}!</p>
<p>Today is {{ .Date }}.</p># Heading {id="{{ .HeadingID }}"}
# Heading {class="{{ .CSSClass }}"}
# Heading {data-value="{{ .Data }}"}Always register goldmark-template BEFORE other extensions that might interfere with template syntax:
goldmark.WithExtensions(
goldmarktemplate.New(), // First
extension.GFM,
)This extension does not validate Go template syntax. Invalid templates pass through unchanged.
// 1. Process Markdown with goldmark-template
md := goldmark.New(goldmark.WithExtensions(goldmarktemplate.New()))
input := []byte("# {{ .Title }}\n\n[Link]({{ .URL }})")
var htmlTmpl bytes.Buffer
if err := md.Convert(input, &htmlTmpl); err != nil {
panic(err)
}
// 2. Process the HTML with Go templates
data := struct {
URL string
Title string
}{
URL: "https://example.com",
Title: "Example Page",
}
tmpl := template.Must(template.New("").Parse(string(htmlTmpl)))
tmpl.Execute(w, data)make testmake lintmake test-coverageThe extension follows goldmark's established patterns:
- Custom Parsers: Template-aware parsers for links, autolinks, and reference definitions. These are taken directly from the goldmark source with the minimal possible changes to allow template actions to be preserved untouched.
- Custom Renderers:
Renderer- Overrides standard elements to preserve template actions properly within attributesTemplateActionHTMLRenderer- Renders standalone template actions
- Custom AST Node:
TemplateActionfor actions that do not appear in positions controlled by other parsers such as images and links
Contributions are welcome! Please ensure:
- All tests pass
- Code is properly formatted (
make fmt) - Linting passes (
make lint) - New features include tests
Apache License 2.0 - see LICENSE file for details.
Built on top of the excellent goldmark Markdown parser by Yusuke Inuzuka.