Source code for jamb.publish.render
"""Render a publish document to a file, driving Quarto where needed."""
from __future__ import annotations
import os
import shutil
import tempfile
from importlib.resources import files
from pathlib import Path
from jamb.publish.document import PublishDocument
from jamb.publish.docx_reference import build_reference_docx
from jamb.publish.formats import QUARTO_TARGET, RENDERED_EXTENSION, OutputFormat
from jamb.publish.qmd import render_qmd
from jamb.publish.quarto import QuartoRenderError, run_quarto
#: Filenames used for styling inputs inside the temporary render directory.
_THEME_NAME = "theme.scss"
_REFERENCE_NAME = "reference.docx"
_TYPST_THEME_NAME = "typst-theme.typ"
def default_theme() -> str:
"""Return the bundled default HTML theme (SCSS) source."""
return (files("jamb.publish") / "assets" / _THEME_NAME).read_text(encoding="utf-8")
def default_typst_theme() -> str:
"""Return the bundled default Typst preamble for PDF output."""
return (files("jamb.publish") / "assets" / _TYPST_THEME_NAME).read_text(encoding="utf-8")
[docs]
def render_document(
doc: PublishDocument,
fmt: OutputFormat,
output_path: str | Path,
*,
template: str | Path | None = None,
) -> None:
"""Render a document to ``output_path`` in the requested format.
Markdown and ``.qmd`` output are written directly. HTML, DOCX, and PDF are
produced by invoking Quarto against a generated ``.qmd`` in an isolated
temporary directory.
Args:
doc: The document to render.
fmt: The target output format.
output_path: Destination file path.
template: Optional styling override appropriate to the format — an
SCSS file for HTML, a reference ``.docx`` for DOCX, or a Typst
preamble for PDF. When omitted, the bundled defaults are applied so
all three formats share the same look.
Raises:
QuartoNotFoundError: When a rendered format is requested but Quarto
is unavailable.
QuartoRenderError: When Quarto fails to produce the output.
"""
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
if fmt in (OutputFormat.MD, OutputFormat.QMD):
output_path.write_text(render_qmd(doc, fmt), encoding="utf-8")
return
with tempfile.TemporaryDirectory() as tmp:
tmpdir = Path(tmp)
theme = reference_doc = typst_header = None
if fmt is OutputFormat.HTML:
scss = Path(template).read_text(encoding="utf-8") if template else default_theme()
(tmpdir / _THEME_NAME).write_text(scss, encoding="utf-8")
theme = _THEME_NAME
elif fmt is OutputFormat.PDF:
typst = Path(template).read_text(encoding="utf-8") if template else default_typst_theme()
(tmpdir / _TYPST_THEME_NAME).write_text(typst, encoding="utf-8")
typst_header = _TYPST_THEME_NAME
elif fmt is OutputFormat.DOCX:
if template:
shutil.copyfile(template, tmpdir / _REFERENCE_NAME)
reference_doc = _REFERENCE_NAME
else:
reference_bytes = build_reference_docx()
if reference_bytes is not None:
(tmpdir / _REFERENCE_NAME).write_bytes(reference_bytes)
reference_doc = _REFERENCE_NAME
source = render_qmd(
doc,
fmt,
theme=theme,
reference_doc=reference_doc,
typst_header=typst_header,
)
qmd_path = tmpdir / "document.qmd"
qmd_path.write_text(source, encoding="utf-8")
result = run_quarto(["render", qmd_path.name, "--to", QUARTO_TARGET[fmt]], cwd=tmpdir)
produced = tmpdir / f"document{RENDERED_EXTENSION[fmt]}"
if result.returncode != 0 or not produced.exists():
if os.environ.get("JAMB_DEBUG"):
debug_copy = output_path.with_suffix(".debug.qmd")
shutil.copyfile(qmd_path, debug_copy)
raise QuartoRenderError(
f"Quarto failed to render {fmt.value} output.",
returncode=result.returncode,
stderr=result.stderr or result.stdout,
qmd_path=str(qmd_path),
)
shutil.move(str(produced), str(output_path))