Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown_it/renderer.py: 26%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2class Renderer
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"""
9from __future__ import annotations
11from collections.abc import Sequence
12import inspect
13from typing import Any, ClassVar, Protocol
15from .common.utils import escapeHtml, unescapeAll
16from .token import Token
17from .utils import EnvType, OptionsDict
20class RendererProtocol(Protocol):
21 __output__: ClassVar[str]
23 def render(
24 self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
25 ) -> Any: ...
28class RendererHTML(RendererProtocol):
29 """Contains render rules for tokens. Can be updated and extended.
31 Example:
33 Each rule is called as independent static function with fixed signature:
35 ::
37 class Renderer:
38 def token_type_name(self, tokens, idx, options, env) {
39 # ...
40 return renderedHTML
42 ::
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>'
50 md = MarkdownIt(renderer_cls=CustomRenderer)
52 result = md.render(...)
54 See https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js
55 for more details and examples.
56 """
58 __output__ = "html"
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 }
67 def render(
68 self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
69 ) -> str:
70 """Takes token stream and generates HTML.
72 :param tokens: list on block tokens to render
73 :param options: params of parser instance
74 :param env: additional data from parsed input
76 """
77 result = ""
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)
88 return result
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.
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 = ""
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)
107 return result
109 def renderToken(
110 self,
111 tokens: Sequence[Token],
112 idx: int,
113 options: OptionsDict,
114 env: EnvType,
115 ) -> str:
116 """Default token renderer.
118 Can be overridden by custom function
120 :param idx: token index to render
121 :param options: params of parser instance
122 """
123 result = ""
124 needLf = False
125 token = tokens[idx]
127 # Tight list paragraphs
128 if token.hidden:
129 return ""
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"
141 # Add token name, e.g. `<img`
142 result += ("</" if token.nesting == -1 else "<") + token.tag
144 # Encode attributes, e.g. `<img src="foo"`
145 result += self.renderAttrs(token)
147 # Add a slash for self-closing tags, e.g. `<img src="foo" /`
148 if token.nesting == 0 and options["xhtmlOut"]:
149 result += " /"
151 # Check if we need to add a newline after this tag
152 if token.block:
153 needLf = True
155 if token.nesting == 1 and (idx + 1 < len(tokens)):
156 nextToken = tokens[idx + 1]
158 if nextToken.type == "inline" or nextToken.hidden:
159 # Block-level tag containing an inline tag.
160 #
161 needLf = False
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
168 result += ">\n" if needLf else ">"
170 return result
172 @staticmethod
173 def renderAttrs(token: Token) -> str:
174 """Render token attributes to string."""
175 result = ""
177 for key, value in token.attrItems():
178 result += " " + escapeHtml(key) + '="' + escapeHtml(str(value)) + '"'
180 return result
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.
190 Don't try to use it! Spec requires to show `alt` content with stripped markup,
191 instead of simple escaping.
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 = ""
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"
208 return result
210 ###################################################
212 def list_item_open(
213 self,
214 tokens: Sequence[Token],
215 idx: int,
216 options: OptionsDict,
217 env: EnvType,
218 ) -> str:
219 token = tokens[idx]
220 result = self.renderToken(tokens, idx, options, env)
221 if token.meta and "checked" in token.meta:
222 checked_attr = ' checked=""' if token.meta["checked"] else ""
223 disabled_attr = (
224 "" if options.get("tasklists_editable", False) else ' disabled=""'
225 )
226 result += (
227 '<input class="task-list-item-checkbox"'
228 f'{disabled_attr} type="checkbox"{checked_attr}> '
229 )
230 return result
232 def code_inline(
233 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
234 ) -> str:
235 token = tokens[idx]
236 return (
237 "<code"
238 + self.renderAttrs(token)
239 + ">"
240 + escapeHtml(tokens[idx].content)
241 + "</code>"
242 )
244 def code_block(
245 self,
246 tokens: Sequence[Token],
247 idx: int,
248 options: OptionsDict,
249 env: EnvType,
250 ) -> str:
251 token = tokens[idx]
253 return (
254 "<pre"
255 + self.renderAttrs(token)
256 + "><code>"
257 + escapeHtml(tokens[idx].content)
258 + "</code></pre>\n"
259 )
261 def fence(
262 self,
263 tokens: Sequence[Token],
264 idx: int,
265 options: OptionsDict,
266 env: EnvType,
267 ) -> str:
268 token = tokens[idx]
269 info = unescapeAll(token.info).strip() if token.info else ""
270 langName = ""
271 langAttrs = ""
273 if info:
274 arr = info.split(maxsplit=1)
275 langName = arr[0]
276 if len(arr) == 2:
277 langAttrs = arr[1]
279 if options.highlight:
280 highlighted = options.highlight(
281 token.content, langName, langAttrs
282 ) or escapeHtml(token.content)
283 else:
284 highlighted = escapeHtml(token.content)
286 if highlighted.startswith("<pre"):
287 return highlighted + "\n"
289 # If language exists, inject class gently, without modifying original token.
290 # May be, one day we will add .deepClone() for token and simplify this part, but
291 # now we prefer to keep things local.
292 if info:
293 # Fake token just to render attributes
294 tmpToken = Token(type="", tag="", nesting=0, attrs=token.attrs.copy())
295 tmpToken.attrJoin("class", options.langPrefix + langName)
297 return (
298 "<pre><code"
299 + self.renderAttrs(tmpToken)
300 + ">"
301 + highlighted
302 + "</code></pre>\n"
303 )
305 return (
306 "<pre><code"
307 + self.renderAttrs(token)
308 + ">"
309 + highlighted
310 + "</code></pre>\n"
311 )
313 def image(
314 self,
315 tokens: Sequence[Token],
316 idx: int,
317 options: OptionsDict,
318 env: EnvType,
319 ) -> str:
320 token = tokens[idx]
322 # "alt" attr MUST be set, even if empty. Because it's mandatory and
323 # should be placed on proper position for tests.
324 if token.children:
325 token.attrSet("alt", self.renderInlineAsText(token.children, options, env))
326 else:
327 token.attrSet("alt", "")
329 return self.renderToken(tokens, idx, options, env)
331 def hardbreak(
332 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
333 ) -> str:
334 return "<br />\n" if options.xhtmlOut else "<br>\n"
336 def softbreak(
337 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
338 ) -> str:
339 return (
340 ("<br />\n" if options.xhtmlOut else "<br>\n") if options.breaks else "\n"
341 )
343 def text(
344 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
345 ) -> str:
346 return escapeHtml(tokens[idx].content)
348 def html_block(
349 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
350 ) -> str:
351 return tokens[idx].content
353 def html_inline(
354 self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
355 ) -> str:
356 return tokens[idx].content