Markdown Info Strings: What CommonMark Guarantees and How Renderers Diverge
The language tag after a Markdown fence is a hint, not a standard. Here is how CommonMark treats it, why renderers disagree, and how to verify your own pipeline.
28 Sept 2025, 02:49 UTC

The same snippet, three different outputs
You write a Python example inside triple backticks, push it to a repository, and watch it render one way on the code host, another way in a static site, and a third way in a generated PDF. The Markdown source is identical. The difference is not a bug in your prose; it is a gap between what the CommonMark specification guarantees and what individual renderers choose to do with the text after the opening fence.
What CommonMark actually promises
CommonMark 0.31.2 defines a fenced code block as a sequence of lines between an opening fence and a closing fence. The opening fence may be followed by an info string—any text on the same line after the backticks or tildes. The specification treats that string as opaque. It does not define a language registry, does not require syntax highlighting, and does not mandate any particular HTML output. The only structural guarantees are that the closing fence must use the same character and at least as many fence characters as the opening fence, and that the content between fences is preserved verbatim, including indentation and blank lines.
That last point matters for configuration files and logs. An indented code block can strip leading whitespace in some contexts, but a fenced block keeps it. The info string itself, however, carries no promised semantics.
Where renderers add their own rules
GitHub Flavored Markdown and many static-site generators read the first whitespace-delimited token of the info string as a language identifier. When you write ```python, the token python is passed to a highlighter or mapped to a CSS class such as language-python or highlight-source-python. If the token is not recognized, behavior varies: some engines fall back to plain text, some emit no class at all, and some pass the tag through to the HTML unchanged.
This means the info string is a hint, not a contract. Identical tags can produce different class names, different highlighting scopes, or no styling across parsers.
A worked diagnostic: compare two engines locally
You can confirm the divergence in under a minute with two common command-line tools. The commands below assume a Unix-like shell and write files to /tmp; replace paths as needed. No elevated permissions are required.
# Create a fixture with a known tag and an unregistered tag
printf '```python\nprint("hello")\n```\n\n```nonexistentlang\nplain text\n```\n' > /tmp/test.md
# Render with the CommonMark reference implementation
cmark /tmp/test.md > /tmp/cmark.html
# Render with Pandoc
pandoc -f markdown -t html /tmp/test.md > /tmp/pandoc.html
Next, inspect the class attributes in each output. The goal is not to memorize the exact HTML, but to see whether the two engines agree.
grep -o 'class="[^"]*"' /tmp/cmark.html
grep -o 'class="[^"]*"' /tmp/pandoc.html
Expected check: depending on the versions installed, one engine may wrap the first block in a language-python class while the other uses a different class name or syntax-highlighted <span> elements. The nonexistentlang block may receive no class, a generic fallback class, or be treated as literal text. If the outputs differ, your documentation pipeline will inherit that difference wherever those files are rendered.
| Check | Command / Action | What to look for |
|---|---|---|
| Fence termination | Render a file containing a longer backtick run inside the block | Block closes early if inner backticks match the fence length |
| Language class | grep -o 'class="[^"]*"' output.html | Presence, absence, or naming of the class for a known tag |
| Unknown tag | Render ```nonexistentlang | Fallback class, no class, or literal pass-through |
Trade-offs and hard limits
- Migration risk. Moving a docs repository between static-site generators, PDF pipelines, or API documentation tools can silently change highlighting or class names because each tool maintains its own list of recognized languages.
- No standard registry. The info string is not a standardized language registry. A tag that works today in one renderer may be ignored in another, or may collide with a different language name.
- Fence-length collisions. If your code sample contains a run of backticks longer than or equal to the opening fence, the block will terminate prematurely. Increase the fence length or switch to tilde fences.
Make the output predictable
If your documentation must look the same everywhere, stop treating the info string as a portable contract. Pick one parser as the source of truth for critical output, render a fixture file in continuous integration, and assert the class attributes or highlighted markup you expect. For snippets that contain backtick runs, use a longer fence or tildes. And when you introduce a new language tag, test it in every renderer that will consume the file before you merge.
The spec gives you a reliable container. Everything after the fence is convention—so verify the convention you depend on.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.