goldmark-template

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
Motivation
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.
Features
- 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
Installation
go get github.com/hermit-ink/goldmark-template
Usage
Basic Usage
package 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>
}
With Parser Options
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 its 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>
}
With GFM Extension
md := goldmark.New(
goldmark.WithExtensions(
goldmarktemplate.New(), // Must come FIRST
extension.GFM,
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
)
Examples
Template Actions in Code
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>
Template Actions in Links and Images
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>
Reference Links with Templates
[Example][ref]
[ref]: {{ .URL }} "{{ .Title }}"
Output:
<p><a href="{{ .URL }}" title="{{ .Title }}">Example</a></p>
Standalone Template Actions
Welcome {{ .User.Name }}!
Today is {{ .Date }}.
Output:
<p>Welcome {{ .User.Name }}!</p>
<p>Today is {{ .Date }}.</p>
Limitations and Caveats
Actions can only be used as values in attributes
# Heading {id="{{ .HeadingID }}"}
# Heading {class="{{ .CSSClass }}"}
# Heading {data-value="{{ .Data }}"}
Extension Order Matters
Always register goldmark-template BEFORE other extensions that might interfere with template syntax:
goldmark.WithExtensions(
goldmarktemplate.New(), // First
extension.GFM,
)
No Template Validation
This extension does not validate Go template syntax. Invalid templates pass
through unchanged.
// 1. Process Markdown with goldmark-template
html := processMarkdown(markdown)
// 2. Process the HTML with Go templates
tmpl := template.Must(template.New("").Parse(html))
tmpl.Execute(w, data)
Development
Running Tests
make test
Linting
make lint
Code Coverage
make test-coverage
Notes
The 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 attributes
TemplateActionHTMLRenderer - Renders standalone template actions
- Custom AST Node:
TemplateAction for actions that do not appear in positions
controlled by other parsers such as images and links
Contributing
Contributions are welcome! Please ensure:
- All tests pass
- Code is properly formatted (
make fmt)
- Linting passes (
make lint)
- New features include tests
License
Apache License 2.0 - see LICENSE file for details.
Acknowledgments
Built on top of the excellent goldmark
Markdown parser by Yusuke Inuzuka.