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.
- legaldown_render-0.2.0/.gitignore +8 -0
- legaldown_render-0.2.0/LICENSE +21 -0
- legaldown_render-0.2.0/PKG-INFO +175 -0
- legaldown_render-0.2.0/README.md +139 -0
- legaldown_render-0.2.0/docs/architecture.md +247 -0
- legaldown_render-0.2.0/docs/concepts.md +107 -0
- legaldown_render-0.2.0/docs/decisions/0001-separate-package.md +32 -0
- legaldown_render-0.2.0/docs/decisions/0002-one-parser.md +51 -0
- legaldown_render-0.2.0/docs/decisions/0003-resolved-document.md +33 -0
- legaldown_render-0.2.0/docs/decisions/0004-html-first.md +40 -0
- legaldown_render-0.2.0/docs/decisions/0005-locale-formatting.md +39 -0
- legaldown_render-0.2.0/docs/decisions/0006-styles-and-options.md +41 -0
- legaldown_render-0.2.0/docs/decisions/0007-one-parser-validator-model.md +66 -0
- legaldown_render-0.2.0/docs/decisions/README.md +27 -0
- legaldown_render-0.2.0/docs/roadmap.md +68 -0
- legaldown_render-0.2.0/docs/style-templates.md +168 -0
- legaldown_render-0.2.0/pyproject.toml +78 -0
- legaldown_render-0.2.0/src/legaldown_render/__init__.py +49 -0
- legaldown_render-0.2.0/src/legaldown_render/api.py +187 -0
- legaldown_render-0.2.0/src/legaldown_render/build.py +609 -0
- legaldown_render-0.2.0/src/legaldown_render/cli.py +137 -0
- legaldown_render-0.2.0/src/legaldown_render/errors.py +32 -0
- legaldown_render-0.2.0/src/legaldown_render/py.typed +0 -0
- legaldown_render-0.2.0/src/legaldown_render/resolve/__init__.py +5 -0
- legaldown_render-0.2.0/src/legaldown_render/resolve/numbering.py +90 -0
- legaldown_render-0.2.0/src/legaldown_render/resolve/resolver.py +904 -0
- legaldown_render-0.2.0/src/legaldown_render/resolve/values.py +87 -0
- legaldown_render-0.2.0/src/legaldown_render/style/__init__.py +54 -0
- legaldown_render-0.2.0/src/legaldown_render/style/builtin/continental.yaml +24 -0
- legaldown_render-0.2.0/src/legaldown_render/style/builtin/default.yaml +7 -0
- legaldown_render-0.2.0/src/legaldown_render/style/builtin/outline.yaml +7 -0
- legaldown_render-0.2.0/src/legaldown_render/style/labels.py +141 -0
- legaldown_render-0.2.0/src/legaldown_render/style/loader.py +360 -0
- legaldown_render-0.2.0/src/legaldown_render/style/model.py +285 -0
- legaldown_render-0.2.0/src/legaldown_render/tree.py +511 -0
- legaldown_render-0.2.0/src/legaldown_render/validator_bridge.py +28 -0
- legaldown_render-0.2.0/src/legaldown_render/writers/__init__.py +21 -0
- legaldown_render-0.2.0/src/legaldown_render/writers/html.py +472 -0
- legaldown_render-0.2.0/src/legaldown_render/writers/text.py +196 -0
- legaldown_render-0.2.0/tests/conformance/__init__.py +0 -0
- legaldown_render-0.2.0/tests/conformance/test_spec_corpus.py +116 -0
- legaldown_render-0.2.0/tests/conftest.py +30 -0
- legaldown_render-0.2.0/tests/documents/features.lgd +101 -0
- legaldown_render-0.2.0/tests/documents/template.lgd +59 -0
- legaldown_render-0.2.0/tests/golden/features.continental.txt +96 -0
- legaldown_render-0.2.0/tests/golden/features.default.html +207 -0
- legaldown_render-0.2.0/tests/golden/features.default.txt +96 -0
- legaldown_render-0.2.0/tests/golden/features.outline.txt +96 -0
- legaldown_render-0.2.0/tests/golden/template.default.html +147 -0
- legaldown_render-0.2.0/tests/golden/template.default.txt +57 -0
- legaldown_render-0.2.0/tests/test_cli.py +92 -0
- legaldown_render-0.2.0/tests/test_golden.py +31 -0
- legaldown_render-0.2.0/tests/test_render.py +1155 -0
- legaldown_render-0.2.0/tests/test_style.py +116 -0
- legaldown_render-0.2.0/tests/test_values.py +54 -0
|
@@ -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 |
|