"""Local Markdown to PDF rendering."""
from __future__ import annotations
import html
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
_IMAGE_RE = re.compile(r"^!\[([^]]*)\]\((?:<([^>]+)>|([^\s)]+))\)$")
_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+)$")
_ORDERED_RE = re.compile(r"^\d+[.)]\s+(.+)$")
_UNORDERED_RE = re.compile(r"^[-*+]\s+(.+)$")
_MATH_RE = re.compile(r"(\$\$.+?\$\$|(? None:
self._browser = browser
@staticmethod
def _render_math(source: str, *, display: bool) -> str:
try:
from latex2mathml.converter import convert
except ImportError as error:
raise RuntimeError(
"PDF formulas require latex2mathml: pip install latex2mathml"
) from error
try:
mathml = convert(source.strip())
except Exception as error:
raise ValueError(f"Invalid LaTeX formula: {source}") from error
css_class = "math display" if display else "math inline"
return f'{mathml}'
@classmethod
def _render_plain_inline(cls, source: str) -> str:
parts: list[str] = []
cursor = 0
for match in _BOLD_RE.finditer(source):
parts.append(html.escape(source[cursor:match.start()]))
parts.append(f"{html.escape(match.group(1))}")
cursor = match.end()
parts.append(html.escape(source[cursor:]))
rendered = "".join(parts)
def link(match: re.Match[str]) -> str:
label = html.escape(match.group(1))
target = html.escape(match.group(2), quote=True)
return f'{label}'
return _LINK_RE.sub(link, rendered)
@classmethod
def _render_inline(cls, source: str) -> str:
result: list[str] = []
cursor = 0
for match in _MATH_RE.finditer(source):
result.append(cls._render_plain_inline(source[cursor:match.start()]))
token = match.group(0)
display = token.startswith("$$")
formula = token[2:-2] if display else token[1:-1]
result.append(cls._render_math(formula, display=display))
cursor = match.end()
result.append(cls._render_plain_inline(source[cursor:]))
return "".join(result)
@staticmethod
def _image_uri(markdown_path: Path, image_path: str) -> str:
path = Path(image_path)
if not path.is_absolute():
path = markdown_path.parent / path
path = path.resolve()
if not path.is_file():
raise FileNotFoundError(f"Markdown image not found: {path}")
return path.as_uri()
@classmethod
def _render_blocks(cls, markdown: str, markdown_path: Path) -> str:
lines = markdown.splitlines()
blocks: list[str] = []
index = 0
first_heading = True
while index < len(lines):
line = lines[index].strip()
if not line:
index += 1
continue
heading = _HEADING_RE.match(line)
if heading:
level = min(len(heading.group(1)), 4)
title_class = ' class="document-title"' if first_heading else ""
blocks.append(
f"'
f"{caption}
{cls._render_inline(' '.join(quote))}" ) continue ordered = _ORDERED_RE.match(line) unordered = _UNORDERED_RE.match(line) if ordered or unordered: pattern = _ORDERED_RE if ordered else _UNORDERED_RE tag = "ol" if ordered else "ul" items: list[str] = [] while index < len(lines): item = pattern.match(lines[index].strip()) if not item: break items.append(f"
{cls._render_inline(' '.join(paragraph))}
") return "\n".join(blocks) @staticmethod def _document(body: str, title: str) -> str: return f"""