Skip to content

LaTeX

The LaTeX body itself comes from tmark's writer; what stays on the Python side is the escaping table the templates and fonts reuse, the asset plumbing, the Pygments bridge of the highlight pass, and the template runtime.

LaTeX escaping and text-transform machinery (backend responsibility).

The writer owns escaping. This module moves :func:escape_latex_chars out of adapters/latex/utils.py (where it was a shared utility) into the LaTeX backend, and gathers the unicode → LaTeX text transforms (dashes, smart quotes, sub/superscript runs, emoji segmentation, math-payload protection) that the legacy adapters/handlers/inline.py PRE phase applied to plain text nodes. The IR carries raw text in :class:~tmark.ir.Str; the writer escapes it here exactly as the old pipeline did, so the produced LaTeX is byte-identical.

escape_latex_chars

escape_latex_chars(
    text: str, *, legacy_accents: bool = False
) -> str

Escape LaTeX special characters leveraging pylatexenc.

Source code in src/texsmith/writers/latex/escaper.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def escape_latex_chars(text: str, *, legacy_accents: bool = False) -> str:
    """Escape LaTeX special characters leveraging pylatexenc."""
    if not text:
        return text
    parts: list[str] = []
    buffer: list[str] = []

    def _encode_chunk(chunk: str) -> str:
        escaped = "".join(_BASIC_LATEX_ESCAPE_MAP.get(char, char) for char in chunk)
        if legacy_accents:
            encoded = unicode_to_latex(escaped, non_ascii_only=True, unknown_char_warning=False)
            return _wrap_latex_output(encoded)
        return escaped

    def _should_skip_encoding(char: str) -> bool:
        try:
            name = unicodedata.name(char)
        except ValueError:
            return False
        if "SUPERSCRIPT" in name or "SUBSCRIPT" in name:
            return True
        return "MODIFIER LETTER" in name and ("SMALL" in name or "CAPITAL" in name)

    for char in text:
        replacement = _COMMON_SYMBOL_MAP.get(char)
        if replacement is not None:
            if buffer:
                parts.append(_encode_chunk("".join(buffer)))
                buffer.clear()
            parts.append(replacement)
            continue
        if _should_skip_encoding(char):
            if buffer:
                parts.append(_encode_chunk("".join(buffer)))
                buffer.clear()
            parts.append(char)
        else:
            buffer.append(char)

    if buffer:
        parts.append(_encode_chunk("".join(buffer)))

    return "".join(parts)

escape_text_segment

escape_text_segment(
    text: str,
    *,
    legacy_accents: bool = False,
    emoji_renderer: Callable[[str], str] | None = None,
) -> str

Escape a plain-text run, splitting out emoji clusters.

Mirrors adapters/handlers/inline.py:_escape_text_segment. Plain fragments go through :func:prepare_plain_text; emoji clusters are handed to emoji_renderer (font command or artifact image). When no renderer is supplied, emoji are escaped as ordinary text.

Source code in src/texsmith/writers/latex/escaper.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
def escape_text_segment(
    text: str,
    *,
    legacy_accents: bool = False,
    emoji_renderer: Callable[[str], str] | None = None,
) -> str:
    """Escape a plain-text run, splitting out emoji clusters.

    Mirrors ``adapters/handlers/inline.py:_escape_text_segment``. Plain
    fragments go through :func:`prepare_plain_text`; emoji clusters are handed
    to ``emoji_renderer`` (font command or artifact image). When no renderer is
    supplied, emoji are escaped as ordinary text.
    """
    chunks: list[str] = []
    for kind, payload in _segment_text_with_emoji(text):
        if kind == "text":
            if payload:
                chunks.append(prepare_plain_text(payload, legacy_accents=legacy_accents))
        elif emoji_renderer is not None:
            chunks.append(emoji_renderer(payload))
        else:
            chunks.append(prepare_plain_text(payload, legacy_accents=legacy_accents))
    return "".join(chunks)

prepare_plain_text

prepare_plain_text(
    text: str, *, legacy_accents: bool = False
) -> str

Escape and apply the unicode text transforms for a plain-text run.

Mirrors adapters/handlers/inline.py:_prepare_plain_text: smart quotes and dashes are normalised first, the text is escaped, then unicode sub/superscript runs become \textsubscript / \textsuperscript.

Source code in src/texsmith/writers/latex/escaper.py
299
300
301
302
303
304
305
306
307
308
309
310
def prepare_plain_text(text: str, *, legacy_accents: bool = False) -> str:
    """Escape and apply the unicode text transforms for a plain-text run.

    Mirrors ``adapters/handlers/inline.py:_prepare_plain_text``: smart quotes
    and dashes are normalised first, the text is escaped, then unicode
    sub/superscript runs become ``\\textsubscript`` / ``\\textsuperscript``.
    """
    text = _replace_unicode_punctuation(text)
    text = _replace_unicode_dashes(text)
    escaped = escape_latex_chars(text, legacy_accents=legacy_accents)
    escaped = _replace_unicode_superscripts(escaped)
    return _replace_unicode_subscripts(escaped)

Asset helpers shared across media and block handlers.

AssetOptions dataclass

AssetOptions(
    assets: AssetRegistry,
    source_dir: Path,
    document_path: Path,
    copy_assets: bool = True,
    convert_assets: bool = False,
    hash_assets: bool = False,
    emitter: Any = None,
    diagrams_backend: str = "playwright",
    project_dir: Path | None = None,
    http_user_agent: str | None = None,
    mermaid_config: Any = None,
    drawio_crop: Any = None,
)

What the asset helpers need to store or convert one image.

Built once per document by the assets pass (PassContext has no single object shaped like this); passed explicitly instead of the RenderContextLike duck a _LegacyContext used to fake around a string-keyed runtime dict.

store_local_image_asset

store_local_image_asset(
    opts: AssetOptions,
    resolved: Path,
    *,
    options: Mapping[str, str] | None = None,
) -> Path

Copy or convert a local asset and register it on the context.

options are the per-image render options (crop for draw.io) the document set through attr_list. They select the converter behaviour and therefore take part in the asset key: the same diagram rendered cropped and uncropped is two distinct assets.

Source code in src/texsmith/writers/latex/assets.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def store_local_image_asset(
    opts: AssetOptions,
    resolved: Path,
    *,
    options: Mapping[str, str] | None = None,
) -> Path:
    """Copy or convert a local asset and register it on the context.

    ``options`` are the per-image render options (``crop`` for draw.io) the
    document set through ``attr_list``. They select the converter behaviour and
    therefore take part in the asset key: the same diagram rendered cropped and
    uncropped is two distinct assets.
    """
    asset_key = _asset_key(resolved, options)
    existing = opts.assets.lookup(asset_key)
    if existing is not None:
        return existing

    suffix = resolved.suffix.lower()
    convert_requested = opts.convert_assets
    needs_conversion = _requires_conversion(suffix, convert_requested)
    emitter = ensure_emitter(opts.emitter)

    if needs_conversion:
        record_event(
            emitter,
            "asset_convert",
            {
                "source": str(resolved),
                "suffix": suffix,
                "reason": "requested" if convert_requested else "forced",
            },
        )
        staged = _convert_local_asset(opts, resolved, suffix, options)
        final_suffix = ".pdf"
    else:
        staged = resolved
        final_suffix = suffix or ".bin"

    record_event(
        emitter,
        "asset_local",
        {
            "source": str(resolved),
            "stored_suffix": final_suffix,
            "converted": needs_conversion,
        },
    )
    return _persist_asset(
        opts,
        asset_key=asset_key,
        staged_path=staged,
        suffix=final_suffix,
        source_path=resolved,
    )

store_remote_image_asset

store_remote_image_asset(
    opts: AssetOptions, url: str
) -> Path

Fetch a remote asset, mirror it locally, and register it.

Source code in src/texsmith/writers/latex/assets.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def store_remote_image_asset(opts: AssetOptions, url: str) -> Path:
    """Fetch a remote asset, mirror it locally, and register it."""
    existing = opts.assets.lookup(url)
    if existing is not None:
        return existing

    convert_requested = opts.convert_assets
    metadata: dict[str, str] = {}
    conversion_root = _conversion_cache_root(opts)
    manifest_path = conversion_root / _ASSET_MANIFEST
    manifest, manifest_dirty = _load_asset_manifest(manifest_path)
    url_suffix = _suffix_from_url(url)
    suffix_hint = _normalise_suffix(url_suffix, default="") if url_suffix else ""
    emitter = ensure_emitter(opts.emitter)

    record_event(
        emitter,
        "asset_fetch",
        {
            "url": url,
            "convert": convert_requested,
            "suffix_hint": suffix_hint,
        },
    )
    fetch_options: dict[str, Any] = {
        "convert": convert_requested,
        "metadata": metadata,
        "manifest": manifest,
        "manifest_path": manifest_path,
        "manifest_dirty": manifest_dirty,
        "emitter": emitter,
    }
    user_agent = opts.http_user_agent
    if user_agent:
        fetch_options["user_agent"] = user_agent
    if convert_requested:
        fetch_options["output_suffix"] = ".pdf"
    elif suffix_hint:
        fetch_options["output_suffix"] = suffix_hint
    artefact = fetch_image(
        url,
        output_dir=conversion_root,
        **fetch_options,
    )

    recorded_suffix = metadata.get("suffix") if metadata else None
    if not recorded_suffix:
        detected_suffix = _detect_file_suffix(Path(artefact))
        if detected_suffix:
            recorded_suffix = detected_suffix
    if recorded_suffix:
        final_suffix = _normalise_suffix(recorded_suffix)
    else:
        final_suffix = Path(artefact).suffix or ".pdf"
    prefer_name = _extract_remote_name(url)

    record_event(
        emitter,
        "asset_fetch_complete",
        {
            "url": url,
            "stored_suffix": final_suffix,
            "converted": convert_requested or suffix_hint in _FORCED_CONVERSION_SUFFIXES,
        },
    )
    if manifest_dirty["dirty"]:
        _save_asset_manifest(manifest_path, manifest)
    return _persist_asset(
        opts,
        asset_key=url,
        staged_path=artefact,
        suffix=final_suffix,
        source_path=None,
        prefer_name=prefer_name,
        force_hash=not bool(prefer_name),
    )

LaTeX-specific utilities exposed by Texsmith.

escape_latex_chars

escape_latex_chars(
    text: str, *, legacy_accents: bool = False
) -> str

Escape LaTeX special characters leveraging pylatexenc.

Source code in src/texsmith/writers/latex/escaper.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def escape_latex_chars(text: str, *, legacy_accents: bool = False) -> str:
    """Escape LaTeX special characters leveraging pylatexenc."""
    if not text:
        return text
    parts: list[str] = []
    buffer: list[str] = []

    def _encode_chunk(chunk: str) -> str:
        escaped = "".join(_BASIC_LATEX_ESCAPE_MAP.get(char, char) for char in chunk)
        if legacy_accents:
            encoded = unicode_to_latex(escaped, non_ascii_only=True, unknown_char_warning=False)
            return _wrap_latex_output(encoded)
        return escaped

    def _should_skip_encoding(char: str) -> bool:
        try:
            name = unicodedata.name(char)
        except ValueError:
            return False
        if "SUPERSCRIPT" in name or "SUBSCRIPT" in name:
            return True
        return "MODIFIER LETTER" in name and ("SMALL" in name or "CAPITAL" in name)

    for char in text:
        replacement = _COMMON_SYMBOL_MAP.get(char)
        if replacement is not None:
            if buffer:
                parts.append(_encode_chunk("".join(buffer)))
                buffer.clear()
            parts.append(replacement)
            continue
        if _should_skip_encoding(char):
            if buffer:
                parts.append(_encode_chunk("".join(buffer)))
                buffer.clear()
            parts.append(char)
        else:
            buffer.append(char)

    if buffer:
        parts.append(_encode_chunk("".join(buffer)))

    return "".join(parts)

Pygments integration helpers for LaTeX rendering.

PygmentsLatexHighlighter

PygmentsLatexHighlighter(
    *,
    commandprefix: str = "PY",
    style: str = "bw",
    verboptions: str | None = None,
)

Convert source code to LaTeX using Pygments.

Source code in src/texsmith/adapters/latex/pygments.py
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    *,
    commandprefix: str = "PY",
    style: str = "bw",
    verboptions: str | None = None,
) -> None:
    self.commandprefix = commandprefix
    self.style = style
    self.verboptions = verboptions or r"breaklines, breakanywhere, commandchars=\\\{\}"

style_key property

