# commonmarker: CommonMark and GFM parsing with a document tree Ruby can modify

> commonmarker is a Ruby gem that wraps the Rust comrak crate, so a Ruby application gets a spec-complete CommonMark parser with the GitHub Flavored Markdown extensions and a mutable document tree. The tree API is the interesting part, since nodes can be rewritten, reordered and rendered back to CommonMark or HTML without string manipulation.

**gjtorikian/commonmarker** — Ruby wrapper for the comrak (CommonMark parser) Rust crate

- Repository: https://github.com/gjtorikian/commonmarker
- Stars: 561 · Forks: 105
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/gjtorikian-commonmarker

## A Ruby gem around a Rust parser, and why that split matters

commonmarker is a Ruby wrapper around comrak, the Rust implementation of the CommonMark parser. The project is written in Rust at its core with a Ruby gem on top, and the repository is laid out accordingly: a lib/ directory for the Ruby code, an ext/ directory holding the Rust extension, a Cargo.toml that declares a workspace whose single member is ext/commonmarker, and a Cargo.lock.

The parsing behaviour is strict in a useful way. The gem passes the whole CommonMark test suite and is therefore described as spec-complete, and it adds the extensions documented in the GitHub Flavored Markdown specification, such as tables, strikethroughs and autolinking. In other words, you are not getting a partial implementation with a friendly name, you are getting a parser that passes the conformance suite and then layers GFM behaviour on top of it.

The Rust core is compiled and shipped as a native extension. The release profile in Cargo.toml shows what that costs and buys: codegen-units set to 1, link time optimisation enabled and symbols stripped. Those settings produce a tighter binary at the price of a slower build, which is the usual trade for a shipped extension, and it is also why the gem is not a pure Ruby install. The repository carries a Gemfile, a Rakefile with a rakelib directory, a commonmarker.gemspec and a .ruby-version, so the Ruby side is a conventional gem layout sitting next to the Cargo workspace.

## Parsing to a document node instead of straight to a string

The most common use is a single call. to_html on a string converts it to HTML, and the options argument is optional:

```ruby
require 'commonmarker'
Commonmarker.to_html('"Hi *there*"', options: {
    parse: { smart: true }
})
# => <p>“Hi <em>there</em>”</p>\n
```

But the gem also gives you the tree. parse returns a :document node, and from there you can print it to HTML, iterate over the children, or modify the document. The document operations are insert_before, insert_after, prepend_child, append_child and delete, and a set of node attributes can be rewritten: url, title, header_level, list_type, list_start, list_tight, fence_info and alert_type.

Position information is available too, which matters when you want to report an error back to the author of the source text:

```ruby
doc = Commonmarker.parse("*Hello* world")
puts doc.first_child.first_child.source_position
# => {:start_line=>1, :start_column=>1, :end_line=>1, :end_column=>7}
```

The two iteration methods differ in a way that is easy to get wrong. walk iterates over a node and recursively over its children, while each iterates only over the direct children and no further. A rewrite that uses each when it means walk will quietly miss nested content, which is the usual reason a transformation leaves some links or some emphasis in place.

## Walking the tree and rewriting links without touching strings

The documented example of tree walking is a link inspection pass, and it is short enough to show the shape of the API:

```ruby
require 'commonmarker'

# parse some string
doc = Commonmarker.parse("# The site\n\n [GitHub](https://www.github.com)")

# Walk tree and print out URLs for links
doc.walk do |node|
  if node.type == :link
    printf("URL = %s\n", node.url)
  end
end
# => URL = https://www.github.com
```

Two details carry the technique. The node carries a type, so you can branch on :link and leave everything else alone. And the transformation that follows in the documentation is not a string replacement: it inserts the link's own child before the link and then deletes the link node, so the link text survives as text while the anchor disappears. The output comment in the documentation shows the residue that this approach leaves behind, an empty anchor element where the link used to be.

The same tree can be rendered back to source. to_commonmark on a node returns raw CommonMark text, so a document can be parsed, edited and re-serialised, which is what you need if you want to store the edited form as Markdown rather than as HTML.

## string_content and literal are not interchangeable

Two accessors read and write the text of a node, and the difference between them is a security detail rather than a naming detail. string_content reads and writes the text of nodes whose content is plain text, such as :text, :code and :code_block. literal reads and writes the raw string a node carries, for every node type that has one, which includes :text, :code, :code_block, :html_block, :html_inline, :raw, :math and :frontmatter.

They exist separately because they mean different things. Rewriting string_content only ever swaps text for text. Rewriting literal on an :html_block, :html_inline or :raw node writes markup that is emitted unescaped:

```ruby
doc = Commonmarker.parse("A <b>bold</b> claim")

doc.walk do |node|
  node.literal = "<i>" if node.type == :html_inline && node.literal == "<b>"
end
```

That single line is the whole warning. If your pipeline takes Markdown that arrives from an untrusted source and then modifies nodes, writing to literal on a raw HTML node lets the content you write reach the browser as markup, and the string you assign is not escaped. Writing to string_content keeps everything on the text side of that line. A tool that sanitises HTML should either refuse to touch raw nodes or handle them separately, and the type check above is the mechanism the API gives you.

One more edge case: a :frontmatter node's literal includes its delimiters and the trailing newlines, so anything you assign to it must include them too.

## Parse options, render options and a nesting limit that raises

The gem accepts the same parse, render and extension options that comrak does, passed as a hash with symbol keys, and the documentation keeps the parse and render distinction visible because comrak does:

```ruby
Commonmarker.to_html('"Hi *there*"', options:{
  parse: { smart: true },
  render: { hardbreaks: false}
})
```

