# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.2] - 2026-07-15

### Fixed

- Formatting-only fix (a line wrap the 0.5.1 lint gate rejected); no functional changes. 0.5.1 was tagged but never published.

## [0.5.1] - 2026-07-15

### Fixed

- **Typst: `@[label]` references to figures failed to compile.** The Typst figure emitter dropped the figure's `{#…}` identifier, so a reference to a captioned figure aborted the Typst build with `label does not exist` (Typst resolves references at compile time, unlike LaTeX's `??` fallback). Figure identifiers are now attached as labels. This blocked the 0.5.0 release pipeline on the `paper` example; 0.5.0 was tagged but never published.

## [0.5.0] - 2026-07-15

### Added

- **`@[label]` cross-reference shorthand.** The reference syntax documented in `docs/syntax/references.md` is now implemented by a bundled Markdown extension (`texsmith.extensions.references:SmartReferenceExtension`, enabled by default). `@[sec:intro]` renders as an internal reference (`\ref{sec:intro}` in LaTeX, `#ref(<sec:intro>, supplement: none)` in Typst), and the brackets can be omitted when the label is a single word (`@sec:intro`, `@tbl:data`); trailing sentence punctuation stays out of the label. E-mail addresses, `@` inside words/URLs and code spans are not affected, and `\@` escapes a literal `@`.
- **Typst cross-reference targets.** The Typst writer now emits heading labels from `{#…}` identifiers (`= Title <sec:intro>`), attaches figure identifiers as labels (`#figure(…) <fig:melt>`), wraps labelled tables in a referenceable `#figure(…) <tbl:data>`, and renders fragment links (`[text](#label)`) as `#link(<label>)[text]` instead of a dead URL link. Note that Typst resolves references at compile time: a reference to a label that does not exist is a hard compile error (LaTeX degrades to `??`).

### Fixed

- **Table labels referenced the enclosing section.** The LaTeX table partials emitted `\label{…}` *before* `\caption{…}`, so `@[tbl:…]`/`\ref` resolved to the current section number instead of the table number. The label is now attached after the caption.

## [0.4.1] - 2026-06-27

### Added

- **Template-scoped render extensions.** A template's `[latex.template]` manifest section can now declare `readers` (a list of importable modules whose `@reads` HTML→IR lowerings are layered on top of the bundled registry) and `writer` (a `"module:Class"` reference to a `LaTeXWriter` subclass adding or overriding `@writes` emitters). They are resolved when the template is selected and applied to the `LaTeXRenderer` **for that template only**, so a template package (exam, thesis, poster…) can own custom constructs without affecting other templates' conversions. This restores third-party custom rendering after the 0.4.0 IR migration removed the global `@renders` / `texsmith.renderers` mechanism; the new hook is deliberately template-scoped (no global reader/writer registration). New public helper `texsmith.readers.html.build_reader_registry(extra_modules=…)`; resolution lives in `texsmith.core.templates.extensions`. Documented in `docs/api/handlers.md`.

## [0.4.0] - 2026-06-27

### Added