style_key: str

Identifier that groups style definitions.

add_break_points

add_break_points(latex: str, chars: str) -> str

Insert \allowbreak after each break character of latex.

Highlighted output interleaves \PY{token} markers with escaped source text; the markers are left untouched so a token name is never rewritten, and only the payload between them gains break opportunities.

Source code in src/texsmith/adapters/latex/pygments.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def add_break_points(self, latex: str, chars: str) -> str:
    """Insert ``\\allowbreak`` after each break character of ``latex``.

    Highlighted output interleaves ``\\PY{token}`` markers with escaped
    source text; the markers are left untouched so a token name is never
    rewritten, and only the payload between them gains break opportunities.
    """
    if not latex or not chars:
        return latex
    pattern = _break_pattern(chars, self.commandprefix)
    if pattern is None:
        return latex

    def _insert(match: re.Match[str]) -> str:
        return match.group(0) + ALLOW_BREAK

    segments = _command_pattern(self.commandprefix).split(latex)
    for index, segment in enumerate(segments):
        if index % 2 == 0 and segment:
            segments[index] = pattern.sub(_insert, segment)
    return "".join(segments)

render

render(
    code: str,
    language: str,
    *,
    linenos: bool,
    highlight_lines: Iterable[int] | None = None,
) -> tuple[str, str]

Return the LaTeX code and style definitions for a payload.

Source code in src/texsmith/adapters/latex/pygments.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def render(
    self,
    code: str,
    language: str,
    *,
    linenos: bool,
    highlight_lines: Iterable[int] | None = None,
) -> tuple[str, str]:
    """Return the LaTeX code and style definitions for a payload."""
    try:
        lexer = get_lexer_by_name(language or "text")
    except ClassNotFound:
        lexer = TextLexer()

    def _format_ranges(values: Iterable[int]) -> str:
        sorted_vals = sorted(set(values))
        if not sorted_vals:
            return ""
        ranges: list[str] = []
        start = end = sorted_vals[0]
        for num in sorted_vals[1:]:
            if num == end + 1:
                end = num
            else:
                ranges.append(f"{start}-{end}" if start != end else str(start))
                start = end = num
        ranges.append(f"{start}-{end}" if start != end else str(start))
        return ",".join(ranges)

    verb_options = self.verboptions
    formatted_highlight = _format_ranges(highlight_lines or [])
    if formatted_highlight:
        verb_options = f"{verb_options},highlightlines={{{formatted_highlight}}}"

    formatter = LatexFormatter(
        full=False,
        linenos=linenos,
        style=self.style,
        commandprefix=self.commandprefix,
        linenostart=1,
        linenostep=1,
        verboptions=verb_options,
        hl_lines=list(highlight_lines or []),
    )
    latex_code = highlight(code, lexer, formatter)
    style_defs = formatter.get_style_defs()
    return latex_code, style_defs

render_inline

render_inline(code: str, language: str) -> tuple[str, str]

Return inline LaTeX macros for a code snippet (no Verbatim env).

Source code in src/texsmith/adapters/latex/pygments.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def render_inline(self, code: str, language: str) -> tuple[str, str]:
    """Return inline LaTeX macros for a code snippet (no Verbatim env)."""
    try:
        lexer = get_lexer_by_name(language or "text")
    except ClassNotFound:
        lexer = TextLexer()

    formatter = LatexFormatter(
        full=False,
        linenos=False,
        style=self.style,
        commandprefix=self.commandprefix,
        nowrap=True,
    )
    latex_code = highlight(code, lexer, formatter)
    style_defs = formatter.get_style_defs()
    return latex_code, style_defs

Utilities for parsing and presenting output from LaTeX engine builds.

LatexLogParser

LatexLogParser()

Incrementally parse LaTeX output into structured messages.

Source code in src/texsmith/adapters/latex/engines/latex/log.py
106
107
108
109
def __init__(self) -> None:
    self._current: LatexMessage | None = None
    self._messages: list[LatexMessage] = []
    self._depth: int = 0

messages property

messages: Sequence[LatexMessage]

Return the messages accumulated so far.

finalize

finalize() -> list[LatexMessage]

Flush any pending message.

Source code in src/texsmith/adapters/latex/engines/latex/log.py
165
166
167
def finalize(self) -> list[LatexMessage]:
    """Flush any pending message."""
    return self._finalize_current()

process_line

process_line(line: str) -> list[LatexMessage]

Process a log line and return messages that have just completed.

Source code in src/texsmith/adapters/latex/engines/latex/log.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def process_line(self, line: str) -> list[LatexMessage]:
    """Process a log line and return messages that have just completed."""
    completed: list[LatexMessage] = []
    segments = self._consume_structure(line)
    if not segments:
        return completed

    for indent_level, payload in segments:
        if not payload or self._should_ignore(payload):
            continue

        severity, summary = self._match_message(payload)
        if severity is not None:
            message_summary = summary if summary else payload
            if (
                severity is LatexMessageSeverity.ERROR
                and message_summary in _ERROR_CONTINUATIONS
                and self._current
                and self._current.severity is LatexMessageSeverity.ERROR
            ):
                self._current.details.append(message_summary)
                continue
            completed.extend(self._finalize_current())
            self._current = LatexMessage(
                severity=severity,
                summary=message_summary,
                indent=indent_level,
            )
            continue

        if self._current and self._is_detail_line(payload):
            self._current.details.append(payload)
            continue

        if self._current and self._merge_path_continuation(payload):
            continue

        if self._current and self._merge_text_continuation(payload, indent_level):
            continue

        completed.extend(self._finalize_current())
        self._current = LatexMessage(
            severity=LatexMessageSeverity.INFO,
            summary=payload,
            indent=indent_level,
        )

    return completed

LatexMessage dataclass

LatexMessage(
    severity: LatexMessageSeverity,
    summary: str,
    details: list[str] = list(),
    indent: int = 0,
)

Structured LaTeX message extracted from the build log.

LatexMessageSeverity

Bases: Enum

Classification severity extracted from LaTeX output.

LatexStreamResult dataclass

LatexStreamResult(
    returncode: int, messages: list[LatexMessage]
)

Result of streaming LaTeX engine output.

parse_latex_log

parse_latex_log(log_path: Path) -> list[LatexMessage]

Parse a LaTeX log file into structured messages.

Source code in src/texsmith/adapters/latex/engines/latex/log.py
485
486
487
488
489
490
491
492
493
494
def parse_latex_log(log_path: Path) -> list[LatexMessage]:
    """Parse a LaTeX log file into structured messages."""
    if not log_path.exists():
        return []
    parser = LatexLogParser()
    with log_path.open("r", encoding="utf-8", errors="replace") as handle:
        for line in handle:
            parser.process_line(line)
    parser.finalize()
    return list(parser.messages)

stream_latexmk_output

stream_latexmk_output(
    command: Sequence[str],
    *,
    cwd: str,
    env: Mapping[str, str],
    console: Console,
    verbosity: int = 0,
) -> LatexStreamResult

Execute a LaTeX engine command and render output incrementally.

Source code in src/texsmith/adapters/latex/engines/latex/log.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def stream_latexmk_output(
    command: Sequence[str],
    *,
    cwd: str,
    env: Mapping[str, str],
    console: Console,
    verbosity: int = 0,
) -> LatexStreamResult:
    """Execute a LaTeX engine command and render output incrementally."""
    from .log_render import LatexLogRenderer

    parser = LatexLogParser()
    renderer = LatexLogRenderer(console)

    with subprocess.Popen(
        command,
        cwd=cwd,
        env=dict(env),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        bufsize=1,
        encoding="utf-8",
        errors="replace",
    ) as process:
        selector = selectors.DefaultSelector()
        if process.stdout:
            selector.register(process.stdout, selectors.EVENT_READ)
        if process.stderr:
            selector.register(process.stderr, selectors.EVENT_READ)

        while selector.get_map():
            for key, _ in selector.select():
                stream_obj = key.fileobj
                if isinstance(stream_obj, int) or not hasattr(stream_obj, "readline"):
                    selector.unregister(stream_obj)
                    continue
                stream = cast(TextIO, stream_obj)
                chunk = stream.readline()
                if chunk:
                    for completed in parser.process_line(chunk):
                        if _should_emit_message(completed, verbosity):
                            renderer.consume(completed)
                else:
                    selector.unregister(stream)

        for completed in parser.finalize():
            if _should_emit_message(completed, verbosity):
                renderer.consume(completed)

        returncode = process.wait()

    renderer.summarize()
    return LatexStreamResult(returncode=returncode, messages=renderer.messages)

LaTeX escaping — single source of truth lives in the LaTeX writer.

Escaping is a backend responsibility, so the canonical implementation moved to :mod:texsmith.writers.latex.escaper. This module re-exports it so existing importers (templates, fonts, extensions, legacy handler helpers) keep a stable import path without duplicating the logic.

escape_latex_chars

escape_latex_chars(
    text: str, *, legacy_accents: bool = False
) -> str

Escape LaTeX special characters leveraging pylatexenc.

Source code in src/texsmith/writers/latex/escaper.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def escape_latex_chars(text: str, *, legacy_accents: bool = False) -> str:
    """Escape LaTeX special characters leveraging pylatexenc."""
    if not text:
        return text
    parts: list[str] = []
    buffer: list[str] = []

    def _encode_chunk(chunk: str) -> str:
        escaped = "".join(_BASIC_LATEX_ESCAPE_MAP.get(char, char) for char in chunk)
        if legacy_accents:
            encoded = unicode_to_latex(escaped, non_ascii_only=True, unknown_char_warning=False)
            return _wrap_latex_output(encoded)
        return escaped

    def _should_skip_encoding(char: str) -> bool:
        try:
            name = unicodedata.name(char)
        except ValueError:
            return False
        if "SUPERSCRIPT" in name or "SUBSCRIPT" in name:
            return True
        return "MODIFIER LETTER" in name and ("SMALL" in name or "CAPITAL" in name)

    for char in text:
        replacement = _COMMON_SYMBOL_MAP.get(char)
        if replacement is not None:
            if buffer:
                parts.append(_encode_chunk("".join(buffer)))
                buffer.clear()
            parts.append(replacement)
            continue
        if _should_skip_encoding(char):
            if buffer:
                parts.append(_encode_chunk("".join(buffer)))
                buffer.clear()
            parts.append(char)
        else:
            buffer.append(char)

    if buffer:
        parts.append(_encode_chunk("".join(buffer)))

    return "".join(parts)

Public template helpers shared across the conversion pipeline.

BaseTemplate

BaseTemplate(root: Path)

Base class shared by template implementations.

Source code in src/texsmith/core/templates/base.py
67
68
69
70
71
72
73
74
75
def __init__(self, root: Path) -> None:
    self.root = root.resolve()
    if not self.root.exists():
        raise TemplateError(f"Template root does not exist: {self.root}")

    manifest_path = _resolve_manifest_path(self.root)
    self.manifest = TemplateManifest.load(manifest_path)
    self.info = self.manifest.latex.template
    self.environment = _build_environment(self.root)

render_template

render_template(template_name: str, **context: Any) -> str

Render a template using the configured Jinja environment.

Source code in src/texsmith/core/templates/base.py
77
78
79
80
81
82
83
84
85
def render_template(self, template_name: str, **context: Any) -> str:
    """Render a template using the configured Jinja environment."""
    try:
        template = self.environment.get_template(template_name)
    except TemplateNotFound as exc:
        raise TemplateError(
            f"Template entry '{template_name}' is missing in {self.root}"
        ) from exc
    return template.render(context)

ResolvedAsset dataclass

ResolvedAsset(
    source: Path,
    destination: Path,
    template: bool = False,
    encoding: str | None = None,
    template_name: str | None = None,
)

Resolved template asset ready to be materialised.

TemplateAsset

Bases: BaseModel

Description of individual template assets.

TemplateAttributeSpec

Bases: BaseModel

Typed attribute definition used to build template defaults.

format says what the value is: markdown (the default) a line or two of prose rendered through the writer, raw a value the template emits as it stands, and file the path of a file whose text the template inlines verbatim — a title page, an imprint, a preamble the author keeps in their own .tex.

default_value

default_value() -> Any

Return a deep copy of the attribute default.

Source code in src/texsmith/core/templates/manifest.py
630
631
632
def default_value(self) -> Any:
    """Return a deep copy of the attribute default."""
    return copy.deepcopy(self._default_cache)

TemplateBinding dataclass

