Repository navigation
Code review fixes and known gaps: values, CLI, links and images, file references, untrusted styles, diagnostics lines, none-scheme designations - #6
Merged
Conversation
- S1: main() reconfigures stdout to UTF-8 and stderr to backslashreplace (guarded by hasattr(..., "reconfigure")); stdin is read as bytes and decoded as UTF-8. Czech text no longer crashes or is corrupted on cp1252. - S4: a non-UTF-8 input file or stdin is "cannot read NAME: not valid UTF-8 (byte 0x.. at offset N)", exit 1 (offset is in the file, not a buffer). A non-UTF-8 style file is a StyleError (style/loader.py _read only). - S5: an OSError writing -o is "cannot write PATH: ...", exit 1. - S6: -o with an extension no writer knows (x.docx, .pdf, .md) and no -f is a usage error (exit 2); no extension keeps the html default; -f wins. - S2: StyleError is defined in errors.py as StyleError(RenderError, ValueError) and re-exported from style/loader.py, legaldown_render.style and the package. An unknown format raises the new OptionsError(RenderError, ValueError), exported from the package. - S3: RenderRefused (now storing .reason) and StyleError define __reduce__ so they round-trip through pickle. - S10 (CLI side): the argparse type _setting turns StyleError into ArgumentTypeError so its message reaches the user. Docs: README (exit codes, encoding, -o extension, error classes), docs/architecture.md (errors). Tests: tests/test_wp2_cli.py (new); no existing test or golden changed.
…ed symbols (R1-R4)
R2: Babel calls for number/money/duration run in a decimal context whose
precision is max(28, len(value) + 8). Amounts of 27+ digits no longer raise
InvalidOperation and fractions over 28 digits are no longer rounded (§10.3).
R3: durations are formatted like number() (every written digit, locale
separators) and the plural category is taken from the displayed value
(Locale.plural_form on the Decimal: "1.0 days", cs "2,5 roku"). Babel's
format_unit, which rounds to 3 decimals, is no longer used.
R1: the CLDR pattern is looked up through one helper (Locale._data is the only
access; Babel 2.x has no public API): requested length, other lengths, the
language-only locale, then English. A raw "duration-day" is never printed.
Formatter.format_duration returns DurationText(text, english_fallback); the
resolver emits one render-locale-fallback Warning per (locale, unit) when
English stands in. Formatter.duration keeps returning str.
R4: an alphabetic currency symbol or code touching a digit on either side gets
U+00A0 in symbol and code mode ("CZK 5.00", "CHF 1.00"); "$5.00", "R$ 5,00",
"CA$5.00" and cs "5,00 Kč" are unchanged.
New: tests/test_wp1_values.py. No existing test or golden changed. Docs: ADR 0005
and docs/architecture.md.
…tives, dropped-comment spacing, {{term:}} text (B2, B3, B4, B8)
- B2: a list item whose model blocks are all comments/raw HTML (kind "html")
is left out, after the whole list is traversed so item numbering and
PlacedMarker.item stay aligned; a list left with no items and an ordinary
quote with no built blocks are left out. A truly empty item and an empty
drafting note keep their place. (build.py _list, list, quote)
- B3: every directive sentinel is wrapped in U+2E31 (category Po, accepted by
markdown-it's isPunctChar) on both sides, as the braces it replaces are
punctuation, so `_text_{{ref: a}}`, `{{ref: a}}_text_`, `__b__{{ref: a}}`
and `*t*{{ref: a}}` parse as with `{x}`. sentinel_re, encoded_sentinel_re
and _restore consume the wrappers; the hidden form's "h" tag is no longer
needed. (build.py)
- B4: _merge_text strips the spaces before a dropped inline when a line break
follows, so no double space is left.
- B8: Resolver._survey collects definition id -> plain rendered term text
(first definition wins, via _title_text); {{term:}} uses it, falling back
to the validator's definition_lookup.
Existing test changed: test_empty_list_items_keep_their_place is extended
(an empty item still keeps its place; a comment-only one goes). Goldens are
unchanged. Docs: CONFORMANCE.md, docs/architecture.md.
render_file() read the file with read_text, so a non-UTF-8 file raised a
bare UnicodeDecodeError. It now reads bytes and raises DocumentError with
the CLI's wording ("not valid UTF-8 (byte 0x.. at offset N)").
Behaviour change: DocumentError is not a ValueError, so code that caught
UnicodeDecodeError/ValueError from render_file for this case must catch
DocumentError (or RenderError). OSError for unreadable files is unchanged.
Test: tests/test_wp2_cli.py::test_render_file_raises_document_error_for_non_utf8.
money with a currency showed the larger of the currency's minor units and the written digits only when the last written digit was non-zero: "1.500" EUR gave "€1.50" and "1.1230" gave "€1.123". The fraction digit count is now fixed in the pattern as max(minor units, written decimals) with currency_digits off, in symbol, code and name mode, under _exact. "5" EUR is still "€5.00", JPY "5" is "¥5", JPY "0.125" is "¥0.125". Tests added to tests/test_wp1_values.py; no existing test or golden changed.
`"Self {{term: s}}" {{def: s}}`, and cycles through several definitions,
raised RecursionError in Resolver._term (already so before WP4, through
_plain_value of the validator's raw term). The term-text computation now
tracks the definitions it has open (the _survey pre-pass and the
_plain_value fallback, _plain_open): a {{term:}} to an id already open shows
its label=, else the definition id, and goes no further. The validator 0.4.0
reports nothing for such documents; the spec's definition-circular Warning
is not emitted by it, and no diagnostic is added here (CONFORMANCE.md notes
it).
Tests: direct and indirect cycles, at the definition site and at reference
sites, in the text and HTML writers.
A comment-only list item is now dropped only when the markers give it no anchor identifier and no condition. The validator does not place a marker on such an item, so this is defensive; the test drives the builder with a made-up marker.
…es, template fields (R6, S7-S11)
- R6: Resolver._placeholder renders [TBD: id] when the effective
placeholders.blank is empty or whitespace (spec §13.5 rule 6); no StyleError.
- S7: extends cycles are detected on resolved paths (Path.resolve), so
../dir/a.yaml, a second spelling, or a symlink no longer recurses until
"File name too long".
- S8: dump_style drops `extends`, so a --print-style dump is self-contained and
reloads from any directory.
- S9: overriding `version` or `extends` is a StyleError (it was silently
ignored, and --print-style could print an unloadable style).
- S10 (loader side): a YAML error in a --set value raises StyleError naming
the key and the YAML problem (with line/column) instead of resurfacing as a
misleading type error; reaches stderr through WP2's argparse wrapper.
- S11: model.TemplateFields / template_settings inventory the {field} names of
every text template setting (verified against numbering.fill,
Resolver._ref, fill_condition); the loader reports unknown
identifier-shaped fields and missing required ones, naming the setting
(labels.condition must contain {condition}, §15.8). Other braces stay
literal. Built-in styles and labels pass.
Existing test changed: tests/test_style.py::test_dump_round_trips now compares
with extends=None, because the flattened dump no longer records what the
style extended (S8). Docs: style-templates.md (field table, flat dump,
overrides), CONFORMANCE.md, architecture.md.
…back (S11 follow-up)
The required-field check no longer refuses an empty template for a setting
the resolver already replaces when empty: `labels.condition or "{condition}"`
(Resolver._condition) and `labels.attachment_file or "{file}"`. An empty
labels.condition shows the bare condition, which meets §15.8, and refusing it
would break existing styles for no gain. TemplateFields.empty_falls_back marks
exactly these two settings. Settings with no fallback (references.format and
the numbering/enumeration/paragraph refs) still require {designation}/{n};
unknown fields are checked in every non-empty template.
Tests: new tests for the empty condition label (renders the bare condition)
and for empty templates without a fallback. docs/style-templates.md updated.
…ty links (B6, W3, B9/W8)
B6 (§2.3): an image whose path has a URI scheme (data: included), starts with
/ or \ (//host too) or is a Windows drive path is replaced by its alt text in
the resolver (Resolver._resolve_inline) and reported as the Core Error
path-not-relative, which --strict refuses. The validator 0.4 does not check
image paths; the renderer's report is skipped when the validator names the same
path. ../ stays allowed (no default document root). The builder now keeps
javascript:/file:/data: links and images as nodes (markdown-it validateLink off)
so they can be reported and shown as text instead of as literal Markdown.
W3 (§8.7): the HTML allow-list becomes a deny-list (javascript, vbscript, file,
data) after the WHATWG normalisation; new urls.py shared by resolver and both
writers so they agree. A refused link is its text only in both writers.
B9/W8: an empty href is just the link text; a link with no text shows its URL;
the text writer omits <url> when it equals the text minus mailto:/tel:.
Changed goldens: features.{default,continental,outline}.txt and
features.default.html: the "[an unsafe one](javascript:alert(1))" source text is
now "an unsafe one" (the link is refused, its text kept).
… gate An image with an empty or whitespace-only path is replaced by its alt text in the resolver (no <img src="">, which requests the page itself) and reports nothing: §2.3 has no rule for an empty path. Tests for both writers. urls.py, html.py and docs/architecture.md no longer say markdown-it refuses unsafe schemes while parsing: its validateLink is off in the builder, and the deny-list is the gate for every href/src the HTML writer emits (Link, Image, Value).
…ms, blank labels (W1, W4-W7)
W1: TextWriter takes the style's attachments.separator (api._writer hands
it over; TextWriter() keeps the default style's "page-break"). "none" prints
no separator; "rule" and "page-break" both print "---" before EACH
attachment (plain text has no pages). The "---" that used to precede the
"Attachments" heading is gone; the heading now stands directly after the body.
W4: "|" in a table cell is escaped as "\|"; the separator row carries
alignment colons from Table.align (":---", ":---:", "---:"), unpadded.
W5: _prefix gives a blank line the right-trimmed prefix, so one quote stays
one quote (">" and "> >" on blank lines). Blocks of a list item are separated
by a blank line; a nested list directly after a paragraph (or another list)
stays tight as before.
W6: an item's content indent is len(label or bullet) + 1, whatever its
condition label.
W7 (text only): a style-blanked label drops the "label: " prefix in the title
block and party details. HTML keeps its empty <dt>.
Existing tests changed because of W5:
- test_nested_lists_line_up_with_their_item_s_text: blank line between the
item's paragraphs.
- test_a_later_paragraph_in_a_list_item_is_the_item_s: blank line before the
item's second paragraph.
- test_a_heading_in_a_quote_or_item_is_a_bold_line_not_a_section: the
two-block quotes printed as two quotes; now one quote with a ">" line.
Goldens changed (features.default/continental/outline.txt, each hunk read):
- W1: "---" moved from before the "Attachments" heading to before each of the
two attachments.
- W4: the table separator row is now "|:---|---:|:---:|" (the document's
table is left/right/center aligned).
template.default.txt is unchanged.
…ceholder inside it is kept
The term text of a {{term:}} was collected in Resolver._survey with a text-only
pass, before sections and anchors were registered and before inconsistent
placeholder types were known: a {{ref:}} in the term was dropped ("Fee per")
and a placeholder used with conflicting types showed a blank. _survey now
records only the first DefinitionSource per id; Resolver._term_text resolves its
children during inline resolution (all targets known), takes plain text and
caches it, under the existing _terms_open cycle guard. It restores used_anchors,
so a definition nested in a term keeps its document-order anchor; before targets
are known (a none-scheme heading) the text-only pass stands in, uncached.
_warn and _not_relative deduplicate, so no repeated diagnostics. Docs updated.
No existing test or golden changed.
R5: a list item in the preamble has no section, so `{section}` was filled
with "" and the continental label read ".1" while its designation was "1".
`numbering.fill` now removes `{section}` and one following "." when the
section is given as the empty string, so the label is "1". Items in a
section keep "1.1". The text and HTML output of a preamble list change
(".1 first pre" becomes "1 first pre"); no golden contains one.
W2: the print stylesheet broke before each `.ld-attachment`, so the
"Attachments" heading was stranded at the foot of the previous page. The
break now goes before the `.ld-attachments` container (its heading goes
with the first attachment), before each later attachment, and the heading
has `break-after: avoid`. Goldens features.default.html and
template.default.html changed only in those three CSS lines
(`--update-golden`, hunks inspected).
Tests: tests/test_wp7_small.py. Docs: CONFORMANCE.md (preamble label) and
docs/style-templates.md (`{section}` in the preamble).
…ck to 'other' (review: values)
Babel's plural operands convert the fraction digits with int(), which Python
limits to 4300 digits; {{duration:}} and name-mode {{money:}} with a longer
fraction raised a raw ValueError. values._plural_category catches it and uses
'other' (the CLDR category of a non-integer in nearly every locale); name-mode
money puts Babel's long-name pattern together with 'other' when its own lookup
would fail. sys.set_int_max_str_digits is process-wide and is not touched.
…ature labels (W1, W7 follow-up) W1: with rule/page-break the text writer prints "---" before the attachments heading (as it did originally) and between consecutive attachments, not between the heading and the first attachment, so the heading stays with what it heads. With none, no "---" at all. Updates the TextWriter docstring, CONFORMANCE row and docs. W7: the blank-label rule also covers the signature blocks (name, title, date, place): a blanked label prints the value alone, and a row with neither label nor value is omitted instead of a lone ":". Goldens (features.default/continental/outline.txt): the "---" moves back before the "Attachments" heading, relative to the previous commit; a "---" now also separates the two attachments. Tests in tests/test_wp5_text.py updated and extended for both changes.
…sage error (review: cli) contract.v2, report.2025-10-27 and .out failed with exit 2 since the S6 fix. Now only .docx .doc .odt .rtf .pdf .md .markdown .epub .tex (any case) need --format; any other extension, or none, keeps the html default. -o and -f --help and the README state the exact rule. tests/test_wp2_cli.py: the S6 parametrization grows to the full list, and tests for the kept default, known extensions and help.
…LF everywhere (review: cli) main() reconfigured sys.stdout/sys.stderr on every call, so an in-process main([...]) changed the caller's streams for good. It now does so only when argv is None (the console script and python -m call it so). Standard output is reconfigured with newline="\n" and -o files are written with newline="\n", so no platform translates LF to CRLF. README states both. tests/test_wp2_cli.py: comments on the exact-byte comparisons (LF on every platform). New tests in tests/test_wp9_review_fixes.py. No golden changed.
…nsions, streams and newlines
…ations (S13, S14, S15, B1, B5, B7, B10, R7/B11, W9/S12)
S13 labels (style/labels.py): the party identifier label is shown for §3.4
identification_number, which natural persons may have too. cs "IČO" ->
"Identifikační číslo", sk "IČO" -> "Identifikačné číslo", fr "Représentée par"
-> "Représenté(e) par"; pl "Reprezentacja" kept (no gendered form). Existing
test changed: tests/test_render.py French label assertion. No golden shows
these labels. New tests/test_wp8_labels.py.
S14: README and __init__ docstring examples open files with encoding="utf-8"
(and write with newline="\n"); the --locale example says it formats values and
README states labels follow the document language; README links are absolute
GitHub URLs for PyPI; CONFORMANCE.md added to the sdist (checked with
python -m build --sdist). docs: locale vs language wording in concepts.md and
style-templates.md.
S15: dropped the deprecated License classifier (license = "MIT" is SPDX);
.github/PUBLISHING.md recommends gating the publish workflow on the tests
(workflow file not changed).
DOC rows, each verified by running an example: CONFORMANCE.md Known
limitations for text after a comment (B1), directives in link/image
destinations (B5), the exact nesting-limit rule (B7), table cells beyond the
header (B10), parties block vs {{party:}} (R7/B11); architecture.md Security
rewritten for trusted style input (W9/S12), link-gate sentences de-duplicated.
…pe controls (review wave 2) - urls.LINK_SCHEMES (http, https, mailto, tel, ftp, ftps, sftp, sip, sips, sms, urn, geo, news, plus scheme-less URLs) replaces the DENIED_SCHEMES deny-list: protocol handlers (ms-msdt:, vscode:, ...) and pseudo-schemes (C:\x, localhost:8080) render as their text. One predicate, is_link_target. - resolver: a refused non-empty link is the Warning render-link-refused. - urls.printable escapes C0/C1, DEL, Cf, U+2028/9 in quoted paths/URLs; used in path-not-relative and the new warning (no ESC/BEL/newline forged on stderr). - path-not-relative dedupe matches the quoted '<path>'; message is generic. - A link with no text shows the URL as written (unquoted), in both writers. - tests/test_wp3_links.py: magnet: is no longer a link; 'a:b' is no longer a safe href (allow-list).
- template_settings: a ref is required to contain {n} only where it is read
(numbering unless scheme none; enumeration when enabled or ordered:
enumerate; paragraphs when numbered). Unknown fields are still flagged.
- Plain labels (labels.* but condition and attachment_file) substitute
nothing, so braces in them are text and they are no longer checked.
- tests/test_wp6_style.py: labels.drafting_note/colon with fields are valid;
paragraph format cases set paragraphs.numbered.
…t file reference findings are reported once CONFORMANCE.md, docs/architecture.md and docs/style-templates.md describe the heading as rendered, the cycle and limit Warnings and their lines, the nesting limit for terms and its scope, and the new test module. The rule lists (module docstring, architecture, line granularity in CONFORMANCE.md) name render-designation-cycle and render-designation-limit.
The assembler does not know the file it was given, so its findings (answer-unknown, answer-invalid, answer-missing) had an empty file even when RenderOptions.filename was set. _assemble now stamps the file name on every diagnostic that names none, on the refusal and on the result (§16.9). Tests extend the assembly test with the warning and the refusal.
A directive in a party's legal_name, label, address, identification_number, date_of_birth, custom field or a representative's name or title was reported at the party's first line. Each field is now resolved at its own key (the signature blocks too); a value the source writes elsewhere, as a << merge does, is at the line it is written. tests/test_diagnostic_lines.py: the party/side test now expects the address key, not the party's line.
…ne order The resolver works out the body before the title block, so a finding seen first in the body and again in the title was reported at the later line. diagnostics.record now keeps one finding (rule, level, message) at the lowest line it was seen at, in Resolver._warn and Resolver._reference, and api.render lists the renderer's own diagnostics in the order of their lines (none last, stable) after the validator's. CONFORMANCE.md and docs/architecture.md say so.
filename is documented as the name without its directory, but render(filename='a/b.lgd') kept it on every diagnostic's file. render now keeps the last path component, split at both / and \.
… root A path with NUL or another control character (after the URL decoding that applies to an image's source) is path-not-relative, so '..%00/x.png' and an include of 'a<NUL>b' no longer pass as plain names. In the walk of a path against the root a segment of '..' followed by dots or spaces counts as '..' (Windows drops trailing dots and spaces), so '... /x' and '..../x' leave the root; 'a..b.png' and '...hidden/x' are names as before. CONFORMANCE.md and docs/architecture.md describe both, and the limitations that a file name with a colon before any slash reads as a URI scheme (write ./report:v2.png) and that an attachment's file is read as a URL only where it becomes a link.
…sted mode A style value written into the stylesheet (fonts, sizes, colours, margins, page size, max width) containing /*, */, </, a backslash, a control character or a line break is now a StyleError naming the setting, in every mode: rejected rather than stripped, since stripping one token can form another. html.extra_css containing </style (any case) is refused. writers.html._css, the last line for a hand-built Style, now also strips backslash, @, control characters and comment delimiters (a lone / stays so calc(100% / 3) survives). load_style(trusted=False) is for a style from somebody else: a built-in name, a Style, a mapping or YAML text, never a file; extends only a built-in; strict grammars per kind of value (declared as field metadata in style/model.py, enforced in _convert, so a Style object and override objects are held to them too); no html.extra_css; YAML text of at most 64 KiB without aliases; messages that name the setting and never quote the style or a path. RenderOptions.untrusted and --untrusted require a document root (OptionsError, exit 2) and load the style untrusted. Defaults are unchanged. No existing test or golden changed.
README section on rendering documents you did not write, the Security and style template chapters, and the conformance notes.
Keeps both: the none-scheme designation pass and the lowest-line rule for repeated findings. The designation pass's diagnostics go through the same Resolver._warn, and its rollback restores the diagnostics as they were (a repeat may have lowered the line of one). CONFORMANCE.md's line-number paragraph carries the designation findings' lines and the repeats rule.
The control-character check ran on the path before what a browser strips at either end was removed, so a YAML block or folded scalar (which ends in a newline), as in 'file: >' for an attachment, was a false path-not-relative. The path is now read as before (tabs and newlines ignored, control characters and spaces at either end stripped) and only a control character left inside it (NUL, ESC, ...) is path-not-relative. Tests: block and folded scalars give no finding; interior NUL and ESC, and '..%00/x.png' as an image, are still refused.
…in its size PyYAML's scanner takes time quadratic in the nesting depth of a flow collection: a 40 KB text of nested brackets took 45 seconds to refuse in untrusted mode. Styles are now read with a SafeLoader that refuses more than 32 levels (flow and block) as the scanner reaches them, trusted or not; a mapping or Style passed in is held to the same depth, and RecursionError stays only as a last resort. The nesting test counts the scanner's work instead of timing it, which also brings the suite back from 110 s to 60 s.
…e outermost term, keep the item piece of a cut reference A heading read from the text of a term that the heading itself uses was memoised with the term cut short in it, so a reference to it showed a text that differed from the heading with no Warning. Its text is not kept now, it is worked out again where the term is not open, and render-designation-cycle reports it. The nesting limit counted the levels the outer block already had, so a term nested about 58 levels in a block, whose definition used another term, showed the inner id with render-term-limit. It now counts from where the outermost open term began (or from the heading, while designations are worked out). A reference to an item or paragraph under a heading longer than 1,000 characters cuts the heading part and keeps the item piece, and says in its own message that the reference shows the first 1,000 characters. The validator's text of a term is searched for a reference only for a term whose definition is not in the body. The docs state when designations are worked out, that a ring of more than 32 headings is reported as a limit, that a heading with 59 or more nested levels can reach the nesting limit after one hop, and that a frontmatter value shown in a heading is not designated through. Tests: the repro, the nesting at 58 and 98 levels in both schemes, the cut reference, and every heading that reads differently from its designation being reported, over random tangles of headings, items and terms.
YAML numbers, booleans and timestamps are read through guarded constructors: text PyYAML cannot convert, or a number of more than 32 characters, is a style error (it raised ValueError or KeyError quoting the text, or a traceback on the command line). Enormous numbers in a mapping, as keys, values or overrides, are refused without str(). Whatever else the loader raises on an untrusted style is reported as one generic style error; a heading level out of range no longer quotes its key. values.date: an untrusted style names short, medium, long or full; a trusted pattern is formatted on fixed dates when the style loads, so one Babel refuses (GGGGGGG, QQQQQQ) is a style error instead of an exception at every render. An untrusted style's free-text settings are at most 200 characters (a long references.format multiplied the output by the number of references), and NUL and lone surrogates are refused in them and, in every mode, in the settings written into the stylesheet. A message lists 20 problems at most. --untrusted --print-style loads the style untrusted; the --untrusted help says a style may be a name or YAML text. stylesheet() writes any </ in extra_css as <\/. Docs describe all of it. No existing test changed.
…and PDF renumbered v0.4 is now the hardening release; DOCX moves to v0.5 and PDF to v0.6. The upstream prerequisites table records what the renderer now reads from the validator (source positions) and what still waits on it (link reference definitions, the path and definition-circular rules, wide table rows).
The Node 20 action majors triggered the runner deprecation warning on every run. Move to the latest majors, which run on node24: - actions/checkout v4 -> v7 - actions/setup-python v5 -> v7 - actions/upload-artifact v4 -> v7 - actions/download-artifact v4 -> v8 Inputs used here (checkout repository/ref/path, setup-python python-version and cache, artifact name/path) are unchanged. The download-artifact v8 digest check defaults to error; the artifacts are produced and consumed by the same workflow run, so it is not affected. pypa/gh-action-pypi-publish is a composite action and is left as is.
Tests now run on Ubuntu and Windows with Python 3.13. Lint, conformance and validator-main also use 3.13. Update the PUBLISHING.md note that describes the CI matrix to match.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes from a code review of the renderer against the LegalDown specification 0.2, followed by the known gaps the review left documented. No version bump: this is meant to go into 0.4.0, which is not released yet.
Correctness
render-locale-fallbackWarning.decimal.InvalidOperation). Long fractions are no longer rounded (§10.3).{{term:}}shows the term as rendered, with its Markdown and any{{ref:}}inside it resolved.continentalno longer labels preamble list items ".1".[TBD: id](§13.5).RecursionError. 0.3.0 crashed on these.render-term-too-long).render-term-limit).nonescheme (§13.3): a{{ref:}}renders the target's heading text as rendered, with the refs and terms in the heading resolved. Before, the refs were dropped. Items and paragraphs under such headings follow.render-designation-cycle.render-designation-limit.File references and documents from others (§2.3)
{{include:}},attachments[].file,amends.file,supersedes.file,translationsvalues and image paths.path-not-relative(Error) applies always.path-outside-root(Error) applies with a document root (RenderOptions.document_root/document_path, CLI--root).{{attach:}}that fails it is not a link.requires_config.RenderOptions.untrusted/--untrusted): requires a document root and loads the style as untrusted input.Styleobjects or in-memory YAML/mappings only.extendsmay only name a built-in.extra_css, no YAML aliases, YAML size, nesting depth and number size are capped, and free text is limited to 200 characters./*,*/,</,\, control characters) are refused, and so is</styleinextra_css.Links and images
<img>.ms-msdt:,javascript:,data:,file:,C:\x) renders as its text with arender-link-refusedWarning. CONFORMANCE marks §8.7 links as a documented partial deviation.Diagnostics
RenderOptions.filename, set byrender_fileand the CLI.Plain-text writer
attachments.separatoris honoured.|, and the separator row carries the column alignment.:.CLI and API
render_fileraisesDocumentError.-owith a document extension the renderer does not write (.docx,.pdf,.md, …) is a usage error.StyleErroris now also aRenderError, and still aValueError.OptionsError.Style validation
{field}is an error.{designation},{condition}, and{n}in settings in use) is checked.extendscycles are detected on resolved paths.--print-styleoutput is self-contained.versionandextendscannot be overridden.--setis reported.CI, docs, labels, packaging
Behaviour changes for callers
StyleErrorderives fromRenderError(it is still aValueError), andOptionsErroris new.render_fileraisesDocumentErrorfor non-UTF-8 input.overridesmay not containversionorextends.{field}, or without a required one, is refused./*,*/,</,\or control characters are refused.-o x.docx|.pdf|.mdexits 2.--strictrefuses them.lineandfile, and are listed in line order.Test plan
pytest -q: 2149 passed withLEGALDOWN_SPEC_DIRset to a LegalDown checkout, including 200 specification examples and fixtures.ruff check .is clean.