import re from textwrap import dedent from html5tagger import HTML, Builder, E # type: ignore from mistune import HTMLRenderer, create_markdown, escape from mistune.directives import RSTDirective, TableOfContents from mistune.util import safe_entity from pygments import highlight from pygments.formatters import html from pygments.lexers import get_lexer_by_name from .code_style import SanicCodeStyle from .plugins.attrs import Attributes from .plugins.columns import Column from .plugins.hook import Hook from .plugins.mermaid import Mermaid from .plugins.notification import Notification from .plugins.span import span from .plugins.tabs import Tabs from .text import slugify class DocsRenderer(HTMLRenderer): def block_code(self, code: str, info: str | None = None): builder = Builder("Block") with builder.div(class_="code-block"): if info: lexer = get_lexer_by_name(info, stripall=False) formatter = html.HtmlFormatter( style=SanicCodeStyle, wrapcode=True, cssclass=f"highlight language-{info}", ) builder(HTML(highlight(code, lexer, formatter))) with builder.div( class_="code-block__copy", onclick="copyCode(this)", ): builder.div(class_="code-block__rectangle code-block__filled").div( class_="code-block__rectangle code-block__outlined" ) else: builder.pre(E.code(escape(code))) return str(builder) def heading(self, text: str, level: int, **attrs) -> str: ident = slugify(text) if level > 1: text += self._make_tag("a", {"href": f"#{ident}", "class": "anchor"}, "#") return self._make_tag( f"h{level}", {"id": ident, "class": f"is-size-{level}"}, text ) def link(self, text: str, url: str, title: str | None = None) -> str: url = self.safe_url(url).removesuffix(".md") if not url.endswith("/"): url += ".html" attributes: dict[str, str] = {"href": url} if title: attributes["title"] = safe_entity(title) if url.startswith("http"): attributes["target"] = "_blank" attributes["rel"] = "nofollow noreferrer" else: attributes["hx-get"] = url attributes["hx-target"] = "#content" attributes["hx-swap"] = "innerHTML" attributes["hx-push-url"] = "true" return self._make_tag("a", attributes, text) def span(self, text, classes, **attrs) -> str: if classes: attrs["class"] = classes return self._make_tag("span", attrs, text) def list(self, text: str, ordered: bool, **attrs) -> str: tag = "ol" if ordered else "ul" attrs["class"] = tag return self._make_tag(tag, attrs, text) def list_item(self, text: str, **attrs) -> str: attrs["class"] = "li" return self._make_tag("li", attrs, text) def table(self, text: str, **attrs) -> str: attrs["class"] = "table is-fullwidth is-bordered" return self._make_tag("table", attrs, text) def _make_tag( self, tag: str, attributes: dict[str, str], text: str | None = None ) -> str: attrs = " ".join(f'{key}="{value}"' for key, value in attributes.items()) if text is None: return f"<{tag} {attrs} />" return f"<{tag} {attrs}>{text}" RST_CODE_BLOCK_PATTERN = re.compile( r"\.\.\scode-block::\s(\w+)\n\n((?:\n|(?:\s\s\s\s[^\n]*))+)" ) _render_markdown = create_markdown( renderer=DocsRenderer(), plugins=[ RSTDirective( [ # Admonition(), Attributes(), Notification(), TableOfContents(), Column(), Mermaid(), Tabs(), Hook(), ] ), "abbr", "def_list", "footnotes", "mark", "table", span, ], ) def render_markdown(text: str) -> str: def replacer(match): language = match.group(1) code_block = dedent(match.group(2)).strip() return f"```{language}\n{code_block}\n```\n\n" text = RST_CODE_BLOCK_PATTERN.sub(replacer, text) return _render_markdown(text)