# gomarkdown keeps CommonMark off by default and calls that a compatibility decision

> The gomarkdown/markdown package is a Go markdown parser and HTML renderer that depends only on the standard library, with an AST you can walk and a renderer you can replace. Its most consequential design choice is that the CommonMark flag is off by default, so existing output does not change when you upgrade.

**gomarkdown/markdown** — markdown parser and HTML renderer for Go

- Repository: https://github.com/gomarkdown/markdown
- Stars: 1,738 · Forks: 199
- Language: Go
- License: NOASSERTION
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/gomarkdown-markdown

## Parser, AST and renderer are three separate packages

The library is split into four importable pieces, and the split is the API.

`parser` builds the document, `ast` defines the tree it produces, `html` renders that tree, and the top level `markdown` package ties them together. Anything you can do to the tree, you can do by walking it, which is the reason to choose this library over a string in, string out converter.

The default path takes about twenty lines. Create a parser with extensions, parse into a document, build a renderer from options, then render:

```go
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse(md)

htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.Renderer{Flags: htmlFlags}
renderer := html.NewRenderer(opts)

return markdown.Render(doc, renderer)
```

The examples directory in the repository shows the variations that matter: `modify_ast.go` for transforming the tree, `render_hook.go` for intercepting rendering, `parser_hook.go` for extending parsing itself, `inline.go`, and `code_highlight.go` for fenced code.

The module targets Go 1.22 and the README claims it depends only on standard library packages, which means no transitive dependency surface to audit.

## CommonMark exists, and turning it on changes more than you expect

`parser.CommonMark` selects the CommonMark 0.31.2 grammar, and the README states that all 652 official examples are tested against exact HTML output. The flag is off by default, and the stated reason is preserving existing parsing and rendering behaviour.

That reason is the whole point and also the trap. Because the flag changes the grammar, enabling it is not a rendering tweak, it is a compatibility decision for your users.

The specific consequence: CommonMark mode uses strict CommonMark syntax regardless of the parser's Extensions setting. Extensions and custom block or inline parsers apply only when the flag is off. So a document that relies on a table extension, or on a block parser you registered through a parser hook, stops being parsed that way the moment you switch.

What still works is the surrounding machinery. The same ast nodes are produced, so AST traversal, transformation, custom renderers and HTML rendering hooks remain available. Reference destination and title overrides also remain available.

Rendering has a separate detail. When `markdown.ToHTML` is called with no renderer, CommonMark documents render with XHTML tags and no smart punctuation, because that is what the official examples expect. Pass your own `html.Renderer` and you get your own flags instead.

## Runtime safety is not output safety, and the README draws the line

The README makes a claim that is easy to quote out of context, so it is worth reading carefully. It says markdown is paranoid when parsing, making it safe to feed untrusted user input without fear of bad things happening, that the test suite stress tests this, and that there are no known inputs that make it crash. It invites crash reports with the offending input.

Then it adds the caveat in capitals of intent: safety here means runtime safety only.

The concrete risk is named and specific. The `parser.Attributes` and `parser.InlineAttributes` extensions let the markdown author set arbitrary HTML attributes, including `onclick`, through an attribute block such as `{: key="value"}`. That is by design, and the README is explicit that you must never enable those extensions for untrusted input, even when you also set `html.SkipHTML`. Those two flags together do not save you.

The recommended path is to sanitize after rendering, using Bluemonday:

```go
maybeUnsafeHTML := markdown.ToHTML(md, nil, nil)
html := bluemonday.UGCPolicy().SanitizeBytes(maybeUnsafeHTML)
```

The repository backs the runtime claim with fuzzing: `fuzz.go` and `fuzz_crashes_test.go` sit at the top level, so continuous fuzzing is wired into the test run.

## Table parsing differs from GitHub in one spot

Tables are supported with a pipe syntax, with a footer separated by a row of equals signs, and with colspan expressed by repeating the pipe symbol. A cell spanning columns is written with an empty cell between pipes.

The escaping rule is where this parser parts company with the site most people know. A pipe inside a code span does not end the cell, so `` `a|b` `` is one cell. Elsewhere you write a literal pipe as `\|`.

GitHub behaves differently: there a pipe always ends the cell, and `\|` is the only way to include a literal one. The README calls out the consequence precisely, noting that a `\|` inside a code span is left as written here rather than being unescaped.

