Fragments¶
Fragments are reusable LATEX snippets injected into template slots. Built-in fragments (e.g., geometry, fonts, glossary, index) and custom ones share the same structure:
fragment.tomlwithname,description, and either anentrypointor afileslist.- Optional
attributessection describing fragment-owned attributes (ownership enforced). - Optional
partialsblock for fragment-scoped partial overrides andrequired_partialsfor dependencies. - Optional
should_renderlogic (via entrypoint) to render only when needed.
Manifest shape¶
name = "my-fragment"
description = "Short description."
[files.0]
path = "my-fragment.jinja.sty" # or .tex
slot = "extra_packages"
type = "package" # package | input | inline
output = "my-fragment"
[attributes.option]
default = true
type = "boolean"
sources = ["press.option", "option"]
description = "Controls feature X."
If you prefer Python logic, point entrypoint at a callable returning a BaseFragment instance.
Fragment pieces¶
package: rendered to a.styfile and injected via\usepackageinto the target slot.input: rendered to.texand injected via\input{}into the target slot.inline: rendered string injected directly into the slot variable (no file emitted).
Slots are validated against the template at render time; inline injections must match declared slots or template variables.
Attributes¶
Fragments can declare attributes (ownership enforced) and resolve overrides using the same TemplateAttributeSpec model as templates. Attribute defaults are injected into the context before rendering fragment pieces. Attribute ownership matters: if two fragments (or a template) claim the same attribute name, TeXSmith raises a TemplateError. Keep each attribute owned by a single fragment or the template to avoid conflicts.
Partials¶
Fragments can ship their own partials so feature-specific rendering stays close to the feature:
partials = ["strong.tex", "codeblock.tex"] # list form
required_partials = ["heading"] # fail fast if missing
or as a mapping when paths and names differ:
[partials]
codeinline = "overrides/inline/code.tex"
Partial precedence is template overrides > fragment overrides > core defaults. Duplicate fragment providers for the same partial abort the render.
Runtime loading¶
- Built-ins are discovered under
src/texsmith/fragments/**/fragment.toml. - Custom fragments can be passed via
press.fragmentsin front matter or CLI attributes. - The registry uses manifest metadata first; entrypoint is a fallback.
- Entrypoint contract (Python): Must expose a module attribute
fragmentthat is aBaseFragmentinstance. Legacycreate_fragment()factories are no longer used.
Fragment contract (Python)¶
- Implement a
BaseFragment[Config]subclass with: - class attributes:
name,description,pieces,attributes(TemplateAttributeSpec map), optionalcontext_defaults,partials,required_partials,source. - methods:
build_config(context, overrides=None) -> Config,inject(config, context, overrides=None) -> None,should_render(config) -> bool. - Define a
Configdataclass withfrom_context(...)andinject_into(context)methods (plus helpers likeenabled()). - Export
fragment = YourFragment()from__init__.py; pointfragment.tomlentrypointtotexsmith.fragments.yourname:fragment. - Injection flow: registry merges attribute defaults, builds config, calls
inject, then checksshould_renderbefore rendering pieces.
Minimal example¶
src/texsmith/fragments/example/__init__.py
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping
from texsmith.core.fragments.base import BaseFragment, FragmentPiece
from texsmith.core.templates.manifest import TemplateAttributeSpec
@dataclass(frozen=True)
class ExampleConfig:
message: str | None
@classmethod
def from_context(cls, ctx: Mapping[str, Any]) -> "ExampleConfig":
return cls(message=ctx.get("example_message"))
def inject_into(self, ctx: dict[str, Any]) -> None:
ctx["ts_example_message"] = self.message or "Hello"
def enabled(self) -> bool:
return bool(self.message)
class ExampleFragment(BaseFragment[ExampleConfig]):
name = "ts-example"
description = "Tiny example fragment."
pieces = [
FragmentPiece(
template_path=Path(__file__).with_name("ts-example.jinja.tex"),
kind="inline",
slot="extra_packages",
)
]
attributes = {
"example_message": TemplateAttributeSpec(
default=None,
type="string",
sources=["press.example.message", "example.message"],
)
}
context_defaults: dict[str, Any] = {"extra_packages": ""}
config_cls = ExampleConfig
source = Path(__file__).with_name("ts-example.jinja.tex")
def build_config(self, context: Mapping[str, Any], overrides=None) -> ExampleConfig:
return self.config_cls.from_context(context)
def inject(self, config: ExampleConfig, context: dict[str, Any], overrides=None) -> None:
config.inject_into(context)
def should_render(self, config: ExampleConfig) -> bool:
return config.enabled()
fragment = ExampleFragment()
src/texsmith/fragments/example/fragment.toml
name = "ts-example"
description = "Tiny example fragment."
entrypoint = "texsmith.fragments.example:fragment"
[[files]]
path = "ts-example.jinja.tex"
slot = "extra_packages"
type = "inline"
Migration notes for fragment authors¶
- Prefer
fragment.tomlwithfiles,attributes, and optionalpartials/required_partials; use an entrypoint only when you need Python logic. - If using Python, return a
BaseFragmentinstance viafragment;create_fragment()andFragmentDefinitionshims have been removed. - Declare attribute ownership (implicit
owner = <fragment name>) to avoid collisions with templates or other fragments. - Keep slot targets aligned with template variables; inline targets must reference declared slots or template variables, otherwise a
TemplateErroris raised.