TemplateBinding(
    runtime: TemplateRuntime | None,
    instance: WrappableTemplate | None,
    name: str | None,
    engine: str | None,
    requires_shell_escape: bool,
    slots: dict[str, TemplateSlot],
    default_slot: str,
    base_level: int | None,
)

Binding between slot requests and a LaTeX template.

pass_specs

pass_specs() -> tuple[PassSpec, ...]

The IR passes the bound template declares ([latex.template] passes).

Empty without a template: the passes of a template apply only while that template renders, never globally.

Source code in src/texsmith/core/templates/runtime.py
50
51
52
53
54
55
56
57
58
def pass_specs(self) -> tuple[PassSpec, ...]:
    """The IR passes the bound template declares (``[latex.template] passes``).

    Empty without a template: the passes of a template apply only while
    that template renders, never globally.
    """
    if self.instance is None:
        return ()
    return self.instance.info.pass_specs()

slot_levels

slot_levels(*, offset: int = 0) -> dict[str, int]

Return the resolved base level for each slot.

Source code in src/texsmith/core/templates/runtime.py
60
61
62
63
def slot_levels(self, *, offset: int = 0) -> dict[str, int]:
    """Return the resolved base level for each slot."""
    base = (self.base_level or 0) + offset
    return {name: slot.resolve_level(base) for name, slot in self.slots.items()}

TemplateError

Bases: LatexRenderingError

Raised when a LaTeX template cannot be loaded or rendered.

TemplateInfo

Bases: BaseModel

Metadata describing the LaTeX template payload.

attribute_defaults

attribute_defaults() -> dict[str, Any]

Return a deep copy of template attribute defaults.

Source code in src/texsmith/core/templates/manifest.py
988
989
990
def attribute_defaults(self) -> dict[str, Any]:
    """Return a deep copy of template attribute defaults."""
    return copy.deepcopy(self._attribute_defaults)

attribute_owners

attribute_owners() -> dict[str, str]

Return attribute ownership map (name -> owner).

Source code in src/texsmith/core/templates/manifest.py
1003
1004
1005
def attribute_owners(self) -> dict[str, str]:
    """Return attribute ownership map (name -> owner)."""
    return dict(self._attribute_owners)

emit_defaults

emit_defaults() -> dict[str, Any]

Return default attributes emitted by the template.

Source code in src/texsmith/core/templates/manifest.py
992
993
994
def emit_defaults(self) -> dict[str, Any]:
    """Return default attributes emitted by the template."""
    return copy.deepcopy(self.emit)

pass_specs

pass_specs() -> tuple[PassSpec, ...]

The IR passes declared by passes, resolved when the manifest loaded.

build_pipeline(extra=info.pass_specs()) places them among the bundled passes by their after constraints, for the duration of this template's render only.

Source code in src/texsmith/core/templates/manifest.py
959
960
961
962
963
964
965
966
def pass_specs(self) -> tuple[PassSpec, ...]:
    """The IR passes declared by ``passes``, resolved when the manifest loaded.

    ``build_pipeline(extra=info.pass_specs())`` places them among the
    bundled passes by their ``after`` constraints, for the duration of this
    template's render only.
    """
    return self._pass_specs

resolve_attributes