- **Typed intermediate representation (IR).** Conversion now flows through an explicit three-stage pipeline — `readers/html` → `texsmith.ir` (a typed, backend-agnostic `Block`/`Inline` document model) → `writers/{latex,typst}` — instead of mutating a BeautifulSoup tree in place into LaTeX strings. The IR is semantic, not LaTeX-shaped (a table carries the validated `texsmith.extensions.tables` model with one-letter semantic alignment, never a `"lrX"` preamble; the backend writer computes the preamble), which is what makes a second output backend possible without duplicating the conversion logic. New public surface: `texsmith.ir` (nodes, enums, `NodeVisitor`), `texsmith.readers.html.HtmlReader`, and `texsmith.writers.latex.LaTeXWriter` / `texsmith.writers.typst.TypstWriter`.
- **Typst output backend (experimental).** A new `--format {latex,typst}` flag (default `latex`) selects the output engine, so a single Markdown/HTML source can be built with a LaTeX engine (Tectonic, TeX Live, lualatex…) **or** with [Typst](https://typst.app/). The `article` and `book` templates are Typst-compatible: title, authors (with affiliations), subtitle, date/version, abstract, table of contents, section/heading numbering, native `#bibliography`, and book chapters. Templates declare a `[typst.template]` manifest section (parallel to `[latex.template]`) with a `template.typ` scaffolding; the Typst path routes through the template system, with a standalone fallback for the no-template case. The covered node subset is broad (text/formatting, headers, lists, code, math via `mitex`, links, images, tables, citations, footnotes, admonitions, definition lists). The backend is **experimental**: math goes through the third-party `mitex` package (which can lag the bundled compiler and fail on some constructs), a few constructs are approximated (`MarginNote` renders as a footnote, the `<abbr>` index marker is dropped, remote/unresolved images are dropped to keep the document compilable), and any node still without a Typst emitter raises an explicit, localised `TypstWriteError` rather than degrading silently. The LaTeX backend remains the default and the supported path. See `docs/guide/plumbing/backends.md` for the full covered/uncovered scope.
- **Pip-installable Typst compiler.** A new `texsmith[typst]` optional extra bundles the Typst compiler (the PyPI `typst` package), so `pip install "texsmith[typst]"` / `uv tool install "texsmith[typst]"` enables `--build` for Typst with no system toolchain — analogous to the live Tectonic download. A system `typst` binary on `PATH` is used as a fallback when present. `.typ` emission never depends on a compiler being installed. (Note: the bundled PyPI compiler can lag recent Typst releases; for documents using recent `mitex` math, prefer a system `typst` binary.)
- New documentation page `docs/guide/plumbing/backends.md` describing the IR pipeline, the `--format` flag, Typst installation paths, and the covered/uncovered Typst scope.
- `examples/typst-hello` and `examples/typst-article` demonstrating the Typst backend end to end.
- **Full Typst coverage of every bundled example.** Every `examples/*` document now also builds with `--format typst`, and each example `Makefile` builds **both** backends (a `latex` / `typst` target each, plus a `make typst` aggregate at the examples root), mirroring `examples/paper`. Closing the remaining gaps required several backend additions: the Typst path now renders **Draw.io / Mermaid diagrams** to PNGs (reusing the shared converter strategies) and rewrites the IR image nodes accordingly; **equation cross-references** (`\eqref` / `\ref` / `\label`) are handled natively (mitex labels do not propagate, so a pure-reference math span becomes a Typst `#ref` and an embedded `\label` is re-attached to the now-numbered equation); the templated path gained a **generic named-slot engine** (any declared slot is filled from the matching section, not just `abstract`), a raw `front_matter` context plus `te` (typst-escape) / `longdate` Jinja filters for **data-driven templates**, and an `asset()` helper that copies a source-relative file (e.g. a signature image) next to the `.typ`. A new `bcp47_language` attribute normaliser maps babel language names (`english`) to ISO codes (`en`).
- **Typst sections for the `colorful-grid`, `recipe`, and `formal-letter` templates.** The poster grid, the data-driven recipe card, and the business letter (the LaTeX-only holdouts) now declare `[typst.template]` blocks with native `template.typ` scaffolding, so `colorful`, `recipe`, and `letter` build with Typst too.
- **Closer Typst/LaTeX article parity.** The Typst article (and book) backend now: renders a bare block image as a centred `#figure` captioned by its alt/title (was a left-aligned inline image); renders the document date through the shared, language-aware `format_date` resolver (`November 16, 2025`); emits an unnumbered **Acronyms** backmatter list of the `<abbr>` definitions used (mirroring the LaTeX glossary fragment); gives section headings more vertical space; and shows the page number only when the document spans more than one page (single-page articles stay unnumbered, like the LaTeX `LastPage>1` guard). The author block no longer renders stray `[…]` brackets (Typst markup treats `[…]` literally).
- **Styled Typst callouts + mustache substitution on the Typst path.** The Typst backend now honours `callouts.style` (`fancy` / `classic` / `minimal`) and the per-kind callout palette — border/title colours and icons from the built-in definitions plus any custom callouts declared in front matter — instead of a single hard-coded block, matching the LaTeX `ts-callouts` fragment. The `admonition` example builds all three styles for Typst as it does for LaTeX. The Typst path also now runs the shared mustache substitution (front matter + HTML), which `prepare_documents` skips, so placeholders like `{{callouts.style}}` resolve in Typst output.

### Changed

- **Rendering architecture rewritten.** Element handling moved from a multi-pass DOM-mutation engine to the reader/writer model above. Extension authors who registered render rules with `@renders` / `RenderPhase` must migrate to `@reads` (lower an HTML element to an IR node) and `@writes(NodeType)` (emit a node for a given backend). `LaTeXRenderer` is now a thin `read → write` orchestrator that reuses the existing Jinja LaTeX partials, so the LaTeX output is preserved — the bundled examples render byte-identically to the previous engine. The font-fallback scan now runs over IR text nodes instead of the final LaTeX, making it backend-agnostic. Net effect on the source tree: ~12 fewer files and ~4k fewer lines.
- `escape_latex_chars` now lives in `texsmith.writers.latex.escaper` (escaping is a backend concern); `texsmith.adapters.latex.utils` re-exports it.
- Pinned Tectonic version for the live download bumped `0.15.0` → `0.16.9` (release asset naming verified unchanged; the `--version` cache probe relies on a flag that was dropped in 0.16.0 and restored in 0.16.9).
- Typst PDF compilation now **prefers the system `typst` binary** over the embedded PyPI compiler, falling back to the package when no binary is on `PATH`. The bundled package floats to the latest Typst (`>=0.15`), which drops symbols `mitex` still emits (e.g. `diff` for `\partial`); the user-installed binary tracks `mitex`'s supported versions, so preferring it keeps recent math compiling.
- **De-duplicated shared logic across backends and fragments.** Citation/footnote-identifier parsing (`_normalise_footnote_id`, `_split_citation_keys`, `_citation_keys_from_payload`, the DOI/citation regexes) and the `_find_image` / `_find_table` IR lookups, previously copied verbatim in both the LaTeX and Typst writers, now live in one shared `texsmith.writers._ir_queries` module. `BaseFragment.build_config` / `inject` gained concrete defaults (`config_cls.from_context` / `config.inject_into`), so the ten fragments that followed that convention no longer each restate it (`frame` / `geometry` keep their bespoke parsing). The `_coerce_string` / `_format_authors` helpers copied across the article/book/letter templates are now a single `TemplateContextHelpers` mixin in `texsmith.templates.common`.
- Merged `texsmith.core.html_utils` (`strip_html_comments`) into `texsmith.adapters.html_utils` and deleted the dead `texsmith.core.templates.text` module (the live `_squash_blank_lines` lives on in `wrapper`); `_discover_template_variables` is now defined once in `context_usage` and imported by `wrapper`. Front-matter glossary parsing moved from `texsmith.core.conversion.glossary` to `texsmith.core.glossary` (it has no conversion-layer dependency and sits beside the other front-matter resolvers).
- Extracted the babel / BCP-47 language tables and mappers out of the 1039-line `texsmith.core.templates.manifest` into a focused `texsmith.core.templates.languages` module (manifest down to ~930 lines); `runtime` now imports the alias table from there instead of reaching into `manifest`.
- Moved the two heading-analysis `HTMLParser` subclasses out of `texsmith.core.documents` (637 → ~547 lines) into `texsmith.core.heading_analysis` (`HeadingLevelScanner`, `HeadingInspector`), and dropped the unused `Document._HEADING_TAGS` class variable.
- Split the LaTeX-log Rich renderer (`LatexLogRenderer`) out of the 720-line `texsmith.adapters.latex.engines.latex.log` into a sibling `log_render` module (log down to ~500 lines), separating log *parsing* from its *presentation*; the streaming entry point imports the renderer lazily to avoid an import cycle.
- Removed the module-level `DoiBibliographyFetcher` cache-and-test-seam in `texsmith.core.conversion.templates` (the last "exported for test monkeypatching" shim). DOI fetching now goes through a single `_resolve_bibliography_fetcher()` factory that lazily imports the real fetcher; `_ensure_doi_support()` only resolves the error type and DOI normaliser. Tests substitute the fetcher by patching the factory rather than a dual-purpose module global.
- Moved the font-provisioning machinery out of the `ts-fonts` fragment into `texsmith.fonts.provisioning`. The fragment package (`texsmith.fragments.fonts`) carried ~780 lines of font tooling — CTAN `.sty` acquisition / stub generation, Plex and emoji font downloads, and the family / glyph-fallback / monospace context builder — leaving the actual fragment only ~70 lines. That tooling now lives under `texsmith.fonts` (where the rest of the font subsystem already is), and the fragment is a thin ~100-line glue module that imports it. Pure relocation, no behaviour change; the LaTeX-oriented output is unchanged.
- Moved the LaTeX-engine orchestration out of the `render` CLI command into `ConversionService.build_pdf`. The command previously re-implemented binary selection (Tectonic/biber/makeglossaries), command/env construction and the engine run inline — duplicating the (until now unused) `build_pdf` service method. `render` now delegates to it and keeps only presentation, the PDF copy and dependency-file emission (1225 → ~1150 lines). `build_pdf` gains an injectable `run_engine` parameter so the engine run stays a clean test seam instead of relying on monkeypatched module globals.
- Moved the Typst conversion pipeline out of the CLI: `ui/cli/commands/typst_emit.py` (which contained no CLI code — only the library functions `render_typst_document` / `build_typst_pdf`) is now `texsmith.core.conversion.typst`, the Typst analogue of the LaTeX engine in `texsmith.core.conversion.core`. The `render` command imports it from there. The Typst writer's `_citation_label` is promoted to the public `citation_label`, and the shared mustache/callout helpers in `core.conversion.templates` are now plain intra-package imports rather than the CLI reaching across layers into core privates.
- Consolidated `.snippet` rendering inside the snippet plugin: a new `render_snippet_latex(html, context)` entry point owns the BeautifulSoup unwrap, fence extraction, asset compilation and figure emission, so the LaTeX writer's `_render_snippet` is now a one-line delegation instead of importing BeautifulSoup and four private snippet helpers. No output change; keeps the deliberate snippet HTML-passthrough but stops it leaking into the writer.
- Collapsed the conversion-orchestration layering: the batch façade (`convert_documents`, `ConversionBundle`, `LaTeXFragment`, `to_template_fragments`) moved from `texsmith.core.conversion.pipeline` into `texsmith.core.conversion.core` (which already owned the single-document engine), and `pipeline.py` was removed — one fewer module/hop for the same job. The top-level `texsmith.convert_documents` / `ConversionBundle` / `LaTeXFragment` public API is unchanged; only the deep `texsmith.core.conversion.pipeline` import path is gone (use `texsmith.core.conversion.core`). Internally, `_render_document` / `_build_runtime_common` now read the document, request flags and generation strategy off the `ConversionContext` instead of receiving them as a long list of unpacked keyword arguments.

### Fixed

- The LaTeX writer now escapes a code block's filename/title before emitting it as the `\begin{code}{lang}{filename}{…}` argument. A title containing a LaTeX special (e.g. `bubble_sort.py`) previously produced `\begin{code}{py}{bubble_sort.py}{}`, which aborts compilation with "Missing $ inserted".
- Headings whose rendered content spans multiple lines (e.g. mkdocstrings API members carrying a `<small>` role label) are now collapsed to a single line before being emitted as a sectioning-command argument. Multi-line `\section{…}` / `\subsection{…}` arguments previously failed with "Paragraph ended before \M@sect was complete", breaking the `TEXSMITH_BUILD=1` documentation PDF.
- The script-aware font-fallback scan no longer aborts a conversion when the Noto/ucharclasses coverage metadata cannot be built (cold cache with no network): it now degrades to an empty fallback index and renders with the default font set, instead of letting the `URLError`/`OSError` propagate out of `ScriptDetector`. This also de-flakes offline test runs that render prose.
- Bundled-tool downloads (Tectonic, Biber, and the `makeglossaries` helper) now retry transient network failures (e.g. "Connection reset by peer" against the CTAN / SourceForge / GitHub mirrors) with a short backoff before failing, instead of aborting the build on a single dropped connection. Non-transient errors (such as TLS certificate failures) still fail fast.
- Internal anchor links carrying link text (e.g. `[Jump](#section)`) now render as `\hyperref[section]{Jump}` (clickable text) instead of emitting the link text followed by a bare `\ref{…}` number; links without text still fall back to `\ref{…}`. (#4)
- Plain Markdown table captions (`Table: …`) that contain inline formatting (`*em*`, `` `code` ``, links) are no longer silently dropped: the inline markup is preserved inside the table `<caption>` instead of the caption being left as a stray paragraph with no caption attached to the table. (#5)

### Removed

- The render-rule engine (`texsmith.core.rules`: `RenderEngine`, `RenderRegistry`, `RenderPhase`, `_DOMVisitor`) and the `@renders` decorator, together with all of `texsmith.adapters.handlers.*` — their HTML→semantic logic now lives in the HTML reader and their semantic→LaTeX logic in the LaTeX writer.
- The magic `LaTeXFormatter.__getattr__` template-dispatch proxy, replaced by explicit `render_template(...)` calls and typed `@writes` dispatch (a node with no emitter now raises a clear, localised error instead of an opaque `AttributeError`). This surfaced and fixed two latent hidden-attribute accesses (snippet figure rendering and the MkDocs heading formatter).
- The obsolete `--debug-rules` flag and its rule-description plumbing (a vestige of the removed engine that emitted a warning on every conversion).
- The `_FallbackConverter` placeholder-PDF shim.
- The top-level shim modules `index.py`, `texlogos.py`, `progressbar.py`, `quotes.py`, and `smart_dashes.py`; their implementations now live under `texsmith.extensions.*` and the entry points point there directly.
- **Codebase-reduction sweep (≈645 fewer source lines, 3 modules deleted).** Removed the remaining shims and dead code on the IR branch: the no-op `LaTeXRenderer.register()`, the deprecated `git_version.format_version` forwarder, the `get_template(..., callbacks=…)` rejection branch, the `formal-letter` template alias, and the camelCase `GeometryFragment.getLatex()` alias. Deleted unused machinery never read by the pipeline — the `SegmentContext` / `AssetMapping` dataclasses with `Document.segments` / `Document.assets`, `ConversionContext.runtime_common`, `GenerationStrategy.prefer_inputs`, `Document.slot_inclusions` (a duplicate of `slot_includes`); the empty `texsmith.core.fonts` package; and the unused `ir.Alignment` / `Alignment.from_short` and `ir.CitationMode` / `Cite.mode` (no reader sets them and no writer reads them). Removed a shadowed duplicate `_discover_local_templates`, an unreachable post-`return` block in the template loader, and a dead `try/except` in `devtools`.
- The custom **`exercise` admonition** support has been removed from the core. The bespoke `<div class="admonition exercise">` handling (gap-fill `<input class=text-with-gap>`, `<details class=solution>` capture, answer notes) was a non-standard admonition flavour with no documentation or examples; it is better implemented as an opt-in extension/plugin than baked into the engine. Removed: the `read_exercise` reader lowering, the writer's `_render_exercise` / `_EXERCISE_NOISE`, and the `DocumentState` fields/methods that only served it (`solutions`, `add_solution`, `exercise_counter`, `next_exercise` — `solutions` was write-only and never emitted anywhere). A `div`/`details` carrying the `exercise` class now degrades to an ordinary admonition/collapsible callout. This also removes one of the three HTML-passthrough roles that leaked raw markup through the IR.
- The entirely unused Markdown-extension registry in `texsmith.extensions` (`ExtensionSpec`, `_EXTENSIONS`, `available_extensions`, `get_extension_spec`, `load_markdown_extension`, `load_mkdocs_plugin`) — it had no callers and had drifted out of sync with the authoritative `texsmith.adapters.markdown.DEFAULT_MARKDOWN_EXTENSIONS`, which is now the single source of truth.

## [0.3.1] - 2026-06-12

### Added

- Template packages can now supply their own attribute `normaliser` without reaching into TeXSmith internals. The manifest `normaliser` field, in addition to the built-in names (`paper_option`, `orientation`, `babel_language`, `callout_style`, `margin_style`, `code_options`), now accepts a fully-qualified callable reference of the form `"package.module:callable"` (the dotted `"package.module.callable"` form is also tolerated). The referenced object is imported and cached (`functools.cache`) the first time it is needed, and validated when the manifest is loaded so misconfiguration fails early with the template name, attribute name, and offending value in the `TemplateError` message (module not found / attribute not found / not callable). A public `texsmith.core.templates.register_attribute_normaliser(name, func, *, override=False)` helper is also exposed for authors who prefer registering a short name at import time; registering over a built-in name raises a `TemplateError` unless `override=True`. The callable contract is unchanged — `(value, spec, fallback) -> value` — and every existing manifest using a built-in name behaves exactly as before.
- Templates can now declare their own default Markdown extensions through a new `markdown_extensions` manifest field (`TemplateInfo.markdown_extensions`). The `render` command resolves the selected template's manifest and layers its extensions on top of the built-in defaults but below the CLI-requested ones (`--extension`), so a template can opt into extra Markdown behaviour by default while authors can still override per document and switch any of them off with `--disable-extension`. `resolve_markdown_extensions` gains a keyword-only `template=` parameter carrying the template-declared list; template-resolution failures are deferred to the main pipeline rather than surfacing twice. New `examples/hello_world_api.py` shows the minimal Python-API path (`ConversionRequest` → `ConversionService.execute` → `build_pdf`) for rendering a "Hello World" article to PDF with Tectonic.
- `bibliography_show_urls` front-matter flag (default `false`) for the `ts-bibliography` fragment. When `false` (the new default), the `biblatex` preamble suppresses raw `url`/`urldate` field output and turns the entry's title into a `\href{<url>}{…}` hyperlink, preserving the underlying style's italic (`\mkbibemph`) or quoted (`\mkbibquote`) formatting. Long URLs no longer leak into justified two-column bibliographies as `\url{…}` chunks that produce streams of `Underfull/Overfull \hbox` warnings; the title stays clickable in the PDF. Set `bibliography_show_urls: true` in the document front matter to restore the previous behaviour and print the full URL inline (with the same hyphenation caveats as before).
- Structured `version:` front-matter schema (validated by Pydantic) that supersedes the previous string-only contract. Five shapes are accepted: free-form text (`version: "Draft 3"`), a list of integers (`version: [2, 3, 0]` → `"2.3.0"`), an explicit semver mapping (`version: {major: 2, minor: 3, patch: 0, pre: "rc1", build: "abc"}` → `"2.3.0-rc1+abc"`), a git derivation (`version: {git: true}` → `git describe --tags --dirty`), and a git-with-suffix variant (`version: {git: true, suffix: "(draft)"}` → `"v0.5.1 (draft)"`). The literal `version: git` shorthand is preserved as a backwards-compatible alias for `{git: true}`. Resolution lives in a new `texsmith.core.document_version` module (`format_version`, `FreeFormVersion`, `SemverListVersion`, `SemverDictVersion`, `GitVersion`, `DocumentVersionError`); the article template feeds the resolved canonical text through `escape_latex_chars` before injection, so structured shapes survive the metadata pipeline untouched (`type = "any"` on the manifest attribute) and validation errors surface as `DocumentVersionError` with the originating Pydantic message.
- Structured `date:` front-matter schema with three magic keywords on top of free-form ISO and string values: `date: today` renders today's system date in long form, `date: commit` renders the date of the last `HEAD` commit (via the new `git_commit_date()` helper in `texsmith.core.git_version`), and `date: none` (the new default when `date` is omitted) emits a bare `\date{}` so `\maketitle` drops the date line. ISO strings (`"2026-03-05"`) and YAML-parsed `date` objects render as long form localised by the document `language` (`"5 mars 2026"` for `language: french`, `"March 5, 2026"` for English; first of the month becomes `"1er"` in French). Resolution lives in a new `texsmith.core.document_date` module (`format_date`); the article template's manifest attribute is `type = "any"` so structured shapes pass through, and `core.metadata` now skips string coercion for structured `date` values during press-metadata flattening.
- `texsmith.core.git_version.git_commit_date()` returns the committer date of `HEAD` (using `git log -1 --format=%cs`) for the repository containing the supplied path, with a per-repo cache and the same warn-on-missing semantics as `git_describe()`. Used by the `date: commit` keyword.
- `article` template `version` front-matter field that surfaces in `\maketitle` underneath the date in a `\small` style. The value is free-form (`version: "Consolidation du draft de janvier 2026"`) or the literal sentinel `version: git`, in which case the template runs `git describe --tags --dirty` against the document's repository (with a 7-character commit-hash fallback, suffixed `-dirty` when the worktree has uncommitted changes) and emits a warning if no git metadata is reachable. The resolver lives in a new `texsmith.core.git_version` module — `git_describe()`, `git_commit_date()`, `resolve_git_root()`, `reset_cache()` — so other templates can opt in. Tag names containing LaTeX-special characters (`_`, `&`, `%`, `#`, …) are escaped by the article template before being injected, so a tag like `v1.0_alpha` renders correctly.
- New `invisible_chars` Markdown extension (registered by default) that replaces stray zero-width characters — ZWSP (`U+200B`), ZWNJ (`U+200C`), ZWJ (`U+200D`), and BOM (`U+FEFF`) — with a non-breaking space (`U+00A0`) in text nodes. Code, `pre`, `kbd`, `script`, and `style` elements are left untouched, so deliberate zero-width sequences inside fenced code blocks still survive. Defends against pasted content silently smuggling glyphs that small-caps fonts (e.g. `lmromancaps10-regular`) cannot render.
- `yaml table` columns can now omit the `name` field. When every column omits it, the rendered output drops the `<thead>` (and the corresponding LaTeX `\midrule` after the header) so the body sits directly under `\toprule` — handy for two-column key/value summaries. Mixed cases (some named, some not) keep the header row and emit blank `<th>` cells for the unnamed columns. Per-column `align`, `width`, and `width-group` keep their usual meaning. The `examples/tables/tables.md` tutorial gains a "Headerless tables" section showcasing the feature.
- Plain Markdown tables now support inline horizontal separators: any body row whose every cell is just three or more dashes (`| --- | ----- | --- |`, surrounding whitespace allowed) is rewritten as a separator row tagged with `data-ts-role="separator"` (a single `<td colspan="N">`) and rendered as a bare `\midrule` in LaTeX. A new `_MarkdownTableSeparatorTreeprocessor` performs the rewrite on `<tbody>` / `<tfoot>` rows and reuses the same separator markup that `yaml table` emits, so both the legacy `render_tables` handler and `render_yaml_table` (when bound via `yaml table-config`) handle the rule uniformly. Cells with fewer than three dashes, or rows where any cell carries non-dash content, are left as regular data rows.

### Changed

- The `"module:attribute"` import-reference parsing now lives in a single helper (`texsmith.core.templates.manifest._import_object`) shared by the attribute-normaliser resolver and fragment-entrypoint loading, replacing two near-duplicate copies. As a side effect, a malformed or unresolvable fragment `entrypoint` now raises a `TemplateError` whose message names the reference and the missing module/attribute (the same actionable wording used for normalisers), instead of leaking a bare `ImportError`/`AttributeError` or the older terse "Fragment entrypoint must be in the form 'module:attribute'." text. Also removed a dead, shadowed duplicate definition of `_render_attribute_markdown` in the manifest module (the second definition always won; the first was unreachable).
- Standalone short-bold paragraphs are now promoted to a `\tslead{…}` lead-in instead of rendering as bare `\textbf{…}`. A `<p>` whose only meaningful child is a single `<strong>` element with under 80 characters of plain text triggers the rule; bold inside running prose, bold over the threshold, and bold labels synthesised by other extensions (e.g. `tabbed-set` labels nested in a `<div>`) keep the original `\textbf{…}` rendering. The macro — `\providecommand{\tslead}[1]{\par\noindent\textbf{#1}\par\nobreak\smallskip}` — is emitted idempotently next to every use (same pattern as `\texsmithHighlight`), so it can be overridden in a custom preamble snippet without touching the converter. Motivation: under `babel-french` (and any class with a non-zero `\parindent`), a bold pseudo-heading wedged between two block environments (`\end{center}` … `\textbf{Méthodologie}` … `\begin{center}`) renders flush left while the same construct after running prose is indented; `\tslead` forces a stable `\noindent` paragraph break + `\smallskip` so both cases align without authors having to sprinkle manual `\noindent`s. Implemented as a new PRE-phase `<p>` handler (`render_lone_bold_paragraph`) and a new `lead.tex` partial; documented under "Standalone bold paragraphs" in `docs/syntax/formatting.md`.
- The article template now emits `\date{}` (no date line) when no `date` is configured, replacing the previous `\date{\today}` fallback. Documents that want today's date must opt in explicitly with `date: today`. Same applies when `date: none` is set explicitly.
- The article template's title block emits a `\date{{\small <version>}}` block when only a version is set (no date), instead of suppressing the version entirely. Combined date+version blocks are unchanged.
- `texsmith.core.git_version.format_version` is now a thin shim that forwards to `texsmith.core.document_version.format_version`. The shim is preserved for backwards compatibility; new code should import from the new module.
- `ts-extra` margin-note safety buffer (`\tsmarginparbuf`, subtracted from the available outer margin when clamping `\marginparwidth`) bumped from `6pt` (~2 mm) to `6mm`, so margin notes typeset via the `{margin}[…]` extension keep a visibly safe gap to the page edge instead of sitting 2-3 mm away. Documents that need the previous, tighter clamp can override it in a custom preamble snippet (`\setlength{\tsmarginparbuf}{6pt}`).

### Fixed

- `letter` template Swiss standard (`sn-right`) now passes the correct `scrletter` LCO option `SN` instead of the non-existent `SNright`, which `KOMA-Script` silently ignored, so right-aligned Swiss letters (SN 010130) now lay out as intended. The bare `sn` and `swiss` aliases now resolve to `sn-right` (the published SN 010130 reference layout) rather than `sn-left`; documents that relied on the old left-aligned default must set `sn-left` explicitly.
- The `footnote` LaTeX partial no longer emits a trailing `%` after the closing brace. Inline footnotes followed by text on the same source line (e.g. `…enseignants[^note] ; suite`) used to render as `\footnote{…}% ; suite`, where the stray `%` silently commented out the rest of the rendered paragraph; the closing brace is now bare so subsequent inline content is preserved.
- Inline `<code>` inside a footnote definition now reaches the LaTeX formatter and is rendered as `\texttt{…}` (with `_`/`#`/`%` escaped) instead of being flattened to raw text. The footnote handler now runs the same rich-text preparation that lists and definition lists already use before extracting the body, so a footnote like ``[^note]: La source `EPFL_REORIENT` documente …`` produces `\texttt{EPFL\_REORIENT}` and no longer trips XeLaTeX with `Missing $ inserted` on the unescaped underscore.
- `article` template now loads `morewrites` first thing after `\documentclass`, virtualising `\newwrite` so XeLaTeX's hard 16-register limit no longer truncates documents that combine many glossary groups (each group costs one `\write`) with biblatex, hyperref's outline, the toc, and `listoftables` / `listoffigures`. Before this change a typical front matter with 5+ glossary groups and `listoftables: true` failed at runtime with `! No room for a new \write` at `\listoftables`, and `tectonic` produced a silently truncated PDF that stopped before the back matter (LOT, glossaries, bibliography). `morewrites` is loaded ahead of `xcolor` so subsequent allocations route through its virtual stream pool. Added to `tlmgr_packages` so non-tectonic builds resolve it automatically.
- `ts-fonts` now activates `U+200B` and maps it to `\hskip\z@`, mirroring the existing `U+2135` (Aleph) precedent. `biblatex`'s `blx-unicode.def` workaround for [biblatex#979](https://github.com/plk/biblatex/issues/979) appends a literal ZWSP inside `\textnohyphenation`, which `babel-french` invokes from its redefinition of `\mkbibnamefamily`; rendered through the active small-caps font that lacks a glyph for `U+200B`, this used to spam `Missing character: There is no ​ (U+200B) in font [lmromancaps10-regular.otf]` warnings on every French-language bibliography. `ts-fonts` is loaded before `biblatex` in every bundled template, so `blx-unicode.def` reads the literal ZWSP under the new active catcode and the resulting macro expands to a zero-width breakable skip — preserving the line-break-opportunity semantics the workaround was after, without ever hitting the font.

## [0.3.0] - 2026-04-26

### Added

- Structured `glossary:` front-matter section, validated with pydantic, that complements the existing `*[KEY]: …` body syntax. Each entry carries an explicit `description` and an optional `group`; groups are declared at the top with their displayed title (`groups: {tech: Acronymes techniques, …}`), and entries with no group fall back to the standard acronym table. TeXSmith emits one localised `\printglossary[type=<group>, title=<title>]` per group in declaration order, plus `\printglossary[type=\acronymtype]` for ungrouped entries; the default acronym title now uses `\acronymname` so `babel` localises it (e.g. *Acronymes* under `language: french`). Front-matter entries that never appear in the body are still defined in the glossary; entries that do appear are auto-substituted with `\acrshort{KEY}` whenever the strict, case-sensitive form occurs in the text — `\Gls`/`\GLS` casing helpers are not synthesised (a documented limitation). The `article` template now loads `babel` before `extra_packages` so the `glossaries` package picks up the active language. New `examples/glossary/` tutorial.
- `article` template now supports `listoffigures: true` and `listoftables: true` front-matter flags (mirroring the `book` template), plus a `lists_position: toc | backmatter` knob to choose whether the lists are inserted right after the table of contents (default) or just before the backmatter and references. Each list is wrapped in `\ifnum\value{figure|table}>0` so empty documents don't emit blank pages.
- New `yaml table` Markdown fence describing complex tables in YAML and compiling to `tabular` / `tabularx` / `longtable` on demand. Supports multi-row and multi-column cells (in headers and body), nested grouped headers, width-groups, separators with optional labels, footers, captions via the standard `Table: …` syntax, and explicit width control. Validation errors surface as inline error admonitions with a clear, localised message. New `examples/tables/` tutorial pairs each YAML source with its rendered output and ships a Makefile to build the demo PDF.
- `ts-extra` now auto-loads the `multirow` package when `\multirow` is detected in the rendered LaTeX.
- `Table: <caption> {#label}` now also works on plain Markdown tables: a tree processor pairs the caption paragraph with the following `<table>` and injects a `<caption>` child carrying the caption text (and optional `id` from the `{#label}` part). Both the yaml-table renderer and the legacy table renderer consume `<caption>` directly, so the LaTeX output gets a proper `\caption{…}` with its `\label{…}`.
- 0.5 em of vertical breathing room is inserted between the caption and the table body for both yaml-tables and plain Markdown tables when a caption is present.
- New `yaml table-config` fence applied right below a plain Markdown table sends per-column attributes (`align`, `width`, `width-group`) and table-level settings to the yaml-table renderer. Marks the table with `data-ts-*` attributes so it goes through the same `tabular` / `tabularx` / `longtable` selection as full yaml-tables, including caption handling. A `BlockProcessor` parses the fence into a real ElementTree marker so a sibling tree processor can bind the config to the preceding `<table>`.
- New explicit `width: X` value (case-insensitive, alias for `tabularx`'s `X` column) marks a single column as flexible without needing a `width-group` shim. When any column carries `width: X`, only that column expands; the others keep their natural width.
- `align` accepts long forms (`left`, `right`, `center`, `centre`, `justify`, `justified`) in addition to the short single-letter forms; both are normalised to the short form internally.
- New `{margin}[note]{side?}` inline shorthand for margin notes, joining the unified `{keyword}[content]` family (alongside `{index}[term]` and `{latex}[payload]`). The optional single-letter suffix picks a side — `l` / `i` force the left margin (scoped `\reversemarginpar`), `r` / `o` the right; without a suffix the note follows the document's default (right in `oneside`, outer in `twoside`). Inline markdown inside the note is preserved (bold, italic, code, links). Compiles to `\marginnote{…}` from the LaTeX `marginnote` package, which `ts-extra` now auto-loads on detection. Three defensive layers keep notes within the page on any geometry: (1) `\marginfont` is configured with `\footnotesize`, `\sloppy`, `\emergencystretch=1em` and lowered hyphenation penalties so long words break (or inter-word spacing stretches) rather than overflow; (2) an `\AtBeginDocument` hook clamps `\marginparwidth` to the horizontal space the document's geometry actually reserves on each side (recto and, in twoside, verso), minus `\marginparsep` and a 6 pt safety buffer; (3) a custom `geometry: {left: 3cm, right: 4cm}` yields notes that still fit on both pages with no manual `marginparwidth=…` tweak. New `examples/marginnote/` tutorial with a Makefile; extension documented in `docs/syntax/notes.md`.

### Fixed

- Two long-standing test assertions that had drifted away from the templates they exercise: `test_render_template_applies_markdown_metadata` expected the title/subtitle pair `\\\large` (pre-spacing format) but the article template now emits `\\[0.5em]\large` (commit `c9aff0f`); `test_article_template_supports_columns_option` expected the `twocolumn` documentclass option, but the article template intentionally omits it and uses `\twocolumn[…]` after `\maketitle` so `\@thanks` stays populated and affiliation footnotes survive (`\tsreplayfn` flushes them). Both assertions updated to match the current — correct — template behaviour.
- Python 3.10 compatibility regression: `enum.StrEnum` is 3.11+, but `requires-python = ">=3.10"` and the CI matrix exercises 3.10. The `tables` extension imported `StrEnum` unconditionally, breaking 50 test files at collection time on 3.10 (every test that transitively touches the markdown processor). Added a tiny version-guarded shim that subclasses `str, Enum` on 3.10 and re-exports the stdlib class on 3.11+. Aligned `ruff target-version` with `requires-python` so the version guard isn't flagged as `UP036 outdated version block`.
- Page-number prefix (`n1` / `n2`) leaking into grouped acronym tables produced by the `glossaries` package. Two-part fix: (1) bumped the `pyxindy` dependency to **0.0.7**, which routes every `markup-locref` slot (`:open` / `:close` / `:sep` / `:prefix`) through the existing `_normalize_markup_string` helper so the xindy `~n` newline escape no longer leaks into the rendered output (`_update_locfmt` previously copied the raw strings verbatim); (2) extended TeXSmith's defensive post-processor (`adapters/latex/engines/__init__.py`) to glob `*-gls` in addition to `*.gls` so files emitted by `\newglossary*{<type>}` (e.g. `glossary.technical-gls`) go through `sanitize_glossary_output` for users still on pyxindy <0.0.7.
- Glossary page numbers were not clickable: the `glossaries` package only wraps numbers in `\hyperpage` when `hyperref` is loaded *before* it. Both the `article` and `book` templates now load `babel` and `hyperref` before `\VAR{extra_packages}` (which carries `glossaries` via `ts-glossary`), enabling both babel-localised acronym titles and hyperref-backed clickable backreferences. Same constraint that applies to babel localisation, made explicit in the preamble comments.
- HTML comments (`<!-- ... -->`) are now stripped before rendering and no longer appear verbatim in the LaTeX output. A shared `core.html_utils.strip_html_comments` helper runs in `extract_slot_fragments` (before node serialisation, where `str(comment_node)` would otherwise silently drop the `<!--`/`-->` delimiters and expose the raw comment text) and from the `discard_unwanted` PRE-phase handler.
- `tabularx` column specifications now use `>{\raggedright\arraybackslash}X`, `>{\raggedleft\arraybackslash}X`, and `>{\centering\arraybackslash}X` for all columns (including the first), replacing the previous `l`/`X` split. This prevents text overflow in two-column (`twocolumn`) document layouts and improves line-breaking throughout.
- Inline formatting (bold, italic, etc.) inside quoted text (`"**bold**"`) is now correctly rendered — `AtomicString` was preventing further inline processing inside `<q>` elements.
- Silent exception swallowing in post-render rule/asset collection now emits a diagnostic warning instead of discarding the error.
- Nested list environments (`\begin{itemize}`) no longer appear glued to the parent item text on the same line; a newline is now inserted between the item content and any nested `\begin{itemize|enumerate|description}`.
- Table caption numbering no longer skips numbers (e.g. 1, 2, 4, …). `ts-extra` previously auto-loaded `ltablex` under single-column layouts, which silently transformed every `tabularx` into a `longtable` internally and bumped the `table` counter twice per use. Replaced by a plain `tabularx` load; explicit `longtable` is still detected on demand.

### Changed

- Removed dead code after the early `return` in `_allow_hyphenation` (unreachable lines).
- Dropped the unused `fragments` field from `ExecutionContext`; fragment resolution was already propagated through `template_overrides`.
- Deduplicated `runtime_common.get("code")` lookups in `_render_slot_fragments` into a single local variable.
- Reverted an uncommitted injection of `template_overrides` into the Jinja render context (`runtime_common`) — it had no consumer and exposed internal state unnecessarily.
- **Tables extension refactor (SSOT).** Introduced `texsmith.extensions.tables.constants` housing `TableAttr` (StrEnum for every `data-ts-*` attribute), `PERCENT_RE`, `ALIGN_ALIASES` / `ALIGN_WRAPPERS`, and a `Priority` class that documents each pipeline position. Removed the duplicate `_PERCENT_RE` in `layout.py`. Collapsed the three identical align/width/width-group validator triplets (`LeafColumn`, `ColumnGroup`, `ColumnConfig`) into a shared `_ColumnAttrs` base. Extracted a single `_consume_fence` helper used by both preprocessors and the block processor; the two preprocessors now fail identically (admonition surfaced on parse error) instead of diverging between silent fall-through and block-level error emission. Caption handling no longer wraps the table in a throwaway `<figure>` / `<figcaption>` to be unwrapped by `render_figures`; the `<caption>` is injected directly into the `<table>`. `render_figures` keeps its figure-with-table path (still reached by `pymdownx.blocks.caption`) and routes via a new `is_texsmith_table()` helper instead of literal attribute comparison. The `build_error_element` helper in `tables.html` lets the preprocessor and the block processor share admonition construction.
- **`slot_options` plumbing simplified.** The per-slot render flags (`SlotOptions`) now live only on `Document` and are read directly by the consumer (`extract_slot_fragments`) instead of being copied through `ExecutionContext` and `BinderContext`. The `parse_slot_mapping_with_options` and `extract_front_matter_slots_with_options` wrappers were removed; `parse_slot_mapping` and `extract_front_matter_slots` now always return a `(selectors, options)` tuple.
- **Fragment resolution extracted.** New `core.fragments.resolution` module with a typed `FragmentModifiers(append, prepend, disable)` dataclass, `parse_modifiers(raw)` for front-matter shapes, and a pure `merge_fragments(defaults, override, *, cli_enable, cli_disable)` function. `_resolve_fragments_list` is now a 15-line orchestrator instead of a 65-line dispatcher with three nested helpers.
- **Silent `except Exception` handlers documented or warned.** Font-fallback scans in `conversion/renderer.py` now emit a warning via the diagnostic emitter on failure; user-supplied `should_render` callbacks in `core/fragments` warn with the fragment name instead of silently swallowing the failure; the third-party template-distribution discovery in `templates/loader.py` keeps its broad catch but carries an explicit comment and `# noqa: BLE001` explaining why it is intentional.

### Removed

- **`ExecutionContext` and `BinderContext` merged into `ConversionContext`.** Both classes carried an almost identical set of fields resolved at two successive stages (pre- and post-template-binding); they were collapsed into a single `core.conversion_contexts.ConversionContext` with optional `config` / `template_binding` fields that become populated when `bind_template` runs. Dead fields dropped: `BinderContext.documents: list[Document]` (only ever appended, never read meaningfully), `BinderContext.bound_segments` (written in `_render_document`, never read). The file `core/execution.py` has been deleted.

- **Function renames tracking the new context class:**
  - `resolve_execution_context` → `resolve_conversion_context` (returns the partially-populated `ConversionContext`).
  - `build_binder_context` → `bind_template` (mutates the context in place to populate `config`, `template_binding`, and the template-refined `slot_requests`; same object returned for chaining). Its keyword parameter `execution=` is renamed to `context=`.
  - `ConversionResult.binder_context` field renamed to `ConversionResult.context`.

## [0.2.2] - 2026-02-24

### Added

- CLI/API version reporting (`texsmith --version`, `get_version()`).
- Configurable HTTP user agent for remote asset and emoji fetching (`--http-user-agent` / `TEXSMITH_HTTP_USER_AGENT`).
- TLS download helper with cert guidance for network fetches.
- Cached Wikipedia images for the book example under `.wiki/` with documentation.

### Changed

- Horizontal rules now force a page break in LaTeX output.
- Asset summaries hide temporary build roots and `.converted` cache files.
- Letter example no longer hard-codes the format, allowing snippet overrides; docs include local build steps.
- Book example images now resolve via raw GitHub URLs to avoid third‑party rate limits.
- CI now runs `apt-get update` before installing system dependencies.

### Fixed

- Playwright SVG conversion now handles `viewBox`-only SVGs to prevent blank PDFs.
- Suppressed noisy Tectonic `lineno` UTF‑8 warnings and made console output encoding-safe.
- XeTeX fallback transitions no longer leak across scripts; arrow glyphs prefer Noto Sans Symbols.
- Convert common Unicode symbols to LaTeX math macros to avoid missing glyphs.

## [0.1.0] - 2025-12-20

### Added

- Initial TeXSmith CLI for Markdown to LaTeX/PDF conversion, including multi-document rendering and slot-aware templates.
- Template system with built-in article/book/letter/snippet variants plus user template discovery and overrides.
- Fragment system for typography, geometry, fonts, glossary/index, code blocks, and callouts.
- Extensions for bibliography/citations, acronyms, mermaid/drawio diagrams, and rich admonitions.
- Asset pipeline for fonts/emoji, hashed assets, and download helpers for Tectonic/Biber/PyXindy.
- Documentation and examples (`docs/`, `mkdocs.yml`, `examples/`) plus developer docs and scripts (`README.md`, `DEVEL.md`, `scripts/`).
- Tooling and quality gates (`pyproject.toml`, `noxfile.py`, `Makefile`, tests, CI, coverage, ruff/pyright configs).

### Changed

- Renamed the project to TeXSmith and refactored the codebase into `src/texsmith` with a `texsmith.core` conversion pipeline.
- Reworked bibliography handling, front matter parsing, slot injection, and template rendering orchestration.
- Switched the default LaTeX engine to Tectonic and improved typography, headings, and smart punctuation defaults.

### Fixed

- Numerous issues across templates (article/book), MkDocs output, snippets, CI, and Windows compatibility.

### History

- 2025-10-18: Seeded the initial LaTeX renderer and template set under `latex/`.
- 2025-10-19: Added performance instrumentation and the first `docs/` content.
- 2025-10-20: CLI accepted Markdown input, introduced article templates, examples, and a `build` command.
- 2025-10-21: Added render phases, acronyms/index extensions, and renamed the project to TeXSmith.
- 2025-10-23: Added the EPFL thesis template, `LICENSE.md`, `DEVEL.md`, and multi-document CLI support.
- 2025-10-27: Major refactor around `texsmith.core`, `ConversionService`, diagnostics, and regression tests.
- 2025-11-16: Added the mermaid extension and expanded built-in templates and docs.
- 2025-11-24: Introduced rule/slot validation diagnostics and refined fragment registry behavior.
- 2025-11-27: Made Tectonic the default engine and added download helpers/Makefiles for fonts and tools.
- 2025-12-01: Added diagram generation via Playwright and `ts-typesetting` fragments.
- 2025-12-11: Polished CLI options, snippets, and CI pipelines leading up to the release tag.
