from __future__ import annotations import logging import re import subprocess from pathlib import Path logger = logging.getLogger(__name__) _TYPST_LOCATION = re.compile(r"\.typ:(\d+):(\d+)") class TypstCompileError(RuntimeError): """Typst compile failure carrying the full compiler stderr for diagnosis.""" def __init__(self, message: str, stderr: str = ""): super().__init__(message) self.stderr = stderr def _source_context(source: str, stderr: str, radius: int = 2) -> str: match = _TYPST_LOCATION.search(stderr) if not match: return "" line_number = int(match.group(1)) lines = source.splitlines() start = max(0, line_number - radius - 1) end = min(len(lines), line_number + radius) return "\n".join(f"{index + 1:>5} | {lines[index]}" for index in range(start, end)) def compile_typst( source: str, font_paths: list[str], output_pdf: Path, typst_bin: str = "typst", work_dir: Path | None = None, ) -> Path: """Compile a Typst source string to PDF. Args: source: Complete Typst source code. font_paths: Directories or file paths passed to --font-path. output_pdf: Destination PDF path. typst_bin: Path/name of the typst binary. work_dir: Directory to write the intermediate .typ file (defaults to output_pdf.parent). Returns: output_pdf path. Raises: TypstCompileError: If the typst process exits non-zero. """ work_dir = work_dir or output_pdf.parent work_dir.mkdir(parents=True, exist_ok=True) typ_path = work_dir / (output_pdf.stem + ".typ") typ_path.write_text(source, encoding="utf-8") cmd = [typst_bin, "compile"] for fp in font_paths: cmd += ["--font-path", fp] cmd += [str(typ_path), str(output_pdf)] logger.debug("typst: %s", " ".join(cmd)) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: stderr = result.stderr or "" tail = "\n".join(stderr.splitlines()[-50:]) context = _source_context(source, stderr) if context: logger.error("Typst source near the failing markup:\n%s", context) tail = f"{tail}\n\nTypst source context:\n{context}" raise TypstCompileError( f"typst compile failed (exit {result.returncode}):\n{tail}", stderr=stderr ) logger.debug("typst compiled → %s", output_pdf) return output_pdf