Skip to content

An unclosed link or image label makes a later code span render as literal text (upstream markdown-it#1201, fixed in markdown-it 15.0.1) #438

Description

@felzab

Versions

  • markdown-it-py 4.2.0, the latest release on PyPI.
  • markdown-it-py master at 6f58654 (2026-09-17) behaves the same.
  • Python 3.14.4 on Windows 11. The code involved is platform-independent.

What happens

When a line has a [ or ![ that never closes, a valid code span after it can come out as plain text with its backticks visible. The CommonMark reference implementation renders it as code.

from markdown_it import MarkdownIt

md = MarkdownIt("commonmark")
print(md.render("[foo `bar` baz`"))
print(md.render("![alt `code` x`"))
print(md.render("[`a` `` x"))

Actual output (markdown-it-py 4.2.0 and master):

<p>[foo `bar` baz`</p>
<p>![alt `code` x`</p>
<p>[`a` `` x</p>

Expected output (the same as commonmark.js 0.31.2 and markdown-it 15.0.2):

<p>[foo <code>bar</code> baz`</p>
<p>![alt <code>code</code> x`</p>
<p>[<code>a</code> `` x</p>

The bracket alone causes it. With the [ removed, or with a ] added before the unmatched backtick run, the span is rendered as code again:

md.renderInline("`a` `` x")    # '<code>a</code> `` x'
md.renderInline("[`a`] `` x")  # '[<code>a</code>] `` x'

Cause

This comes from markdown-it JS and was reported there as markdown-it/markdown-it#1201. The JS fix is commit 5e9b1cc, "Rework backticks cache to remove side effects", released in markdown-it 15.0.1. The v14 line was not patched: markdown-it 14.3.2 still has the bug. markdown-it-py's port.yaml names JS 14.1.0.

In markdown_it/rules_inline/backticks.py, the rule remembers which backtick runs it has seen. It stores that memory in state.backticks and sets state.backticksScanned once it has scanned to the end of the line. That memory is only correct if the line is read once, from left to right. But parseLinkLabel looks ahead for the closing ] by running the inline rules silently through skipToken, and this lookahead shares the same state. The sequence is:

  1. The lookahead reads the code span, so the span's closing run is consumed and never recorded.
  2. A later unmatched run scans to the end of the line, which sets backticksScanned = True while the record is incomplete.
  3. No ] is found, so the text is parsed for real. At the span's opening run, the check state.backticksScanned and state.backticks.get(openerLength, 0) <= start concludes there is no closing run, and the opening backticks are emitted as text.

Suggested fix

Port 5e9b1cc. It builds the table of the last run of each length from the whole string in a single pass the first time the rule runs, so how the parser walks the line no longer affects it. It also measures the whole of a run that straddles posMax. The commit adds five fixtures to commonmark_extras.txt. The first two are the examples above. The other three ([`](), a](/x)``b`` and ``[foo bar` baz``) already pass in markdown-it-py today, and they would guard the port against regressions.

Why it matters

Code spans bind more tightly than links (CommonMark 0.31.2, section 6.1). This input is valid, and the output differs from the specification. The bundled spec tests do not catch it because no spec example puts an unclosed label before an unmatched backtick run. The failure is silent: a tool that reads code_inline tokens, such as a linter checking paths or identifiers written in code spans, gets a text token and skips the span without any warning.

Related, lower priority

The same JS release also includes 988c82b, "don't strip spaces from all-space code spans". markdown-it-py already handles all-space spans correctly, but it decides whether to strip padding with len(token.content.strip()) > 0. Python's str.strip() also removes tabs, while CommonMark removes padding unless the content is entirely spaces. So ` \t ` renders as <code> \t </code>, where commonmark.js and markdown-it 15.0.2 give <code>\t</code>. This may deserve its own issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions