diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d50cb3e..6de0c3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## Unreleased + +* ✨ Add `highlight_verbatim` option to pass highlighter output through verbatim, without the `
` wrapper, in [#256](https://github.com/executablebooks/markdown-it-py/issues/256)
+
## 4.2.0 - 2026-05-07
* ✨ Add `make_fence_rule()` factory for configurable fence markers in [#394](https://github.com/executablebooks/markdown-it-py/pull/394)
diff --git a/docs/using.md b/docs/using.md
index 507f49c1..557cc7c9 100644
--- a/docs/using.md
+++ b/docs/using.md
@@ -303,6 +303,45 @@ def function(renderer, tokens, idx, options, env):
+++
+### Code highlighting
+
+Fenced code blocks are rendered as `...
` by default.
+You can customize this with the `highlight` option, which should be a function
+`(content, lang, attrs) -> str` returning escaped HTML:
+
+```python
+from markdown_it import MarkdownIt
+
+def highlight(content, lang, attrs):
+ return f'{content}'
+
+md = MarkdownIt("commonmark", {"highlight": highlight})
+md.render("```python\nprint('hi')\n```")
+```
+
+If the highlighter returns a string starting with `...
`.
+
+If your highlighter produces a complete block of HTML that does not start
+with `` wrapper, or a `` with custom
+attributes), set the Python-only `highlight_verbatim` option to `True` to
+skip the `` wrapper entirely and pass the highlighter output
+through verbatim (a trailing newline is added, as with the pre-continues
+heuristic):
+
+```python
+md = MarkdownIt("commonmark", {"highlight": highlight, "highlight_verbatim": True})
+md.render("```python\nprint('hi')\n```")
+```
+
+Note that `highlight_verbatim` only applies when the `highlight` function
+returns a non-empty string; if it returns an empty string (or `None`), the
+content is escaped and wrapped in `` as usual.
+
++++
+
You can inject render methods into the instantiated render class.
```{jupyter-execute}
diff --git a/markdown_it/renderer.py b/markdown_it/renderer.py
index f690b091..e7202339 100644
--- a/markdown_it/renderer.py
+++ b/markdown_it/renderer.py
@@ -277,9 +277,14 @@ def fence(
langAttrs = arr[1]
if options.highlight:
- highlighted = options.highlight(
- token.content, langName, langAttrs
- ) or escapeHtml(token.content)
+ highlighted = options.highlight(token.content, langName, langAttrs)
+ if highlighted:
+ if options.get("highlight_verbatim", False):
+ # Pass the highlighter output through verbatim —
+ # byte-exact, no wrapper, no added newline.
+ return highlighted
+ else:
+ highlighted = escapeHtml(token.content)
else:
highlighted = escapeHtml(token.content)
diff --git a/markdown_it/utils.py b/markdown_it/utils.py
index 09e60163..cbd1d321 100644
--- a/markdown_it/utils.py
+++ b/markdown_it/utils.py
@@ -36,6 +36,20 @@ class OptionsType(TypedDict):
"""CSS language prefix for fenced blocks."""
highlight: Callable[[str, str, str], str] | None
"""Highlighter function: (content, lang, attrs) -> str."""
+ highlight_verbatim: NotRequired[bool]
+ """Pass highlighter output through verbatim, without the ```` wrapper.
+
+ When ``True`` and the ``highlight`` function returns a non-empty string,
+ the returned HTML is used as-is (a trailing newline is added), instead of
+ being wrapped in ``...
``. This is useful when the
+ highlighter produces a complete block of HTML (e.g. starting with a
+ ```` or a ```` with custom attributes) that should not be
+ wrapped again.
+
+ This is a Python only option. The default is ``False``, in which case the
+ output is only passed through verbatim if it already starts with `` MarkdownIt:
+ return MarkdownIt("commonmark", {"highlight": highlight, **options})
+
+
+def test_default_wraps_non_pre_output():
+ """Default behavior: highlighter output not starting with {content}
"
+
+ md = _md(highlight)
+ assert md.render("```python\nhl\n```") == (
+ "hl\n
\n"
+ )
+
+
+def test_default_pre_continues_heuristic():
+ """Default behavior: output starting with {content}"
+
+ md = _md(highlight)
+ assert md.render("```python\nhl\n```") == "hl\n
\n"
+
+
+def test_verbatim_passes_through_non_pre_output():
+ """With highlight_verbatim, non-{content}"
+
+ md = _md(highlight, highlight_verbatim=True)
+ assert md.render("```python\nhl\n```") == "hl\n"
+
+
+def test_verbatim_passes_through_pre_output():
+ """With highlight_verbatim, {content}"
+
+ md = _md(highlight, highlight_verbatim=True)
+ assert md.render("```python\nhl\n```") == "hl\n
"
+
+
+def test_verbatim_skips_lang_class_injection():
+ """With highlight_verbatim, no language class is injected into the output."""
+
+ def highlight(content, lang, attrs):
+ assert lang == "python"
+ return f"{content}"
+
+ md = _md(highlight, highlight_verbatim=True)
+ assert md.render("```python\nhl\n```") == "hl\n"
+
+
+def test_verbatim_falls_back_when_highlighter_returns_empty():
+ """Falsy highlighter output still falls back to the escaped wrapper."""
+
+ def highlight(content, lang, attrs):
+ return ""
+
+ md = _md(highlight, highlight_verbatim=True)
+ assert md.render("```python\nhl\n```") == (
+ 'hl\n
\n'
+ )
+
+
+def test_verbatim_without_highlighter_uses_default_wrapper():
+ """Without a highlighter, highlight_verbatim has no effect."""
+
+ md = _md(None, highlight_verbatim=True)
+ assert md.render("```python\nhl\n```") == (
+ 'hl\n
\n'
+ )
+
+
+def test_verbatim_can_be_set_after_construction():
+ """The option can be toggled on the instance after construction."""
+
+ def highlight(content, lang, attrs):
+ return f"{content}"
+
+ md = _md(highlight)
+ assert md.render("```\nhl\n```") == "hl\n
\n"
+ md.options["highlight_verbatim"] = True
+ assert md.render("```\nhl\n```") == "hl\n"
+ md.options["highlight_verbatim"] = False
+ assert md.render("```\nhl\n```") == "hl\n
\n"
+
+
+def test_verbatim_preserves_absence_of_trailing_newline():
+ """No trailing newline is added — verbatim is byte-exact.
+
+ The pre-continues heuristic path keeps its historical ``+ "\\n"``
+ (wrapped blocks are renderer-owned); the verbatim path returns the
+ highlighter's bytes untouched, newline or none.
+ """
+
+ def highlight(content, lang, attrs):
+ return "hl" # no trailing newline
+
+ md = _md(highlight, highlight_verbatim=True)
+ assert md.render("```\nhl\n```") == "hl"
+
+
+def test_verbatim_is_byte_exact():
+ """Verbatim means byte-exact: render output == highlighter return.
+
+ Regression for the trailing-newline defect (matrix-03): with
+ highlight_verbatim, the renderer must not append, strip, or alter
+ a single byte of the highlighter's return value — with or without
+ a trailing newline.
+ """
+
+ def hl_no_nl(content, lang, attrs):
+ return "no-newline"
+
+ def hl_with_nl(content, lang, attrs):
+ return "with-newline\n"
+
+ assert (
+ _md(hl_no_nl, highlight_verbatim=True).render("```x\nc\n```")
+ == "no-newline"
+ )
+ assert (
+ _md(hl_with_nl, highlight_verbatim=True).render("```x\nc\n```")
+ == "with-newline\n"
+ )