That is the kind of detail that produces a diff when you move content between a repository README and your own site. Documentation containing pipes in code spans will render differently, and it will render differently in a way that looks like a bug rather than a documented difference.

Intra-word emphasis suppression is the other default worth knowing. Because underscore is common inside identifiers, the parser can treat emphasis markers as ordinary characters when they occur inside a word, which avoids mangling code discussion in prose.

## Thread safety, speed and the absence of releases

Two claims that affect how you deploy it. Multiple parsers can run in different goroutines without ill effect, because there is no dependence on global shared state. And it is fast enough to render on demand in most web applications without caching the output.

The second claim is about whether you need a render cache. For per request rendering of user supplied content, that removes an entire component. It is a design consequence of the parser doing no I/O and holding no shared state.

Standards compliance is stated as output validating under the W3C tool for HTML 4.01 and XHTML 1.0 Transitional. Compatibility with the Markdown 1.0.3 test suite passes with the `--tidy` option; without it, the README says the remaining differences are mostly whitespace and entity escaping, where it considers itself more consistent.

Maintenance facts are thinner than the feature list. The repository is not archived and the last push was on 2026-10-11, but there are no GitHub releases at all, so there is no version tag to pin and no changelog in the conventional sense. The module path is the stable identifier, and go.mod declares Go 1.22 with no dependencies. The licence file is `LICENSE.txt`, and GitHub reports it as a custom licence it cannot classify, so read that file before vendoring rather than assuming MIT or BSD from the ecosystem's habits.

## Against goldmark, and against calling a renderer directly

goldmark is the better known CommonMark implementation in Go, and if CommonMark conformance is your primary requirement it is the more defensible default, particularly since in this library the flag is off by default and disables your extensions when on.

What this library offers instead is a more granular pipeline. You can walk and mutate the AST, swap the renderer entirely, or register custom block and inline parsers, and those hooks are demonstrated in the repository examples rather than described in prose. If your use case is normalising user input, extracting headings, or injecting content into a specific place, the AST is the reason to be here.

Against calling a renderer directly, for a server that shells out to a binary: this library has no process spawn, no output parsing and no version drift between the binary and the server. The separate `gomarkdown/mdtohtml` command exists for the cases where you do want a binary, installed with `go get -u github.com/gomarkdown/mdtohtml` and run as `mdtohtml input-file [output-file]`. That command is also the reference example of how to use the library.

One historical note worth knowing if you are porting older code: the repository carries a `changes-from-blackfriday.md` file at the root, which documents what changed when this code diverged from the blackfriday parser many Go projects depended on.

## Conclusion

Adopt gomarkdown when your Go program needs to render Markdown and you want an AST you can transform before rendering, or you need to swap the HTML renderer without swapping the parser. Do not turn on parser.CommonMark expecting your existing documents to render differently in a good way, because the flag bypasses your Extensions setting entirely, including any custom block or inline parsers you registered. Verify first by enabling CommonMark on a document you already render and diffing the output, and if the input is user supplied, pipe the rendered HTML through Bluemonday, since the parser.Attributes extension lets an author set arbitrary attributes such as onclick.

## FAQ

### How do I convert Markdown to HTML in Go with gomarkdown?

Build a parser with extensions using parser.NewWithExtensions, parse the bytes into a document, build an html.Renderer from html.RendererOptions with your flags, then call markdown.Render with the document and the renderer.

### Does gomarkdown support CommonMark?

Yes, through the parser.CommonMark flag, which selects the CommonMark 0.31.2 grammar and is tested against all 652 official examples. The flag is off by default to preserve existing behaviour, and when on it uses strict CommonMark regardless of your Extensions setting.

### Is gomarkdown safe for untrusted Markdown input?

For runtime safety yes, since the parser is described as paranoid and fuzzed. For output safety no: the parser.Attributes and parser.InlineAttributes extensions let an author set arbitrary attributes such as onclick, so render the output and pass it through a sanitizer like Bluemonday.

### Does gomarkdown have any dependencies?

The README states it depends only on Go standard library packages, and go.mod declares no dependencies with a Go 1.22 directive. That is what makes it viable inside a service with a strict dependency policy.

## Sources

- [gomarkdown/markdown on GitHub](https://github.com/gomarkdown/markdown)
- [Issues](https://github.com/gomarkdown/markdown/issues)
- [README](https://github.com/gomarkdown/markdown/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gomarkdown-markdown
