legaldown-render 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. legaldown_render-0.2.0/.gitignore +8 -0
  2. legaldown_render-0.2.0/LICENSE +21 -0
  3. legaldown_render-0.2.0/PKG-INFO +175 -0
  4. legaldown_render-0.2.0/README.md +139 -0
  5. legaldown_render-0.2.0/docs/architecture.md +247 -0
  6. legaldown_render-0.2.0/docs/concepts.md +107 -0
  7. legaldown_render-0.2.0/docs/decisions/0001-separate-package.md +32 -0
  8. legaldown_render-0.2.0/docs/decisions/0002-one-parser.md +51 -0
  9. legaldown_render-0.2.0/docs/decisions/0003-resolved-document.md +33 -0
  10. legaldown_render-0.2.0/docs/decisions/0004-html-first.md +40 -0
  11. legaldown_render-0.2.0/docs/decisions/0005-locale-formatting.md +39 -0
  12. legaldown_render-0.2.0/docs/decisions/0006-styles-and-options.md +41 -0
  13. legaldown_render-0.2.0/docs/decisions/0007-one-parser-validator-model.md +66 -0
  14. legaldown_render-0.2.0/docs/decisions/README.md +27 -0
  15. legaldown_render-0.2.0/docs/roadmap.md +68 -0
  16. legaldown_render-0.2.0/docs/style-templates.md +168 -0
  17. legaldown_render-0.2.0/pyproject.toml +78 -0
  18. legaldown_render-0.2.0/src/legaldown_render/__init__.py +49 -0
  19. legaldown_render-0.2.0/src/legaldown_render/api.py +187 -0
  20. legaldown_render-0.2.0/src/legaldown_render/build.py +609 -0
  21. legaldown_render-0.2.0/src/legaldown_render/cli.py +137 -0
  22. legaldown_render-0.2.0/src/legaldown_render/errors.py +32 -0
  23. legaldown_render-0.2.0/src/legaldown_render/py.typed +0 -0
  24. legaldown_render-0.2.0/src/legaldown_render/resolve/__init__.py +5 -0
  25. legaldown_render-0.2.0/src/legaldown_render/resolve/numbering.py +90 -0
  26. legaldown_render-0.2.0/src/legaldown_render/resolve/resolver.py +904 -0
  27. legaldown_render-0.2.0/src/legaldown_render/resolve/values.py +87 -0
  28. legaldown_render-0.2.0/src/legaldown_render/style/__init__.py +54 -0
  29. legaldown_render-0.2.0/src/legaldown_render/style/builtin/continental.yaml +24 -0
  30. legaldown_render-0.2.0/src/legaldown_render/style/builtin/default.yaml +7 -0
  31. legaldown_render-0.2.0/src/legaldown_render/style/builtin/outline.yaml +7 -0
  32. legaldown_render-0.2.0/src/legaldown_render/style/labels.py +141 -0
  33. legaldown_render-0.2.0/src/legaldown_render/style/loader.py +360 -0
  34. legaldown_render-0.2.0/src/legaldown_render/style/model.py +285 -0
  35. legaldown_render-0.2.0/src/legaldown_render/tree.py +511 -0
  36. legaldown_render-0.2.0/src/legaldown_render/validator_bridge.py +28 -0
  37. legaldown_render-0.2.0/src/legaldown_render/writers/__init__.py +21 -0
  38. legaldown_render-0.2.0/src/legaldown_render/writers/html.py +472 -0
  39. legaldown_render-0.2.0/src/legaldown_render/writers/text.py +196 -0
  40. legaldown_render-0.2.0/tests/conformance/__init__.py +0 -0
  41. legaldown_render-0.2.0/tests/conformance/test_spec_corpus.py +116 -0
  42. legaldown_render-0.2.0/tests/conftest.py +30 -0
  43. legaldown_render-0.2.0/tests/documents/features.lgd +101 -0
  44. legaldown_render-0.2.0/tests/documents/template.lgd +59 -0
  45. legaldown_render-0.2.0/tests/golden/features.continental.txt +96 -0
  46. legaldown_render-0.2.0/tests/golden/features.default.html +207 -0
  47. legaldown_render-0.2.0/tests/golden/features.default.txt +96 -0
  48. legaldown_render-0.2.0/tests/golden/features.outline.txt +96 -0
  49. legaldown_render-0.2.0/tests/golden/template.default.html +147 -0
  50. legaldown_render-0.2.0/tests/golden/template.default.txt +57 -0
  51. legaldown_render-0.2.0/tests/test_cli.py +92 -0
  52. legaldown_render-0.2.0/tests/test_golden.py +31 -0
  53. legaldown_render-0.2.0/tests/test_render.py +1155 -0
  54. legaldown_render-0.2.0/tests/test_style.py +116 -0
  55. legaldown_render-0.2.0/tests/test_values.py +54 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ForLegalAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.5
