🐛 FIX: render hardbreak and inline HTML in image alt text (#409)

`RendererHTML.renderInlineAsText` builds the `alt` attribute for images
by walking the inline token stream, but it only handles `text`, `image`
and `softbreak` tokens. `hardbreak`, `html_inline` and `html_block` are
silently dropped, so a hard line break or inline HTML inside an image
description disappears from the alt text.

```python
from markdown_it import MarkdownIt
md = MarkdownIt("commonmark")

md.render("![foo\\\nbar](/url)")   # hard break (backslash)
# now:  <p><img src="/url" alt="foobar" /></p>
# want: <p><img src="/url" alt="foo\nbar" /></p>   (soft break already does this)

md.render("![a<b>c](/url)")        # inline HTML
# now:  <p><img src="/url" alt="ac" /></p>
# want: <p><img src="/url" alt="a&lt;b&gt;c" /></p>
```

### Why this is the intended behaviour

Two independent references, both pointing the same way:

- **JS parity.** This project is a port of `markdown-it` and the
changelog pins the parser to `markdown-it v14.1.0`. Upstream
`renderInlineAsText` at that version handles `text`, `image`,
`html_inline`, `html_block`, `softbreak` **and** `hardbreak`. The Python
port is behind its own declared target for the last three.
- **Internal consistency.** `softbreak` and `hardbreak` are treated as
an identical pair everywhere else in the codebase; #157 previously added
the `softbreak` branch to this very function but stopped there. This
change is the sibling of that fix.

HTML content in `alt` is escaped downstream by `renderAttrs` →
`escapeHtml` (so `<b>` becomes `&lt;b&gt;`), matching the JS behaviour —
no markup reaches the attribute unescaped.

### The fix

Two grouped branches added to `renderInlineAsText`, making it a 1:1
match of the upstream reference:

```python
elif token.type in ("html_inline", "html_block"):
    result += token.content
elif token.type in ("softbreak", "hardbreak"):
    result += "\n"
```

### Verification

- Added two cases to `tests/test_port/fixtures/commonmark_extras.md`
(hard break and inline HTML in an image description), mirroring the
existing #157 soft-break fixture. Both **fail without the change** and
**pass with it**.
- `renderInlineAsText` has a single production caller (the `image`
renderer), and the soft-break / plain-text paths are unchanged (verified
as controls).
- Full test suite: **983 passed**. `pre-commit` ruff, ruff-format and
mypy (strict) all clean.

---

This PR was authored by an AI coding agent (Claude Code) running on this
account: the AI found the bug, ran the repro, wrote the tests, and wrote
this description. The human account holder reviews every change and is
accountable for it. The verification above is real and re-runnable from
the diff. If this isn't the kind of contribution you want, say so and
I'll close it.

Signed-off-by: chuenchen309 <48723787+chuenchen309@users.noreply.github.com>
Co-authored-by: Chris Sewell <chrisj_sewell@hotmail.com>
GitOrigin-RevId: 8501cf3369dc879353c5e5a2280941f73c42d392
Change-Id: Ifca14f1ecc8e9c02e24e8c52f69f60d3599074a4
1 file changed
tree: 396549bf5459351cb4fbab1e9f582e4e0e52563b
  1. .github/
  2. docs/
  3. markdown_it/
  4. scripts/
  5. .gitignore
  6. .pre-commit-config.yaml
  7. .readthedocs.yml
  8. AGENTS.md
  9. CHANGELOG.md
  10. codecov.yml
  11. LICENSE
  12. LICENSE.markdown-it
  13. pyproject.toml
  14. README.md
  15. SECURITY.md
  16. tox.ini
README.md

markdown-it-py

Github-CI Coverage Status PyPI Conda PyPI - Downloads

Markdown parser done right.

This is a Python port of markdown-it, and some of its associated plugins. For more details see: https://markdown-it-py.readthedocs.io.

For details on markdown-it itself, see:

See also: markdown-it-pyrs for an experimental Rust binding, for even more speed!

Installation

PIP

pip install markdown-it-py[plugins]

or with extras

pip install markdown-it-py[linkify,plugins]

Conda

conda install -c conda-forge markdown-it-py

or with extras

conda install -c conda-forge markdown-it-py linkify-it-py mdit-py-plugins

Usage

Python API Usage

Render markdown to HTML with markdown-it-py and a custom configuration with and without plugins and features:

from markdown_it import MarkdownIt
from mdit_py_plugins.front_matter import front_matter_plugin
from mdit_py_plugins.footnote import footnote_plugin

md = (
    MarkdownIt('commonmark', {'breaks':True,'html':True})
    .use(front_matter_plugin)
    .use(footnote_plugin)
    .enable('table')
)
text = ("""
---
a: 1
---

a | b
- | -
1 | 2

A footnote [^1]

[^1]: some details
""")
tokens = md.parse(text)
html_text = md.render(text)

## To export the html to a file, uncomment the lines below:
# from pathlib import Path
# Path("output.html").write_text(html_text)

Command-line Usage

Render markdown to HTML with markdown-it-py from the command-line:

usage: markdown-it [-h] [-v] [--stdin|filenames [filenames ...]]

Parse one or more markdown files, convert each to HTML, and print to stdout

positional arguments:
  --stdin        read source Markdown file from standard input
  filenames      specify an optional list of files to convert

optional arguments:
  -h, --help     show this help message and exit
  -v, --version  show program's version number and exit

Interactive:

  $ markdown-it
  markdown-it-py [version 0.0.0] (interactive)
  Type Ctrl-D to complete input, or Ctrl-C to exit.
  >>> # Example
  ... > markdown *input*
  ...
  <h1>Example</h1>
  <blockquote>
  <p>markdown <em>input</em></p>
  </blockquote>

Batch:

  $ markdown-it README.md README.footer.md > index.html

References / Thanks

Big thanks to the authors of markdown-it:

Also John MacFarlane for his work on the CommonMark spec and reference implementations.