Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions markdown_it/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,22 @@ def render(
:param env: additional data from parsed input

"""
result = ""
# Collect the fragments and join them once: repeated ``str +=`` is only
# linear on interpreters that can extend a string in place (the default
# CPython build), and quadratic in the output size elsewhere (e.g. PyPy,
# GraalPy and free-threaded CPython 3.13).
out: list[str] = []

for i, token in enumerate(tokens):
if token.type == "inline":
if token.children:
result += self.renderInline(token.children, options, env)
out.append(self.renderInline(token.children, options, env))
elif token.type in self.rules:
result += self.rules[token.type](tokens, i, options, env)
out.append(self.rules[token.type](tokens, i, options, env))
else:
result += self.renderToken(tokens, i, options, env)
out.append(self.renderToken(tokens, i, options, env))

return result
return "".join(out)

def renderInline(
self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
Expand All @@ -107,15 +111,15 @@ def renderInline(
:param options: params of parser instance
:param env: additional data from parsed input (references, for example)
"""
result = ""
out: list[str] = []

for i, token in enumerate(tokens):
if token.type in self.rules:
result += self.rules[token.type](tokens, i, options, env)
out.append(self.rules[token.type](tokens, i, options, env))
else:
result += self.renderToken(tokens, i, options, env)
out.append(self.renderToken(tokens, i, options, env))

return result
return "".join(out)

def renderToken(
self,
Expand Down Expand Up @@ -205,20 +209,20 @@ def renderInlineAsText(
:param options: params of parser instance
:param env: additional data from parsed input
"""
result = ""
out: list[str] = []

for token in tokens or []:
if token.type in ("text", "code_inline"):
result += token.content
out.append(token.content)
elif token.type == "image":
if token.children:
result += self.renderInlineAsText(token.children, options, env)
out.append(self.renderInlineAsText(token.children, options, env))
elif token.type in ("html_inline", "html_block"):
result += token.content
out.append(token.content)
elif token.type in ("softbreak", "hardbreak"):
result += "\n"
out.append("\n")

return result
return "".join(out)

###################################################

Expand Down
58 changes: 58 additions & 0 deletions tests/test_api/test_renderer_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""How the renderer combines the strings returned by its rules."""

from types import MethodType

import pytest

from markdown_it import MarkdownIt
from markdown_it.renderer import RendererHTML
from markdown_it.token import Token


def _renderer_with(values):
"""A renderer whose ``custom`` rule returns ``values[idx]`` for token ``idx``."""
renderer = RendererHTML()
renderer.rules["custom"] = MethodType(
lambda self, tokens, idx, opts, env: values[idx], renderer
)
return renderer, [Token("custom", "", 0) for _ in values]


class Wrapped(str):
"""A ``str`` subclass whose overloaded addition must not be used."""

def __radd__(self, other):
raise AssertionError("not called")

__add__ = __radd__


@pytest.mark.parametrize("method", ["render", "renderInline"])
def test_render_rule_results_are_joined_into_a_plain_str(method):
# The fragments are joined in order, and the result is a plain ``str``
# even when a rule returns a ``str`` subclass (overloaded addition is not used).
renderer, tokens = _renderer_with(["a", Wrapped("b"), "c"])
result = getattr(renderer, method)(tokens, MarkdownIt().options, {})
assert type(result) is str
assert result == "abc"


@pytest.mark.parametrize("method", ["render", "renderInline"])
def test_render_rule_invalid_result_raises(method):
renderer, tokens = _renderer_with(["first", None, "third"])
with pytest.raises(TypeError):
getattr(renderer, method)(tokens, MarkdownIt().options, {})


def test_render_inline_as_text_is_joined_into_a_plain_str():
# Image ``alt`` text is collected the same way: joined in order into a plain
# ``str``, without using a ``str`` subclass's overloaded addition.
def text(content):
token = Token("text", "", 0)
token.content = content
return token

tokens = [text("y"), Token("softbreak", "br", 0), text(Wrapped("x"))]
result = RendererHTML().renderInlineAsText(tokens, MarkdownIt().options, {})
assert type(result) is str
assert result == "y\nx"
Loading