1"""
2class Renderer
3
4Generates HTML from parsed token stream. Each instance has independent
5copy of rules. Those can be rewritten with ease. Also, you can add new
6rules if you create plugin and adds new token types.
7"""
8from __future__ import annotations
9
10from collections.abc import Sequence
11import inspect
12from typing import Any, ClassVar, Protocol
13
14from .common.utils import escapeHtml, unescapeAll
15from .token import Token
16from .utils import EnvType, OptionsDict
17
18
19class RendererProtocol(Protocol):
20 __output__: ClassVar[str]
21
22 def render(
23 self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
24 ) -> Any:
25 ...
26
27
28class RendererHTML(RendererProtocol):
29 """Contains render rules for tokens. Can be updated and extended.
30
31 Example:
32
33 Each rule is called as independent static function with fixed signature:
34
35 ::
36
37 class Renderer:
38 def token_type_name(self, tokens, idx, options, env) {
39 # ...
40 return renderedHTML
41
42 ::
43
44 class CustomRenderer(RendererHTML):
45 def strong_open(self, tokens, idx, options, env):
46 return '<b>'
47 def strong_close(self, tokens, idx, options, env):
48 return '</b>'
49
50 md = MarkdownIt(renderer_cls=CustomRenderer)
51
52 result = md.render(...)
53
54 See https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js
55 for more details and examples.
56 """
57
58 __output__ = "html"
59
60 def __init__(self, parser: Any = None):
61 self.rules = {
62 k: v
63 for k, v in inspect.getmembers(self, predicate=inspect.ismethod)
64 if not (k.startswith("render") or k.startswith("_"))
65 }
66
67 def render(
68 self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
69 ) -> str:
70 """Takes token stream and generates HTML.
71
72 :param tokens: list on block tokens to render
73 :param options: params of parser instance
74 :param env: additional data from parsed input
75
76 """
77 result = ""
78
79 for i, token in enumerate(tokens):
80 if token.type == "inline":
81 if token.children:
82 result += self.renderInline(token.children, options, env)
83 elif token.type in self.rules:
84 result += self.rules[token.type](tokens, i, options, env)
85 else:
86 result += self.renderToken(tokens, i, options, env)
87
88 return result
89
90 def renderInline(
91 self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
92 ) -> str:
93 """The same as ``render``, but for single token of `inline` type.
94
95 :param tokens: list on block tokens to render
96 :param options: params of parser instance
97 :param env: additional data from parsed input (references, for example)
98 """
99 result = ""
100
101 for i, token in enumerate(tokens):
102 if token.type in self.rules:
103 result += self.rules[token.type](tokens, i, options, env)
104 else:
105 result += self.renderToken(tokens, i, options, env)
106
107 return result
108
109 def renderToken(
110 self,
111 tokens: Sequence[Token],
112 idx: int,
113 options: OptionsDict,
114 env: EnvType,
115 ) -> str:
116 """Default token renderer.
117
118 Can be overridden by custom function
119
120 :param idx: token index to render
121 :param options: params of parser instance
122 """
123 result = ""
124 needLf = False
125 token = tokens[idx]
126
127 # Tight list paragraphs
128 if token.hidden:
129 return ""
130
131 # Insert a newline between hidden paragraph and subsequent opening
132 # block-level tag.
133 #
134 # For example, here we should insert a newline before blockquote:
135 # - a
136 # >
137 #
138 if token.block and token.nesting != -1 and idx and tokens[idx - 1].hidden:
139 result += "\n"
140
141 # Add token name, e.g. `<img`
142 result += ("</" if token.nesting == -1 else "<") + token.tag
143
144 # Encode attributes, e.g. `<img src="foo"`
145 result += self.renderAttrs(token)
146
147 # Add a slash for self-closing tags, e.g. `<img src="foo" /`
148 if token.nesting == 0 and options["xhtmlOut"]:
149 result += " /"
150
151 # Check if we need to add a newline after this tag
152 if token.block:
153 needLf = True
154
155 if token.nesting == 1 and (idx + 1 < len(tokens)):
156 nextToken = tokens[idx + 1]
157
158 if nextToken.type == "inline" or nextToken.hidden: # noqa: SIM114
159 # Block-level tag containing an inline tag.
160 #
161 needLf = False
162
163 elif nextToken.nesting == -1 and nextToken.tag == token.tag:
164 # Opening tag + closing tag of the same type. E.g. `<li></li>`.
165 #
166 needLf = False
167
168 result += ">\n" if needLf else ">"
169
170 return result
171
172 @staticmethod
173 def renderAttrs(token: Token) -> str:
174 """Render token attributes to string."""
175 result = ""
176
177 for key, value in token.attrItems():
178 result += " " + escapeHtml(key) + '="' + escapeHtml(str(value)) + '"'
179
180 return result
181
182 def renderInlineAsText(
183 self,
184 tokens: Sequence[Token] | None,
185 options: OptionsDict,
186 env: EnvType,
187 ) -> str:
188 """Special kludge for image `alt` attributes to conform CommonMark spec.
189
190 Don't try to use it! Spec requires to show `alt` content with stripped markup,
191 instead of simple escaping.
192
193 :param tokens: list on block tokens to render
194 :param options: params of parser instance
195 :param env: additional data from parsed input
196 """
197 result = ""
198
199 for token in tokens or []:
200 if token.type == "text":
201 result += token.content
202 elif token.type == "image":
203 if token.children:
204 result += self.renderInlineAsText(token.children, options, env)
205 elif token.type == "softbreak":
206 result += "\n"
207
208 return result
209
210 ###################################################
211
212 def code_inline(
213 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
214 ) -> str:
215 token = tokens[idx]
216 return (
217 "<code"
218 + self.renderAttrs(token)
219 + ">"
220 + escapeHtml(tokens[idx].content)
221 + "</code>"
222 )
223
224 def code_block(
225 self,
226 tokens: Sequence[Token],
227 idx: int,
228 options: OptionsDict,
229 env: EnvType,
230 ) -> str:
231 token = tokens[idx]
232
233 return (
234 "<pre"
235 + self.renderAttrs(token)
236 + "><code>"
237 + escapeHtml(tokens[idx].content)
238 + "</code></pre>\n"
239 )
240
241 def fence(
242 self,
243 tokens: Sequence[Token],
244 idx: int,
245 options: OptionsDict,
246 env: EnvType,
247 ) -> str:
248 token = tokens[idx]
249 info = unescapeAll(token.info).strip() if token.info else ""
250 langName = ""
251 langAttrs = ""
252
253 if info:
254 arr = info.split(maxsplit=1)
255 langName = arr[0]
256 if len(arr) == 2:
257 langAttrs = arr[1]
258
259 if options.highlight:
260 highlighted = options.highlight(
261 token.content, langName, langAttrs
262 ) or escapeHtml(token.content)
263 else:
264 highlighted = escapeHtml(token.content)
265
266 if highlighted.startswith("<pre"):
267 return highlighted + "\n"
268
269 # If language exists, inject class gently, without modifying original token.
270 # May be, one day we will add .deepClone() for token and simplify this part, but
271 # now we prefer to keep things local.
272 if info:
273 # Fake token just to render attributes
274 tmpToken = Token(type="", tag="", nesting=0, attrs=token.attrs.copy())
275 tmpToken.attrJoin("class", options.langPrefix + langName)
276
277 return (
278 "<pre><code"
279 + self.renderAttrs(tmpToken)
280 + ">"
281 + highlighted
282 + "</code></pre>\n"
283 )
284
285 return (
286 "<pre><code"
287 + self.renderAttrs(token)
288 + ">"
289 + highlighted
290 + "</code></pre>\n"
291 )
292
293 def image(
294 self,
295 tokens: Sequence[Token],
296 idx: int,
297 options: OptionsDict,
298 env: EnvType,
299 ) -> str:
300 token = tokens[idx]
301
302 # "alt" attr MUST be set, even if empty. Because it's mandatory and
303 # should be placed on proper position for tests.
304 if token.children:
305 token.attrSet("alt", self.renderInlineAsText(token.children, options, env))
306 else:
307 token.attrSet("alt", "")
308
309 return self.renderToken(tokens, idx, options, env)
310
311 def hardbreak(
312 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
313 ) -> str:
314 return "<br />\n" if options.xhtmlOut else "<br>\n"
315
316 def softbreak(
317 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
318 ) -> str:
319 return (
320 ("<br />\n" if options.xhtmlOut else "<br>\n") if options.breaks else "\n"
321 )
322
323 def text(
324 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
325 ) -> str:
326 return escapeHtml(tokens[idx].content)
327
328 def html_block(
329 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
330 ) -> str:
331 return tokens[idx].content
332
333 def html_inline(
334 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
335 ) -> str:
336 return tokens[idx].content