Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,9 @@ markdown-it-py) come from PyPI.
- The distribution is named `legaldown-render`; the import name is `legaldown_render`
(`pip install legaldown-render` → `import legaldown_render`).
- A release of `legaldown-validator` with a new minor version needs a renderer release too: the
dependency is pinned to one minor version (`pyproject.toml`).
dependency is pinned to one minor version (`pyproject.toml`). Publish it once that validator
release is on PyPI, since until then nothing can satisfy the range (the `validator-main` CI job
overrides the installed validator with `main`, so it tests main regardless of the range). The
tests fail on any `legaldown.LegaldownDeprecationWarning`, so a release never calls what the
validator's next minor version removes.
- The package ships a `py.typed` marker, so type checkers use its annotations directly.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,7 @@ jobs:
python-version: "3.12"
cache: pip
- run: pip install -e ".[dev]"
# Force-reinstalled last, with --no-deps, so main is tested even when its
# version is outside the renderer's range.
- run: pip install --force-reinstall --no-deps "legaldown-validator @ git+https://github.com/ForLegalAI/legaldown-validator@main"
- run: pytest -q
6 changes: 3 additions & 3 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Conformance

`legaldown-render` 0.2.0 targets **Level 2 — Rendering** of the LegalDown specification **0.2**
(§17.3). Core parsing and validation come from `legaldown-validator` 0.3.0, which claims Level 1
— Core. Its own [CONFORMANCE.md](https://github.com/ForLegalAI/legaldown-validator/blob/main/CONFORMANCE.md)
`legaldown-render` targets **Level 2 — Rendering** of the LegalDown specification **0.2**
(§17.3), as 0.2.0 did. Core parsing and validation come from `legaldown-validator` 0.4 (0.3.0 up
to `legaldown-render` 0.2.0), which claims Level 1 — Core. Its own [CONFORMANCE.md](https://github.com/ForLegalAI/legaldown-validator/blob/main/CONFORMANCE.md)
lists the Core rules it covers.

It is checked against the specification's own examples and fixtures corpus (`tests/conformance/`):
Expand Down
52 changes: 28 additions & 24 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ This page describes how the renderer is built. The decisions behind it are in
│ normalize_source(): strip BOM, unify line endings
▼
┌─────────────────────────┐
│ 2. Assemble (answers) │ only with an answers set: legaldown-validator's assemble() turns
└───────────┬─────────────┘ the template into its assembled document, which the rest renders
│ 2. Assemble (answers) │ only with an answers set: legaldown-validator's Template and Form
└───────────┬─────────────┘ turn the template into its assembled document, which the rest renders
▼
┌─────────────────────────┐
│ 1. Parse & validate │ legaldown-validator: Document, ValidationResult (Core diagnostics,
└───────────┬─────────────┘ resolved identifiers, party/side/definition/attachment lookups)
│ 1. Parse & validate │ legaldown-validator: Document, ValidationResult (Core diagnostics;
└───────────┬─────────────┘ its index: identifiers, markers, party/side/definition/attachment lookups)
▼
┌─────────────────────────┐
│ 3. Build build.py │ source + validator → render tree (source inlines)
Expand All @@ -38,8 +38,8 @@ This page describes how the renderer is built. The decisions behind it are in
Stage 2, **assembly** with an answers set (§15.7), runs only when the job has answers, before
the document is parsed for rendering. A template rendered with answers is assembled first and the
assembled document rendered; without answers, it renders as its **template view** (§15.8).
Assembly is the core package's (`legaldown.assemble`), exact to the byte as §15.7.2 requires; the
renderer only decides when to refuse:
Assembly is the core package's (`legaldown.parse_template(source).form(answers).assemble()`),
exact to the byte as §15.7.2 requires; the renderer only decides when to refuse:

- the template has Errors: §15.7.2 defines assembly only for a template without them
- the answers do (`answer-invalid`, `answer-missing`)
Expand All @@ -55,11 +55,11 @@ semantic settings, and only stage 5 knows about file formats.

### 1. Parse and validate

`legaldown.parse_document` and `legaldown.validate_document` provide:
`legaldown.parse` and `legaldown.validate` provide:

- the **metadata**: title, sides and parties, attachments, questions, language
- the **section identifiers** exactly as the validator resolved them: explicit `{#id}`, or
generated by §5.3 with §5.5 collision handling. These come from `ValidationResult.sections`,
generated by §5.3 with §5.5 collision handling. These come from `result.index.sections`,
so the renderer can never compute a different anchor than the one the validator checked
- **lookups** for definitions, parties, sides, and attachments, already reduced to display names
(§3.6)
Expand All @@ -76,16 +76,17 @@ never decides a structural or LegalDown question itself:

| Question | Answered by |
|---|---|
| Sections, headings, identifiers | `Document.sections`, `ValidationResult.sections` |
| Blocks: paragraphs, lists and items, quotes, tables, code, rules | `Document` blocks. List items hold blocks (nested lists, code, quotes, tables), and a quote's content is the validator's `quote_content()` |
| Where a marker (`{#id when=…}`) is placed, and what it means | `ValidationResult.placed_markers`: each marker's block, field, offset, identifier, and condition |
| Whether the document is a template | `ValidationResult.is_template` |
| Whether a quote is a drafting note | The validator's `is_drafting_note()` |
| Sections, headings, identifiers | `Document.sections`, `result.index.sections` |
| Blocks: paragraphs, lists and items, quotes, tables, code, rules | `Document` blocks. List items hold blocks (nested lists, code, quotes, tables), a quote's content is the validator's `quote_blocks()`, and a code block's is `code_content()` |
| Where a marker (`{#id when=…}`) is placed, and what it means | `result.index.placed_markers`: each marker's block, field, offset, identifier, and condition |
| Whether the document is a template | `result.index.is_template` |
| Whether a quote is a drafting note, and its content | The validator's `is_drafting_note()` and `drafting_note_blocks()` |
| Which list item a marker belongs to | `PlacedMarker.item`: the item's number among the list's items, in pre-order |
| A lifted definition, `{{ref:}}` or `{{term:}}` block's source | The validator's `render_block()` |

The few imports beyond the validator's public API (quote content, fence helpers, the
answers-file reader) are all in `validator_bridge.py`, the list for roadmap item U3.
These are all the validator's public API: the model and the result from `legaldown`, the
reading of source text from `legaldown.syntax`, and the language's constants and rules from
`legaldown.grammar`.

Within one block's text, **markdown-it-py parses inline Markdown only**: emphasis, links, code
spans, inline HTML. Directives are protected first by **sentinels**. Every directive, and every
Expand Down Expand Up @@ -116,7 +117,7 @@ Guessing at lost structure was tried, and it traded each gap for new bugs.

1. **Survey.** It collects which definitions exist, whether template constructs are used, and
which placeholder ids are used with conflicting types (§10.7).
2. **Structure.** Section numbers are the validator's own (`ValidationResult.sections`), so a
2. **Structure.** Section numbers are the validator's own (`result.index.sections`), so a
rendered number always matches the validator's: **alternatives** share a number (§15.8), and
a skipped heading level counts as 1. The style only formats them, the n-th part of a number
with the n-th level format. For paragraphs and list items, **alternatives** follow the
Expand Down Expand Up @@ -189,7 +190,7 @@ src/legaldown_render/
├── style/
│ ├── model.py the style dataclasses (every field has a default)
│ ├── loader.py layering, extends, overrides, validation
│ ├── labels.py built-in labels per language (en, cs)
│ ├── labels.py built-in labels per language (en, cs, de, fr, pl, sk)
│ └── builtin/ default.yaml, continental.yaml, outline.yaml
└── writers/ html.py, text.py
```
Expand All @@ -198,18 +199,20 @@ src/legaldown_render/

| Dependency | Why |
|---|---|
| `legaldown-validator>=0.3.0,<0.4` | The only LegalDown parser; Core validation |
| `legaldown-validator>=0.4.0,<0.5` | The only LegalDown parser; Core validation |
| `markdown-it-py` | Inline Markdown inside one block's text (ADR 0007) |
| `babel` | CLDR locale data (ADR 0005) |
| `pyyaml` | Style templates |

All of them are pure Python. The DOCX and PDF writers will bring their dependencies in as extras.

The renderer uses the validator's public API (`legaldown`, `legaldown.validator`), except for
the few names in `validator_bridge.py`: `quote_content` and `MAX_QUOTE_DEPTH`, the fence
helpers, and the answers-file reader. The pin to one minor version exists because of them.
Asking the validator to export them publicly is roadmap item U3
([validator#93](https://github.com/ForLegalAI/legaldown-validator/issues/93)).
The renderer uses only the validator's public API: `legaldown`, and the tooling modules
`legaldown.syntax` and `legaldown.grammar` (roadmap item U3, done in validator 0.4.0). Before 1.0
a minor release of the validator may change that API, so the dependency is pinned to one minor
version, and the tests fail on any of its deprecation warnings, since the next minor release
removes what is deprecated. The validator raises every one of them as a
`legaldown.LegaldownDeprecationWarning`, the one category `filterwarnings` in `pyproject.toml`
turns into an error.

## Errors and diagnostics

Expand All @@ -223,7 +226,8 @@ Diagnostics reuse `legaldown.Diagnostic`. The renderer adds rules only it can ev
`ref-not-enumerated`, a specification rule id, plus renderer-specific ids prefixed `render-`:
`render-not-processed`, `render-locale-fallback`, and `render-ref-ambiguous`. The validator's
diagnostics carry their line (§16.9), and the CLI prints it as `file:line:`. The renderer's own
have none yet, because a block's line is not public in the validator (U2).
have none yet: they are about a directive within a block, and the validator makes only a block's
line public (`Document.line_of`, U2).

## Security

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,5 @@ style template.
| **Numbering scope** | A region that is numbered independently: the main body, or each attachment when the style template restarts numbering (§13.8) |
| **Anchor** | A link target in the output. Section, item, and paragraph anchors share one namespace in the source (§5.6). The renderer maps them, together with definition anchors, into the output's single anchor space without collisions, by prefixing definition anchors with `def:`, a character no identifier can contain |
| **Failure marker** | The visible bracketed text the specification requires in place of something that did not resolve, for example `[BROKEN REF: id]` or `[INVALID DATE: value]` |
| **Diagnostic** | A finding with a stable rule id, a severity, and a message (source lines will follow once the validator provides them). It uses the same type and rule ids as the validator, plus the rules only a renderer can evaluate, such as `ref-not-enumerated` |
| **Diagnostic** | A finding with a stable rule id, a severity, and a message. The validator's carry their source line; the renderer's own do not yet. It uses the same type and rule ids as the validator, plus the rules only a renderer can evaluate, such as `ref-not-enumerated` |
| **Template view** | How a template is rendered without answers: conditional units marked, every `{{choose:}}` phrase shown, drafting notes styled distinctly (§15.8) |
3 changes: 3 additions & 0 deletions docs/decisions/0002-one-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ cannot carry everything a renderer needs:
- The renderer depends on markdown-it-py.
- The outline check turns any disagreement between the two parsers into a loud bug report.
- Some renderer features wait for upstream changes. That is deliberate.
- 2026-10-03: since validator 0.4.0, the directive grammar and the other readings of source
text come from its supported tooling modules, `legaldown.syntax` and `legaldown.grammar`.
`legaldown.directives` is internal there.
6 changes: 6 additions & 0 deletions docs/decisions/0007-one-parser-validator-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ module, `validator_bridge.py`. That module is the list for roadmap item U3.
the builder turns its listed items and their depths into nested lists, and "2.1(b)(i)"
replaced "2.1(c)".
- The build stage is smaller: about 100 lines of position rules and special cases are gone.
- 2026-10-03: the bridge is gone since validator 0.4.0, which made all of it public (roadmap
U3). The result's decisions moved to `result.index` (`placed_markers`, `is_template`,
`sections`). A quote's content comes from `legaldown.syntax.quote_blocks`, and a drafting
note's from `drafting_note_blocks`, which replaced the builder's own removal of the
`[!DRAFTING]` marker (validator #88). A code block's content comes from `code_content`, and an
answers file is read by `legaldown.load_answers`. `validator_bridge.py` was deleted.
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ here.
| # | Where | Change | Status |
|---|---|---|---|
| U1 | validator | **Keep nested list structure** in the model ([#14](https://github.com/ForLegalAI/legaldown-validator/issues/14)) | Done in [validator#61](https://github.com/ForLegalAI/legaldown-validator/pull/61); the renderer nests lists from it (v0.2) |
| U2 | validator | **Source positions** (line numbers) on sections, blocks, and diagnostics ([#27](https://github.com/ForLegalAI/legaldown-validator/issues/27)) | Done for the validator's diagnostics in 0.3.0 ([validator#85](https://github.com/ForLegalAI/legaldown-validator/pull/85)); the CLI prints them. Still open: a block's line, for the renderer's own diagnostics ([#93](https://github.com/ForLegalAI/legaldown-validator/issues/93)) |
| U3 | validator | **Public API** ([#26](https://github.com/ForLegalAI/legaldown-validator/issues/26)) for what the renderer imports from submodules, and the validator's own **placed markers** and **template decision** | Mostly done in 0.3.0 ([validator#90](https://github.com/ForLegalAI/legaldown-validator/pull/90)): the renderer reads `placed_markers` and `is_template` from the result. Left in `validator_bridge.py`: `quote_content`, the fence helpers, the answers-file reader ([#93](https://github.com/ForLegalAI/legaldown-validator/issues/93)), and a drafting note's content ([#88](https://github.com/ForLegalAI/legaldown-validator/issues/88)). Until then, the dependency is pinned to one minor version |
| U2 | validator | **Source positions** (line numbers) on sections, blocks, and diagnostics ([#27](https://github.com/ForLegalAI/legaldown-validator/issues/27)) | Done for the validator's diagnostics in 0.3.0 ([validator#85](https://github.com/ForLegalAI/legaldown-validator/pull/85)); the CLI prints them. 0.4.0 makes a block's and a list item's line public (`Document.line_of`, `Document.layout()`). Still open: the line of a directive within a block, which the renderer's own diagnostics are about ([#93](https://github.com/ForLegalAI/legaldown-validator/issues/93)) |
| U3 | validator | **Public API** ([#26](https://github.com/ForLegalAI/legaldown-validator/issues/26)) for what the renderer imports from submodules, and the validator's own **placed markers** and **template decision** | Done. Mostly in 0.3.0 ([validator#90](https://github.com/ForLegalAI/legaldown-validator/pull/90)): `placed_markers` and `is_template` on the result. The rest in 0.4.0: quote and drafting-note content, code content, and the answers-file reader ([#93](https://github.com/ForLegalAI/legaldown-validator/issues/93), [#88](https://github.com/ForLegalAI/legaldown-validator/issues/88)), in the tooling modules `legaldown.syntax` and `legaldown.grammar`. The renderer adopted it and dropped `validator_bridge.py`. The dependency stays pinned to one minor version, since before 1.0 a minor release may change the API |
| U1b | validator | **Parser gaps** the renderer inherits. Fixed in validator `main`: list markers (#21), tables (#22, #44), comments and HTML blocks (#23, #59), the signature-block cutoff (#24), empty comments (#28), indented code (#9, #41), empty list items (#46), content after a nested list (#64), nested marker changes (#65), headings in quotes and items (#78), raw-html (#40). hard breaks (#25). Still open: link reference definitions ([#92](https://github.com/ForLegalAI/legaldown-validator/issues/92)) | Mostly done; adopted with 0.3.0. Listed in `CONFORMANCE.md` |
| U4a | validator | **Specification 0.2** (templates, §15) | Done in 0.2.0 |
| U4b | validator | Template **assembly** with an answers set (§15.7, [#30](https://github.com/ForLegalAI/legaldown-validator/issues/30)) | Done in [validator#55](https://github.com/ForLegalAI/legaldown-validator/pull/55); the renderer uses it (v0.2) |
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ classifiers = [
dependencies = [
# The only LegalDown parser (docs/decisions/0002). A minor bump may change
# the model, so the range is pinned to one minor version.
"legaldown-validator>=0.3.0,<0.4",
"legaldown-validator>=0.4.0,<0.5",
# Inline Markdown (emphasis, links, code spans) within the validator's
# blocks, parsed in inline mode only (docs/decisions/0007).
"markdown-it-py>=3.0,<5",
Expand Down Expand Up @@ -76,3 +76,9 @@ pythonpath = ["src"]
markers = [
"conformance: renders the LegalDown specification's examples and fixtures (needs a checkout of the specification)",
]
# A deprecated legaldown-validator API fails the tests: the next minor
# release removes it, and the dependency range admits only one minor version.
# Every deprecation the validator raises is of this one category.
filterwarnings = [
"error::legaldown.LegaldownDeprecationWarning",
]
Loading
Loading