import re import shutil import typing from pathlib import Path from typing import Any from jinja2 import Environment from jinja2 import FileSystemLoader from jinja2 import select_autoescape from mistletoe import Document # type: ignore from mistletoe import HTMLRenderer # type: ignore from mistletoe import block_token # type: ignore from pygments import highlight # type: ignore from pygments.formatters import HtmlFormatter # type: ignore from pygments.lexers import get_lexer_by_name as get_lexer # type: ignore from pygments.lexers import guess_lexer # type: ignore from app.config import VERSION from app.source import CustomRenderer from app.utils.datetime import now _FORMATTER = HtmlFormatter() _FORMATTER.noclasses = True class DocRenderer(CustomRenderer): def __init__( self, depth=5, omit_title=True, filter_conds=[], ) -> None: super().__init__( enable_mentionify=False, enable_hashtagify=False, ) self._headings: list[tuple[int, str, str]] = [] self._ids: set[str] = set() self.depth = depth self.omit_title = omit_title self.filter_conds = filter_conds @property def toc(self): """ Returns table of contents as a block_token.List instance. """ def get_indent(level): if self.omit_title: level -= 1 return " " * 4 * (level - 1) def build_list_item(heading): level, content, title_id = heading template = '{indent}- {content}\n' return template.format( indent=get_indent(level), content=content, id=title_id ) lines = [build_list_item(heading) for heading in self._headings] items = block_token.tokenize(lines) return items[0] def render_heading(self, token): """ Overrides super().render_heading; stores rendered heading first, then returns it. """ template = '{inner}' inner = self.render_inner(token) title_id = inner.lower().replace(" ", "-") if title_id in self._ids: i = 1 while 1: title_id = f"{title_id}_{i}" if title_id not in self._ids: break self._ids.add(title_id) rendered = template.format(level=token.level, inner=inner, id=title_id) content = self.parse_rendered_heading(rendered) if not ( self.omit_title and token.level == 1 or token.level > self.depth or any(cond(content) for cond in self.filter_conds) ): self._headings.append((token.level, content, title_id)) return rendered @staticmethod def parse_rendered_heading(rendered): """ Helper method; converts rendered heading to plain text. """ return re.sub(r"<.+?>", "", rendered) def render_block_code(self, token: typing.Any) -> str: code = token.children[0].content lexer = get_lexer(token.language) if token.language else guess_lexer(code) return highlight(code, lexer, _FORMATTER) def markdownify(content: str) -> tuple[str, Any]: with DocRenderer() as renderer: rendered_content = renderer.render(Document(content)) with HTMLRenderer() as html_renderer: toc = html_renderer.render(renderer.toc) return rendered_content, toc def main() -> None: # Setup Jinja loader = FileSystemLoader("docs/templates") env = Environment(loader=loader, autoescape=select_autoescape()) template = env.get_template("layout.html") shutil.rmtree("docs/dist", ignore_errors=True) Path("docs/dist").mkdir(exist_ok=True) shutil.rmtree("docs/dist/static", ignore_errors=True) shutil.copytree("docs/static", "docs/dist/static") last_updated = now().replace(second=0, microsecond=0).isoformat() readme = Path("README.md") content, toc = markdownify(readme.read_text().removeprefix("# microblog.pub")) template.stream( content=content, version=VERSION, path="/", last_updated=last_updated, ).dump("docs/dist/index.html") install = Path("docs/install.md") content, toc = markdownify(install.read_text()) template.stream( content=content.replace("[TOC]", toc), version=VERSION, path="/installing.html", last_updated=last_updated, ).dump("docs/dist/installing.html") user_guide = Path("docs/user_guide.md") content, toc = markdownify(user_guide.read_text()) template.stream( content=content.replace("[TOC]", toc), version=VERSION, path="/user_guide.html", last_updated=last_updated, ).dump("docs/dist/user_guide.html") developer_guide = Path("docs/developer_guide.md") content, toc = markdownify(developer_guide.read_text()) template.stream( content=content.replace("[TOC]", toc), version=VERSION, path="/developer_guide.html", last_updated=last_updated, ).dump("docs/dist/developer_guide.html") if __name__ == "__main__": main()