legaldown-render 0.2.0__py3-none-any.whl
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/__init__.py +49 -0
- legaldown_render/api.py +187 -0
- legaldown_render/build.py +609 -0
- legaldown_render/cli.py +137 -0
- legaldown_render/errors.py +32 -0
- legaldown_render/py.typed +0 -0
- legaldown_render/resolve/__init__.py +5 -0
- legaldown_render/resolve/numbering.py +90 -0
- legaldown_render/resolve/resolver.py +904 -0
- legaldown_render/resolve/values.py +87 -0
- legaldown_render/style/__init__.py +54 -0
- legaldown_render/style/builtin/continental.yaml +24 -0
- legaldown_render/style/builtin/default.yaml +7 -0
- legaldown_render/style/builtin/outline.yaml +7 -0
- legaldown_render/style/labels.py +141 -0
- legaldown_render/style/loader.py +360 -0
- legaldown_render/style/model.py +285 -0
- legaldown_render/tree.py +511 -0
- legaldown_render/validator_bridge.py +28 -0
- legaldown_render/writers/__init__.py +21 -0
- legaldown_render/writers/html.py +472 -0
- legaldown_render/writers/text.py +196 -0
- legaldown_render-0.2.0.dist-info/METADATA +175 -0
- legaldown_render-0.2.0.dist-info/RECORD +27 -0
- legaldown_render-0.2.0.dist-info/WHEEL +4 -0
- legaldown_render-0.2.0.dist-info/entry_points.txt +2 -0
- legaldown_render-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""legaldown-render — the reference renderer for LegalDown.
|
|
2
|
+
|
|
3
|
+
Render a LegalDown document to HTML or plain text. Section numbers,
|
|
4
|
+
cross-references, defined terms, and formatted values are generated at render
|
|
5
|
+
time, under a style template (§13.7)::
|
|
6
|
+
|
|
7
|
+
from legaldown_render import render
|
|
8
|
+
|
|
9
|
+
result = render(open("contract.lgd").read(), format="html", style="default")
|
|
10
|
+
for diagnostic in result.diagnostics:
|
|
11
|
+
print(diagnostic.level, diagnostic.rule, diagnostic.message)
|
|
12
|
+
open("contract.html", "w").write(result.output)
|
|
13
|
+
|
|
14
|
+
Parsing and Core validation come from legaldown-validator; this package
|
|
15
|
+
claims the Rendering conformance level (§17.3). Exact coverage:
|
|
16
|
+
https://github.com/ForLegalAI/legaldown-render/blob/main/CONFORMANCE.md
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .api import RenderOptions, RenderResult, render, render_file
|
|
21
|
+
from .errors import DocumentError, InternalError, RenderError, RenderRefused
|
|
22
|
+
from .style import Style, StyleError, builtin_styles, dump_style, load_style
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.0"
|
|
25
|
+
|
|
26
|
+
#: The LegalDown specification version this renderer targets.
|
|
27
|
+
SPEC_VERSION = "0.2"
|
|
28
|
+
|
|
29
|
+
#: Conformance level per specification §17.
|
|
30
|
+
CONFORMANCE_LEVEL = "rendering"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"__version__",
|
|
34
|
+
"SPEC_VERSION",
|
|
35
|
+
"CONFORMANCE_LEVEL",
|
|
36
|
+
"render",
|
|
37
|
+
"render_file",
|
|
38
|
+
"RenderOptions",
|
|
39
|
+
"RenderResult",
|
|
40
|
+
"Style",
|
|
41
|
+
"StyleError",
|
|
42
|
+
"load_style",
|
|
43
|
+
"dump_style",
|
|
44
|
+
"builtin_styles",
|
|
45
|
+
"RenderError",
|
|
46
|
+
"DocumentError",
|
|
47
|
+
"RenderRefused",
|
|
48
|
+
"InternalError",
|
|
49
|
+
]
|
legaldown_render/api.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""The rendering pipeline and its public API (docs/architecture.md).
|
|
2
|
+
|
|
3
|
+
Preferences are split in two, on purpose:
|
|
4
|
+
|
|
5
|
+
* the **style template** says how documents look — numbering, enumeration,
|
|
6
|
+
locale, labels, typography. It is a reusable YAML file (``--style``), with
|
|
7
|
+
single values overridable per job (``--set key=value``, ``overrides``);
|
|
8
|
+
* **render options** say what this one job does — the output format, strict
|
|
9
|
+
mode, the final check, the answers to a template, a full HTML page or a
|
|
10
|
+
fragment. They never change how a document
|
|
11
|
+
looks, so they are not part of the style.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
from legaldown import AssemblyError, Diagnostic, Document, ValidationResult, assemble, parse_document, validate_document
|
|
22
|
+
|
|
23
|
+
from .build import build_tree, normalize_source
|
|
24
|
+
from .errors import DocumentError, RenderRefused
|
|
25
|
+
from .resolve import Formatter, parse_locale, resolve_tree
|
|
26
|
+
from .style import Style, StyleError, load_style
|
|
27
|
+
from .tree import RenderTree, assert_resolved
|
|
28
|
+
from .writers import FORMATS, HtmlWriter, TextWriter
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class RenderOptions:
|
|
33
|
+
"""Settings for one render job."""
|
|
34
|
+
|
|
35
|
+
#: ``html`` or ``text``.
|
|
36
|
+
format: str = "html"
|
|
37
|
+
#: A built-in style name, a path to a YAML style template, or a Style.
|
|
38
|
+
style: str | Path | Style | None = None
|
|
39
|
+
#: Per-job style overrides by dotted key, e.g. ``{"numbering.scheme": "none"}``.
|
|
40
|
+
overrides: Mapping[str, Any] = field(default_factory=dict)
|
|
41
|
+
#: Shorthand for ``overrides={"locale": ...}``: the formatting locale.
|
|
42
|
+
locale: str | None = None
|
|
43
|
+
#: Refuse to render (raise RenderRefused) when the document has errors,
|
|
44
|
+
#: instead of rendering them as visible failure markers.
|
|
45
|
+
strict: bool = False
|
|
46
|
+
#: The final check (§15.9): the document is meant for signature, so a
|
|
47
|
+
#: remaining blank (``placeholder-unfilled``) or template construct
|
|
48
|
+
#: (``template-construct-present``) is an Error. With ``strict``, such a
|
|
49
|
+
#: document is refused.
|
|
50
|
+
final: bool = False
|
|
51
|
+
#: An answers set (§15.7.1): question id to answer, as YAML or JSON
|
|
52
|
+
#: would load it. With answers, a template is assembled first and the
|
|
53
|
+
#: assembled document is rendered (§15.8); without, a template renders
|
|
54
|
+
#: as its template view.
|
|
55
|
+
answers: Mapping[str, Any] | None = None
|
|
56
|
+
#: HTML only: a complete page (True) or just the ``<article>`` (False).
|
|
57
|
+
standalone: bool = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class RenderResult:
|
|
62
|
+
"""What a render produced."""
|
|
63
|
+
|
|
64
|
+
output: str
|
|
65
|
+
#: Core (validator) and Rendering diagnostics, each with its stable rule id.
|
|
66
|
+
diagnostics: list[Diagnostic]
|
|
67
|
+
format: str
|
|
68
|
+
#: The resolved tree, for tools that want more than the output text.
|
|
69
|
+
tree: RenderTree
|
|
70
|
+
style: Style
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def ok(self) -> bool:
|
|
74
|
+
"""True when the document has no errors."""
|
|
75
|
+
return not any(diagnostic.level == "error" for diagnostic in self.diagnostics)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def render(source: str, options: RenderOptions | None = None, /, **settings: Any) -> RenderResult:
|
|
79
|
+
"""Render LegalDown *source*.
|
|
80
|
+
|
|
81
|
+
Settings are a :class:`RenderOptions` or the same fields as keywords::
|
|
82
|
+
|
|
83
|
+
render(text, format="html", style="continental", locale="cs-CZ")
|
|
84
|
+
|
|
85
|
+
Problems in the document never raise: they render as failure markers and
|
|
86
|
+
are listed in ``diagnostics``. Raises :class:`StyleError` for an invalid
|
|
87
|
+
style or setting, :class:`DocumentError` for a document that cannot be
|
|
88
|
+
read, and :class:`RenderRefused` in strict mode when the document has
|
|
89
|
+
errors, or when a template cannot be assembled with ``answers``.
|
|
90
|
+
"""
|
|
91
|
+
if options is None:
|
|
92
|
+
options = RenderOptions(**settings)
|
|
93
|
+
elif settings:
|
|
94
|
+
raise TypeError("pass either a RenderOptions or keyword settings, not both")
|
|
95
|
+
if options.format not in FORMATS:
|
|
96
|
+
raise ValueError(f"unknown output format '{options.format}' (known: {', '.join(FORMATS)})")
|
|
97
|
+
overrides = dict(options.overrides)
|
|
98
|
+
if options.locale:
|
|
99
|
+
overrides["locale"] = options.locale
|
|
100
|
+
style = load_style(options.style, overrides=overrides)
|
|
101
|
+
|
|
102
|
+
source = normalize_source(source)
|
|
103
|
+
assembled: list[Diagnostic] = []
|
|
104
|
+
if options.answers is not None:
|
|
105
|
+
source, assembled = _assemble(source, options.answers)
|
|
106
|
+
document = _parse(source)
|
|
107
|
+
result = validate_document(document, final=options.final)
|
|
108
|
+
diagnostics = assembled + list(result.diagnostics)
|
|
109
|
+
if options.strict and any(d.level == "error" for d in diagnostics):
|
|
110
|
+
raise RenderRefused(diagnostics)
|
|
111
|
+
|
|
112
|
+
tree, extra = _resolve(document, result, style)
|
|
113
|
+
diagnostics += [d for d in extra if d not in diagnostics]
|
|
114
|
+
if options.strict and any(d.level == "error" for d in diagnostics):
|
|
115
|
+
raise RenderRefused(diagnostics)
|
|
116
|
+
|
|
117
|
+
assert_resolved(tree)
|
|
118
|
+
output = _writer(options, style).write(tree)
|
|
119
|
+
return RenderResult(output, diagnostics, options.format, tree, style)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def render_file(path: str | Path, options: RenderOptions | None = None, /, **settings: Any) -> RenderResult:
|
|
123
|
+
"""Render the LegalDown file at *path* (see :func:`render`)."""
|
|
124
|
+
return render(Path(path).read_text(encoding="utf-8"), options, **settings)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _assemble(source: str, answers: Mapping[str, Any]) -> tuple[str, list[Diagnostic]]:
|
|
128
|
+
"""*source* assembled with *answers* (§15.7), and the assembly's
|
|
129
|
+
Warnings (such as ``answer-unknown``). Raises RenderRefused when it
|
|
130
|
+
cannot be assembled: the template has Errors, for which §15.7.2 defines
|
|
131
|
+
no output, or the answers do (``answer-invalid``, ``answer-missing``),
|
|
132
|
+
or the template needs files a renderer below Full does not read
|
|
133
|
+
(includes, LegalDown attachments, translations; §17.6)."""
|
|
134
|
+
findings = validate_document(_parse(source)).diagnostics
|
|
135
|
+
errors = sum(1 for d in findings if d.level == "error")
|
|
136
|
+
if errors:
|
|
137
|
+
raise RenderRefused(findings, f"Assembly refused: the template has {errors} error(s), "
|
|
138
|
+
"and only a template without errors can be assembled (§15.7.2).")
|
|
139
|
+
try:
|
|
140
|
+
result = assemble(source, answers)
|
|
141
|
+
except AssemblyError as error:
|
|
142
|
+
raise DocumentError(f"The template cannot be assembled: {error}") from error
|
|
143
|
+
if not result.ok:
|
|
144
|
+
errors = [d for d in result.diagnostics if d.level == "error"]
|
|
145
|
+
raise RenderRefused(result.diagnostics, f"Assembly refused: {len(errors)} error(s) in the answers or "
|
|
146
|
+
"the template (§15.7).")
|
|
147
|
+
# The source was normalized, and assembly keeps its line breaks.
|
|
148
|
+
return result.output, list(result.diagnostics)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _parse(source: str) -> Document:
|
|
152
|
+
try:
|
|
153
|
+
return parse_document(source)
|
|
154
|
+
except (ValueError, yaml.YAMLError) as error:
|
|
155
|
+
raise DocumentError(f"The document cannot be read: {error}") from error
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _resolve(document: Document, result: ValidationResult, style: Style) -> tuple[RenderTree, list[Diagnostic]]:
|
|
159
|
+
diagnostics: list[Diagnostic] = []
|
|
160
|
+
# Raw HTML is never emitted; the validator reports it (raw-html, §8.7).
|
|
161
|
+
tree = build_tree(document, result)
|
|
162
|
+
locale = parse_locale(style.locale) if style.locale else None
|
|
163
|
+
if style.locale and locale is None:
|
|
164
|
+
raise StyleError("setting", [f"locale: '{style.locale}' is not a known locale (e.g. en-US, cs-CZ)"])
|
|
165
|
+
if locale is None:
|
|
166
|
+
# No locale set: the document's language is the hint (§10.1).
|
|
167
|
+
locale = parse_locale(tree.language)
|
|
168
|
+
if locale is None:
|
|
169
|
+
diagnostics.append(Diagnostic(
|
|
170
|
+
rule="render-locale-fallback",
|
|
171
|
+
level="info",
|
|
172
|
+
message=f"No formatting locale is set and the document language '{tree.language}' is not a "
|
|
173
|
+
f"known locale; values are formatted for 'en'. Set one with --locale or the style's "
|
|
174
|
+
f"'locale'.",
|
|
175
|
+
))
|
|
176
|
+
locale = parse_locale("en")
|
|
177
|
+
assert locale is not None
|
|
178
|
+
resolved, found = resolve_tree(tree, document, result, style, Formatter(locale, style.values))
|
|
179
|
+
return resolved, diagnostics + found
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _writer(options: RenderOptions, style: Style) -> HtmlWriter | TextWriter:
|
|
183
|
+
if options.format == "text":
|
|
184
|
+
return TextWriter()
|
|
185
|
+
from . import __version__
|
|
186
|
+
|
|
187
|
+
return HtmlWriter(style, standalone=options.standalone, generator=f"legaldown-render {__version__}")
|