website/generator/markdown.py

111 lines
3.6 KiB
Python

from typing import Any
from collections.abc import Callable
import mistune
import mistune.directives
import mistune.util
import mistune.toc
import pygments
import pygments.lexers
import pygments.formatters
import html
import markupsafe
class LevelAdjustingHTMLRenderer(mistune.HTMLRenderer):
header_level_delta: int
def __init__(self, header_level_delta: None | int = None, **kwargs):
if header_level_delta is None:
header_level_delta = 0
self.header_level_delta = header_level_delta
super().__init__(**kwargs)
def heading(self, text: str, level: int, **attrs) -> str:
if 1 > level + self.header_level_delta > 6:
raise ValueError(
f"cannot render header with level {level} as it would result in a level outside of 1 <= n <= 6"
)
return super().heading(text, level + self.header_level_delta, **attrs)
class SyntaxHighlightingHTMLRenderer(mistune.HTMLRenderer):
def block_code(self, code: str, info: str | None = None) -> str:
if info is None:
return super().block_code(code)
return pygments.highlight(
code.replace("\t", " "),
pygments.lexers.get_lexer_by_name(info),
pygments.formatters.HtmlFormatter(
noclasses=True, # Use inline styles
nobackground=True, # Don't set the background colour of the container
wrapcode=True,
),
)
class LazyLoadingImageHTMLRenderer(mistune.HTMLRenderer):
def image(self, text: str, url: str, title: None | str = None) -> str:
src = self.safe_url(url)
alt = mistune.util.escape(mistune.util.striptags(text))
s = '<img loading="lazy" src="' + src + '" alt="' + alt + '"'
if title:
s += ' title="' + mistune.util.safe_entity(title) + '"'
return s + " />"
class CustomHTMLRenderer(
LevelAdjustingHTMLRenderer,
SyntaxHighlightingHTMLRenderer,
LazyLoadingImageHTMLRenderer,
):
pass
class CustomMarkdown(mistune.Markdown):
def parse(
self, s: str, state: None | mistune.BlockState = None
) -> tuple[str | list[dict[str, Any]], mistune.BlockState]:
r, state = super().parse(s, state=state)
r = '<div class="rendered-markdown">' + r + "</div>"
return r, state
def create(
escape: bool = True, header_level_delta: None | int = None
) -> CustomMarkdown:
r = mistune.create_markdown(
plugins=[
"strikethrough",
"table",
"footnotes",
"url",
mistune.directives.FencedDirective(
[mistune.directives.Figure(), mistune.directives.TableOfContents()]
),
],
renderer=CustomHTMLRenderer(
header_level_delta=header_level_delta, escape=escape
),
)
mistune.toc.add_toc_hook(r)
r.__class__ = CustomMarkdown # this is as close as you're going to get to a typecast into a subclass
return r
def new_simple_renderer():
m = mistune.create_markdown(
plugins=["strikethrough", "table", "url"],
renderer=CustomHTMLRenderer(),
)
return lambda x: markupsafe.Markup(m(x)) # wrapping this way makes jinja2 treat it as safe content by default
def render_toc_from_state(render_state: dict[str, Any], min_level: int = 1) -> str:
# hilarious note: if you supply a raw filter object, this dies. hence the cast to list. see https://github.com/lepture/mistune/pull/407
return mistune.toc.render_toc_ul(
list(filter(lambda x: x[0] >= min_level, render_state.env["toc_items"]))
)