-
-
Notifications
You must be signed in to change notification settings - Fork 295
Expand file tree
/
Copy pathmarkdown.py
More file actions
294 lines (234 loc) · 11.3 KB
/
Copy pathmarkdown.py
File metadata and controls
294 lines (234 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import re
from textwrap import indent
from typing import Any, Dict, Iterable, cast
from ..core import BaseRenderer, BlockState
from ..util import strip_end
from ._list import render_list, render_list_item
fenced_re = re.compile(r"^[`~]+", re.M)
_backtick_run_re = re.compile(r"`+")
#: leading markers that would be parsed as a new block (list, heading, block
#: quote) if they appear unescaped at the start of a line.
_block_prefix_re = re.compile(r"^(\s*)(>|[-+*]|#{1,6}|\d{1,9}[.)])(\s|$)")
class MarkdownRenderer(BaseRenderer):
"""A renderer to re-format Markdown text."""
NAME = "markdown"
def __call__(self, tokens: Iterable[Dict[str, Any]], state: BlockState) -> str:
out = self.render_tokens(tokens, state)
# special handle for line breaks
out += "\n\n".join(self.render_referrences(state)) + "\n"
return strip_end(out)
def render_referrences(self, state: BlockState) -> Iterable[str]:
ref_links = state.env["ref_links"]
for key in ref_links:
attrs = ref_links[key]
text = "[" + attrs["label"] + "]: " + attrs["url"]
title = attrs.get("title")
if title:
text += ' "' + _escape_title(title) + '"'
yield text
def render_children(self, token: Dict[str, Any], state: BlockState) -> str:
children = token["children"]
return self.render_tokens(children, state)
def text(self, token: Dict[str, Any], state: BlockState) -> str:
raw = cast(str, token["raw"])
# a text token that is made up entirely of "*"/"_" is a literal
# emphasis delimiter -- either an escaped marker from the source
# (``\*``) or an unmatched leftover -- so every character must stay
# escaped, or it would re-parse as emphasis on the round-trip. Prose
# punctuation such as ``2 * 3`` or ``snake_case`` arrives mixed with
# other characters and is left untouched.
if raw and all(c in "*_" for c in raw):
return "".join("\\" + c for c in raw)
# a backtick always opens a code span, so it must stay escaped to
# survive a re-parse as literal text.
return raw.replace("`", "\\`")
def emphasis(self, token: Dict[str, Any], state: BlockState) -> str:
return "*" + self.render_children(token, state) + "*"
def strong(self, token: Dict[str, Any], state: BlockState) -> str:
return "**" + self.render_children(token, state) + "**"
def link(self, token: Dict[str, Any], state: BlockState) -> str:
label = cast(str, token.get("label"))
text = self.render_children(token, state)
out = "[" + text + "]"
if label:
return out + "[" + label + "]"
attrs = token["attrs"]
url: str = attrs["url"]
title = attrs.get("title")
if text == url and not title:
return "<" + text + ">"
elif "mailto:" + text == url and not title:
return "<" + text + ">"
out += "("
if "(" in url or ")" in url:
out += "<" + url + ">"
else:
out += url
if title:
out += ' "' + _escape_title(title) + '"'
return out + ")"
def image(self, token: Dict[str, Any], state: BlockState) -> str:
return "!" + self.link(token, state)
def codespan(self, token: Dict[str, Any], state: BlockState) -> str:
code = cast(str, token["raw"])
# The delimiter must be a run of backticks longer than any backtick run
# inside the content, otherwise the content would close the span early.
longest = max((len(run) for run in _backtick_run_re.findall(code)), default=0)
fence = "`" * (longest + 1)
# A space on each side keeps the delimiter from merging with a leading or
# trailing backtick; the parser strips this padding back off.
if code.startswith("`") or code.endswith("`"):
return fence + " " + code + " " + fence
return fence + code + fence
def linebreak(self, token: Dict[str, Any], state: BlockState) -> str:
return " \n"
def softbreak(self, token: Dict[str, Any], state: BlockState) -> str:
return "\n"
def blank_line(self, token: Dict[str, Any], state: BlockState) -> str:
return ""
def inline_html(self, token: Dict[str, Any], state: BlockState) -> str:
return cast(str, token["raw"])
def paragraph(self, token: Dict[str, Any], state: BlockState) -> str:
text = self.render_children(token, state)
return _escape_block_prefix(text) + "\n\n"
def heading(self, token: Dict[str, Any], state: BlockState) -> str:
level = cast(int, token["attrs"]["level"])
text = self.render_children(token, state)
# An ATX heading ("# ...") occupies a single line, so a heading whose
# rendered text spans several lines -- a multi-line setext heading --
# must be re-emitted in setext form. As ATX the continuation lines
# would fall out of the heading and become a paragraph on a re-parse.
# Only levels 1 and 2 reach this branch, since an ATX heading never
# contains a line break.
if "\n" in text and level in (1, 2):
underline = "=" if level == 1 else "-"
return text + "\n" + underline * 3 + "\n\n"
marker = "#" * level
return marker + " " + text + "\n\n"
def thematic_break(self, token: Dict[str, Any], state: BlockState) -> str:
return "***\n\n"
def block_text(self, token: Dict[str, Any], state: BlockState) -> str:
return _escape_block_prefix(self.render_children(token, state)) + "\n"
def block_code(self, token: Dict[str, Any], state: BlockState) -> str:
attrs = token.get("attrs", {})
info = cast(str, attrs.get("info", ""))
code = cast(str, token["raw"])
if code and code[-1] != "\n":
code += "\n"
marker = token.get("marker")
if not marker:
marker = _get_fenced_marker(code)
marker2 = cast(str, marker)
return marker2 + info + "\n" + code + marker2 + "\n\n"
def block_quote(self, token: Dict[str, Any], state: BlockState) -> str:
# strip the children's trailing blank lines first so the quote marker is
# not added to a dangling empty line; stripping it back off afterwards
# would also eat a ">" that ends the content (an autolink or HTML tag).
text = _render_block_quote_children(self, token, state).rstrip("\n")
text = indent(text, "> ", lambda _: True)
return text + "\n\n"
def block_html(self, token: Dict[str, Any], state: BlockState) -> str:
return cast(str, token["raw"]) + "\n\n"
def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
return ""
def list(self, token: Dict[str, Any], state: BlockState) -> str:
return render_list(self, token, state)
def list_item(self, token: Dict[str, Any], state: BlockState) -> str:
return render_list_item(self, token, state)
def task_list_item(self, token: Dict[str, Any], state: BlockState) -> str:
checked = token.get("attrs", {}).get("checked")
marker = "[x] " if checked else "[ ] "
return render_list_item(self, token, state, marker)
def table(self, token: Dict[str, Any], state: BlockState) -> str:
children = token.get("children", [])
if not children:
return "\n"
head = children[0]
body = children[1] if len(children) > 1 else None
head_cells = head.get("children", [])
align = [_table_cell_align(cell) for cell in head_cells]
lines = [
_render_table_row(self, head_cells, state),
_render_table_delimiter(align),
]
if body:
for row in body.get("children", []):
lines.append(_render_table_row(self, row.get("children", []), state))
return "\n".join(lines) + "\n\n"
def table_head(self, token: Dict[str, Any], state: BlockState) -> str:
cells = token.get("children", [])
return (
_render_table_row(self, cells, state)
+ "\n"
+ _render_table_delimiter([_table_cell_align(c) for c in cells])
)
def table_body(self, token: Dict[str, Any], state: BlockState) -> str:
return "\n".join(self.render_token(row, state).rstrip("\n") for row in token.get("children", []))
def table_row(self, token: Dict[str, Any], state: BlockState) -> str:
return _render_table_row(self, token.get("children", []), state) + "\n"
def table_cell(self, token: Dict[str, Any], state: BlockState) -> str:
return _render_table_cell(self, token, state)
def _escape_title(title: str) -> str:
"""Escape a link/image title for emission inside double quotes. The closing
quote would otherwise end the title early on a re-parse; a backslash is
escaped first so it can't combine with the following character."""
return title.replace("\\", "\\\\").replace('"', '\\"')
def _render_block_quote_children(renderer: MarkdownRenderer, token: Dict[str, Any], state: BlockState) -> str:
children = token["children"]
parts: list[str] = []
previous_type = ""
for child in children:
child_type = child["type"]
if parts and previous_type != "blank_line" and child_type != "blank_line":
parts[-1] = parts[-1].rstrip("\n") + "\n"
parts.append(renderer.render_token(child, state))
previous_type = child_type
return "".join(parts)
def _escape_block_prefix(text: str) -> str:
"""Backslash-escape a leading block marker on each line so that literal
text is not re-parsed as a list, heading or block quote."""
return "\n".join(_escape_line_prefix(line) for line in text.split("\n"))
def _escape_line_prefix(line: str) -> str:
m = _block_prefix_re.match(line)
if not m:
return line
indent_, marker = m.group(1), m.group(2)
return indent_ + marker[:-1] + "\\" + marker[-1] + line[m.end(2) :]
def _get_fenced_marker(code: str) -> str:
found = fenced_re.findall(code)
if not found:
return "```"
ticks = [] # `
waves = [] # ~
for s in found:
if s[0] == "`":
ticks.append(len(s))
else:
waves.append(len(s))
if not ticks:
return "```"
if not waves:
return "~~~"
return "`" * (max(ticks) + 1)
def _render_table_row(renderer: MarkdownRenderer, cells: Iterable[Dict[str, Any]], state: BlockState) -> str:
return "| " + " | ".join(_render_table_cell(renderer, cell, state) for cell in cells) + " |"
def _render_table_delimiter(aligns: Iterable[Any]) -> str:
cells = []
for align in aligns:
if align == "left":
cells.append(":---")
elif align == "center":
cells.append(":---:")
elif align == "right":
cells.append("---:")
else:
cells.append("---")
return "| " + " | ".join(cells) + " |"
def _render_table_cell(renderer: MarkdownRenderer, token: Dict[str, Any], state: BlockState) -> str:
if "children" in token:
text = renderer.render_children(token, state)
else:
text = cast(str, token.get("raw", ""))
return text.replace("\n", " ").replace("|", "\\|").strip()
def _table_cell_align(token: Dict[str, Any]) -> Any:
return token.get("attrs", {}).get("align")