Repository navigation
Renderer v0.1: HTML and text output for LegalDown 0.2 - #1
Merged
Merged
Conversation
…cisions Design phase only, no code. Documents the pipeline (parse/validate via legaldown-validator, build, resolve, write), the format-neutral resolved document, the planned package layout, the style template draft, the milestones, and the upstream changes the renderer needs first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Implements the pipeline from docs/architecture.md on top of legaldown-validator 0.2.0: - build: source -> render tree. The LegalDown layer (directives, defined terms, markers, identifiers) comes from the validator; CommonMark structure from markdown-it-py, joined through sentinels, with an outline check that both parsers agree on the sections. - resolve: numbering under four schemes and custom level formats, list and paragraph labels, item designations such as 2.1(b)(i), anchors, every directive with its failure marker, the template view, title block, attachment placeholders, and signature blocks. - style templates: a validated dataclass model, layering through extends and per-job --set overrides, three built-in styles, and labels in English and Czech. - writers: plain text (the test oracle) and a self-contained HTML page. - legaldown-render CLI, golden and unit tests, a conformance run over the specification's examples and fixtures, and CI. Docs updated to match: architecture, styles and preferences, roadmap, CONFORMANCE.md, and ADR 0006 (styles vs render options). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- HTML: judge link schemes as a browser parses the URL (tabs, newlines,
and edge control characters removed), so "java\tscript:" is not a link.
- Build: a defined term's sentinel spans its quotation marks only, so
emphasis around it keeps pairing with its closers; sentinel characters
already in the source are protected instead of read as indexes.
- Resolve: an alternative section restarts its subsection numbers;
alternative paragraphs and list items share a number like sections;
drop a dead anchor fallback.
- Text writer: an item that opens with a nested list prints its label
first.
- assert_resolved raises InternalError (CLI exit 70), not TypeError.
- Style overrides: --set values are typed by the setting they target, so
text settings keep "[__]", "{designation}", or " | " as written;
dotted and nested overrides merge in order and copy the caller's values.
Each finding has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Build: sentinels carry a random per-build nonce, so no document text
(literal, entity, or percent-encoded) can pass for one; this replaces
escaping the sentinel characters. A {{def:}} and its spacing become a
hidden payload, so literal restoration (code, URLs) gives back the
source exactly. Emphasis that wraps only a defined term goes with the
term, leaving its look to the style (§7.2); markers that pair with text
beyond the term stay. No spacing constant is copied from the validator.
- Link and image titles are inline content, so directives in them resolve.
- Resolve: alternatives share the first one's number wherever they are
among their siblings, and only when their conditions are mutually
exclusive (the validator's exclusive(), §15.4).
- --set: YAML-quoted text values are unquoted again.
Each finding has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Build: the hidden {{def:}} sentinel is led by a punctuation character,
so an underscore emphasis closer before it still closes. Definition
spans store what they are made of (marks, emphasis) and derive the
rest, and _protect makes one pass over the sorted directives.
- A defined term in a link title or alt text shows its term but no
longer takes the definition's anchor, so {{term:}} links always have a
target.
- Alternatives: sections share a number exactly when the validator's own
numbering says so, so rendered numbers match ValidationResult.sections.
Paragraphs and list items follow the same rule: adjacent, same id, a
valid condition, and a full presence (own and enclosing conditions)
exclusive with every unit already holding the number.
- --set: a quoted text value that is not valid YAML is a usage error;
documented how to keep quotation marks in a value.
Each finding has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Markers are placed exactly as the validator places them: in the
preamble only a paragraph's condition, and only in a template; never
on a preamble list item. Whether a document is a template is decided
as the validator decides it, in the builder.
- Hidden sentinels have their own form, so a literal U+2E31 in the
source is never taken into one (an include paragraph keeps its anchor).
- Headings are numbered with their level clamped to 1-5, as the
validator numbers them.
- Definition anchors are given out in one place; {{term:}} links are
settled after resolution from the anchors actually assigned. This
replaces the hand-kept definition_ids predicate and its duplicate tree
walkers (new tree.map_tree_inlines).
- The none scheme's designation comes from a text-only pass: no anchors
spent, references left out at any depth; titles are resolved once.
- Terms nested anywhere in a title or alt text take no anchor.
- --set: only a single quoted YAML string is unquoted; any other value,
quotes included, is kept as written.
Each finding has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Builder: a malformed {{include:}} is not an include-only paragraph, as
in the validator's is_include_only; a {{choose:}} in frontmatter makes
a template (read over the validator's own frontmatter fields); a later
paragraph of a list item places its marker, as the validator's model
reads it as top-level (legaldown-validator#14), designated as its item.
- The template flag lives on the builder instead of being threaded
through every block call.
- Resolve the preamble before the sections, so the first definition of a
term in document order keeps the anchor.
- Headings: five levels throughout. A level-6 heading is stored clamped
to 5 like the validator numbers it, so writers nest it where its number
puts it; schemes, heading styles, and validation cover levels 1-5.
- --set: only one pair of matching outer quotes without that quote inside
is removed; nothing else is interpreted (no escapes, no comments).
Each finding has a regression test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
The render tree is now built from legaldown-validator's Document only (ADR 0007, superseding the markdown-it-py half of ADR 0002): - Sections, blocks, list items, quotes, tables, and code come from the validator's model; quote content and code inside items are read by the validator's parser. - Markers are placed exactly where the validator's find_markers() places them, and the template decision is the validator's own formula over those markers. The mirrored position rules, the outline check, and the template re-derivation are gone. - markdown-it-py stays for inline Markdown within one block's text only; the sentinel machinery for directives is unchanged. - All imports beyond the validator's public API live in validator_bridge.py, the list for roadmap item U3. Documents that crashed the outline check (a heading inside an HTML comment, a "Signature Block" heading) now render as the validator reads them, and anchors, markers, and the template view always agree with its diagnostics. Until the validator's model keeps more (legaldown-validator#14), lists render one level deep, table alignment is not kept, and paragraph line breaks are joined; CONFORMANCE.md lists these. Also: numbering.levels accepts at most five formats; counters are sized for five levels; the resolver's loose-paragraph path is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Quote content and item code are read by the validator's parser as a
body: never as frontmatter, never cut at a signature-block heading, and
errors fall back to a paragraph.
- Cutting a marker keeps the space after a lifted {{ref:}}/{{term:}}, and
the marker is located with comments blanked, so a copy inside a
trailing comment is not taken for it.
- An item opening with a quote or a code fence renders them (a drafting
note included), instead of literal text.
- A paragraph that is an HTML block is dropped whole, text between its
tags included; a joined backslash hard break is a line break again;
table rows are padded or trimmed to the header's width.
- One lexer cache per render, shared by the bridge and the builder.
- The copied template formula is checked against the validator's own
value (captured from validate_document) on every example, fixture, and
test document.
- CONFORMANCE.md and the roadmap link the validator issues filed for the
parser gaps the renderer inherits (#21-#25) and the API it needs
(#26, #27).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Removes the three workarounds the eighth review found fragile, and follows the validator's model instead, with each gap documented and filed upstream: - No HTML-block detection: it dropped real text joined to a comment line, and anchors with it. Every tag is still dropped and reported; the text between an HTML block's tags now renders (legaldown-validator#23). - No hard-break rewrite: it fired on any backslash-space, in titles and cells too. A joined backslash break shows as written (legaldown-validator#25). - No signature-heading split in quotes and items: it cut fenced code and missed setext and conditional headings (legaldown-validator#24). - The unreachable parse fallback is gone; quote and item text is still read as a body, never as frontmatter. - The validator-template spy moved from the package into tests/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Strip a comment left open in a paragraph up to its closing "-->" across the following blocks, within one section, quote or item: the one documented exception to following the validator's model (§8.6) - Leave one space where inline HTML or a comment was dropped - Count the raw-html Warning per text with dropped HTML, not per tag - Stop pinning validator bugs #24 and #25 in tests; assert only the content that must render - Assert no escaped script tag is emitted either - Bind the template spy's arguments by signature - Fix stale comments and CONFORMANCE rows (comments, extended tables) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Take an opener as a comment only when it is not escaped and not the empty comment <!--> or <!---> (complete in CommonMark) - Strip a comment opened in a list item, through the items and blocks that follow; a comment ending inside a list keeps the remaining items - Leave a space where a <br> between two words was dropped - Skip marker placement for blocks read only for a comment's end - Document the two gaps left until the validator's model drops such comments (legaldown-validator#23), and empty comments (#28) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
The validator's final check reports a remaining blank (placeholder-unfilled) or template construct (template-construct-present) as an Error; with --strict, such a document is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
contents: { enabled, depth (1-5), attachments } lists the sections down
to the depth, then the attachment placeholders, after the title block.
Entries hold plain text, so no link, definition, or anchor from a heading
is repeated; in HTML each entry links to its section. Conditional
sections carry their condition label in the template view. The
"Contents" label follows the document language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Every generated word follows the document's language (§13.7). French
labels put a no-break space before the colon. A test checks that every
built-in language sets every label and keeps its {condition} and {file}
fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- Contents: filter and indent by how deep a section's number goes, not its heading level; keep each heading's formatting (prompts, failure markers) but none of its links or anchors; list attachments under their heading; leave out an empty heading; emit the contents CSS only when the contents are enabled - Reject a boolean or float for an integer choice such as contents.depth - A per-language labels.colon between a label and its value, so French output spaces every colon - Render the represented_by label: a representative is a labelled row of the party's details - Export Contents from legaldown_render.style; mention the questions key in the --final help Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- A representative with only a name or only a title renders without a stray comma - An empty labels.colon is kept, not replaced by ": " - Without an attachments label, the contents list attachments at the top level instead of under an empty entry - A term defined in a heading shows in the contents in its style, not as a second <dfn> - The attachments heading has an anchor (ld:attachments, which no identifier can take), linked from the contents - A section's contents depth is the depth of the validator's number - The contents writer no longer guards against links unlinked() has already removed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
- An identifier with a colon (invalid, reported by the validator) never becomes an anchor, so it cannot clash with the renderer's own def: and ld:attachments anchors - A defined term in a heading keeps its formatting in the contents, as a span instead of a second <dfn> - An empty attachments label leaves out the attachments heading - A representative with neither name nor title has no row - Date and Place keep a colon's trailing no-break space - Drop two fallbacks that could never apply Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt
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.
Summary
This is the first version of
legaldown-render, the reference renderer for LegalDown specification 0.2. It aims at conformance level Rendering (§17.3). Parsing and Core validation come fromlegaldown-validator0.2.0. The renderer builds only from the validator's document model, with one parser (ADR 0007).Not for release yet. Several validator fixes should land first; see the known limitations below.
Pipeline
parse_documentandvalidate_document.{{choose:}}, drafting notes, alternatives sharing a numberhtml(escaped throughout, with CSS generated from the style) andtext. The text writer is designed to serve as the test oracle.Styles and preferences
extendschain, then the chosen style, then--setoverrides. Every value is validated, and a typo gets a suggested key.default,continental,outline.contentssetting (enabled,depth,attachments). Entries are plain text, and each one links to its section.Final check (§15.9)
--finalorfinal=Truepasses the validator's final check through: a remaining blank or template construct is an Error. Combined with--strict, such a document is refused.Interfaces
render()andrender_file().legaldown-renderCLI.Docs
CONFORMANCE.mdKnown limitations (from the validator's model)
These are tracked on the validator and listed in
CONFORMANCE.md:The one deliberate exception to building from the model: a comment left open in a paragraph or a list item is stripped up to its
-->, within one section (§8.6).Testing
ruff check .is clean.🤖 Generated with Claude Code
https://claude.ai/code/session_011ZFB1eaR4Fv8vUr2y8RXQt