Skip to content

Commit 614b019

Browse files
committed
fix(renderer): render plugin list and table nodes
Refs #441, #419, #401.
1 parent 1141eec commit 614b019

4 files changed

Lines changed: 129 additions & 9 deletions

File tree

src/mistune/renderers/_list.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ def render_list(renderer: "BaseRenderer", token: Dict[str, Any], state: "BlockSt
2222
return strip_end(text) + "\n"
2323

2424

25-
def _render_list_item(
25+
def render_list_item(
2626
renderer: "BaseRenderer",
27-
parent: Dict[str, Any],
2827
item: Dict[str, Any],
2928
state: "BlockState",
29+
marker: str = "",
3030
) -> str:
31-
leading = cast(str, parent["leading"])
31+
parent = item.get("parent")
32+
if not parent:
33+
parent = {"leading": "- ", "tight": False}
34+
35+
leading = cast(str, parent["leading"]) + marker
3236
text = ""
3337
for tok in item["children"]:
3438
if tok["type"] == "list":
@@ -53,12 +57,15 @@ def _render_ordered_list(renderer: "BaseRenderer", token: Dict[str, Any], state:
5357
start = attrs.get("start", 1)
5458
for item in token["children"]:
5559
leading = str(start) + token["bullet"] + " "
56-
parent = {
60+
item["parent"] = {
5761
"leading": leading,
5862
"tight": token["tight"],
5963
}
60-
yield _render_list_item(renderer, parent, item, state)
61-
start += 1
64+
try:
65+
yield renderer.render_token(item, state)
66+
finally:
67+
item.pop("parent", None)
68+
start += 1
6269

6370

6471
def _render_unordered_list(renderer: "BaseRenderer", token: Dict[str, Any], state: "BlockState") -> Iterable[str]:
@@ -67,4 +74,8 @@ def _render_unordered_list(renderer: "BaseRenderer", token: Dict[str, Any], stat
6774
"tight": token["tight"],
6875
}
6976
for item in token["children"]:
70-
yield _render_list_item(renderer, parent, item, state)
77+
item["parent"] = parent
78+
try:
79+
yield renderer.render_token(item, state)
80+
finally:
81+
item.pop("parent", None)

src/mistune/renderers/markdown.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from ..core import BaseRenderer, BlockState
66
from ..util import strip_end
7-
from ._list import render_list
7+
from ._list import render_list, render_list_item
88

99
fenced_re = re.compile(r"^[`~]+", re.M)
1010

@@ -134,6 +134,45 @@ def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
134134
def list(self, token: Dict[str, Any], state: BlockState) -> str:
135135
return render_list(self, token, state)
136136

137+
def list_item(self, token: Dict[str, Any], state: BlockState) -> str:
138+
return render_list_item(self, token, state)
139+
140+
def task_list_item(self, token: Dict[str, Any], state: BlockState) -> str:
141+
checked = token.get("attrs", {}).get("checked")
142+
marker = "[x] " if checked else "[ ] "
143+
return render_list_item(self, token, state, marker)
144+
145+
def table(self, token: Dict[str, Any], state: BlockState) -> str:
146+
children = token.get("children", [])
147+
if not children:
148+
return "\n"
149+
150+
head = children[0]
151+
body = children[1] if len(children) > 1 else None
152+
head_cells = head.get("children", [])
153+
align = [_table_cell_align(cell) for cell in head_cells]
154+
lines = [
155+
_render_table_row(self, head_cells, state),
156+
_render_table_delimiter(align),
157+
]
158+
if body:
159+
for row in body.get("children", []):
160+
lines.append(_render_table_row(self, row.get("children", []), state))
161+
return "\n".join(lines) + "\n\n"
162+
163+
def table_head(self, token: Dict[str, Any], state: BlockState) -> str:
164+
cells = token.get("children", [])
165+
return _render_table_row(self, cells, state) + "\n" + _render_table_delimiter([_table_cell_align(c) for c in cells])
166+
167+
def table_body(self, token: Dict[str, Any], state: BlockState) -> str:
168+
return "\n".join(self.render_token(row, state).rstrip("\n") for row in token.get("children", []))
169+
170+
def table_row(self, token: Dict[str, Any], state: BlockState) -> str:
171+
return _render_table_row(self, token.get("children", []), state) + "\n"
172+
173+
def table_cell(self, token: Dict[str, Any], state: BlockState) -> str:
174+
return _render_table_cell(self, token, state)
175+
137176

138177
def _escape_block_prefix(text: str) -> str:
139178
"""Backslash-escape a leading block marker on each line so that literal
@@ -168,3 +207,33 @@ def _get_fenced_marker(code: str) -> str:
168207
if not waves:
169208
return "~~~"
170209
return "`" * (max(ticks) + 1)
210+
211+
212+
def _render_table_row(renderer: MarkdownRenderer, cells: Iterable[Dict[str, Any]], state: BlockState) -> str:
213+
return "| " + " | ".join(_render_table_cell(renderer, cell, state) for cell in cells) + " |"
214+
215+
216+
def _render_table_delimiter(aligns: Iterable[Any]) -> str:
217+
cells = []
218+
for align in aligns:
219+
if align == "left":
220+
cells.append(":---")
221+
elif align == "center":
222+
cells.append(":---:")
223+
elif align == "right":
224+
cells.append("---:")
225+
else:
226+
cells.append("---")
227+
return "| " + " | ".join(cells) + " |"
228+
229+
230+
def _render_table_cell(renderer: MarkdownRenderer, token: Dict[str, Any], state: BlockState) -> str:
231+
if "children" in token:
232+
text = renderer.render_children(token, state)
233+
else:
234+
text = cast(str, token.get("raw", ""))
235+
return text.replace("\n", " ").replace("|", "\\|").strip()
236+
237+
238+
def _table_cell_align(token: Dict[str, Any]) -> Any:
239+
return token.get("attrs", {}).get("align")

src/mistune/renderers/rst.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from ..core import BaseRenderer, BlockState
55
from ..util import strip_end
6-
from ._list import render_list
6+
from ._list import render_list, render_list_item
77

88

99
class RSTRenderer(BaseRenderer):
@@ -147,3 +147,11 @@ def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
147147

148148
def list(self, token: Dict[str, Any], state: BlockState) -> str:
149149
return render_list(self, token, state)
150+
151+
def list_item(self, token: Dict[str, Any], state: BlockState) -> str:
152+
return render_list_item(self, token, state)
153+
154+
def task_list_item(self, token: Dict[str, Any], state: BlockState) -> str:
155+
checked = token.get("attrs", {}).get("checked")
156+
marker = "[x] " if checked else "[ ] "
157+
return render_list_item(self, token, state, marker)

tests/test_renderers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,35 @@ def render_wiki(renderer, key, title):
8181
renderer.render_token(token, BlockState()),
8282
'<a href="/wiki/python">Python</a>',
8383
)
84+
85+
86+
class TestMarkdownRendererPlugins(TestCase):
87+
def test_task_list_item_can_be_overridden(self):
88+
class MyRenderer(MarkdownRenderer):
89+
def task_list_item(self, token, state):
90+
return "TASK\n"
91+
92+
md = create_markdown(renderer=MyRenderer(), plugins=["task_lists"])
93+
self.assertEqual(md("- [x] a task"), "TASK\n")
94+
95+
def test_task_list_item_default_markdown(self):
96+
md = create_markdown(renderer=MarkdownRenderer(), plugins=["task_lists"])
97+
self.assertEqual(md("- [x] a task"), "- [x] a task\n")
98+
99+
def test_table_markdown(self):
100+
md = create_markdown(renderer=MarkdownRenderer(), plugins=["table"])
101+
result = md("| A | B |\n|---|---|\n| 1 | 2 |\n")
102+
self.assertEqual(result, "| A | B |\n| --- | --- |\n| 1 | 2 |\n")
103+
104+
def test_standalone_list_item(self):
105+
renderer = MarkdownRenderer()
106+
token = {
107+
"type": "list_item",
108+
"children": [
109+
{
110+
"type": "paragraph",
111+
"children": [{"type": "text", "raw": "x"}],
112+
}
113+
],
114+
}
115+
self.assertEqual(renderer.render_token(token, BlockState()), "- x\n\n")

0 commit comments

Comments
 (0)