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