2
+ Name: legaldown-render
3
+ Version: 0.2.0
4
+ Summary: Reference renderer for the LegalDown legal document format: HTML and plain text, DOCX and PDF planned
5
+ Project-URL: Homepage, https://github.com/ForLegalAI/legaldown-render
6
+ Project-URL: Repository, https://github.com/ForLegalAI/legaldown-render
7
+ Project-URL: Issues, https://github.com/ForLegalAI/legaldown-render/issues
8
+ Project-URL: Specification, https://github.com/ForLegalAI/LegalDown
9
+ Author: ForLegalAI s.r.o.
10
+ Maintainer: ForLegalAI s.r.o.
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: contracts,html,legal,legaldown,legaltech,markdown,renderer
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Legal Industry
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Text Processing :: Markup :: HTML
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: babel<3,>=2.14
28
+ Requires-Dist: legaldown-validator<0.4,>=0.3.0
29
+ Requires-Dist: markdown-it-py<5,>=3.0
30
+ Requires-Dist: pyyaml>=6.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: build>=1.2; extra == 'dev'
33
+ Requires-Dist: pytest>=8.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.6; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ <div align="center">
38
+
39
+ # legaldown-render 🖨️
40
+
41
+ ### The reference renderer for [LegalDown](https://github.com/ForLegalAI/LegalDown)
42
+
43
+ **Turn a LegalDown document into HTML or plain text, with section numbers, cross-references,
44
+ defined terms, and formatted values all generated at render time.**
45
+
46
+ Targets specification **v0.2** · Conformance level **Rendering** · DOCX and PDF planned
47
+
48
+ </div>
49
+
50
+ ---
51
+
52
+ ## What it does
53
+
54
+ [LegalDown](https://github.com/ForLegalAI/LegalDown) documents never hardcode section numbers,
55
+ reference text, or formatted values. The source says `{{ref: late-payment}}`, and the renderer
56
+ turns it into "4.2(b)" with a link. This package is that renderer:
57
+
58
+ 🔢 **Numbers** sections, list items, and paragraphs under a configurable scheme (§13.1, §13.2).
59
+ References to items resolve to designations such as "2.1(b)".
60
+
61
+ 🔗 **Resolves** cross-references, defined terms, parties, sides, and attachment references into
62
+ display text with links (§13.3–§13.5).
63
+
64
+ 🌍 **Formats** dates, money, and durations for a locale chosen at render time. A Czech locale
65
+ gives "1. června 2026", "10 000,00 Kč", and "12 měsíců", and amounts are never rounded (§10).
66
+
67
+ 🎨 **Applies a style template**, so the same source renders as a US-style contract or a
68
+ continental one without editing the text (§13.7).
69
+
70
+ 📝 **Shows templates as templates, or fills them in.** Without answers, conditional clauses are
71
+ marked, every `{{choose:}}` phrase is shown, drafting notes are styled, and placeholders render as
72
+ blanks. With an answers set, the template is assembled and the finished document rendered (§15.8).
73
+
74
+ 🚩 **Never hides a problem.** Anything unresolved renders as a visible marker, such as
75
+ `[BROKEN REF: id]`, and is reported with the specification's stable rule id. Nothing is dropped
76
+ silently (§17.5).
77
+
78
+ Parsing and Core validation come from
79
+ [`legaldown-validator`](https://github.com/ForLegalAI/legaldown-validator), the reference parser.
80
+ This package never reimplements the LegalDown grammar.
81
+
82
+ ## Install
83
+
84
+ ```bash
85
+ pip install legaldown-render
86
+ ```
87
+
88
+ Python 3.11 or newer. The dependencies are all pure Python: `legaldown-validator`,
89
+ `markdown-it-py`, `babel`, and `pyyaml`.
90
+
91
+ ## Command line
92
+
93
+ ```bash
94
+ legaldown-render contract.lgd -o contract.html # HTML page
95
+ legaldown-render contract.lgd -o contract.txt # plain text (format from extension)
96
+ legaldown-render contract.lgd --style continental --locale cs-CZ -o smlouva.html
97
+ legaldown-render contract.lgd --set numbering.scheme=legal-outline --set enumeration.enabled=false
98
+ legaldown-render contract.lgd --set contents.enabled=true --set contents.depth=3 # table of contents
99
+ legaldown-render contract.lgd --strict # refuse if the document has errors
100
+ legaldown-render contract.lgd --final --strict # refuse if blanks or template constructs remain
101
+ legaldown-render template.lgd --answers answers.yaml -o nda.html # fill in a template, then render it
102
+ legaldown-render --print-style --style continental # every setting, as YAML
103
+ ```
104
+
105
+ Diagnostics go to stderr, one per line with its rule id. The exit code is 0 when the document
106
+ rendered, even if it has errors (they show as markers), 1 when rendering was refused (strict mode,
107
+ or a template that cannot be assembled with its answers) or a file cannot be read, and 2 for an
108
+ invalid style or setting.
109
+
110
+ ## Python
111
+
112
+ ```python
113
+ from legaldown_render import render
114
+
115
+ result = render(open("contract.lgd").read(), format="html", style="continental", locale="cs-CZ")
116
+ for diagnostic in result.diagnostics:
117
+ print(diagnostic.level, diagnostic.rule, diagnostic.message)
118
+ open("contract.html", "w").write(result.output)
119
+ ```
120
+
121
+ `render()` also accepts a `RenderOptions`. Pass `answers={...}` to render a template filled in.
122
+ Problems in the document never raise. Only an invalid style (`StyleError`), an unreadable document
123
+ (`DocumentError`), strict mode, or a template that cannot be assembled with its answers
124
+ (`RenderRefused`) do.
125
+
126
+ ## Styles and preferences
127
+
128
+ A **style template** says how documents look: numbering, list enumeration, locale, labels,
129
+ typography. It is a YAML file you can share across a team:
130
+
131
+ ```yaml
132
+ version: 1
133
+ extends: continental
134
+ locale: cs-CZ
135
+ numbering:
136
+ levels:
137
+ - { counter: decimal, label: "Článek {n}", ref: "{n}" }
138
+ definitions: { style: small-caps }
139
+ typography: { font_family: "Georgia, serif", justify: true }
140
+ ```
141
+
142
+ **Render options** say what one job does: the output format, strict mode, the final check, the
143
+ answers to a template, a full page or an HTML fragment. Values layer in this order: built-in defaults, then the `extends` chain, then your
144
+ style, then per-job `--set` overrides. Every value is validated before rendering starts. See
145
+ [Styles and preferences](docs/style-templates.md).
146
+
147
+ Built-in styles are `default` (1. / 1.1, with (a) / (i) list items), `continental` (numbered
148
+ articles, 2.1-style items, numbered paragraphs), and `outline` (I. / A. / 1. / a.).
149
+
150
+ ## Documentation
151
+
152
+ | Document | What it covers |
153
+ |---|---|
154
+ | [Concepts](docs/concepts.md) | What a renderer is responsible for, principles, and vocabulary |
155
+ | [Architecture](docs/architecture.md) | The pipeline, the render tree, the package layout, and testing |
156
+ | [Styles and preferences](docs/style-templates.md) | The style format, layering, and every setting |
157
+ | [Conformance](CONFORMANCE.md) | What the Rendering level requires, and what is covered |
158
+ | [Roadmap](docs/roadmap.md) | Milestones, and the changes needed upstream |
159
+ | [Decision records](docs/decisions/) | Why the main choices were made |
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ pip install -e ".[dev]"
165
+ pytest # unit, golden, and CLI tests
166
+ pytest --update-golden # regenerate tests/golden/ after an intended output change
167
+ LEGALDOWN_SPEC_DIR=../LegalDown pytest -m conformance # the specification's examples and fixtures
168
+ ruff check .
169
+ ```
170
+
171
+ Releases go to PyPI from a GitHub Release; see [PUBLISHING.md](.github/PUBLISHING.md).
172
+
173
+ ## License
174
+
175
+ [MIT](LICENSE)
@@ -0,0 +1,139 @@
1
+ <div align="center">
2
+
3
+ # legaldown-render 🖨️
4
+
5
+ ### The reference renderer for [LegalDown](https://github.com/ForLegalAI/LegalDown)
6
+
7
+ **Turn a LegalDown document into HTML or plain text, with section numbers, cross-references,
8
+ defined terms, and formatted values all generated at render time.**
9
+
10
+ Targets specification **v0.2** · Conformance level **Rendering** · DOCX and PDF planned
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## What it does
17
+
18
+ [LegalDown](https://github.com/ForLegalAI/LegalDown) documents never hardcode section numbers,
19
+ reference text, or formatted values. The source says `{{ref: late-payment}}`, and the renderer
20
+ turns it into "4.2(b)" with a link. This package is that renderer:
21
+
22
+ 🔢 **Numbers** sections, list items, and paragraphs under a configurable scheme (§13.1, §13.2).
23
+ References to items resolve to designations such as "2.1(b)".
24
+
25
+ 🔗 **Resolves** cross-references, defined terms, parties, sides, and attachment references into
26
+ display text with links (§13.3–§13.5).
27
+
28
+ 🌍 **Formats** dates, money, and durations for a locale chosen at render time. A Czech locale
29
+ gives "1. června 2026", "10 000,00 Kč", and "12 měsíců", and amounts are never rounded (§10).
30
+
31
+ 🎨 **Applies a style template**, so the same source renders as a US-style contract or a
32
+ continental one without editing the text (§13.7).
33
+
34
+ 📝 **Shows templates as templates, or fills them in.** Without answers, conditional clauses are
35
+ marked, every `{{choose:}}` phrase is shown, drafting notes are styled, and placeholders render as
36
+ blanks. With an answers set, the template is assembled and the finished document rendered (§15.8).
37
+
38
+ 🚩 **Never hides a problem.** Anything unresolved renders as a visible marker, such as
39
+ `[BROKEN REF: id]`, and is reported with the specification's stable rule id. Nothing is dropped
40
+ silently (§17.5).
41
+
42
+ Parsing and Core validation come from
43
+ [`legaldown-validator`](https://github.com/ForLegalAI/legaldown-validator), the reference parser.
44
+ This package never reimplements the LegalDown grammar.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install legaldown-render
50
+ ```
51
+
52
+ Python 3.11 or newer. The dependencies are all pure Python: `legaldown-validator`,
53
+ `markdown-it-py`, `babel`, and `pyyaml`.
54
+
55
+ ## Command line
56
+
57
+ ```bash
58
+ legaldown-render contract.lgd -o contract.html # HTML page
59
+ legaldown-render contract.lgd -o contract.txt # plain text (format from extension)
60
+ legaldown-render contract.lgd --style continental --locale cs-CZ -o smlouva.html
61
+ legaldown-render contract.lgd --set numbering.scheme=legal-outline --set enumeration.enabled=false
62
+ legaldown-render contract.lgd --set contents.enabled=true --set contents.depth=3 # table of contents
63
+ legaldown-render contract.lgd --strict # refuse if the document has errors
64
+ legaldown-render contract.lgd --final --strict # refuse if blanks or template constructs remain
65
+ legaldown-render template.lgd --answers answers.yaml -o nda.html # fill in a template, then render it
66
+ legaldown-render --print-style --style continental # every setting, as YAML
67
+ ```
68
+
69
+ Diagnostics go to stderr, one per line with its rule id. The exit code is 0 when the document
70
+ rendered, even if it has errors (they show as markers), 1 when rendering was refused (strict mode,
71
+ or a template that cannot be assembled with its answers) or a file cannot be read, and 2 for an
72
+ invalid style or setting.
73
+
74
+ ## Python
75
+
76
+ ```python
77
+ from legaldown_render import render
78
+
79
+ result = render(open("contract.lgd").read(), format="html", style="continental", locale="cs-CZ")
80
+ for diagnostic in result.diagnostics:
81
+ print(diagnostic.level, diagnostic.rule, diagnostic.message)
82
+ open("contract.html", "w").write(result.output)
83
+ ```
84
+
85
+ `render()` also accepts a `RenderOptions`. Pass `answers={...}` to render a template filled in.
86
+ Problems in the document never raise. Only an invalid style (`StyleError`), an unreadable document
87
+ (`DocumentError`), strict mode, or a template that cannot be assembled with its answers
88
+ (`RenderRefused`) do.
89
+
90
+ ## Styles and preferences
91
+
92
+ A **style template** says how documents look: numbering, list enumeration, locale, labels,
93
+ typography. It is a YAML file you can share across a team:
94
+
95
+ ```yaml
96
+ version: 1
97
+ extends: continental
98
+ locale: cs-CZ
99
+ numbering:
100
+ levels:
101
+ - { counter: decimal, label: "Článek {n}", ref: "{n}" }
102
+ definitions: { style: small-caps }
103
+ typography: { font_family: "Georgia, serif", justify: true }
104
+ ```
105
+
106
+ **Render options** say what one job does: the output format, strict mode, the final check, the
107
+ answers to a template, a full page or an HTML fragment. Values layer in this order: built-in defaults, then the `extends` chain, then your
108
+ style, then per-job `--set` overrides. Every value is validated before rendering starts. See
109
+ [Styles and preferences](docs/style-templates.md).
110
+
111
+ Built-in styles are `default` (1. / 1.1, with (a) / (i) list items), `continental` (numbered
112
+ articles, 2.1-style items, numbered paragraphs), and `outline` (I. / A. / 1. / a.).
113
+
114
+ ## Documentation
115
+
116
+ | Document | What it covers |
117
+ |---|---|
118
+ | [Concepts](docs/concepts.md) | What a renderer is responsible for, principles, and vocabulary |
119
+ | [Architecture](docs/architecture.md) | The pipeline, the render tree, the package layout, and testing |
120
+ | [Styles and preferences](docs/style-templates.md) | The style format, layering, and every setting |
121
+ | [Conformance](CONFORMANCE.md) | What the Rendering level requires, and what is covered |
122
+ | [Roadmap](docs/roadmap.md) | Milestones, and the changes needed upstream |
123
+ | [Decision records](docs/decisions/) | Why the main choices were made |
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ pip install -e ".[dev]"
129
+ pytest # unit, golden, and CLI tests
130
+ pytest --update-golden # regenerate tests/golden/ after an intended output change
131
+ LEGALDOWN_SPEC_DIR=../LegalDown pytest -m conformance # the specification's examples and fixtures
132
+ ruff check .
133
+ ```
134
+
135
+ Releases go to PyPI from a GitHub Release; see [PUBLISHING.md](.github/PUBLISHING.md).
136
+
137
+ ## License
138
+
139
+ [MIT](LICENSE)
@@ -0,0 +1,247 @@
1
+ # Architecture
2
+
3
+ This page describes how the renderer is built. The decisions behind it are in
4
+ [decision records](decisions/). Read [Concepts](concepts.md) first for the vocabulary, and
5
+ [Styles and preferences](style-templates.md) for configuration.
6
+
7
+ ---
8
+
9
+ ## The pipeline
10
+
11
+ ```
12
+ source .lgd
13
+ │ normalize_source(): strip BOM, unify line endings
14
+ ▼
15
+ ┌─────────────────────────┐
16
+ │ 2. Assemble (answers) │ only with an answers set: legaldown-validator's assemble() turns
17
+ └───────────┬─────────────┘ the template into its assembled document, which the rest renders
18
+ ▼
19
+ ┌─────────────────────────┐
20
+ │ 1. Parse & validate │ legaldown-validator: Document, ValidationResult (Core diagnostics,
21
+ └───────────┬─────────────┘ resolved identifiers, party/side/definition/attachment lookups)
22
+ ▼
23
+ ┌─────────────────────────┐
24
+ │ 3. Build build.py │ source + validator → render tree (source inlines)
25
+ └───────────┬─────────────┘
26
+ ▼ style template (style/) ──┐
27
+ ┌─────────────────────────┐ │
28
+ │ 4. Resolve resolve/ │ ◄──────────────────────────┘ numbers, labels, anchors, display text,
29
+ └───────────┬─────────────┘ failure markers + Rendering diagnostics
30
+ ▼
31
+ ┌─────────────────────────┐
32
+ │ 5. Write writers/ │ html │ text (docx, pdf later)
33
+ └───────────┬─────────────┘
34
+ ▼
35
+ RenderResult(output, diagnostics, format, tree, style)
36
+ ```
37
+
38
+ Stage 2, **assembly** with an answers set (§15.7), runs only when the job has answers, before
39
+ the document is parsed for rendering. A template rendered with answers is assembled first and the
40
+ assembled document rendered; without answers, it renders as its **template view** (§15.8).
41
+ Assembly is the core package's (`legaldown.assemble`), exact to the byte as §15.7.2 requires; the
42
+ renderer only decides when to refuse:
43
+
44
+ - the template has Errors: §15.7.2 defines assembly only for a template without them
45
+ - the answers do (`answer-invalid`, `answer-missing`)
46
+ - the template needs other files (include fragments, LegalDown attachment files, translations),
47
+ which the renderer does not read below the Full level (§17.6)
48
+
49
+ A refusal raises `RenderRefused` with the findings. Assembly Warnings, such as `answer-unknown`,
50
+ are reported with the rest. Blanks left unanswered stay, and render as blanks: the result is a
51
+ draft (§15.1), which `--final` reports.
52
+
53
+ `api.render()` runs the pipeline. Each stage returns frozen values. Only stage 4 reads the style's
54
+ semantic settings, and only stage 5 knows about file formats.
55
+
56
+ ### 1. Parse and validate
57
+
58
+ `legaldown.parse_document` and `legaldown.validate_document` provide:
59
+
60
+ - the **metadata**: title, sides and parties, attachments, questions, language
61
+ - the **section identifiers** exactly as the validator resolved them: explicit `{#id}`, or
62
+ generated by §5.3 with §5.5 collision handling. These come from `ValidationResult.sections`,
63
+ so the renderer can never compute a different anchor than the one the validator checked
64
+ - **lookups** for definitions, parties, sides, and attachments, already reduced to display names
65
+ (§3.6)
66
+ - the **Core diagnostics**, each with a stable rule id
67
+
68
+ A document that cannot be read at all, such as one whose frontmatter is not a YAML mapping,
69
+ raises `DocumentError`. Every other problem renders.
70
+
71
+ ### 3. Build the render tree (`build.py`)
72
+
73
+ There is one parser: the validator's ([ADR 0007](decisions/0007-one-parser-validator-model.md)).
74
+ The builder walks the validator's `Document` and turns each block into render-tree blocks. It
75
+ never decides a structural or LegalDown question itself:
76
+
77
+ | Question | Answered by |
78
+ |---|---|
79
+ | Sections, headings, identifiers | `Document.sections`, `ValidationResult.sections` |
80
+ | 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()` |
81
+ | Where a marker (`{#id when=…}`) is placed, and what it means | `ValidationResult.placed_markers`: each marker's block, field, offset, identifier, and condition |
82
+ | Whether the document is a template | `ValidationResult.is_template` |
83
+ | Whether a quote is a drafting note | The validator's `is_drafting_note()` |
84
+ | Which list item a marker belongs to | `PlacedMarker.item`: the item's number among the list's items, in pre-order |
85
+ | A lifted definition, `{{ref:}}` or `{{term:}}` block's source | The validator's `render_block()` |
86
+
87
+ The few imports beyond the validator's public API (quote content, fence helpers, the
88
+ answers-file reader) are all in `validator_bridge.py`, the list for roadmap item U3.
89
+
90
+ Within one block's text, **markdown-it-py parses inline Markdown only**: emphasis, links, code
91
+ spans, inline HTML. Directives are protected first by **sentinels**. Every directive, and every
92
+ `"Term" {{def:}}` span, is replaced with a private-use token, `\ue000<nonce>:<n>\ue001`, so
93
+ Markdown cannot reinterpret directive syntax, such as an underscore in an identifier. The nonce is
94
+ random for each build, so no text in a document, whether written literally, as an entity, or
95
+ percent-encoded, can pass for a sentinel. Emphasis that wraps only a defined term is dropped with
96
+ it, because the style decides how defined terms look (§7.2). A `{{def:}}` becomes a sentinel
97
+ that renders nothing, led by a punctuation character so that an emphasis closer just before it
98
+ still closes. A defined term in a link title or alt text shows its term but is not the
99
+ definition's anchor.
100
+
101
+ An HTML block in the model (`kind="html"`), a comment block included, renders nothing (§8.6).
102
+ Within a block's text, inline comments and tags are dropped; where one stood between two spaces,
103
+ one space stays. The validator reports raw HTML other than comments (`raw-html`, §8.7). A heading
104
+ in a list item or a quote (`kind="heading"`) is not a section (§4.1) and shows as a bold line.
105
+
106
+ The builder never works around the validator's model. Where the model loses or misreads
107
+ something, the output follows the model, and the gap is listed in `CONFORMANCE.md` and filed on
108
+ the validator:
109
+ - link reference definitions (`[label]: url`) render as paragraphs
110
+
111
+ Guessing at lost structure was tried, and it traded each gap for new bugs.
112
+
113
+ ### 4. Resolve (`resolve/`)
114
+
115
+ `Resolver.run()` works in three steps:
116
+
117
+ 1. **Survey.** It collects which definitions exist, whether template constructs are used, and
118
+ which placeholder ids are used with conflicting types (§10.7).
119
+ 2. **Structure.** Section numbers are the validator's own (`ValidationResult.sections`), so a
120
+ rendered number always matches the validator's: **alternatives** share a number (§15.8), and
121
+ a skipped heading level counts as 1. The style only formats them, the n-th part of a number
122
+ with the n-th level format. For paragraphs and list items, **alternatives** follow the
123
+ validator's rule, applied the same way: an alternative directly follows its
124
+ sibling, has the same identifier and a valid condition, and its full presence (its own condition
125
+ and every enclosing one) excludes that of every unit already holding the number (§15.4, the
126
+ validator's `exclusive()`). It then labels
127
+ list items by depth and top-level paragraphs, and allocates output anchors. Every anchor
128
+ target is registered with its **designation**: "4.2", "4.2(b)(ii)", or "Termination (a)"
129
+ under the `none` scheme.
130
+ 3. **Inlines.** It replaces every source inline with a resolved one:
131
+
132
+ | Directive | Resolves to | Failure markers |
133
+ |---|---|---|
134
+ | `{{ref:}}` | `CrossRef(designation, anchor)` (§13.3). A ref to an unnumbered item or paragraph falls back to its section's designation, with `ref-not-enumerated` | `[BROKEN REF: id]` |
135
+ | `{{term:}}` | `TermRef(label or term, def anchor)` (§7.3, §13.4) | `[UNDEFINED: id]` |
136
+ | `"Term" {{def:}}` | `DefinedTerm` without quotation marks, anchored at `def:<id>` (§7.2) | n/a |
137
+ | `{{date:}}` `{{money:}}` `{{duration:}}` | `Value`, formatted for the locale by Babel (ADR 0005) | `[INVALID DATE: …]`, `[INVALID AMOUNT: …]`, `[UNKNOWN CURRENCY: …]`, `[INVALID DURATION: …]`, `[INVALID DURATION UNIT: …]` |
138
+ | `{{party:}}` `{{side:}}` | `Value(label or display name)` (§3.6) | `[INVALID PARTY: …]`, `[UNKNOWN PARTY: …]`, and the side forms |
139
+ | `{{field:}}` | `Value(raw value)` | `[INVALID FIELD: …]` |
140
+ | `{{placeholder:}}` | `Blank`, with the question's prompt if the style asks for it | `[INVALID PLACEHOLDER: …]`, `[UNKNOWN CURRENCY: …]`, `[INVALID DURATION UNIT: …]` |
141
+ | `{{attach:}}` | `Value(label or title)`, linked to the attachment placeholder or file (§6.4) | `[UNKNOWN ATTACHMENT: id]` |
142
+ | `{{choose:}}` | `Choice(all phrases)`, shown as `[a / b]` (§15.8) | `[INVALID CHOICE: q]` |
143
+ | `{{include:}}` | Not processed at this level (§17.5), with `render-not-processed` | `[NOT PROCESSED: include path]` |
144
+ | unknown name | Never printed verbatim (§11.5) | `[UNKNOWN DIRECTIVE: name]` |
145
+
146
+ A `label` never hides a failure marker (§11.5). Frontmatter strings go through the same resolver,
147
+ so placeholders in frontmatter render as blanks (§3.10).
148
+
149
+ The resolver also builds the **title block** (effective date, sides and parties), **attachment
150
+ placeholders** (§13.8; files are not read at this level), and **signature blocks** (§2.2, always
151
+ with `legal_name`, per §3.6).
152
+
153
+ **Anchors.** Section, item, paragraph, and attachment anchors use their identifiers. Definition
154
+ anchors are `def:<id>`. Identifiers cannot contain `:`, so the two can never collide in HTML's
155
+ single id space (§5.6). When alternatives share an identifier, only the first keeps the anchor,
156
+ so ids stay unique. A definition's anchor is given out in one place, when its defined term is
157
+ resolved. Terms in titles or alt text get none. After everything is resolved, one pass drops the
158
+ link of any `{{term:}}` whose definition got no anchor, so term links always have a target.
159
+
160
+ Under the `none` scheme, a section's designation is its heading text, computed by a text-only
161
+ pass that gives out no anchors and leaves references out. Headings are numbered with their level
162
+ clamped to 1–5, as the validator numbers them.
163
+
164
+ ### 5. Write (`writers/`)
165
+
166
+ > **A writer contains no LegalDown rules.** `tree.assert_resolved()` rejects a tree that still
167
+ > holds a source inline.
168
+
169
+ | Writer | Notes |
170
+ |---|---|
171
+ | `TextWriter` | Plain text (§13.6). A soft line break inside a paragraph is a space, a hard break a new line, and lists are indented with their labels. It is the **test oracle** for resolution |
172
+ | `HtmlWriter` | A self-contained HTML5 page, or with `standalone=False` just the `<article>`. Semantic markup, with labels as real text rather than CSS counters, and a stylesheet generated from the style's presentation settings, including print rules |
173
+ | DOCX, PDF | Planned ([ADR 0004](decisions/0004-html-first.md)) |
174
+
175
+ ## Package layout
176
+
177
+ ```
178
+ src/legaldown_render/
179
+ ├── __init__.py public API, __version__, SPEC_VERSION, CONFORMANCE_LEVEL
180
+ ├── api.py render(), render_file(), RenderOptions, RenderResult
181
+ ├── cli.py the legaldown-render command
182
+ ├── errors.py RenderError, DocumentError, RenderRefused, InternalError
183
+ ├── tree.py render tree nodes (source and resolved inlines, blocks, document)
184
+ ├── build.py stage 3
185
+ ├── resolve/
186
+ │ ├── resolver.py stage 4
187
+ │ ├── numbering.py counters, the §13.1 scheme presets, label templates
188
+ │ └── values.py Babel formatting of dates, money, durations
189
+ ├── style/
190
+ │ ├── model.py the style dataclasses (every field has a default)
191
+ │ ├── loader.py layering, extends, overrides, validation
192
+ │ ├── labels.py built-in labels per language (en, cs)
193
+ │ └── builtin/ default.yaml, continental.yaml, outline.yaml
194
+ └── writers/ html.py, text.py
195
+ ```
196
+
197
+ ## Dependencies
198
+
199
+ | Dependency | Why |
200
+ |---|---|
201
+ | `legaldown-validator>=0.3.0,<0.4` | The only LegalDown parser; Core validation |
202
+ | `markdown-it-py` | Inline Markdown inside one block's text (ADR 0007) |
203
+ | `babel` | CLDR locale data (ADR 0005) |
204
+ | `pyyaml` | Style templates |
205
+
206
+ All of them are pure Python. The DOCX and PDF writers will bring their dependencies in as extras.
207
+
208
+ The renderer uses the validator's public API (`legaldown`, `legaldown.validator`), except for
209
+ the few names in `validator_bridge.py`: `quote_content` and `MAX_QUOTE_DEPTH`, the fence
210
+ helpers, and the answers-file reader. The pin to one minor version exists because of them.
211
+ Asking the validator to export them publicly is roadmap item U3
212
+ ([validator#93](https://github.com/ForLegalAI/legaldown-validator/issues/93)).
213
+
214
+ ## Errors and diagnostics
215
+
216
+ - **Document problems** render as failure markers and are listed in `diagnostics`. `strict=True`
217
+ raises `RenderRefused` instead, carrying the diagnostics.
218
+ - **Configuration problems** (an invalid style, setting, locale, or format) raise before
219
+ rendering, with every problem named.
220
+ - **Internal inconsistencies** raise `InternalError`, and the CLI exits with 70.
221
+
222
+ Diagnostics reuse `legaldown.Diagnostic`. The renderer adds rules only it can evaluate:
223
+ `ref-not-enumerated`, a specification rule id, plus renderer-specific ids prefixed `render-`:
224
+ `render-not-processed`, `render-locale-fallback`, and `render-ref-ambiguous`. The validator's
225
+ diagnostics carry their line (§16.9), and the CLI prints it as `file:line:`. The renderer's own
226
+ have none yet, because a block's line is not public in the validator (U2).
227
+
228
+ ## Security
229
+
230
+ - Raw HTML in the source is never emitted (§8.7).
231
+ - Every text node and attribute is escaped.
232
+ - Links and images whose URL has a scheme other than `http`, `https`, `mailto`, or `tel` render
233
+ as text. markdown-it-py also refuses `javascript:`-style URLs when parsing.
234
+ - At Rendering level, the only files read are the document and the style template. Attachment
235
+ files and includes are never opened.
236
+ - Style values that are placed into CSS are stripped of `;{}<>`. `html.extra_css` is trusted
237
+ configuration, so a hosted service must not accept styles from untrusted users.
238
+
239
+ ## Testing
240
+
241
+ | Suite | What it checks |
242
+ |---|---|
243
+ | `tests/test_golden.py` | The test documents under each built-in style, compared byte for byte with `tests/golden/`. Regenerate with `pytest --update-golden` and review the diff |
244
+ | `tests/test_render.py` | Numbering schemes, designations, fallbacks, every failure marker, template view, labels by language, HTML safety, unique anchors, determinism, strict mode, locales |
245
+ | `tests/test_style.py`, `tests/test_values.py` | Style layering and validation; counters and locale formatting |
246
+ | `tests/test_cli.py` | The command |
247
+ | `tests/conformance/` | Every example and fixture in the specification repository renders, every internal link has a target, and each invalid fixture renders its rule's failure marker. Needs `$LEGALDOWN_SPEC_DIR` or a sibling `../LegalDown` checkout |