resolve_attributes(
    overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Return defaults merged with overrides using the attribute specification.

Source code in src/texsmith/core/templates/manifest.py
 999
1000
1001
def resolve_attributes(self, overrides: Mapping[str, Any] | None = None) -> dict[str, Any]:
    """Return defaults merged with overrides using the attribute specification."""
    return self._attribute_resolver.merge(overrides)

resolve_slots

resolve_slots() -> tuple[dict[str, TemplateSlot], str]

Return declared slots ensuring a single default sink exists.

Source code in src/texsmith/core/templates/manifest.py
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
def resolve_slots(self) -> tuple[dict[str, TemplateSlot], str]:
    """Return declared slots ensuring a single default sink exists."""
    resolved = {
        name: slot if isinstance(slot, TemplateSlot) else TemplateSlot.model_validate(slot)
        for name, slot in self.slots.items()
    }

    if "mainmatter" not in resolved:
        resolved["mainmatter"] = TemplateSlot(default=True)

    defaults = [name for name, slot in resolved.items() if slot.default]
    if not defaults:
        resolved["mainmatter"] = resolved["mainmatter"].model_copy(update={"default": True})
        defaults = ["mainmatter"]
    elif len(defaults) > 1:
        formatted = ", ".join(defaults)
        raise TemplateError(f"Multiple default slots declared: {formatted}")

    return resolved, defaults[0]

TemplateManifest

Bases: BaseModel

Structured manifest describing a multi-backend template.

latex is always present (the primary backend); typst is optional and only present for templates that also support the Typst backend.

load classmethod

load(manifest_path: Path) -> TemplateManifest

Load and validate a manifest from disk.

Source code in src/texsmith/core/templates/manifest.py
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
@classmethod
def load(cls, manifest_path: Path) -> TemplateManifest:
    """Load and validate a manifest from disk."""
    try:
        content = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
    except FileNotFoundError as exc:  # pragma: no cover - sanity check
        raise TemplateError(f"Template manifest is missing: {manifest_path}") from exc
    except OSError as exc:  # pragma: no cover - IO failure
        raise TemplateError(f"Failed to read template manifest: {exc}") from exc
    except tomllib.TOMLDecodeError as exc:
        raise TemplateError(f"Invalid template manifest: {exc}") from exc

    try:
        return cls.model_validate(content)
    except ValidationError as exc:
        raise TemplateError(f"Template manifest validation failed: {exc}") from exc

section

section(backend: str) -> TemplateInfo

Return the :class:TemplateInfo for backend (latex/typst).

Raises :class:TemplateError when a template is asked for a backend it does not declare — explicit, never a silent fallback to LaTeX.

Source code in src/texsmith/core/templates/manifest.py
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
def section(self, backend: str) -> TemplateInfo:
    """Return the :class:`TemplateInfo` for ``backend`` (``latex``/``typst``).

    Raises :class:`TemplateError` when a template is asked for a backend it
    does not declare — explicit, never a silent fallback to LaTeX.
    """
    if backend == "latex":
        return self.latex.template
    if backend == "typst":
        if self.typst is None:
            raise TemplateError(
                f"Template '{self.latex.template.name}' does not declare a "
                "[typst.template] section; it cannot be rendered with --format typst."
            )
        return self.typst.template
    raise TemplateError(f"Unknown template backend '{backend}'.")

TemplateRuntime dataclass

TemplateRuntime(
    instance: WrappableTemplate,
    name: str,
    engine: str | None,
    requires_shell_escape: bool,
    slots: dict[str, TemplateSlot],
    default_slot: str,
    base_level: int | None,
    extras: dict[str, Any] = dict(),
)

Resolved template metadata reused across conversions.

TemplateSlot

Bases: BaseModel

Configuration describing how content is injected into a template slot.

resolve_level

resolve_level(fallback: int) -> int

Return the base level applied to rendered headings for this slot.

Source code in src/texsmith/core/templates/manifest.py
 93
 94
 95
 96
 97
 98
 99
100
def resolve_level(self, fallback: int) -> int:
    """Return the base level applied to rendered headings for this slot."""
    base = fallback
    if self.base_level is not None:
        base = self.base_level
    elif self.depth is not None:
        base = fallback + LATEX_HEADING_LEVELS[self.depth]
    return base + self.offset

TemplateWrapResult dataclass

TemplateWrapResult(
    latex_output: str,
    template_context: dict[str, Any],
    output_path: Path | None,
    asset_paths: list[Path] = list(),
    asset_pairs: list[tuple[Path, Path]] = list(),
    rendered_fragments: list[str] = list(),
)

Result artefacts produced after wrapping LaTeX with a template.

WrappableTemplate

WrappableTemplate(root: Path)

Bases: BaseTemplate

Template capable of wrapping a generated LaTeX fragment.

Source code in src/texsmith/core/templates/base.py
67
68
69
70
71
72
73
74
75
def __init__(self, root: Path) -> None:
    self.root = root.resolve()
    if not self.root.exists():
        raise TemplateError(f"Template root does not exist: {self.root}")

    manifest_path = _resolve_manifest_path(self.root)
    self.manifest = TemplateManifest.load(manifest_path)
    self.info = self.manifest.latex.template
    self.environment = _build_environment(self.root)

iter_assets

iter_assets() -> Iterable[ResolvedAsset]

Yield declared template assets.

Source code in src/texsmith/core/templates/base.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def iter_assets(self) -> Iterable[ResolvedAsset]:
    """Yield declared template assets."""
    for destination, asset in self.info.assets.items():
        dest_path = Path(destination)
        if dest_path.is_absolute():
            raise TemplateError(
                f"Template asset destination must be relative, got '{destination}'."
            )

        source_path = Path(asset.source)
        if not source_path.is_absolute():
            source_path = (self.root / source_path).resolve()
        if not source_path.exists():
            raise TemplateError(
                f"Declared template asset '{asset.source}' is missing under {self.root}."
            )

        template_name: str | None = None
        if asset.template:
            if source_path.is_dir():
                raise TemplateError(
                    f"Templated assets must reference files, got directory '{asset.source}'."
                )
            try:
                relative = source_path.relative_to(self.root)
            except ValueError:  # pragma: no cover - defensive
                common_dir = self.root.parent / "common"
                try:
                    relative = source_path.relative_to(common_dir)
                except ValueError as nested_exc:
                    raise TemplateError(
                        f"Templated asset '{asset.source}' must live inside the template root "
                        "or the shared 'common' directory."
                    ) from nested_exc
            template_name = relative.as_posix()

        yield ResolvedAsset(
            source=source_path,
            destination=dest_path,
            template=asset.template,
            encoding=asset.encoding,
            template_name=template_name,
        )

prepare_context

prepare_context(
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Build the rendering context shared by the template and its assets.

Source code in src/texsmith/core/templates/base.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def prepare_context(
    self,
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    """Build the rendering context shared by the template and its assets."""
    attribute_context = self.info.resolve_attributes(overrides)
    context = dict(attribute_context)
    for key, value in self.info.emit_defaults().items():
        context.setdefault(key, value)
    if overrides:
        for key, value in overrides.items():
            if key in context and key not in self.info.emit:
                continue
            context[key] = value

    context.setdefault("frontmatter", "")
    context.setdefault("backmatter", "")
    context.setdefault("index_entries", False)
    context.setdefault("has_index", False)
    context.setdefault("index_terms", [])
    context.setdefault("index_registry", [])
    context.setdefault("index_engine", "auto")
    context.setdefault("acronyms", {})
    context.setdefault("citations", [])
    context.setdefault("bibliography_entries", {})
    context.setdefault("bibliography_resource", None)

    slots, default_slot = self.info.resolve_slots()
    for name in slots:
        context.setdefault(name, "")

    if default_slot == "mainmatter":
        context["mainmatter"] = latex_body
    else:
        context.setdefault("mainmatter", "")
        context[default_slot] = latex_body

    return context

wrap_document

wrap_document(
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
    context: Mapping[str, Any] | None = None,
) -> str

Render the template entry point using the provided LaTeX payload.

Source code in src/texsmith/core/templates/base.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def wrap_document(
    self,
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
    context: Mapping[str, Any] | None = None,
) -> str:
    """Render the template entry point using the provided LaTeX payload."""
    if context is None:
        context = self.prepare_context(latex_body, overrides=overrides)
    else:
        context = dict(context)
        context.setdefault("frontmatter", "")
        context.setdefault("backmatter", "")
        slots, default_slot = self.info.resolve_slots()
        for name in slots:
            context.setdefault(name, "")
        if default_slot == "mainmatter":
            context["mainmatter"] = latex_body
        else:
            context.setdefault("mainmatter", "")
            context[default_slot] = latex_body

    engine = str(context.get("index_engine") or "").strip().lower()
    if not engine or engine == "auto":
        context["index_engine"] = _detect_index_engine()
    else:
        context["index_engine"] = engine

    return self.render_template(self.info.entrypoint, **context)

build_template_overrides

build_template_overrides(
    front_matter: Mapping[str, Any] | None,
) -> dict[str, Any]

Build template overrides from front matter while preserving metadata.

Source code in src/texsmith/core/templates/runtime.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def build_template_overrides(front_matter: Mapping[str, Any] | None) -> dict[str, Any]:
    """Build template overrides from front matter while preserving metadata."""
    if not front_matter or not isinstance(front_matter, Mapping):
        return {}

    overrides = dict(front_matter)
    press_section = overrides.get("press")
    if isinstance(press_section, Mapping):
        overrides["press"] = dict(press_section)

    fragments = overrides.get("fragments")
    if fragments is None and isinstance(press_section, Mapping):
        fragments = press_section.get("fragments")
    if isinstance(fragments, list):
        overrides["fragments"] = list(fragments)

    callouts_section = overrides.get("callouts")
    if callouts_section is None and isinstance(press_section, Mapping):
        callouts_section = press_section.get("callouts")
    if isinstance(callouts_section, Mapping):
        overrides["callouts"] = dict(callouts_section)

    callouts_style = overrides.get("callouts_style")
    if callouts_style is None and isinstance(press_section, Mapping):
        callouts_style = press_section.get("callouts_style")
    if callouts_style is not None:
        overrides["callout_style"] = callouts_style

    base_override = overrides.get("base_level")
    if base_override is None and isinstance(press_section, Mapping):
        base_override = press_section.get("base_level")
    if base_override is not None:
        try:
            overrides["base_level"] = coerce_base_level(base_override)
        except TemplateError:
            overrides["base_level"] = base_override

    return overrides

coerce_base_level

coerce_base_level(
    value: Any, *, allow_none: bool = True
) -> int | None

Normalise base-level metadata to an integer or None.

Source code in src/texsmith/core/templates/runtime.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def coerce_base_level(value: Any, *, allow_none: bool = True) -> int | None:
    """Normalise base-level metadata to an integer or ``None``."""
    if value is None:
        if allow_none:
            return None
        raise TemplateError("Base level value is missing.")

    if isinstance(value, bool):
        raise TemplateError("Base level must be an integer, booleans are not supported.")

    if isinstance(value, (int, float)):
        return int(value)

    if isinstance(value, str):
        candidate = value.strip().lower()
        if not candidate:
            if allow_none:
                return None
            raise TemplateError("Base level value cannot be empty.")
        alias_map = {
            "part": -1,
            "chapter": 0,
            "section": 1,
            "subsection": 2,
        }
        if candidate in alias_map:
            return alias_map[candidate]
        try:
            return int(candidate)
        except ValueError as exc:  # pragma: no cover - defensive
            raise TemplateError(
                f"Invalid base level '{value}'. Expected an integer value or one of "
                f"{', '.join(alias_map)}."
            ) from exc

    raise TemplateError(
        f"Base level should be provided as an integer value, got type '{type(value).__name__}'."
    )

copy_template_assets

copy_template_assets(
    template: WrappableTemplate,
    output_dir: Path,
    *,
    context: Mapping[str, Any] | None = None,
    overrides: Mapping[str, Any] | None = None,
    assets: Iterable[ResolvedAsset] | None = None,
) -> list[Path]

Copy the template declared assets into the selected output directory.

Source code in src/texsmith/core/templates/loader.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def copy_template_assets(
    template: WrappableTemplate,
    output_dir: Path,
    *,
    context: Mapping[str, Any] | None = None,
    overrides: Mapping[str, Any] | None = None,
    assets: Iterable[ResolvedAsset] | None = None,
) -> list[Path]:
    """Copy the template declared assets into the selected output directory."""
    output_dir = Path(output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    if context is None:
        render_context = template.prepare_context("", overrides=overrides)
    else:
        render_context = dict(context)

    written: list[Path] = []
    asset_entries = list(assets) if assets is not None else list(template.iter_assets())
    for asset in asset_entries:
        destination_path = (output_dir / asset.destination).resolve()

        if asset.template:
            if asset.template_name is None:  # pragma: no cover - defensive
                raise TemplateError(
                    f"Templated asset '{asset.source}' is missing template metadata."
                )
            destination_path.parent.mkdir(parents=True, exist_ok=True)
            rendered = template.render_template(asset.template_name, **render_context)
            destination_path.write_text(
                rendered,
                encoding=asset.encoding or "utf-8",
            )
        elif asset.source.is_dir():
            shutil.copytree(asset.source, destination_path, dirs_exist_ok=True)
        else:
            destination_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(asset.source, destination_path)

        written.append(destination_path)

    return written

declared_containers

declared_containers(template: str | None) -> frozenset[str]

The container names template declares (containers), for both backends.

Read straight from the manifest, because it has to be known before the documents are parsed: tmark reports solution as container-unknown while it parses, and no pass — not even the template's own, which rewrites that very container — can take a record back. The parse is one for both backends (prepare_documents runs before the LaTeX binding is resolved and before the Typst template is loaded), so the names of [latex.template] and [typst.template] are read together.

A template that cannot be loaded declares nothing: that failure belongs to the render, which reports it with its own message.

Source code in src/texsmith/core/templates/runtime.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def declared_containers(template: str | None) -> frozenset[str]:
    """The container names ``template`` declares (``containers``), for both backends.

    Read straight from the manifest, because it has to be known *before* the
    documents are parsed: tmark reports ``::: solution`` as
    ``container-unknown`` while it parses, and no pass — not even the
    template's own, which rewrites that very container — can take a record
    back. The parse is one for both backends (``prepare_documents`` runs before
    the LaTeX binding is resolved and before the Typst template is loaded), so
    the names of ``[latex.template]`` and ``[typst.template]`` are read
    together.

    A template that cannot be loaded declares nothing: that failure belongs to
    the render, which reports it with its own message.
    """
    if not template:
        return frozenset()
    try:
        instance = load_template(template)
    except TemplateError:
        return frozenset()
    manifest = instance.manifest
    names = set(manifest.latex.template.containers)
    if manifest.typst is not None:
        names.update(manifest.typst.template.containers)
    return frozenset(names)

discover_templates

discover_templates() -> list[dict[str, str]]

Return available templates in discovery order.

Source code in src/texsmith/core/templates/loader.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def discover_templates() -> list[dict[str, str]]:
    """Return available templates in discovery order."""
    entries: list[dict[str, str]] = []

    def _record(origin: str, name: str, root: Path) -> None:
        entries.append({"name": name, "origin": origin, "root": str(root.resolve())})

    for slug in iter_builtin_templates():
        try:
            template = load_builtin_template(slug)
        except TemplateError:
            continue
        if template is not None:
            _record("builtin", slug, template.root)

    try:
        for dist in metadata.distributions():
            name = dist.metadata.get("Name", "")
            if not name.lower().startswith("texsmith-template-"):
                continue
            slug = name[len("texsmith-template-") :]
            root = _resolve_packaged_template_root(slug)
            if root is None:
                continue
            _record("package", slug, root)
    except Exception:
        # A broken third-party template distribution (corrupt metadata, missing
        # file, unreadable entry-point) must not take down the whole template
        # listing — users would lose access to their builtin and local
        # templates too. Swallow silently; builtins and local scans still run.
        pass

    seen_locals: set[str] = set()
    for candidate in _iter_local_candidates(""):
        if not _looks_like_template_root(candidate):
            continue
        key = str(candidate.resolve())
        if key in seen_locals:
            continue
        seen_locals.add(key)
        _record("local", candidate.name, candidate)

    home_root = get_user_dir().data_dir("templates", create=False)
    if home_root.exists():
        for child in sorted(home_root.iterdir()):
            if child.is_dir() and _looks_like_template_root(child):
                _record("home", child.name, child)

    return sorted(entries, key=lambda entry: (entry["origin"], entry["name"]))

extract_base_level_override

extract_base_level_override(
    overrides: Mapping[str, Any] | None,
) -> Any

Extract a base level override from template metadata overrides.

Source code in src/texsmith/core/templates/runtime.py
105
106
107
108
109
110
111
112
113
114
115
116
def extract_base_level_override(overrides: Mapping[str, Any] | None) -> Any:
    """Extract a base level override from template metadata overrides."""
    if not overrides:
        return None

    press_section = overrides.get("press")
    direct_candidate = overrides.get("base_level")
    if direct_candidate is not None:
        return direct_candidate
    if isinstance(press_section, Mapping):
        return press_section.get("base_level")
    return None

extract_language_from_front_matter

extract_language_from_front_matter(
    front_matter: Mapping[str, Any] | None,
) -> str | None

Inspect front matter for language hints.

Source code in src/texsmith/core/templates/runtime.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def extract_language_from_front_matter(
    front_matter: Mapping[str, Any] | None,
) -> str | None:
    """Inspect front matter for language hints."""
    if not isinstance(front_matter, Mapping):
        return None

    for key in ("language", "lang"):
        value = front_matter.get(key)
        if isinstance(value, str):
            stripped = value.strip()
            if stripped:
                return stripped

    press_entry = front_matter.get("press")
    if isinstance(press_entry, Mapping):
        for key in ("language", "lang"):
            value = press_entry.get(key)
            if isinstance(value, str):
                stripped = value.strip()
                if stripped:
                    return stripped
    return None

load_template

load_template(identifier: str) -> WrappableTemplate

Load a template selected by name or filesystem path.

Source code in src/texsmith/core/templates/loader.py
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
def load_template(identifier: str) -> WrappableTemplate:
    """Load a template selected by name or filesystem path."""
    path_candidate = Path(identifier).expanduser()
    looks_like_path = (
        path_candidate.is_absolute()
        or "/" in identifier
        or "\\" in identifier
        or identifier.startswith(".")
    )
    if looks_like_path and path_candidate.exists():
        return _load_path_template(path_candidate)

    slug = _slug_from_identifier(identifier)

    builtin = load_builtin_template(identifier)
    if builtin is not None:
        return builtin

    packaged_root = _resolve_packaged_template_root(slug)
    if packaged_root is not None:
        return _load_path_template(packaged_root)

    for candidate in _iter_local_candidates(slug):
        if _looks_like_template_root(candidate):
            return _load_path_template(candidate)

    home_candidate = _home_template_candidate(slug)
    if home_candidate is not None and _looks_like_template_root(home_candidate):
        return _load_path_template(home_candidate)

    raise TemplateError(
        f"Unable to load template '{identifier}'. Provide a valid path or "
        "install a package exposing a 'texsmith.templates' entry point."
    )

load_template_runtime

load_template_runtime(template: str) -> TemplateRuntime

Resolve template metadata for repeated conversions.

Source code in src/texsmith/core/templates/runtime.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def load_template_runtime(template: str) -> TemplateRuntime:
    """Resolve template metadata for repeated conversions."""
    template_instance = load_template(template)

    template_base = coerce_base_level(
        template_instance.info.get_attribute_default("base_level"),
    )

    slots, default_slot = template_instance.info.resolve_slots()
    extras_payload = getattr(template_instance, "extras", {}) or {}
    extras = {key: value for key, value in extras_payload.items()}
    declared_fragments = (
        list(template_instance.info.fragments)
        if template_instance.info.fragments is not None
        else None
    )
    extras.setdefault(
        "fragments",
        declared_fragments if declared_fragments is not None else [],
    )

    return TemplateRuntime(
        instance=template_instance,
        name=template_instance.info.name,
        engine=template_instance.info.engine,
        requires_shell_escape=bool(template_instance.info.shell_escape),
        slots=slots,
        default_slot=default_slot,
        base_level=template_base,
        extras=extras,
    )

normalise_template_language

normalise_template_language(
    value: str | None,
) -> str | None

Normalise language codes and map them through babel aliases when available.

Source code in src/texsmith/core/templates/runtime.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def normalise_template_language(value: str | None) -> str | None:
    """Normalise language codes and map them through babel aliases when available."""
    if value is None:
        return None

    stripped = value.strip()
    if not stripped:
        return None

    lowered = stripped.lower().replace("_", "-")
    alias = _BABEL_LANGUAGE_ALIASES.get(lowered)
    if alias:
        return alias

    primary = lowered.split("-", 1)[0]
    alias = _BABEL_LANGUAGE_ALIASES.get(primary)
    if alias:
        return alias

    if lowered.isalpha():
        return lowered

    return None

register_attribute_normaliser

register_attribute_normaliser(
    name: str,
    func: Callable[[Any, TemplateAttributeSpec, Any], Any],
    *,
    override: bool = False,
) -> None

Register a custom attribute normaliser under a short name.

Public counterpart of the internal _register_attribute_normaliser decorator. Template packages may call this at import time so that a manifest can refer to the normaliser by name. Registering over an existing built-in name raises TemplateError unless override=True.

Source code in src/texsmith/core/templates/manifest.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def register_attribute_normaliser(
    name: str,
    func: Callable[[Any, TemplateAttributeSpec, Any], Any],
    *,
    override: bool = False,
) -> None:
    """Register a custom attribute normaliser under a short ``name``.

    Public counterpart of the internal ``_register_attribute_normaliser``
    decorator. Template packages may call this at import time so that a
    manifest can refer to the normaliser by ``name``. Registering over an
    existing built-in name raises ``TemplateError`` unless ``override=True``.
    """
    if name in _BUILTIN_NORMALISER_NAMES and not override:
        raise TemplateError(
            f"Attribute normaliser '{name}' shadows a built-in normaliser. "
            f"Pass override=True to replace it intentionally."
        )
    if name in _ATTRIBUTE_NORMALISERS and not override:
        warnings.warn(
            f"Overriding already-registered attribute normaliser '{name}'.",
            stacklevel=2,
        )
    _ATTRIBUTE_NORMALISERS[name] = func

resolve_template_binding

resolve_template_binding(
    *,
    template: str | None,
    template_runtime: TemplateRuntime | None,
    template_overrides: Mapping[str, Any],
    slot_requests: Mapping[str, str],
    warn: Callable[[str], None] | None = None,
) -> tuple[TemplateBinding, dict[str, str]]

Resolve template runtime metadata and apply slot overrides.

Source code in src/texsmith/core/templates/runtime.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def resolve_template_binding(
    *,
    template: str | None,
    template_runtime: TemplateRuntime | None,
    template_overrides: Mapping[str, Any],
    slot_requests: Mapping[str, str],
    warn: Callable[[str], None] | None = None,
) -> tuple[TemplateBinding, dict[str, str]]:
    """Resolve template runtime metadata and apply slot overrides."""
    runtime = template_runtime
    if runtime is None and template:
        runtime = load_template_runtime(template)

    if runtime is not None:
        # Adjust base level for book parts when requested via overrides.
        binding_base_level = runtime.base_level
        part_flag = None
        press_section = template_overrides.get("press")
        if isinstance(template_overrides.get("part"), bool):
            part_flag = template_overrides.get("part")
        elif isinstance(press_section, Mapping) and isinstance(press_section.get("part"), bool):
            part_flag = press_section.get("part")
        if part_flag and runtime.name == "book":
            binding_base_level = coerce_base_level("part")

        binding = TemplateBinding(
            runtime=runtime,
            instance=runtime.instance,
            name=runtime.name,
            engine=runtime.engine,
            requires_shell_escape=runtime.requires_shell_escape,
            slots=runtime.slots,
            default_slot=runtime.default_slot,
            base_level=binding_base_level,
        )
    else:
        binding = TemplateBinding(
            runtime=None,
            instance=None,
            name=None,
            engine=None,
            requires_shell_escape=False,
            slots={"mainmatter": TemplateSlot(default=True)},
            default_slot="mainmatter",
            base_level=None,
        )

    base_override = coerce_base_level(extract_base_level_override(template_overrides))
    if base_override is not None:
        binding.base_level = base_override

    filtered: dict[str, str] = {}
    for slot_name, selector in slot_requests.items():
        if slot_name not in binding.slots:
            if warn is not None:
                template_hint = f"template '{binding.name}'" if binding.name else "the template"
                warn(
                    f"slot '{slot_name}' is not defined by {template_hint}; "
                    f"content will remain in '{binding.default_slot}'."
                )
            continue
        if binding.runtime is None:
            if warn is not None:
                warn(f"slot '{slot_name}' was requested but no template is selected; ignoring.")
            continue
        filtered[slot_name] = selector

    return binding, filtered

resolve_template_language

resolve_template_language(
    explicit: str | None,
    front_matter: Mapping[str, Any] | None,
) -> str

Resolve the effective template language from CLI and front matter inputs.

Source code in src/texsmith/core/templates/runtime.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def resolve_template_language(
    explicit: str | None,
    front_matter: Mapping[str, Any] | None,
) -> str:
    """Resolve the effective template language from CLI and front matter inputs."""
    candidates = (
        normalise_template_language(explicit),
        normalise_template_language(extract_language_from_front_matter(front_matter)),
    )

    for candidate in candidates:
        if candidate:
            return candidate

    return DEFAULT_TEMPLATE_LANGUAGE

wrap_template_document

wrap_template_document(
    *,
    template: WrappableTemplate,
    default_slot: str,
    slot_outputs: Mapping[str, str],
    slot_output_overrides: Mapping[str, str] | None = None,
    document_state: DocumentState,
    template_overrides: Mapping[str, Any] | None,
    output_dir: Path,
    copy_assets: bool = True,
    output_name: str | None = None,
    bibliography_path: Path | None = None,
    emitter: DiagnosticEmitter | None = None,
    fragments: list[str] | None = None,
    template_runtime: TemplateRuntime | None = None,
) -> TemplateWrapResult

Wrap LaTeX content using a template and optional asset copying.

Source code in src/texsmith/core/templates/wrapper.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def wrap_template_document(
    *,
    template: WrappableTemplate,
    default_slot: str,
    slot_outputs: Mapping[str, str],
    slot_output_overrides: Mapping[str, str] | None = None,
    document_state: DocumentState,
    template_overrides: Mapping[str, Any] | None,
    output_dir: Path,
    copy_assets: bool = True,
    output_name: str | None = None,
    bibliography_path: Path | None = None,
    emitter: DiagnosticEmitter | None = None,
    fragments: list[str] | None = None,
    template_runtime: TemplateRuntime | None = None,
) -> TemplateWrapResult:
    """Wrap LaTeX content using a template and optional asset copying."""
    output_dir = Path(output_dir).resolve()
    resolved_slots = {name: value for name, value in slot_outputs.items()}
    override_slots = (
        {name: value for name, value in slot_output_overrides.items()}
        if slot_output_overrides
        else None
    )

    def _process_slot(value: Any) -> Any:
        return value

    main_slot_content = _process_slot(resolved_slots.get(default_slot, ""))
    resolved_slots[default_slot] = main_slot_content
    resolved_slots.setdefault(default_slot, main_slot_content)
    processed_override_slots: dict[str, Any] | None = None
    if override_slots is not None:
        processed_override_slots = {
            name: _process_slot(value) for name, value in override_slots.items()
        }
        processed_override_slots.setdefault(default_slot, resolved_slots.get(default_slot, ""))

    overrides_payload = dict(template_overrides) if template_overrides else None
    source_dir = None
    overrides_press = overrides_payload.get("press") if overrides_payload else None
    if isinstance(overrides_payload, Mapping):
        raw_source_dir = overrides_payload.get("_source_dir") or overrides_payload.get("source_dir")
        if isinstance(raw_source_dir, (str, Path)) and str(raw_source_dir):
            source_dir = Path(raw_source_dir)
    if source_dir is None and isinstance(overrides_press, Mapping):
        source_dir_raw = overrides_press.get("_source_dir") or overrides_press.get("source_dir")
        if source_dir_raw:
            source_dir = Path(source_dir_raw)
    fragment_names = list(fragments or [])
    if not fragment_names:
        if template_runtime is not None:
            fragment_names = list(template_runtime.extras.get("fragments") or [])
        else:
            manifest_fragments = getattr(template.info, "fragments", None)
            fragment_names = list(manifest_fragments or [])
    # A contract a body named must be loaded, whatever the caller asked for: a
    # fragment that is never rendered cannot activate itself on
    # ``ts_required_fragments``, and the macro stays undefined. The callers pass
    # an explicit list wherever one is configured — the snippet compiler always
    # does — so without this union a nested build loses the fragments its own
    # body requires.
    fragment_names = list(
        dict.fromkeys([*fragment_names, *sorted(document_state.required_fragments)])
    )

    template_context = template.prepare_context(
        main_slot_content,
        overrides=overrides_payload,
    )
    engine = str(template_context.get("index_engine") or "").strip().lower()
    if not engine or engine == "auto":
        template_context["index_engine"] = _detect_index_engine()
    else:
        template_context["index_engine"] = engine
    if isinstance(overrides_press, Mapping):
        template_context.setdefault("press", overrides_press)
    root_name: str | None = None
    if output_name:
        root_name = Path(output_name).stem
    if root_name:
        template_context.setdefault("root_filename", root_name)

    for slot_name, raw_content in resolved_slots.items():
        if slot_name == default_slot:
            continue
        processed_content = _process_slot(raw_content)
        resolved_slots[slot_name] = processed_content
        template_context[slot_name] = processed_content

    # ``has_index``/``index_terms``/``index_registry`` keep ``BaseTemplate``'s
    # defaults (``False``/``[]``/``[]``): the per-term list they used to read
    # (``DocumentState.index_entries``) had no writer since the Python index
    # pass was deleted, so those three were always the default in practice.
    # ``ts-index`` activates from the contract path below (``contract_active``)
    # or an explicit ``fragments`` override; ``index_entries`` only decides
    # whether an *active* fragment also emits ``\printindex``.
    template_context["index_entries"] = document_state.has_index_entries
    _merge_front_matter_glossary(document_state, overrides_payload, template_context)
    template_context["acronyms"] = document_state.acronyms.copy()
    template_context["acronym_groups"] = list(document_state.acronym_groups)
    template_context["acronym_entry_groups"] = dict(document_state.acronym_entry_groups)
    template_context["citations"] = list(document_state.citations)
    template_context["bibliography_entries"] = document_state.bibliography
    _apply_font_scan(document_state, template_context)
    # Where a fragment provisions the files it names: the directory this
    # wrapper writes the document into. ``conversion.execution`` sets the
    # same key on every converted document's overrides; a book's overrides
    # are the site's, which carry none, so the Noto font its ``ts-fonts``
    # copied landed under ``build/fonts`` of the working directory while the
    # ``.sty`` beside the ``.tex`` asked for ``fonts/``.
    template_context.setdefault("output_dir", str(output_dir))
    # IR path: the contracts and packages the bodies named (empty on the HTML
    # path, where the fragments keep sniffing the rendered LaTeX).
    template_context[REQUIRED_FRAGMENTS_KEY] = sorted(document_state.required_fragments)
    template_context["index_registries"] = list(document_state.index_registries)
    if document_state.contract_path:
        # The contract path: the fragments read ``ts_active_fragments``
        # (``contract_active``) instead of sniffing the rendered LaTeX.
        inject_requires(
            template_context,
            {
                "fragments": sorted(document_state.required_fragments),
                "packages": list(document_state.required_packages),
                "index": list(document_state.index_registries)
                or ([""] if document_state.has_index_entries else []),
                "bibliography": bool(document_state.citations),
                "citations": list(document_state.citations),
                "acronyms": list(document_state.acronyms),
                "shell_escape": bool(document_state.requires_shell_escape),
            },
            front_matter=overrides_payload if isinstance(overrides_payload, Mapping) else None,
        )

    fragment_attributes: dict[str, Any] = {}
    if fragment_names:
        fragment_attributes = inject_fragment_attributes(
            fragment_names,
            context=template_context,
            overrides=overrides_payload,
            source_dir=source_dir,
            declared_attribute_owners=(
                template.info.attribute_owners() if hasattr(template, "info") else {}
            ),
        )

    code_section = template_context.get("code")
    code_engine = None
    code_style = "bw"
    if isinstance(code_section, Mapping):
        raw_engine = code_section.get("engine")
        code_engine = raw_engine if isinstance(raw_engine, str) else None
        raw_style = code_section.get("style")
        if isinstance(raw_style, str) and raw_style.strip():
            code_style = raw_style.strip()
    elif isinstance(code_section, str):
        code_engine = code_section
    code_engine = (code_engine or "pygments").strip().lower()
    template_context["code_engine"] = code_engine or "pygments"
    template_context.setdefault("code_style", code_style)
    if "code" not in template_context:
        template_context["code"] = {
            "engine": template_context["code_engine"],
            "style": template_context["code_style"],
        }
    elif isinstance(template_context["code"], dict):
        template_context["code"].setdefault("style", template_context["code_style"])
    if code_engine == "pygments" and getattr(document_state, "pygments_styles", {}):
        styles = getattr(document_state, "pygments_styles", {})
        template_context["pygments_style_defs"] = "\n".join(styles.values())

    template_context["requires_shell_escape"] = bool(
        template_context.get("requires_shell_escape", False)
        or getattr(document_state, "requires_shell_escape", False)
        or (template_runtime.requires_shell_escape if template_runtime else False)
        or code_engine == "minted"
    )
    if template_runtime and template_runtime.engine:
        template_context.setdefault("latex_engine", template_runtime.engine)

    emitter_obj = ensure_emitter(emitter)

    if document_state.citations and bibliography_path is not None:
        template_context["bibliography"] = bibliography_path.stem
        template_context["bibliography_resource"] = bibliography_path.name
        template_context.setdefault("bibliography_style", "numeric")

    template_context["ts_uses_callouts"] = bool(getattr(document_state, "callouts_used", False))

    # Render fragments and inject declarations into template variables.
    requested_fragments = list(fragment_names)
    callout_overrides = overrides_payload.get("callouts") if overrides_payload else None
    callouts_defs = normalise_callouts(
        merge_callouts(
            DEFAULT_CALLOUTS, callout_overrides if isinstance(callout_overrides, Mapping) else None
        )
    )
    template_context.setdefault("callouts_definitions", callouts_defs)
    variable_injections: dict[str, list[str]] = {}
    fragment_providers: dict[str, list[str]] = {}
    declared_slots, _default_slot = template.info.resolve_slots()
    declared_slot_names = set(declared_slots.keys())
    declared_vars = _discover_template_variables(template)
    rendered_fragments: set[str] = set()
    if fragment_names:
        fragment_context: dict[str, Any] = template_context

        def _reassert_effective_emoji_mode(target: dict[str, Any]) -> None:
            effective_mode = target.get("_texsmith_effective_emoji_mode")
            if not effective_mode:
                return
            target["emoji"] = effective_mode
            target["emoji_mode"] = effective_mode
            fonts_section = target.get("fonts")
            if isinstance(fonts_section, Mapping):
                if isinstance(fonts_section, dict):
                    fonts_section["emoji"] = effective_mode
                else:
                    updated_fonts = dict(fonts_section)
                    updated_fonts["emoji"] = effective_mode
                    target["fonts"] = updated_fonts

        if overrides_payload:
            for key, value in overrides_payload.items():
                if key in fragment_attributes:
                    continue
                fragment_context.setdefault(key, value)
            press_section = overrides_payload.get("press")
            if isinstance(press_section, Mapping):
                for key, value in press_section.items():
                    fragment_context.setdefault(key, value)
            _reassert_effective_emoji_mode(fragment_context)
        else:
            _reassert_effective_emoji_mode(fragment_context)
        fragment_result = render_fragments(
            fragment_names,
            context=fragment_context,
            output_dir=output_dir,
            source_dir=source_dir,
            overrides=overrides_payload,
            declared_slots=declared_slot_names,
            declared_variables=declared_vars,
            template_name=template.info.name,
            emitter=emitter_obj,
        )
        variable_injections = fragment_result.variable_injections
        fragment_providers = fragment_result.providers
        for provider_list in fragment_providers.values():
            rendered_fragments.update(provider_list)
    template_context["requested_fragments"] = requested_fragments
    template_context["fragments"] = sorted(rendered_fragments)

    if variable_injections:
        for variable_name, injections in variable_injections.items():
            base = template_context.get(variable_name, "")
            parts: list[str] = [base] if base else []
            parts.extend(injections)
            template_context[variable_name] = "\n".join(part for part in parts if part)

    final_slots = (
        processed_override_slots if processed_override_slots is not None else resolved_slots
    )
    for slot_name, value in final_slots.items():
        template_context[slot_name] = value
    main_slot_content = final_slots.get(default_slot, main_slot_content)

    # Append pdfLaTeX-specific packages when not using LuaLaTeX.
    extra_lines = [line for line in template_context.get("extra_packages", "").splitlines() if line]
    engine = str(template_context.get("latex_engine") or "").strip().lower()
    if engine and engine != "lualatex":
        for package in template_context.get("pdflatex_extra_packages") or []:
            if package:
                extra_lines.append(f"\\usepackage{{{package}}}")
    template_context["extra_packages"] = "\n".join(extra_lines)

    latex_output = template.wrap_document(
        main_slot_content,
        context=template_context,
    )
    latex_output = _squash_blank_lines(latex_output)

    asset_paths: list[Path] = []
    asset_pairs: list[tuple[Path, Path]] = []
    if copy_assets:
        declared_assets = list(template.iter_assets())
        asset_paths = copy_template_assets(
            template,
            output_dir,
            context=template_context,
            overrides=overrides_payload,
            assets=declared_assets,
        )
        asset_pairs = [(entry.source, dest) for entry, dest in zip(declared_assets, asset_paths)]

    output_path: Path | None = None
    if output_name:
        output_dir.mkdir(parents=True, exist_ok=True)
        output_path = output_dir / output_name
        output_path.write_text(latex_output, encoding="utf-8")
        template_context.setdefault("root_filename", output_path.stem)

    return TemplateWrapResult(
        latex_output=latex_output,
        template_context=template_context,
        output_path=output_path,
        asset_paths=asset_paths,
        asset_pairs=asset_pairs,
        rendered_fragments=sorted(rendered_fragments),
    )

Core classes used to load and wrap LaTeX templates.

BaseTemplate

BaseTemplate(root: Path)

Base class shared by template implementations.

Source code in src/texsmith/core/templates/base.py
67
68
69
70
71
72
73
74
75
def __init__(self, root: Path) -> None:
    self.root = root.resolve()
    if not self.root.exists():
        raise TemplateError(f"Template root does not exist: {self.root}")

    manifest_path = _resolve_manifest_path(self.root)
    self.manifest = TemplateManifest.load(manifest_path)
    self.info = self.manifest.latex.template
    self.environment = _build_environment(self.root)

render_template

render_template(template_name: str, **context: Any) -> str

Render a template using the configured Jinja environment.

Source code in src/texsmith/core/templates/base.py
77
78
79
80
81
82
83
84
85
def render_template(self, template_name: str, **context: Any) -> str:
    """Render a template using the configured Jinja environment."""
    try:
        template = self.environment.get_template(template_name)
    except TemplateNotFound as exc:
        raise TemplateError(
            f"Template entry '{template_name}' is missing in {self.root}"
        ) from exc
    return template.render(context)

ResolvedAsset dataclass

ResolvedAsset(
    source: Path,
    destination: Path,
    template: bool = False,
    encoding: str | None = None,
    template_name: str | None = None,
)

Resolved template asset ready to be materialised.

TemplateError

Bases: LatexRenderingError

Raised when a LaTeX template cannot be loaded or rendered.

WrappableTemplate

WrappableTemplate(root: Path)

Bases: BaseTemplate

Template capable of wrapping a generated LaTeX fragment.

Source code in src/texsmith/core/templates/base.py
67
68
69
70
71
72
73
74
75
def __init__(self, root: Path) -> None:
    self.root = root.resolve()
    if not self.root.exists():
        raise TemplateError(f"Template root does not exist: {self.root}")

    manifest_path = _resolve_manifest_path(self.root)
    self.manifest = TemplateManifest.load(manifest_path)
    self.info = self.manifest.latex.template
    self.environment = _build_environment(self.root)

iter_assets

iter_assets() -> Iterable[ResolvedAsset]

Yield declared template assets.

Source code in src/texsmith/core/templates/base.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def iter_assets(self) -> Iterable[ResolvedAsset]:
    """Yield declared template assets."""
    for destination, asset in self.info.assets.items():
        dest_path = Path(destination)
        if dest_path.is_absolute():
            raise TemplateError(
                f"Template asset destination must be relative, got '{destination}'."
            )

        source_path = Path(asset.source)
        if not source_path.is_absolute():
            source_path = (self.root / source_path).resolve()
        if not source_path.exists():
            raise TemplateError(
                f"Declared template asset '{asset.source}' is missing under {self.root}."
            )

        template_name: str | None = None
        if asset.template:
            if source_path.is_dir():
                raise TemplateError(
                    f"Templated assets must reference files, got directory '{asset.source}'."
                )
            try:
                relative = source_path.relative_to(self.root)
            except ValueError:  # pragma: no cover - defensive
                common_dir = self.root.parent / "common"
                try:
                    relative = source_path.relative_to(common_dir)
                except ValueError as nested_exc:
                    raise TemplateError(
                        f"Templated asset '{asset.source}' must live inside the template root "
                        "or the shared 'common' directory."
                    ) from nested_exc
            template_name = relative.as_posix()

        yield ResolvedAsset(
            source=source_path,
            destination=dest_path,
            template=asset.template,
            encoding=asset.encoding,
            template_name=template_name,
        )

prepare_context

prepare_context(
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Build the rendering context shared by the template and its assets.

Source code in src/texsmith/core/templates/base.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def prepare_context(
    self,
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    """Build the rendering context shared by the template and its assets."""
    attribute_context = self.info.resolve_attributes(overrides)
    context = dict(attribute_context)
    for key, value in self.info.emit_defaults().items():
        context.setdefault(key, value)
    if overrides:
        for key, value in overrides.items():
            if key in context and key not in self.info.emit:
                continue
            context[key] = value

    context.setdefault("frontmatter", "")
    context.setdefault("backmatter", "")
    context.setdefault("index_entries", False)
    context.setdefault("has_index", False)
    context.setdefault("index_terms", [])
    context.setdefault("index_registry", [])
    context.setdefault("index_engine", "auto")
    context.setdefault("acronyms", {})
    context.setdefault("citations", [])
    context.setdefault("bibliography_entries", {})
    context.setdefault("bibliography_resource", None)

    slots, default_slot = self.info.resolve_slots()
    for name in slots:
        context.setdefault(name, "")

    if default_slot == "mainmatter":
        context["mainmatter"] = latex_body
    else:
        context.setdefault("mainmatter", "")
        context[default_slot] = latex_body

    return context

wrap_document

wrap_document(
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
    context: Mapping[str, Any] | None = None,
) -> str

Render the template entry point using the provided LaTeX payload.

Source code in src/texsmith/core/templates/base.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def wrap_document(
    self,
    latex_body: str,
    *,
    overrides: Mapping[str, Any] | None = None,
    context: Mapping[str, Any] | None = None,
) -> str:
    """Render the template entry point using the provided LaTeX payload."""
    if context is None:
        context = self.prepare_context(latex_body, overrides=overrides)
    else:
        context = dict(context)
        context.setdefault("frontmatter", "")
        context.setdefault("backmatter", "")
        slots, default_slot = self.info.resolve_slots()
        for name in slots:
            context.setdefault(name, "")
        if default_slot == "mainmatter":
            context["mainmatter"] = latex_body
        else:
            context.setdefault("mainmatter", "")
            context[default_slot] = latex_body

    engine = str(context.get("index_engine") or "").strip().lower()
    if not engine or engine == "auto":
        context["index_engine"] = _detect_index_engine()
    else:
        context["index_engine"] = engine

    return self.render_template(self.info.entrypoint, **context)

load_specialised_template

load_specialised_template(
    path: Path,
) -> WrappableTemplate | None

Import a template-specific module to retrieve a specialised implementation.

Source code in src/texsmith/core/templates/base.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def load_specialised_template(path: Path) -> WrappableTemplate | None:
    """Import a template-specific module to retrieve a specialised implementation."""
    init_path = path / "__init__.py"
    if not init_path.exists():
        return None

    resolved_init = init_path.resolve()
    module_name = f"_texsmith_template_{hash(resolved_init) & 0xFFFFFFFF:x}"
    spec = importlib.util.spec_from_file_location(
        module_name,
        resolved_init,
        submodule_search_locations=[str(path.resolve())],
    )
    if spec is None or spec.loader is None:  # pragma: no cover - defensive
        return None

    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    try:
        spec.loader.exec_module(module)
    except Exception as exc:  # pragma: no cover - surface import errors
        sys.modules.pop(module_name, None)
        raise TemplateError(f"Failed to import template module at '{path}': {exc}") from exc

    for attribute in ("Template", "template", "load_template", "get_template"):
        candidate = getattr(module, attribute, None)
        if candidate is None:
            continue
        specialised = _coerce_template(candidate)
        if specialised is not None:
            return specialised

    return None

Helpers for loading template instances from disk or entry points.

copy_template_assets

copy_template_assets(
    template: WrappableTemplate,
    output_dir: Path,
    *,
    context: Mapping[str, Any] | None = None,
    overrides: Mapping[str, Any] | None = None,
    assets: Iterable[ResolvedAsset] | None = None,
) -> list[Path]

Copy the template declared assets into the selected output directory.

Source code in src/texsmith/core/templates/loader.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def copy_template_assets(
    template: WrappableTemplate,
    output_dir: Path,
    *,
    context: Mapping[str, Any] | None = None,
    overrides: Mapping[str, Any] | None = None,
    assets: Iterable[ResolvedAsset] | None = None,
) -> list[Path]:
    """Copy the template declared assets into the selected output directory."""
    output_dir = Path(output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    if context is None:
        render_context = template.prepare_context("", overrides=overrides)
    else:
        render_context = dict(context)

    written: list[Path] = []
    asset_entries = list(assets) if assets is not None else list(template.iter_assets())
    for asset in asset_entries:
        destination_path = (output_dir / asset.destination).resolve()

        if asset.template:
            if asset.template_name is None:  # pragma: no cover - defensive
                raise TemplateError(
                    f"Templated asset '{asset.source}' is missing template metadata."
                )
            destination_path.parent.mkdir(parents=True, exist_ok=True)
            rendered = template.render_template(asset.template_name, **render_context)
            destination_path.write_text(
                rendered,
                encoding=asset.encoding or "utf-8",
            )
        elif asset.source.is_dir():
            shutil.copytree(asset.source, destination_path, dirs_exist_ok=True)
        else:
            destination_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(asset.source, destination_path)

        written.append(destination_path)

    return written

discover_templates

discover_templates() -> list[dict[str, str]]

Return available templates in discovery order.

Source code in src/texsmith/core/templates/loader.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def discover_templates() -> list[dict[str, str]]:
    """Return available templates in discovery order."""
    entries: list[dict[str, str]] = []

    def _record(origin: str, name: str, root: Path) -> None:
        entries.append({"name": name, "origin": origin, "root": str(root.resolve())})

    for slug in iter_builtin_templates():
        try:
            template = load_builtin_template(slug)
        except TemplateError:
            continue
        if template is not None:
            _record("builtin", slug, template.root)

    try:
        for dist in metadata.distributions():
            name = dist.metadata.get("Name", "")
            if not name.lower().startswith("texsmith-template-"):
                continue
            slug = name[len("texsmith-template-") :]
            root = _resolve_packaged_template_root(slug)
            if root is None:
                continue
            _record("package", slug, root)
    except Exception:
        # A broken third-party template distribution (corrupt metadata, missing
        # file, unreadable entry-point) must not take down the whole template
        # listing — users would lose access to their builtin and local
        # templates too. Swallow silently; builtins and local scans still run.
        pass

    seen_locals: set[str] = set()
    for candidate in _iter_local_candidates(""):
        if not _looks_like_template_root(candidate):
            continue
        key = str(candidate.resolve())
        if key in seen_locals:
            continue
        seen_locals.add(key)
        _record("local", candidate.name, candidate)

    home_root = get_user_dir().data_dir("templates", create=False)
    if home_root.exists():
        for child in sorted(home_root.iterdir()):
            if child.is_dir() and _looks_like_template_root(child):
                _record("home", child.name, child)

    return sorted(entries, key=lambda entry: (entry["origin"], entry["name"]))

load_template

load_template(identifier: str) -> WrappableTemplate

Load a template selected by name or filesystem path.

Source code in src/texsmith/core/templates/loader.py
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
def load_template(identifier: str) -> WrappableTemplate:
    """Load a template selected by name or filesystem path."""
    path_candidate = Path(identifier).expanduser()
    looks_like_path = (
        path_candidate.is_absolute()
        or "/" in identifier
        or "\\" in identifier
        or identifier.startswith(".")
    )
    if looks_like_path and path_candidate.exists():
        return _load_path_template(path_candidate)

    slug = _slug_from_identifier(identifier)

    builtin = load_builtin_template(identifier)
    if builtin is not None:
        return builtin

    packaged_root = _resolve_packaged_template_root(slug)
    if packaged_root is not None:
        return _load_path_template(packaged_root)

    for candidate in _iter_local_candidates(slug):
        if _looks_like_template_root(candidate):
            return _load_path_template(candidate)

    home_candidate = _home_template_candidate(slug)
    if home_candidate is not None and _looks_like_template_root(home_candidate):
        return _load_path_template(home_candidate)

    raise TemplateError(
        f"Unable to load template '{identifier}'. Provide a valid path or "
        "install a package exposing a 'texsmith.templates' entry point."
    )

Pydantic models describing LaTeX template manifests.

CompatInfo

Bases: BaseModel

Compatibility constraints declared by the template.

LatexSection

Bases: BaseModel

Section grouping LaTeX-specific manifest settings.

TemplateAsset

Bases: BaseModel

Description of individual template assets.

TemplateAttributeResolver

TemplateAttributeResolver(
    specs: Mapping[str, TemplateAttributeSpec],
)

Resolve attribute values from overrides using a typed specification.

Source code in src/texsmith/core/templates/manifest.py
761
762
def __init__(self, specs: Mapping[str, TemplateAttributeSpec]):
    self._specs = dict(specs)

TemplateAttributeSpec

Bases: BaseModel

Typed attribute definition used to build template defaults.

format says what the value is: markdown (the default) a line or two of prose rendered through the writer, raw a value the template emits as it stands, and file the path of a file whose text the template inlines verbatim — a title page, an imprint, a preamble the author keeps in their own .tex.

default_value

default_value() -> Any

Return a deep copy of the attribute default.

Source code in src/texsmith/core/templates/manifest.py
630
631
632
def default_value(self) -> Any:
    """Return a deep copy of the attribute default."""
    return copy.deepcopy(self._default_cache)

TemplateError

Bases: LatexRenderingError

Raised when a LaTeX template cannot be loaded or rendered.

TemplateInfo

Bases: BaseModel

Metadata describing the LaTeX template payload.

attribute_defaults

attribute_defaults() -> dict[str, Any]

Return a deep copy of template attribute defaults.

Source code in src/texsmith/core/templates/manifest.py
988
989
990
def attribute_defaults(self) -> dict[str, Any]:
    """Return a deep copy of template attribute defaults."""
    return copy.deepcopy(self._attribute_defaults)

attribute_owners

attribute_owners() -> dict[str, str]

Return attribute ownership map (name -> owner).

Source code in src/texsmith/core/templates/manifest.py
1003
1004
1005
def attribute_owners(self) -> dict[str, str]:
    """Return attribute ownership map (name -> owner)."""
    return dict(self._attribute_owners)

emit_defaults

emit_defaults() -> dict[str, Any]

Return default attributes emitted by the template.

Source code in src/texsmith/core/templates/manifest.py
992
993
994
def emit_defaults(self) -> dict[str, Any]:
    """Return default attributes emitted by the template."""
    return copy.deepcopy(self.emit)

pass_specs

pass_specs() -> tuple[PassSpec, ...]

The IR passes declared by passes, resolved when the manifest loaded.

build_pipeline(extra=info.pass_specs()) places them among the bundled passes by their after constraints, for the duration of this template's render only.

Source code in src/texsmith/core/templates/manifest.py
959
960
961
962
963
964
965
966
def pass_specs(self) -> tuple[PassSpec, ...]:
    """The IR passes declared by ``passes``, resolved when the manifest loaded.

    ``build_pipeline(extra=info.pass_specs())`` places them among the
    bundled passes by their ``after`` constraints, for the duration of this
    template's render only.
    """
    return self._pass_specs

resolve_attributes

resolve_attributes(
    overrides: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Return defaults merged with overrides using the attribute specification.

Source code in src/texsmith/core/templates/manifest.py
 999
1000
1001
def resolve_attributes(self, overrides: Mapping[str, Any] | None = None) -> dict[str, Any]:
    """Return defaults merged with overrides using the attribute specification."""
    return self._attribute_resolver.merge(overrides)

resolve_slots

resolve_slots() -> tuple[dict[str, TemplateSlot], str]

Return declared slots ensuring a single default sink exists.

Source code in src/texsmith/core/templates/manifest.py
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
def resolve_slots(self) -> tuple[dict[str, TemplateSlot], str]:
    """Return declared slots ensuring a single default sink exists."""
    resolved = {
        name: slot if isinstance(slot, TemplateSlot) else TemplateSlot.model_validate(slot)
        for name, slot in self.slots.items()
    }

    if "mainmatter" not in resolved:
        resolved["mainmatter"] = TemplateSlot(default=True)

    defaults = [name for name, slot in resolved.items() if slot.default]
    if not defaults:
        resolved["mainmatter"] = resolved["mainmatter"].model_copy(update={"default": True})
        defaults = ["mainmatter"]
    elif len(defaults) > 1:
        formatted = ", ".join(defaults)
        raise TemplateError(f"Multiple default slots declared: {formatted}")

    return resolved, defaults[0]

TemplateManifest

Bases: BaseModel

Structured manifest describing a multi-backend template.

latex is always present (the primary backend); typst is optional and only present for templates that also support the Typst backend.

load classmethod

load(manifest_path: Path) -> TemplateManifest

Load and validate a manifest from disk.

Source code in src/texsmith/core/templates/manifest.py
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
@classmethod
def load(cls, manifest_path: Path) -> TemplateManifest:
    """Load and validate a manifest from disk."""
    try:
        content = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
    except FileNotFoundError as exc:  # pragma: no cover - sanity check
        raise TemplateError(f"Template manifest is missing: {manifest_path}") from exc
    except OSError as exc:  # pragma: no cover - IO failure
        raise TemplateError(f"Failed to read template manifest: {exc}") from exc
    except tomllib.TOMLDecodeError as exc:
        raise TemplateError(f"Invalid template manifest: {exc}") from exc

    try:
        return cls.model_validate(content)
    except ValidationError as exc:
        raise TemplateError(f"Template manifest validation failed: {exc}") from exc

section

section(backend: str) -> TemplateInfo

Return the :class:TemplateInfo for backend (latex/typst).

Raises :class:TemplateError when a template is asked for a backend it does not declare — explicit, never a silent fallback to LaTeX.

Source code in src/texsmith/core/templates/manifest.py
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
def section(self, backend: str) -> TemplateInfo:
    """Return the :class:`TemplateInfo` for ``backend`` (``latex``/``typst``).

    Raises :class:`TemplateError` when a template is asked for a backend it
    does not declare — explicit, never a silent fallback to LaTeX.
    """
    if backend == "latex":
        return self.latex.template
    if backend == "typst":
        if self.typst is None:
            raise TemplateError(
                f"Template '{self.latex.template.name}' does not declare a "
                "[typst.template] section; it cannot be rendered with --format typst."
            )
        return self.typst.template
    raise TemplateError(f"Unknown template backend '{backend}'.")

TemplateSlot

Bases: BaseModel

Configuration describing how content is injected into a template slot.

resolve_level

resolve_level(fallback: int) -> int

Return the base level applied to rendered headings for this slot.

Source code in src/texsmith/core/templates/manifest.py
 93
 94
 95
 96
 97
 98
 99
100
def resolve_level(self, fallback: int) -> int:
    """Return the base level applied to rendered headings for this slot."""
    base = fallback
    if self.base_level is not None:
        base = self.base_level
    elif self.depth is not None:
        base = fallback + LATEX_HEADING_LEVELS[self.depth]
    return base + self.offset

TypstSection

Bases: BaseModel

Section grouping Typst-specific manifest settings.

Mirrors :class:LatexSection: it reuses the backend-agnostic :class:TemplateInfo (attributes / slots) so a single template package can declare a parallel [typst.template] block driving the Typst backend.

register_attribute_normaliser

register_attribute_normaliser(
    name: str,
    func: Callable[[Any, TemplateAttributeSpec, Any], Any],
    *,
    override: bool = False,
) -> None

Register a custom attribute normaliser under a short name.

Public counterpart of the internal _register_attribute_normaliser decorator. Template packages may call this at import time so that a manifest can refer to the normaliser by name. Registering over an existing built-in name raises TemplateError unless override=True.

Source code in src/texsmith/core/templates/manifest.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def register_attribute_normaliser(
    name: str,
    func: Callable[[Any, TemplateAttributeSpec, Any], Any],
    *,
    override: bool = False,
) -> None:
    """Register a custom attribute normaliser under a short ``name``.

    Public counterpart of the internal ``_register_attribute_normaliser``
    decorator. Template packages may call this at import time so that a
    manifest can refer to the normaliser by ``name``. Registering over an
    existing built-in name raises ``TemplateError`` unless ``override=True``.
    """
    if name in _BUILTIN_NORMALISER_NAMES and not override:
        raise TemplateError(
            f"Attribute normaliser '{name}' shadows a built-in normaliser. "
            f"Pass override=True to replace it intentionally."
        )
    if name in _ATTRIBUTE_NORMALISERS and not override:
        warnings.warn(
            f"Overriding already-registered attribute normaliser '{name}'.",
            stacklevel=2,
        )
    _ATTRIBUTE_NORMALISERS[name] = func

Runtime helpers for binding LaTeX templates to rendered documents.

TemplateBinding dataclass

TemplateBinding(
    runtime: TemplateRuntime | None,
    instance: WrappableTemplate | None,
    name: str | None,
    engine: str | None,
    requires_shell_escape: bool,
    slots: dict[str, TemplateSlot],
    default_slot: str,
    base_level: int | None,
)

Binding between slot requests and a LaTeX template.

pass_specs

pass_specs() -> tuple[PassSpec, ...]

The IR passes the bound template declares ([latex.template] passes).

Empty without a template: the passes of a template apply only while that template renders, never globally.

Source code in src/texsmith/core/templates/runtime.py
50
51
52
53
54
55
56
57
58
def pass_specs(self) -> tuple[PassSpec, ...]:
    """The IR passes the bound template declares (``[latex.template] passes``).

    Empty without a template: the passes of a template apply only while
    that template renders, never globally.
    """
    if self.instance is None:
        return ()
    return self.instance.info.pass_specs()

slot_levels

slot_levels(*, offset: int = 0) -> dict[str, int]

Return the resolved base level for each slot.

Source code in src/texsmith/core/templates/runtime.py
60
61
62
63
def slot_levels(self, *, offset: int = 0) -> dict[str, int]:
    """Return the resolved base level for each slot."""
    base = (self.base_level or 0) + offset
    return {name: slot.resolve_level(base) for name, slot in self.slots.items()}

TemplateRuntime dataclass

TemplateRuntime(
    instance: WrappableTemplate,
    name: str,
    engine: str | None,
    requires_shell_escape: bool,
    slots: dict[str, TemplateSlot],
    default_slot: str,
    base_level: int | None,
    extras: dict[str, Any] = dict(),
)

Resolved template metadata reused across conversions.

build_template_overrides

build_template_overrides(
    front_matter: Mapping[str, Any] | None,
) -> dict[str, Any]

Build template overrides from front matter while preserving metadata.

Source code in src/texsmith/core/templates/runtime.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def build_template_overrides(front_matter: Mapping[str, Any] | None) -> dict[str, Any]:
    """Build template overrides from front matter while preserving metadata."""
    if not front_matter or not isinstance(front_matter, Mapping):
        return {}

    overrides = dict(front_matter)
    press_section = overrides.get("press")
    if isinstance(press_section, Mapping):
        overrides["press"] = dict(press_section)

    fragments = overrides.get("fragments")
    if fragments is None and isinstance(press_section, Mapping):
        fragments = press_section.get("fragments")
    if isinstance(fragments, list):
        overrides["fragments"] = list(fragments)

    callouts_section = overrides.get("callouts")
    if callouts_section is None and isinstance(press_section, Mapping):
        callouts_section = press_section.get("callouts")
    if isinstance(callouts_section, Mapping):
        overrides["callouts"] = dict(callouts_section)

    callouts_style = overrides.get("callouts_style")
    if callouts_style is None and isinstance(press_section, Mapping):
        callouts_style = press_section.get("callouts_style")
    if callouts_style is not None:
        overrides["callout_style"] = callouts_style

    base_override = overrides.get("base_level")
    if base_override is None and isinstance(press_section, Mapping):
        base_override = press_section.get("base_level")
    if base_override is not None:
        try:
            overrides["base_level"] = coerce_base_level(base_override)
        except TemplateError:
            overrides["base_level"] = base_override

    return overrides

coerce_base_level

coerce_base_level(
    value: Any, *, allow_none: bool = True
) -> int | None

Normalise base-level metadata to an integer or None.

Source code in src/texsmith/core/templates/runtime.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def coerce_base_level(value: Any, *, allow_none: bool = True) -> int | None:
    """Normalise base-level metadata to an integer or ``None``."""
    if value is None:
        if allow_none:
            return None
        raise TemplateError("Base level value is missing.")

    if isinstance(value, bool):
        raise TemplateError("Base level must be an integer, booleans are not supported.")

    if isinstance(value, (int, float)):
        return int(value)

    if isinstance(value, str):
        candidate = value.strip().lower()
        if not candidate:
            if allow_none:
                return None
            raise TemplateError("Base level value cannot be empty.")
        alias_map = {
            "part": -1,
            "chapter": 0,
            "section": 1,
            "subsection": 2,
        }
        if candidate in alias_map:
            return alias_map[candidate]
        try:
            return int(candidate)
        except ValueError as exc:  # pragma: no cover - defensive
            raise TemplateError(
                f"Invalid base level '{value}'. Expected an integer value or one of "
                f"{', '.join(alias_map)}."
            ) from exc

    raise TemplateError(
        f"Base level should be provided as an integer value, got type '{type(value).__name__}'."
    )

declared_containers

declared_containers(template: str | None) -> frozenset[str]

The container names template declares (containers), for both backends.

Read straight from the manifest, because it has to be known before the documents are parsed: tmark reports solution as container-unknown while it parses, and no pass — not even the template's own, which rewrites that very container — can take a record back. The parse is one for both backends (prepare_documents runs before the LaTeX binding is resolved and before the Typst template is loaded), so the names of [latex.template] and [typst.template] are read together.

A template that cannot be loaded declares nothing: that failure belongs to the render, which reports it with its own message.

Source code in src/texsmith/core/templates/runtime.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def declared_containers(template: str | None) -> frozenset[str]:
    """The container names ``template`` declares (``containers``), for both backends.

    Read straight from the manifest, because it has to be known *before* the
    documents are parsed: tmark reports ``::: solution`` as
    ``container-unknown`` while it parses, and no pass — not even the
    template's own, which rewrites that very container — can take a record
    back. The parse is one for both backends (``prepare_documents`` runs before
    the LaTeX binding is resolved and before the Typst template is loaded), so
    the names of ``[latex.template]`` and ``[typst.template]`` are read
    together.

    A template that cannot be loaded declares nothing: that failure belongs to
    the render, which reports it with its own message.
    """
    if not template:
        return frozenset()
    try:
        instance = load_template(template)
    except TemplateError:
        return frozenset()
    manifest = instance.manifest
    names = set(manifest.latex.template.containers)
    if manifest.typst is not None:
        names.update(manifest.typst.template.containers)
    return frozenset(names)

extract_base_level_override

extract_base_level_override(
    overrides: Mapping[str, Any] | None,
) -> Any

Extract a base level override from template metadata overrides.

Source code in src/texsmith/core/templates/runtime.py
105
106
107
108
109
110
111
112
113
114
115
116
def extract_base_level_override(overrides: Mapping[str, Any] | None) -> Any:
    """Extract a base level override from template metadata overrides."""
    if not overrides:
        return None

    press_section = overrides.get("press")
    direct_candidate = overrides.get("base_level")
    if direct_candidate is not None:
        return direct_candidate
    if isinstance(press_section, Mapping):
        return press_section.get("base_level")
    return None

extract_language_from_front_matter

extract_language_from_front_matter(
    front_matter: Mapping[str, Any] | None,
) -> str | None

Inspect front matter for language hints.

Source code in src/texsmith/core/templates/runtime.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def extract_language_from_front_matter(
    front_matter: Mapping[str, Any] | None,
) -> str | None:
    """Inspect front matter for language hints."""
    if not isinstance(front_matter, Mapping):
        return None

    for key in ("language", "lang"):
        value = front_matter.get(key)
        if isinstance(value, str):
            stripped = value.strip()
            if stripped:
                return stripped

    press_entry = front_matter.get("press")
    if isinstance(press_entry, Mapping):
        for key in ("language", "lang"):
            value = press_entry.get(key)
            if isinstance(value, str):
                stripped = value.strip()
                if stripped:
                    return stripped
    return None

load_template_runtime

load_template_runtime(template: str) -> TemplateRuntime

Resolve template metadata for repeated conversions.

Source code in src/texsmith/core/templates/runtime.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def load_template_runtime(template: str) -> TemplateRuntime:
    """Resolve template metadata for repeated conversions."""
    template_instance = load_template(template)

    template_base = coerce_base_level(
        template_instance.info.get_attribute_default("base_level"),
    )

    slots, default_slot = template_instance.info.resolve_slots()
    extras_payload = getattr(template_instance, "extras", {}) or {}
    extras = {key: value for key, value in extras_payload.items()}
    declared_fragments = (
        list(template_instance.info.fragments)
        if template_instance.info.fragments is not None
        else None
    )
    extras.setdefault(
        "fragments",
        declared_fragments if declared_fragments is not None else [],
    )

    return TemplateRuntime(
        instance=template_instance,
        name=template_instance.info.name,
        engine=template_instance.info.engine,
        requires_shell_escape=bool(template_instance.info.shell_escape),
        slots=slots,
        default_slot=default_slot,
        base_level=template_base,
        extras=extras,
    )

normalise_template_language

normalise_template_language(
    value: str | None,
) -> str | None

Normalise language codes and map them through babel aliases when available.

Source code in src/texsmith/core/templates/runtime.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def normalise_template_language(value: str | None) -> str | None:
    """Normalise language codes and map them through babel aliases when available."""
    if value is None:
        return None

    stripped = value.strip()
    if not stripped:
        return None

    lowered = stripped.lower().replace("_", "-")
    alias = _BABEL_LANGUAGE_ALIASES.get(lowered)
    if alias:
        return alias

    primary = lowered.split("-", 1)[0]
    alias = _BABEL_LANGUAGE_ALIASES.get(primary)
    if alias:
        return alias

    if lowered.isalpha():
        return lowered

    return None

resolve_template_binding

resolve_template_binding(
    *,
    template: str | None,
    template_runtime: TemplateRuntime | None,
    template_overrides: Mapping[str, Any],
    slot_requests: Mapping[str, str],
    warn: Callable[[str], None] | None = None,
) -> tuple[TemplateBinding, dict[str, str]]

Resolve template runtime metadata and apply slot overrides.

Source code in src/texsmith/core/templates/runtime.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def resolve_template_binding(
    *,
    template: str | None,
    template_runtime: TemplateRuntime | None,
    template_overrides: Mapping[str, Any],
    slot_requests: Mapping[str, str],
    warn: Callable[[str], None] | None = None,
) -> tuple[TemplateBinding, dict[str, str]]:
    """Resolve template runtime metadata and apply slot overrides."""
    runtime = template_runtime
    if runtime is None and template:
        runtime = load_template_runtime(template)

    if runtime is not None:
        # Adjust base level for book parts when requested via overrides.
        binding_base_level = runtime.base_level
        part_flag = None
        press_section = template_overrides.get("press")
        if isinstance(template_overrides.get("part"), bool):
            part_flag = template_overrides.get("part")
        elif isinstance(press_section, Mapping) and isinstance(press_section.get("part"), bool):
            part_flag = press_section.get("part")
        if part_flag and runtime.name == "book":
            binding_base_level = coerce_base_level("part")

        binding = TemplateBinding(
            runtime=runtime,
            instance=runtime.instance,
            name=runtime.name,
            engine=runtime.engine,
            requires_shell_escape=runtime.requires_shell_escape,
            slots=runtime.slots,
            default_slot=runtime.default_slot,
            base_level=binding_base_level,
        )
    else:
        binding = TemplateBinding(
            runtime=None,
            instance=None,
            name=None,
            engine=None,
            requires_shell_escape=False,
            slots={"mainmatter": TemplateSlot(default=True)},
            default_slot="mainmatter",
            base_level=None,
        )

    base_override = coerce_base_level(extract_base_level_override(template_overrides))
    if base_override is not None:
        binding.base_level = base_override

    filtered: dict[str, str] = {}
    for slot_name, selector in slot_requests.items():
        if slot_name not in binding.slots:
            if warn is not None:
                template_hint = f"template '{binding.name}'" if binding.name else "the template"
                warn(
                    f"slot '{slot_name}' is not defined by {template_hint}; "
                    f"content will remain in '{binding.default_slot}'."
                )
            continue
        if binding.runtime is None:
            if warn is not None:
                warn(f"slot '{slot_name}' was requested but no template is selected; ignoring.")
            continue
        filtered[slot_name] = selector

    return binding, filtered

resolve_template_language

resolve_template_language(
    explicit: str | None,
    front_matter: Mapping[str, Any] | None,
) -> str

Resolve the effective template language from CLI and front matter inputs.

Source code in src/texsmith/core/templates/runtime.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def resolve_template_language(
    explicit: str | None,
    front_matter: Mapping[str, Any] | None,
) -> str:
    """Resolve the effective template language from CLI and front matter inputs."""
    candidates = (
        normalise_template_language(explicit),
        normalise_template_language(extract_language_from_front_matter(front_matter)),
    )

    for candidate in candidates:
        if candidate:
            return candidate

    return DEFAULT_TEMPLATE_LANGUAGE