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):
__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:
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:
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:
result = ""
needLf = False
token = tokens[idx]
if token.hidden:
return ""
if token.block and token.nesting != -1 and idx and tokens[idx - 1].hidden:
result += "\n"
result += ("</" if token.nesting == -1 else "<") + token.tag
result += self.renderAttrs(token)
if token.nesting == 0 and options["xhtmlOut"]:
result += " /"
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: needLf = False
elif nextToken.nesting == -1 and nextToken.tag == token.tag:
needLf = False
result += ">\n" if needLf else ">"
return result
@staticmethod
def renderAttrs(token: Token) -> str:
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:
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 info:
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]
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