123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- """
- 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 Sequence
- import inspect
- from typing import Any, ClassVar, Protocol
- from .common.utils import escapeHtml, unescapeAll
- from .token import Token
- from .utils import EnvType, OptionsDict
- class RendererProtocol(Protocol):
- __output__: ClassVar[str]
- def render(
- self, tokens: Sequence[Token], options: OptionsDict, env: EnvType
- ) -> 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: Any = 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: EnvType
- ) -> 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":
- if token.children:
- 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: EnvType
- ) -> 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: EnvType,
- ) -> 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 and (idx + 1 < len(tokens)):
- nextToken = tokens[idx + 1]
- if nextToken.type == "inline" or nextToken.hidden: # noqa: SIM114
- # 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: EnvType,
- ) -> 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":
- if token.children:
- 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: OptionsDict, env: EnvType
- ) -> 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: EnvType,
- ) -> 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: EnvType,
- ) -> 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: EnvType,
- ) -> 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.
- if token.children:
- token.attrSet("alt", self.renderInlineAsText(token.children, options, env))
- else:
- token.attrSet("alt", "")
- return self.renderToken(tokens, idx, options, env)
- def hardbreak(
- self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
- ) -> str:
- return "<br />\n" if options.xhtmlOut else "<br>\n"
- def softbreak(
- self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
- ) -> str:
- return (
- ("<br />\n" if options.xhtmlOut else "<br>\n") if options.breaks else "\n"
- )
- def text(
- self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
- ) -> str:
- return escapeHtml(tokens[idx].content)
- def html_block(
- self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
- ) -> str:
- return tokens[idx].content
- def html_inline(
- self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
- ) -> str:
- return tokens[idx].content
|