There is also a small convention worth knowing: to disable any option that is not a boolean, you pass in nil.

The parse options include smart punctuation, which converts quotes, full stops and hyphens into typographic forms and is off by default; default_info_string, the default info string for fenced code blocks; relaxed_tasklist_matching, which lets any non-space character mark a task as checked instead of only x and X; relaxed_autolinks, which allows links to be recognised in brackets and permits any URL scheme; leave_footnote_definitions, which keeps footnote definitions in their original positions instead of moving them to the end of the document, and which only affects the AST; ignore_setext, which ignores setext style headings; and sourcepos_chars, which switches source position tracking from bytes to characters, which matters for multi-byte UTF-8 documents.

The one that reads like a security control is max_nesting_depth, defaulting to 1000. A node may not nest deeper than that, and a deeper document raises Commonmarker::NestingDepthExceededError rather than being parsed. That is a bounded recursion guarantee for a parser that will handle Markdown that arrives untrusted, and it is worth knowing that the error exists so you can catch it rather than let it surface as a generic failure.

## Extensions are on by default, which is a decision you inherit

The gem enables several extensions out of the box for end user convenience. From the GitHub Flavored Markdown specification come strikethrough, table, autolink and tasklist, and shortcodes are enabled as well. The syntax_highlighter plugin is also on by default, using the base16-ocean.dark theme.

That default is convenient and it is also a decision you inherit. Autolink means a bare URL in the text becomes a link without the author writing one, and relaxed_autolinks, if you turn it on, goes further and permits any URL scheme. Tasklist makes list items interactive checkboxes in the rendered output, and the syntax highlighter changes the markup around fenced code blocks, which is the kind of change that breaks a pipeline that compares rendered HTML against a stored expectation.

If your content is authored by people who did not grow up with GitHub, the defaults may be wrong for you, and the gem exposes comrak's extension options so you can turn them off individually. The documentation points at the options and plugins section for the full list. The broader point is that the extension set is a compatibility surface, not a private detail, and the fact that it can differ between installations makes it worth pinning in your configuration rather than assuming it.

## MIT licensing, a Ruby gem release train and the cost of the Rust dependency

The licence is MIT, with the text in LICENSE.txt, which leaves the usual questions about using the gem inside a commercial product out of scope. The repository is not archived and the last push was on 2026-10-16, and the releases are frequent and clearly versioned: v2.10.1 on 2026-10-16, v2.10.0 on 2026-08-24 and v2.9.0 on 2026-07-14. A CHANGELOG.md tracks the changes, and the project keeps a CODE_OF_CONDUCT.txt alongside a .rubocop.yml.

The upgrade cost is the part that catches people. The gem is a wrapper around a Rust crate, so an upgrade can pull in a new comrak release, which can change parsing behaviour, which can change the HTML your users see. That is a small risk for most content and a real one for a product that renders user generated Markdown and compares the result, where a change in how autolinks or task lists are handled shows up as a diff.

The alternative is to decide not to use the gem at all. If your service is written in Rust, or if you already run comrak in another part of the stack, calling comrak directly gives you the same parser and the same options without the Ruby wrapper, and the wrapper's real contribution is the Ruby facing document API rather than the parsing. If your application only needs Markdown in and HTML out, the to_html call is the entire surface, and the tree, the source positions and the options you never pass are weight you carry for nothing. Take the tree when you need to edit documents, and take the simpler path when you do not.

## Conclusion

commonmarker fits a Ruby application that has to render Markdown it did not write, that needs to inspect or rewrite the document rather than just emit HTML, and that can accept a native extension in its build. It is the wrong choice for a Ruby project that must build without a Rust toolchain, for a service that only needs to render Markdown to HTML and nothing else, and for a case where you want to remain on the upstream comrak release train, since the gem tracks the crate rather than the other way round. Before adopting it, check the default extensions against what your content actually needs, because strikethrough, table, autolink, tasklist, shortcodes and the syntax highlighter are all on by default, and decide whether you want the mutable tree at all, since to_html on its own is the whole surface most applications use.

## FAQ

### What is commonmarker and what does it wrap?

commonmarker is a Ruby gem that wraps comrak, the Rust CommonMark parser. It passes the full CommonMark test suite and is therefore spec-complete, and it adds the GitHub Flavored Markdown extensions such as tables, strikethroughs and autolinking.

### How do I convert Markdown to HTML with commonmarker?

Call to_html on a UTF-8 string, with an optional options hash such as parse: { smart: true }. The gem expects UTF-8 input, so the encoding has to be right before the string is passed in, and the options argument covers the same parse, render and extension settings comrak accepts.

### How do I modify a parsed document in commonmarker?

Parse with Commonmarker.parse to get a document node, then walk it with walk for a recursive visit or each for direct children only. Nodes support insert_before, insert_after, prepend_child, append_child and delete, plus attributes such as url, title and header_level, and a document can be rendered back to Markdown with to_commonmark.

### What is the difference between string_content and literal in commonmarker?

string_content reads and writes plain text content, and rewriting it only swaps text for text. literal reads and writes the raw string a node carries, and assigning to literal on an :html_block, :html_inline or :raw node writes markup that is emitted unescaped, which matters when the Markdown comes from an untrusted source.

## Sources

- [gjtorikian/commonmarker on GitHub](https://github.com/gjtorikian/commonmarker)
- [Issues](https://github.com/gjtorikian/commonmarker/issues)
- [License: MIT](https://github.com/gjtorikian/commonmarker/blob/main/LICENSE)
- [README](https://github.com/gjtorikian/commonmarker/blob/main/README.md)
- [Releases](https://github.com/gjtorikian/commonmarker/releases)

---

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