Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5ac6eeb
Add design docs: concepts, architecture, style templates, roadmap, de…
claude Sep 23, 2026
08a217d
Build the renderer: HTML and text output for LegalDown 0.2
claude Sep 23, 2026
d7936d1
Fix code review findings
claude Sep 23, 2026
f75b5e8
Fix second code review findings
claude Sep 23, 2026
102947c
Document nonce sentinels and alternative numbering; use escapes in tests
claude Sep 23, 2026
aafdfc5
Fix third code review findings
claude Sep 23, 2026
ebbe04e
Fix fourth code review findings
claude Sep 23, 2026
afa1d5e
Use an escape for U+2E31 in a test string
claude Sep 23, 2026
4f9ad76
Fix fifth code review findings
claude Sep 23, 2026
9a82927
Build from the validator's document model: one parser
claude Sep 23, 2026
1361bff
Fix seventh code review findings; link validator issues
claude Sep 24, 2026
81a532d
Stop guessing at structure the validator's model lost
claude Sep 24, 2026
da818fb
Fix ninth code review findings: comments across blocks, raw HTML count
claude Sep 24, 2026
e4692f4
Fix tenth code review findings in comment stripping
claude Sep 24, 2026
03dd9ea
Add the final check (§15.9): --final and RenderOptions.final
claude Sep 24, 2026
8df1b9b
Add a table of contents as a style setting
claude Sep 24, 2026
07636be
Add built-in labels for German, French, Polish, and Slovak
claude Sep 24, 2026
ef7d831
Write no-break spaces in tests as escapes
claude Sep 24, 2026
04589fa
Fix code review findings on the contents, labels, and final check
claude Sep 24, 2026
e43abf3
Fix second review findings on the contents and labels
claude Sep 24, 2026
ce48304
Fix third review findings on anchors, contents, and labels
claude Sep 24, 2026
3834f3f
README: install from GitHub until the first release
claude Sep 24, 2026
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
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install ruff
- run: ruff check .

test:
name: Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install -e ".[dev]"
- run: pytest -q

conformance:
name: Specification examples and fixtures
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check out the LegalDown specification
uses: actions/checkout@v4
with:
repository: ForLegalAI/LegalDown
ref: v0.2
path: .legaldown-spec
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -e ".[dev]"
- run: pytest -q -m conformance
env:
LEGALDOWN_SPEC_DIR: .legaldown-spec

validator-main:
# Surfaces changes in legaldown-validator's model before they are released
# (docs/decisions/0001). Informational: a failure here is a heads-up.
name: Against legaldown-validator main
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -e ".[dev]"
- run: pip install --force-reinstall --no-deps "legaldown-validator @ git+https://github.com/ForLegalAI/legaldown-validator@main"
- run: pytest -q
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
build/
dist/
.pytest_cache/
.ruff_cache/
87 changes: 87 additions & 0 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Conformance

