summaryrefslogtreecommitdiffstats
path: root/markdown_it/renderer.py
diff options
context:
space:
mode:
Diffstat (limited to 'markdown_it/renderer.py')
-rw-r--r--markdown_it/renderer.py339
1 files changed, 339 insertions, 0 deletions
diff --git a/markdown_it/renderer.py b/markdown_it/renderer.py
new file mode 100644
index 0000000..b8bfe4d
--- /dev/null
+++ b/markdown_it/renderer.py
@@ -0,0 +1,339 @@
+"""
+class Renderer
+
+Generates HTML from parsed token stream. Each instance has independent
+copy of rules. Those can be rewritten with ease. Also, you can add new
+rules if you create plugin and adds new token types.
+"""
+from __future__ import annotations
+
+from collections.abc import MutableMapping, Sequence
+import inspect
+from typing import Any, ClassVar
+
+from .common.utils import escapeHtml, unescapeAll
+from .token import Token
+from .utils import OptionsDict
+
+try:
+ from typing import Protocol
+except ImportError: # Python <3.8 doesn't have `Protocol` in the stdlib
+ from typing_extensions import Protocol # type: ignore[misc]
+
+
+class RendererProtocol(Protocol):
+ __output__: ClassVar[str]
+
+ def render(
+ self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping
+ ) -> Any:
+ ...
+
+
+class RendererHTML(RendererProtocol):
+ """Contains render rules for tokens. Can be updated and extended.
+
+ Example:
+
+ Each rule is called as independent static function with fixed signature:
+
+ ::
+
+ class Renderer:
+ def token_type_name(self, tokens, idx, options, env) {
+ # ...
+ return renderedHTML
+
+ ::
+
+ class CustomRenderer(RendererHTML):
+ def strong_open(self, tokens, idx, options, env):
+ return '<b>'
+ def strong_close(self, tokens, idx, options, env):
+ return '</b>'
+
+ md = MarkdownIt(renderer_cls=CustomRenderer)
+
+ result = md.render(...)
+
+ See https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js
+ for more details and examples.
+ """
+
+ __output__ = "html"
+
+ def __init__(self, parser=None):
+ self.rules = {
+ k: v
+ for k, v in inspect.getmembers(self, predicate=inspect.ismethod)
+ if not (k.startswith("render") or k.startswith("_"))
+ }
+
+ def render(
+ self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping
+ ) -> str:
+ """Takes token stream and generates HTML.
+
+ :param tokens: list on block tokens to render
+ :param options: params of parser instance
+ :param env: additional data from parsed input
+
+ """
+ result = ""
+
+ for i, token in enumerate(tokens):
+
+ if token.type == "inline":
+ assert token.children is not None
+ result += self.renderInline(token.children, options, env)
+ elif token.type in self.rules:
+ result += self.rules[token.type](tokens, i, options, env)
+ else:
+ result += self.renderToken(tokens, i, options, env)
+
+ return result
+
+ def renderInline(
+ self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping
+ ) -> str:
+ """The same as ``render``, but for single token of `inline` type.
+
+ :param tokens: list on block tokens to render
+ :param options: params of parser instance
+ :param env: additional data from parsed input (references, for example)
+ """
+ result = ""
+
+ for i, token in enumerate(tokens):
+ if token.type in self.rules:
+ result += self.rules[token.type](tokens, i, options, env)
+ else:
+ result += self.renderToken(tokens, i, options, env)
+
+ return result
+
+ def renderToken(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: MutableMapping,
+ ) -> str:
+ """Default token renderer.
+
+ Can be overridden by custom function
+
+ :param idx: token index to render
+ :param options: params of parser instance
+ """
+ result = ""
+ needLf = False
+ token = tokens[idx]
+
+ # Tight list paragraphs
+ if token.hidden:
+ return ""
+
+ # Insert a newline between hidden paragraph and subsequent opening
+ # block-level tag.
+ #
+ # For example, here we should insert a newline before blockquote:
+ # - a
+ # >
+ #
+ if token.block and token.nesting != -1 and idx and tokens[idx - 1].hidden:
+ result += "\n"
+
+ # Add token name, e.g. `<img`
+ result += ("</" if token.nesting == -1 else "<") + token.tag
+
+ # Encode attributes, e.g. `<img src="foo"`
+ result += self.renderAttrs(token)
+
+ # Add a slash for self-closing tags, e.g. `<img src="foo" /`
+ if token.nesting == 0 and options["xhtmlOut"]:
+ result += " /"
+
+ # Check if we need to add a newline after this tag
+ if token.block:
+ needLf = True
+
+ if token.nesting == 1:
+ if idx + 1 < len(tokens):
+ nextToken = tokens[idx + 1]
+
+ if nextToken.type == "inline" or nextToken.hidden:
+ # Block-level tag containing an inline tag.
+ #
+ needLf = False
+
+ elif nextToken.nesting == -1 and nextToken.tag == token.tag:
+ # Opening tag + closing tag of the same type. E.g. `<li></li>`.
+ #
+ needLf = False
+
+ result += ">\n" if needLf else ">"
+
+ return result
+
+ @staticmethod
+ def renderAttrs(token: Token) -> str:
+ """Render token attributes to string."""
+ result = ""
+
+ for key, value in token.attrItems():
+ result += " " + escapeHtml(key) + '="' + escapeHtml(str(value)) + '"'
+
+ return result
+
+ def renderInlineAsText(
+ self,
+ tokens: Sequence[Token] | None,
+ options: OptionsDict,
+ env: MutableMapping,
+ ) -> str:
+ """Special kludge for image `alt` attributes to conform CommonMark spec.
+
+ Don't try to use it! Spec requires to show `alt` content with stripped markup,
+ instead of simple escaping.
+
+ :param tokens: list on block tokens to render
+ :param options: params of parser instance
+ :param env: additional data from parsed input
+ """
+ result = ""
+
+ for token in tokens or []:
+ if token.type == "text":
+ result += token.content
+ elif token.type == "image":
+ assert token.children is not None
+ result += self.renderInlineAsText(token.children, options, env)
+ elif token.type == "softbreak":
+ result += "\n"
+
+ return result
+
+ ###################################################
+
+ def code_inline(self, tokens: Sequence[Token], idx: int, options, env) -> str:
+ token = tokens[idx]
+ return (
+ "<code"
+ + self.renderAttrs(token)
+ + ">"
+ + escapeHtml(tokens[idx].content)
+ + "</code>"
+ )
+
+ def code_block(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: MutableMapping,
+ ) -> str:
+ token = tokens[idx]
+
+ return (
+ "<pre"
+ + self.renderAttrs(token)
+ + "><code>"
+ + escapeHtml(tokens[idx].content)
+ + "</code></pre>\n"
+ )
+
+ def fence(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: MutableMapping,
+ ) -> str:
+ token = tokens[idx]
+ info = unescapeAll(token.info).strip() if token.info else ""
+ langName = ""
+ langAttrs = ""
+
+ if info:
+ arr = info.split(maxsplit=1)
+ langName = arr[0]
+ if len(arr) == 2:
+ langAttrs = arr[1]
+
+ if options.highlight:
+ highlighted = options.highlight(
+ token.content, langName, langAttrs
+ ) or escapeHtml(token.content)
+ else:
+ highlighted = escapeHtml(token.content)
+
+ if highlighted.startswith("<pre"):
+ return highlighted + "\n"
+
+ # If language exists, inject class gently, without modifying original token.
+ # May be, one day we will add .deepClone() for token and simplify this part, but
+ # now we prefer to keep things local.
+ if info:
+ # Fake token just to render attributes
+ tmpToken = Token(type="", tag="", nesting=0, attrs=token.attrs.copy())
+ tmpToken.attrJoin("class", options.langPrefix + langName)
+
+ return (
+ "<pre><code"
+ + self.renderAttrs(tmpToken)
+ + ">"
+ + highlighted
+ + "</code></pre>\n"
+ )
+
+ return (
+ "<pre><code"
+ + self.renderAttrs(token)
+ + ">"
+ + highlighted
+ + "</code></pre>\n"
+ )
+
+ def image(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: MutableMapping,
+ ) -> str:
+ token = tokens[idx]
+
+ # "alt" attr MUST be set, even if empty. Because it's mandatory and
+ # should be placed on proper position for tests.
+
+ assert (
+ token.attrs and "alt" in token.attrs
+ ), '"image" token\'s attrs must contain `alt`'
+
+ # Replace content with actual value
+
+ token.attrSet("alt", self.renderInlineAsText(token.children, options, env))
+
+ return self.renderToken(tokens, idx, options, env)
+
+ def hardbreak(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args
+ ) -> str:
+ return "<br />\n" if options.xhtmlOut else "<br>\n"
+
+ def softbreak(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, *args
+ ) -> str:
+ return (
+ ("<br />\n" if options.xhtmlOut else "<br>\n") if options.breaks else "\n"
+ )
+
+ def text(self, tokens: Sequence[Token], idx: int, *args) -> str:
+ return escapeHtml(tokens[idx].content)
+
+ def html_block(self, tokens: Sequence[Token], idx: int, *args) -> str:
+ return tokens[idx].content
+
+ def html_inline(self, tokens: Sequence[Token], idx: int, *args) -> str:
+ return tokens[idx].content