File size: 2,490 Bytes
f66643d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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