microblog/scripts/build_docs.py

167 lines
5.1 KiB
Python

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}- <a href="#{id}" rel="nofollow">{content}</a>\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 = '<h{level} id="{id}">{inner}</h{level}>'
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()