diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:24:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-29 04:24:24 +0000 |
commit | 12e8343068b906f8b2afddc5569968a8a91fa5b0 (patch) | |
tree | 75cc5e05a4392ea0292251898f992a15a16b172b /markdown_it/renderer.py | |
parent | Initial commit. (diff) | |
download | markdown-it-py-12e8343068b906f8b2afddc5569968a8a91fa5b0.tar.xz markdown-it-py-12e8343068b906f8b2afddc5569968a8a91fa5b0.zip |
Adding upstream version 2.1.0.upstream/2.1.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | markdown_it/renderer.py | 339 |
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 |