`legaldown-render` 0.1.0 targets **Level 2 — Rendering** of the LegalDown specification **0.2**
(§17.3). Core parsing and validation come from `legaldown-validator` 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/`):

- every document renders to HTML and to text
- every internal link resolves to an anchor that exists
- each invalid fixture renders its rule's failure marker

## Rendering requirements (§17.3)

| Requirement | Status |
|---|---|
| Section numbering generated at render time, configurable per job (§13.1) | ✅ `decimal`, `legal-outline`, `mixed`, `none`, and custom level formats; per job with `--set numbering.scheme=…` |
| Resolution of every §11.1 directive except `{{include:}}`, with every failure marker (§6.3, §7.3, §13.3–§13.5, §15.8) | ✅ |
| `{{include:}}` below Full (§17.5) | ✅ Renders `[NOT PROCESSED: include path]` with a Warning (`render-not-processed`) |
| Party and side display rules (§3.6) | ✅ |
| At least one output format (§13.6) | ✅ HTML (recommended) and plain text (optional) |
| Comment stripping (§8.6) | ✅ Including a comment across blank lines, up to the end of its section (see below) |
| Template view without answers (§15.8) | ✅ Conditions marked with style-defined labels; every `{{choose:}}` phrase shown as `[a / b]`; drafting notes styled; alternatives share a number |
| `{{attach:}}` resolves to the declared title without reading the file | ✅ Linked to an attachment placeholder, or to the file when placeholders are omitted |

## SHOULD and MAY features

| Feature | Status |
|---|---|
| List enumeration (§13.2) | ✅ Configurable per depth; can be disabled. Ordered lists are always renumbered. Lists have one level until the validator keeps nesting (see below) |
| Paragraph numbering and section-qualified items (§13.2) | ✅ Style settings; the `continental` style uses both |
| Style templates in a separate file (§13.7) | ✅ YAML, layered and validated; see [docs/style-templates.md](docs/style-templates.md) |
| Signature blocks (§2.2) | ✅ One per party, with `legal_name` and each representative |
| Labels following the document language (§13.7) | ✅ Built in for `en`, `cs`, `de`, `fr`, `pl`, and `sk`; any label can be set by a style |
| `ref-not-enumerated` Warning (§6.3, §16.3) | ✅ |
| `raw-html` Warning; raw HTML never emitted (§8.7) | ✅ No tag is ever emitted. Text between an HTML block's tags renders as text until the validator's model recognises HTML blocks (see below) |
| Automatic term recognition (§7.4, MAY) | ❌ Not supported |
| Extended tables through raw HTML (§9.2, MAY) | ❌ Not supported; the tags are dropped with the Warning, and the cell text renders as paragraph text |
| Question prompts next to blanks (§15.8, MAY) | ✅ `placeholders.show_prompt` |
| Final check (§15.9, SHOULD) | ✅ `--final` / `final=True`: the validator's `placeholder-unfilled` and `template-construct-present` Errors; with `--strict`, the document is refused |

## Known limitations

- **Templates with answers.** Assembly (§15.7) is a Core capability that `legaldown-validator`
0.2.0 does not provide yet, so a template always renders as its template view.
- **Diagnostics have no line numbers**, because the validator's diagnostics do not carry them yet.
- **Malformed directives** (a §11.2 grammar violation) render their type's marker without a value,
for example `[INVALID AMOUNT]`. The specification defines no marker for this case.
- **One parser.** The renderer builds from the validator's document model only
([ADR 0007](docs/decisions/0007-one-parser-validator-model.md)). Anchors, markers, and the
template view therefore always agree with the validator's diagnostics. The price is that some
structure renders as the validator's model holds it, until that model keeps more
([legaldown-validator#14](https://github.com/ForLegalAI/legaldown-validator/issues/14),
roadmap U1–U2):
- **Nested lists** render as one level of items, so an item is referenced as "2.1(c)", never
"2.1(b)(i)" ([#14](https://github.com/ForLegalAI/legaldown-validator/issues/14), [#16](https://github.com/ForLegalAI/legaldown-validator/issues/16)). The resolver and writers already handle nesting.
- **`*` and `+` bullets and `1)` ordered lists** render as one paragraph ([#21](https://github.com/ForLegalAI/legaldown-validator/issues/21)).
- **Tables:** column alignment is not kept, and an escaped pipe (`\|`) or a pipe inside a
code span splits a cell ([#22](https://github.com/ForLegalAI/legaldown-validator/issues/22)). Rows are padded or trimmed to the header's width.
- **Line breaks** within a paragraph are joined, and a backslash hard break shows as a literal
backslash. Link reference definitions (`[label]: url`) are not resolved
([#25](https://github.com/ForLegalAI/legaldown-validator/issues/25)).
- **A heading inside a multi-line HTML comment** is a section: the comment ends at the heading,
and its closing `-->` shows as text. In an **HTML block**, every tag is dropped and reported (`raw-html`), but the
text between the tags renders as paragraph text ([#23](https://github.com/ForLegalAI/legaldown-validator/issues/23)).
- **A comment across blank lines** is stripped up to its `-->` (the one place the renderer
reads past the model), but the model still holds its content as blocks. So a `{#id}` inside
it is still a valid target for the validator, and a reference to it renders
`[BROKEN REF: id]` with no Error. When the comment ends inside a quote, code block, or table,
the rest of that block renders as plain paragraph text ([#23](https://github.com/ForLegalAI/legaldown-validator/issues/23)).
- **After an empty comment** (`<!-->`, `<!--->`), a directive before a later `-->` on the same
line is shown as its source text, unresolved and unchecked ([#28](https://github.com/ForLegalAI/legaldown-validator/issues/28)).
- **Everything after a `# Signature Block {#signature-block}` heading** is dropped
([#24](https://github.com/ForLegalAI/legaldown-validator/issues/24)).
- **Indented code blocks** are read as paragraphs ([#9](https://github.com/ForLegalAI/legaldown-validator/issues/9)).

## Beyond this level (Full, §17.4)

These are not processed, and each renders visibly rather than being dropped:

- includes (§12)
- attachment contents and per-attachment numbering scopes (§13.8)
- cross-scope reference qualification (§13.3)
- amendment processing (§7.5)
- bilingual sets (§14)
- file existence checks
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<div align="center">

# legaldown-render 🖨️

### The reference renderer for [LegalDown](https://github.com/ForLegalAI/LegalDown)

**Turn a LegalDown document into HTML or plain text, with section numbers, cross-references,
defined terms, and formatted values all generated at render time.**

Targets specification **v0.2** · Conformance level **Rendering** · DOCX and PDF planned

</div>

---

## What it does

[LegalDown](https://github.com/ForLegalAI/LegalDown) documents never hardcode section numbers,
reference text, or formatted values. The source says `{{ref: late-payment}}`, and the renderer
turns it into "4.2(b)" with a link. This package is that renderer:

🔢 **Numbers** sections, list items, and paragraphs under a configurable scheme (§13.1, §13.2).
References to items resolve to designations such as "2.1(b)".

🔗 **Resolves** cross-references, defined terms, parties, sides, and attachment references into
display text with links (§13.3–§13.5).

🌍 **Formats** dates, money, and durations for a locale chosen at render time. A Czech locale
gives "1. června 2026", "10 000,00 Kč", and "12 měsíců", and amounts are never rounded (§10).

🎨 **Applies a style template**, so the same source renders as a US-style contract or a
continental one without editing the text (§13.7).

📝 **Shows templates as templates.** Conditional clauses are marked, every `{{choose:}}` phrase is
shown, drafting notes are styled, and placeholders render as blanks (§15.8).

🚩 **Never hides a problem.** Anything unresolved renders as a visible marker, such as
`[BROKEN REF: id]`, and is reported with the specification's stable rule id. Nothing is dropped
silently (§17.5).

Parsing and Core validation come from
[`legaldown-validator`](https://github.com/ForLegalAI/legaldown-validator), the reference parser.
This package never reimplements the LegalDown grammar.

## Install

`legaldown-render` is not on PyPI yet. Until its first release, install it from GitHub:

```bash
pip install "legaldown-render @ git+https://github.com/ForLegalAI/legaldown-render"
```

Python 3.11 or newer. The dependencies are all pure Python: `legaldown-validator`,
`markdown-it-py`, `babel`, and `pyyaml`.

## Command line

```bash
legaldown-render contract.lgd -o contract.html # HTML page
legaldown-render contract.lgd -o contract.txt # plain text (format from extension)
legaldown-render contract.lgd --style continental --locale cs-CZ -o smlouva.html
legaldown-render contract.lgd --set numbering.scheme=legal-outline --set enumeration.enabled=false
legaldown-render contract.lgd --set contents.enabled=true --set contents.depth=3 # table of contents
legaldown-render contract.lgd --strict # refuse if the document has errors
legaldown-render contract.lgd --final --strict # refuse if blanks or template constructs remain
legaldown-render --print-style --style continental # every setting, as YAML
```

Diagnostics go to stderr, one per line with its rule id. The exit code is 0 when the document
rendered, even if it has errors (they show as markers), 1 when rendering was refused or the file
cannot be read, and 2 for an invalid style or setting.

## Python

```python
from legaldown_render import render

result = render(open("contract.lgd").read(), format="html", style="continental", locale="cs-CZ")
for diagnostic in result.diagnostics:
print(diagnostic.level, diagnostic.rule, diagnostic.message)
open("contract.html", "w").write(result.output)
```

`render()` also accepts a `RenderOptions`. Problems in the document never raise. Only an invalid
style (`StyleError`), an unreadable document (`DocumentError`), or strict mode
(`RenderRefused`) do.

## Styles and preferences

A **style template** says how documents look: numbering, list enumeration, locale, labels,
typography. It is a YAML file you can share across a team:

```yaml
version: 1
extends: continental
locale: cs-CZ
numbering:
levels:
- { counter: decimal, label: "Článek {n}", ref: "{n}" }
definitions: { style: small-caps }
typography: { font_family: "Georgia, serif", justify: true }
```

**Render options** say what one job does: the output format, strict mode, the final check, a full
page or an HTML fragment. Values layer in this order: built-in defaults, then the `extends` chain, then your
style, then per-job `--set` overrides. Every value is validated before rendering starts. See
[Styles and preferences](docs/style-templates.md).

Built-in styles are `default` (1. / 1.1, with (a) / (i) list items), `continental` (numbered
articles, 2.1-style items, numbered paragraphs), and `outline` (I. / A. / 1. / a.).

## Documentation

| Document | What it covers |
|---|---|
| [Concepts](docs/concepts.md) | What a renderer is responsible for, principles, and vocabulary |
| [Architecture](docs/architecture.md) | The pipeline, the render tree, the package layout, and testing |
| [Styles and preferences](docs/style-templates.md) | The style format, layering, and every setting |
| [Conformance](CONFORMANCE.md) | What the Rendering level requires, and what is covered |
| [Roadmap](docs/roadmap.md) | Milestones, and the changes needed upstream |
| [Decision records](docs/decisions/) | Why the main choices were made |

## Development

```bash
pip install -e ".[dev]"
pytest # unit, golden, and CLI tests
pytest --update-golden # regenerate tests/golden/ after an intended output change
LEGALDOWN_SPEC_DIR=../LegalDown pytest -m conformance # the specification's examples and fixtures
ruff check .
```

## License

[MIT](LICENSE)
Loading
Loading