#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: 2026-02-08
# File: src/scitex_writer/_compile/content.py
"""Content/preview compilation for LaTeX snippets.
Compiles raw LaTeX content to PDF using the shell scripts layer:
scripts/python/tex_snippet2full.py → builds .tex document
scripts/shell/compile_content.sh → compiles to PDF via latexmk
"""
from __future__ import annotations
import subprocess
import tempfile
from pathlib import Path
from typing import Literal, Optional
def _get_scripts_dir(project_dir: Optional[str] = None) -> Path:
"""Get the scripts directory, preferring package scripts over project.
Search order:
1. repo/scripts/ (development mode - always up to date)
2. package/_scripts/ (installed package fallback)
3. project_dir/scripts/ (user's project - may be from old template)
"""
# First priority: package/development scripts (always up to date)
pkg_dir = Path(__file__).resolve().parent.parent # src/scitex_writer/
for candidate in [
pkg_dir.parent.parent / "scripts", # development: repo/scripts/
pkg_dir / "_scripts", # installed package fallback
]:
if candidate.exists() and (candidate / "shell").exists():
return candidate
# Fallback: project's own scripts directory (may be outdated clone)
if project_dir:
proj_scripts = Path(project_dir) / "scripts"
if proj_scripts.exists() and (proj_scripts / "shell").exists():
return proj_scripts
raise FileNotFoundError("Cannot find scripts directory")
def compile_content(
latex_content: str,
project_dir: Optional[str] = None,
color_mode: Literal["light", "dark"] = "light",
name: str = "content",
timeout: int = 60,
keep_aux: bool = False,
) -> dict:
"""Compile raw LaTeX content to PDF.
Creates a standalone document from the provided LaTeX content and compiles
it to PDF. Supports light/dark color modes for comfortable viewing.
Parameters
----------
latex_content : str
Raw LaTeX content. Can be a complete document (with \\documentclass)
or body-only content (will be wrapped automatically).
project_dir : str, optional
Path to scitex-writer project. If provided, PDF is copied to
the project's .preview/ directory.
color_mode : str
Color theme: 'light' (default) or 'dark' (Monaco #1E1E1E).
name : str
Output filename (without extension).
timeout : int
Compilation timeout in seconds.
keep_aux : bool
Keep auxiliary files after compilation.
Returns
-------
dict
Result with keys: success, output_pdf, temp_dir, color_mode,
log, message/error.
"""
# Sanitize name: strip .tex extension if present
if name.endswith(".tex"):
name = name[:-4]
try:
scripts_dir = _get_scripts_dir(project_dir)
doc_builder = scripts_dir / "python" / "tex_snippet2full.py"
compiler = scripts_dir / "shell" / "compile_content.sh"
temp_dir = Path(tempfile.mkdtemp(prefix=f"scitex_content_{name}_"))
body_file = temp_dir / "body.tex"
tex_file = temp_dir / f"{name}.tex"
pdf_file = temp_dir / f"{name}.pdf"
# Write body content
body_file.write_text(latex_content, encoding="utf-8")
# Step 1: Build complete LaTeX document
is_complete = "\\documentclass" in latex_content
build_cmd = [
"python3",
str(doc_builder),
"--body-file",
str(body_file),
"--output",
str(tex_file),
"--color-mode",
color_mode,
]
if is_complete:
build_cmd.append("--complete-document")
build_result = subprocess.run(
build_cmd,
capture_output=True,
text=True,
timeout=30,
)
if build_result.returncode != 0:
return {
"success": False,
"output_pdf": None,
"error": f"Document build failed: {build_result.stderr}",
}
# Step 2: Compile to PDF
compile_cmd = [
"bash",
str(compiler),
"--tex-file",
str(tex_file),
"--output-dir",
str(temp_dir),
"--job-name",
name,
"--timeout",
str(timeout),
]
if keep_aux:
compile_cmd.append("--keep-aux")
compile_cmd.append("--quiet")
# Add preview dir if project_dir is provided
preview_dir = None
if project_dir:
preview_dir = Path(project_dir).resolve() / ".preview"
compile_cmd.extend(["--preview-dir", str(preview_dir)])
compile_result = subprocess.run(
compile_cmd,
capture_output=True,
text=True,
timeout=timeout + 10,
)
# Read log file
log_content = _read_log(temp_dir, name)
# Determine final PDF path
final_pdf = pdf_file
if compile_result.returncode == 0 and preview_dir:
preview_pdf = preview_dir / f"{name}.pdf"
if preview_pdf.exists():
final_pdf = preview_pdf
if compile_result.returncode == 0 and pdf_file.exists():
return {
"success": True,
"output_pdf": str(final_pdf),
"temp_dir": str(temp_dir),
"color_mode": color_mode,
"log": log_content,
"message": f"Content compiled successfully: {name}",
}
else:
return {
"success": False,
"output_pdf": None,
"temp_dir": str(temp_dir),
"color_mode": color_mode,
"log": log_content,
"stdout": _truncate(compile_result.stdout),
"stderr": _truncate(compile_result.stderr),
"error": f"Compilation failed with exit code {compile_result.returncode}",
}
except subprocess.TimeoutExpired:
return {
"success": False,
"error": f"Content compilation timed out after {timeout} seconds",
}
except Exception as e:
return {
"success": False,
"error": str(e),
}
def _read_log(temp_dir: Path, name: str) -> str:
"""Read and truncate log file."""
log_file = temp_dir / f"{name}.log"
if log_file.exists():
content = log_file.read_text(encoding="utf-8", errors="replace")
return content[-5000:] if len(content) > 5000 else content
return ""
def _truncate(text: str, limit: int = 2000) -> str:
"""Truncate text to limit."""
return text[-limit:] if len(text) > limit else text
__all__ = ["compile_content"]
# EOF