111 lines
3.6 KiB
Python
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"]))
|
|
)
|