Custom markup language for legal documents
Files and images attached to this public thread are only available to signed-in users.
Sign inThis is an excellent project idea. Legal documents have rigid hierarchical structures (Parts → Sections → Clauses → Sub-clauses) that general-purpose markup languages like Markdown handle poorly.
Key features to consider for your language:
- Explicit hierarchy - sections know their level and parent
- Automatic numbering - "Section 3.2.1" computed, not hardcoded
- Cross-references -
@ref(section:definitions)that update automatically - Definitions - first-class support for defined terms
- Schedules/Annexes - separate numbering schemes
- Recitals/Whereas clauses - lettered lists (a), (b), (c)
- Metadata - document type, jurisdiction, version, effective date
Rough syntax sketch:
@document(type: "policy", version: "1.0")
@definitions {
"Controller" = "the party responsible for..."
"Data Subject" = "an identified natural person..."
}
@part(id: "general") "General Provisions" {
@section(id: "scope") "Scope" {
This policy applies to all processing of personal data.
@clause {
The @term(Controller) must ensure compliance.
}
}
}
Before diving deeper: what document types do you work with most? And are there specific pain points (like broken cross-references, manual renumbering) you'd especially want solved?
Excellent, this clarifies things nicely. The separation of content from styling (like CSS for HTML) is the right approach.
Proposed language: LegalMark (.lm files)
---
title: "Data Protection Policy"
version: "2.1"
date: 2026-01-29
style: "company-standard.lms" # separate style file
---
@section "Introduction" {
This policy sets out the requirements for data protection.
@section "Scope" {
The policy applies to:
@list {
@item {
All employees, including contractors.
This includes temporary staff engaged for any period.
}
@item {
All systems processing personal data.
}
}
}
}
@appendix "Definitions" (id: definitions) {
@define "Personal Data" {
Any information relating to an identified natural person.
}
}
# Cross-reference example:
See @ref(definitions) for defined terms.
Key design decisions:
- Braces
{}for explicit block scope (solves multi-paragraph bullets) - Nesting depth = numbering level (1 → 1.1 → 1.1.1)
- Appendices get letter numbering (A, B, C)
- Styling in separate
.lmsfiles
Want me to write up a formal specification document for this syntax, or shall we iterate on the design first?
Good catches. Here's the expanded syntax:
Escaping: Backslash escapes: \{ and \} for literal braces.
Images and Links:
@image "diagrams/org-chart.svg" (
id: org-chart
caption: "Figure 1: Organisational Structure"
width: 80%
)
@link "https://ico.org.uk/guidance" "ICO Guidance"
# Or inline:
See the @link("policies/related.pdf", "Related Policy") for details.
Tables:
@table (id: roles, caption: "Data Protection Roles") {
@header { Role | Responsibility | Contact }
@row { DPO | Oversight and compliance | dpo@example.com }
@row {
Controller |
Day-to-day decisions regarding processing.
Must document all processing activities. |
controller@example.com
}
}
Referencing figures/tables:
As shown in @ref(org-chart) and @ref(roles), the structure...
The renderer would automatically number figures (Figure 1, Figure 2) and tables (Table 1, Table 2) and resolve references.
Updated escape characters:
\{\}- literal braces\@- literal @ symbol\\- literal backslash\|- literal pipe (in tables)
Shall I draft a formal specification document we can refine, then move to implementation?
Created the specification at ~/legalmark/SPECIFICATION.md.
What's included:
- Complete syntax for sections, lists, tables, images, links
- Cross-references with automatic numbering
- Definitions/glossary support
- Appendices with letter-based numbering
- Escaping rules
- Style file structure (
.lms) - Full working example document
- Informal grammar definition
Next steps when you're ready:
- Review and refine the spec
- Build a parser (I'd suggest Python or TypeScript)
- Create HTML renderer
- Create PDF renderer (via WeasyPrint, Pandoc, or direct)
Shall I make any changes to the spec, or shall we start on the parser?
Done. The specification is now at ~/src/legalmark/SPECIFICATION.md. Let me know once you've reviewed it and we can start on the parser.
You're welcome! Enjoy reviewing it, and just pick up when you're ready to build the parser.
Updated SPECIFICATION.md to v0.2.0. Here's what changed:
- Format versioning (Section 1.3) -
legalmark: "1.0"required in both.lmand.lmsfrontmatter - Table of contents (Section 2.2) -
toc: truein frontmatter, depth controlled by style - Effects system (Section 7) - defined in
.lms, applied inline with@effect(name){text}, nestable - Table row striping (Section 14.3) -
stripe: odd | even | nonein style - Headers and footers (Section 14.2) - with variables like
@page,@title,@version - Page break control (Section 14.5) - per section level and appendix
- Font features (Section 14.4) - OpenType tags:
liga,onum,smcp, etc. - Compilation model (Section 15) - explicit 3-pass process documented
- Release 2 (Section 18) - code blocks, cover pages, background images, orphan/widow control
It's mostly a Word legacy. Both CSS (in browsers) and modern PDF libraries like WeasyPrint enable kerning by default. You'd only need to explicitly control it if someone wanted to disable it for some reason.
We've already covered that case - kern is listed in the OpenType features table (Section 14.4) with the note "usually on by default", so a user could add or remove it if needed. I think that's sufficient without making it any more prominent.
No problem at all. Take your time reviewing it, and when you're ready we can start building the parser and renderers. Enjoy!
Done. Added classification as a freeform text field in the frontmatter, and @classification as a variable available in headers and footers. The example style file now uses it in the footer's right position. Spec bumped to 0.2.1.
Committed and pushed to origin/master.
You're welcome! Let me know when you've finished reviewing and we can start on the parser.
I've reviewed v0.2.1. The foundations hold up: explicit {} blocks, numbering at compile time, and a separate style file. I would still change a fair amount before writing the parser. I put the full review in a document with an Adopt/Reject/Later picker for each recommendation: LegalMark spec review
The most important changes:
- Numbered clauses, not just numbered headings. Policies and regulatory plans mostly number paragraphs ("3.2 Staff must…"), often with no heading. The spec can't express that yet. I propose an untitled
@clausethat shares numbering with sections, plus list items you can reference, so "clause 3.2(a)" works. - One syntax for every element:
@name(arguments){body}. There are currently four different calling styles, and the grammar doesn't match its own examples. Your@effect(red-text){…}design already follows this pattern and stays as it is. - Bugs in the spec's own examples:
dpo@example.comwould be parsed as an element called@example.- In style files,
#is both the comment marker and the start of colours like#ff0000. - The example frontmatter isn't valid YAML.
- Files declare
legalmark: "1.0"while the spec is at 0.2.1.
- Rules for where each element may appear. One example: plain text after a subsection becomes an error, because it belongs to no heading. That's the exact problem you started with.
- Missing features policies need: footnotes, a document-control block (owner, approver, review date, version history), configurable reference labels ("clause", "Schedule", "Annex"), plural forms for defined terms, and
@includefor long documents.
For the build, I suggest generating HTML first and then producing PDF with WeasyPrint, a Python tool that turns HTML and CSS into PDF. It already handles headers and footers, page numbers in the contents page, font features, and orphan/widow control. That makes orphan/widow control cheap enough to move into Release 1.
Your local checkout was still on v0.1, behind origin/master. I fast-forwarded it to v0.2.1 before reviewing. I haven't edited the spec itself yet.
You're welcome! There's now a local copy at ~/src/legalmark/REVIEW-v0.2.1.md. It isn't committed.
It's a snapshot without the Adopt/Reject/Later pickers. Mark your decisions in the online review, where you've already marked clause numbering (1.1) as Adopt. Once you've been through the rest, I can update the spec to match.
I committed your decisions as f6178a9, and spec v0.3.0 is drafted in SPECIFICATION.md. The commit isn't pushed. I've left the spec uncommitted so you can read it first.
Everything you marked Adopt is in. Reference labels and the table extensions (both marked Later) are listed under Release 2 in Section 23. Appendix C summarises every change since 0.2.1.
There are no tests or parser yet to check the spec against. I checked it by hand: the rewritten examples follow the new rules, and every internal section reference points to a section that exists.
Where I went beyond or differed from the review:
- Paragraph numbering is switched on in the document's frontmatter (
number-paragraphs: true), not in the style file. It changes clause numbers and which documents are valid, so swapping style files shouldn't renumber a document. - Footnotes need a space before them. Under the new
@rule,GDPR@footnote{…}would be read as literal text, like an email address. You writeGDPR @footnote{…}, and the renderer removes the space so the marker sits against the word. - Only tables and images with captions are numbered and can be referenced. Otherwise logos and decorative images would become "Figure 3".
- Reference labels are fixed for now ("Section 3", "Clause 3.2", "Clause 3.2(a)"), because configurable labels were deferred.
- Simpler placeholders:
{owner}instead of{meta.owner}, and{n}for the full number instead of{1}.{2}. - One table rule kept despite deferring tables: a
|only splits cells at the row's own level. The existing multi-line cell example is ambiguous without it. Repeating header rows across pages went to Release 2 with the rest.
The PEG grammar is a precise grammar notation, but a few context-dependent rules (such as the @ rule) are stated in prose rather than in the notation.
The spec now says example documents and expected results live in a tests/ folder (Section 21.2). That folder doesn't exist yet; the format for expected results will be settled alongside the parser.
Section 6.1 of SPECIFICATION.md now says this plainly. It has a new example of a section that contains only paragraphs, and a sentence pointing to the "content before, not after" rule. The spec is still uncommitted.
I've committed spec v0.3.0 as 63dcf75. It includes the clarification about sections and clauses.
Neither this commit nor the review commit f6178a9 has been pushed. Both are only in your local master, two commits ahead of origin/master.
I've written the parser and its tests. A LegalMark file now turns into a document tree, with clear error messages for mistakes. Everything parses, including the spec's complete example. All 36 tests pass, and nothing is committed yet.
What the parser handles
- Everything at the syntax level: frontmatter (all values kept as text), the single
@name(arguments){body}form, block vs inline placement, paragraphs, table cells, emphasis, escapes, the@rule (sodpo@example.comworks), footnotes, and@include. - Errors give the file, line, column, a fix hint, and a stable code. For example:
syntax.lm:6:13: error: 'Bad_Id' is not a valid ID … [invalid-id]. Try it withuv run legalmark parse file.lm.
Tests
- tests/conformance/ holds 13 cases, each a
.lmfile with its expected tree or expected errors. The expected results are written by hand, not copied from the parser's output. Because the files aren't Python, the Rust build can reuse the same suite later. - Every
lmcode block in the spec is also a test, so the spec and parser can't drift apart. The one invalid example (Section 5.3) is skipped until structure checking exists. - To check the tests can actually fail, I broke five rules in the parser one at a time (for example, letting
@after a letter start an element); every break was caught. I also fed it 60,000 random malformed documents, with no crashes or hangs.
Not built yet: checking where elements are allowed, the "no loose content" rule, numbering, resolving references and defined terms, style files, and HTML output. I'd do structure checking and numbering next, then style files and HTML.
Spec edits (uncommitted):
- Every code block now carries a language tag (
lm,lms,yaml,peg). The spec-examples test relies on these tags. - I wrote down two rules the parser settled: comment lines don't split a paragraph, and a run of whitespace counts as one space.
- Section 21.2 now defines the test-suite format.
One interpretation to flag: emphasis only pairs * runs of equal length, so ***a* b** stays literal rather than nesting. Easy to change if you'd prefer otherwise.
Editor extensions: a Tree-sitter grammar for Zed (which also covers Neovim and Helix), a TextMate grammar for VS Code, and syntax/legalmark.vim for Vim. I'd write them after the HTML renderer, once the syntax has had more real use.
Structure checking and numbering now work on top of the parser. The spec's complete example numbers exactly as Section 22.1 describes, and all 43 tests pass. I've committed nothing yet this round. The two earlier commits (56ff56c spec, 2ecf268 parser) are still not pushed.
uv run legalmark check file.lm now reports problems, or prints the numbered outline:
1 Introduction
1.1 (clause)
1.2 (clause)
1.3 Scope
2 Roles and Responsibilities
Table 1: Data Protection Roles
Figure 1: Data Protection Reporting Structure
3 Compliance
3.1 (clause)
...
Appendix A Definitions
What the resolver does (in resolver.py):
- Structure checks:
- every element is in an allowed place
- no loose content after a subsection or clause
- appendices come last, and
@tocand@document-controlsit in the preamble - table header and cell counts are correct
- footnotes and links don't appear in titles
- Numbering:
- sections and clauses share one counter, and effects don't change it
- unnumbered sections are skipped, along with their children
- appendices are lettered, and their contents numbered
A.1 - list items get
(a),(i), … with references like "Clause 2(b)(i)" - captioned tables and figures, footnotes, and "number every paragraph" mode are handled
- References and terms:
- missing targets, duplicate IDs, and unnumbered targets are errors
- plural and other forms of a term are matched
- duplicate definitions are errors, and unused terms are warnings
@metavalues are filled in, and missing image files are reported
Tests: six new cases, each with hand-worked expected numbering or errors. The cases include the spec's complete example, and the invalid example in Section 5.3 is now actually checked. I broke eight resolver rules one at a time (for example, giving sections and clauses separate counters), and the tests caught every one. About 15,000 generated documents ran through the full pipeline with no crashes or hangs.
Two things wait for style-file support:
- Undefined
@effectnames aren't reported yet, because effects are defined in the style file. - List numbering styles currently use the spec's defaults rather than reading
list.levelNfrom a style.
Spec edits (uncommitted):
- Clauses inside unnumbered sections are unnumbered. The spec previously said clauses were always numbered, which contradicted the rule that descendants of an unnumbered section aren't numbered.
- List depth counts every enclosing list, and alphabetic numbering runs past
ztoaa,ab, …. - A renderer may skip structure checking when the document has syntax errors, to avoid a cascade of follow-on errors.
- The test-suite format gains
.resolved.json.
Next up is reading style files, then the HTML renderer.
Style files, HTML output, and PDF output all work. The spec's complete example renders as a 6-page A4 PDF using its own example style file, and all 51 tests pass. Nothing from this round is committed yet.
uv run legalmark html document.lm # self-contained HTML, images embedded
uv run legalmark pdf document.lm # PDF via WeasyPrint
Page 1: document control and revision history, and a contents page with dot leaders and page numbers. No header on the first page, as the style specifies.

Page 2: hanging numbers (1. / 1.1 / (a)), the inline definition shown in bold quotes, the footnote at the foot of the page, and header and footer filled in from the frontmatter.

Page 4: cross-references resolved ("Section 1.3(a)", "Clause 3.3(a)"), and the warning effect from the style file drawn around clause 3.2.

The full PDF is at .amp/in/artifacts/complete-example.pdf.
What's new:
- Style files (style.py): every Release 1 selector and property, checked values,
extends, the spec's defaults, and errors with line numbers. Documents now take their list numbering styles from the style file, and undefined effects are reported. - HTML (html.py): one self-contained page whose stylesheet is generated from the style file. Footnotes become endnotes in this version.
- PDF (pdf.py): the same HTML laid out by WeasyPrint. That gives page-margin headers and footers (with first/odd/even variants), footnotes at the page foot, and contents page numbers. Page breaks, widow and orphan control, and hyphenation also come from the style file.
Bugs the screenshots caught, now fixed:
- the appendix label and title were drawn over each other
- tables ran past the right margin
- justified text in table cells gave stretched word spacing
- header and footer rules were broken into segments
- a WeasyPrint bug hid all footnotes when the footnote area had a border, padding, and a top margin together
Tests: new cases cover style errors, extends and its cycles, and list styles taken from a style file. Renderer tests check number formats, header and footer output, both footnote modes, and the PDF page count. I broke four rules in the style and rendering code one at a time, and the tests caught each.
Limits:
- There's no browser on the Pi, so I checked the browser version of the HTML by having WeasyPrint lay it out with its screen styles. That's close to a browser but not identical.
- Fonts, sizes, and spacing that the style file doesn't set come from renderer defaults I picked.
- DOCX output isn't started.
Spec edit (uncommitted): Section 19.7 now says that inside an appendix, section A.1 uses the section.level2 rule and clause A.1.1 uses section.level3.
I've built LegalMark syntax highlighting for Helix, Zed, VS Code and Vim/Neovim, for both .lm documents and .lms style files. It's all in editors/, not committed yet.
- Helix and Zed: these share a new Tree-sitter grammar for each file type.
- VS Code: uses its own TextMate grammars.
- Vim/Neovim: uses plain Vim syntax files.
Every editor colours structure elements, titles, IDs and @ref targets, terms, links, strings, escapes, emphasis, comments and the YAML frontmatter. They all follow the spec's lexical rules:
//is a comment only at the start of a line.- An email address isn't treated as an element.
id: xis an option, buthttps://…is a value.@comment{…}can contain balanced braces.- Unknown element names stay uncoloured, so typos stand out.
What I tested:
- Helix 23.10: it builds and loads both grammars, and I inspected the colours in a real session.
- Vim 9.1: checked in a headless session and inspected the rendering.
- Tree-sitter grammars: both parse every conformance file and spec example; the only errors are in the two files written to contain them. They also pass corpus tests and 45 highlight assertions.
- VS Code: both grammars pass
vscode-tmgrammar-test. - Zed: its queries compile against the grammars, but Zed and VS Code aren't installed here, so I haven't run the extensions inside either.
uv run pytest now has 53 passing tests, including a new Vim check. Install steps are in editors/README.md.



Things to know:
- Zed needs one extra step at commit time. It builds the grammar from a pushed commit, so
editors/zed/extension.tomlstill has a placeholder. When you ask me to commit, I'll do it in two commits: first the editor files, then a follow-up that fills in the first commit's ID. - Helix on this machine has a config problem.
HELIX_RUNTIMEis set to a literal~/src/helix/runtime, but the runtime is at~/src/helix/helix/runtime. So Helix currently can't find its grammars or themes for any language. - Development tooling. Node and the tree-sitter CLI are downloaded into
.amp/in/tools, which git ignores, for regenerating the grammars later.
Which editor will you write the policy in? I can install it for that editor here (and fix HELIX_RUNTIME if it's Helix), then help with the first policy and stylesheet.
Vim is set up for LegalMark, and Helix can find its runtime again. Both take effect when you open a new shell; already-open shells still have the old HELIX_RUNTIME, so run source ~/.zshenv in those.
Vim: I linked ~/.vim/pack/legalmark/start/legalmark to ~/src/legalmark/editors/vim. Your ~/.vimrc needed no changes, because it already turns on syntax highlighting and filetype plugin indent on. I opened the example document in Vim with your vimrc: it detected the filetype and highlighted @section and its title. Because it's a symlink, changes to the syntax files in the repo apply straight away.
Helix: line 2 of ~/.zshenv had two problems. zsh doesn't expand ~ inside double quotes, so Helix was looking for a directory literally named ~. The path was also missing the inner helix/ directory, since the repo is checked out at ~/src/helix/helix. Probably the export was copied from Helix's install instructions, which assume a checkout at ~/src/helix. It now reads:
export HELIX_RUNTIME="$HOME/src/helix/helix/runtime"
In a new shell, hx --health shows highlight queries for Rust, YAML, Markdown and the rest, where it showed none before. Helix doesn't know about LegalMark files yet; that's the separate setup in editors/README.md if you want it later.
The editor support in the repo is still uncommitted. When you commit, I'll do it in two commits so Zed's grammar reference can point at the first one.
When you're ready, I can help draft the first policy and its stylesheet.
The editor support is committed and pushed to master on Forgejo as two commits:
c52065eAdd editor support for Helix, Zed, VS Code and Vim: the grammars, all four editors' files, the Vim test and the READMEs.a2b4887Point the Zed extension at the grammar commit: sets bothrevvalues ineditors/zed/extension.tomltoc52065e.
One thing may stop the Zed install: the Forgejo repository is private, and an anonymous git ls-remote asks for a username. Zed downloads the grammars from the repository URL in extension.toml. I haven't been able to confirm whether it uses your laptop's git credentials for that.
On your laptop:
Clone or pull the repo.
In Zed, run zed: install dev extension and choose
editors/zed.If the grammar fetch fails with an authentication error, point the extension at your local clone instead. In
editors/zed/extension.toml, change both grammarrepositorylines to the clone's path (leave the top-levelrepositoryfield alone), then reinstall:repository = "file:///path/to/your/legalmark"Keep that edit local rather than committing it.
revcan stay as it is, because your clone contains that commit.
Making the repository public would also fix it, if you're happy for it to be public.
Not at the moment. A style file can only give font family names, such as body: "Arial", sans-serif, 11pt, and the spec has no way to point at a font file or URL. The renderer then uses whatever fonts are installed on the machine that builds the output, so for now installing them yourself is the way to go.
Installing fonts on the Pi: PDFs are built here, and WeasyPrint finds fonts through fontconfig, so install them in your user font directory:
mkdir -p ~/.local/share/fonts
cp *.ttf *.otf ~/.local/share/fonts/
fc-cache -f
fc-list | grep -i "font name"
Use the family name that fc-list shows in the .lms file. The PDF embeds the font, so anyone can open it without installing anything. The HTML output, however, depends on the reader having the font, and falls back to the generic family otherwise.
If we add font sources to the spec later, the format matters:
| Output | Font URL | Local font file |
|---|---|---|
| Works: WeasyPrint downloads and embeds it. But every build needs the network, and the result depends on what the server sends that day. | Works and is reproducible. | |
| HTML | Works in browsers, but loading from a third-party font server sends the reader's IP address there. That's an awkward look for a data protection policy, and it fails offline. | Can be embedded in the file, as images already are. |
| Word | Doesn't work: Word can't load fonts from the web. The fonts must be installed for each reader, or embedded in the .docx. |
Can be embedded, if the font licence allows it. |
| Output | Font URL | Local font file |
|---|---|---|
| Works: WeasyPrint downloads and embeds it. But every build needs the network, and the result depends on what the server sends that day. | Works and is reproducible. | |
| HTML | Works in browsers, but loading from a third-party font server sends the reader's IP address there. That's an awkward look for a data protection policy, and it fails offline. | Can be embedded in the file, as images already are. |
| Word | Doesn't work: Word can't load fonts from the web. The fonts must be installed for each reader, or embedded in the .docx. |
Can be embedded, if the font licence allows it. |
So if we add anything, it should be local font files referenced from the .lms, with each renderer embedding them. URLs would add network and privacy problems and still not help with Word. Check the licence of any font you embed: Google Fonts (OFL) are fine, but many commercial fonts restrict embedding.
I can add font files to the Release 1 spec if you'd like, as a separate spec commit.
Sounds good. Use the family name exactly as fc-list shows it in the .lms file. If a font doesn't seem to apply, fc-match "Family Name" tells you what fontconfig is actually using instead.
When you're ready to draft the first policy and its stylesheet in Vim, check them with uv run legalmark check policy.lm, then build the PDF with uv run legalmark pdf policy.lm. I'm happy to look over the drafts or the output.
I've fixed the heading overlap and the list spacing in the renderer; both are uncommitted. To colour body text, add font-colour to the paragraph rule in ripplyn.lms. That reaches most text, though not the numbers and bullets.
1. Headings with hanging-indent: 0. Someone (another agent, or you?) had already started a fix in src/legalmark/html.py, with a test in tests/test_render.py. With an indent of zero, the number now sits inline and the title follows it on the same line; clause numbers work the same way. That fix worked, so I kept it. I added one thing: Instrument Sans has a very narrow space, so "5.1Data-at-rest" looked run together, and I put a small fixed gap after the number.
2. List spacing. A list now takes the paragraph spacing-after below it. item-spacing applies only between items, so the gap above a list matches the gap below it. I added a sentence to section 19.8 of the spec saying so, so other renderers do the same.


I added a regression test for the list spacing. It and the hanging-indent test both fail without the changes, and all 55 tests pass with them.
3. Body text colour. Add this to ripplyn.lms:
paragraph {
font-colour: #333333
}
That colours all paragraph text, including text in list items, table cells, definitions and clauses. It doesn't colour list markers like "(a)" and "•", clause numbers, defined term names, captions, the contents page, or headers and footers. So with a strong colour, the numbers stay black next to coloured text. There are two ways to fix that:
- Renderer only: list markers and clause numbers follow the paragraph colour, since they belong to the paragraph. No spec change needed.
- Spec change: add a document-wide default, for example
fonts { colour: #333333 }. It would apply to all text unless a rule overrides it.
I'd do both.
Separately, your header and footer use {reference}, {status} and {classification}, but the policy has none of those fields. That's why the footer starts with ", 2026-11-06". Add the fields to the frontmatter, or take them out of ripplyn.lms.
Both colour changes are in, uncommitted.
Document colour: fonts takes a new colour property, which sets the colour of all text: body, headings, numbers, captions, the contents page, headers and footers. Any rule with its own font-colour overrides it for its own text. For example, your section headings keep #556B80.
fonts {
body: "Geist", sans-serif, 10pt
heading: "Instrument Sans", sans-serif
features: liga, onum
colour: #333333
}
Numbers follow their paragraphs: clause numbers and list bullets now take the paragraph rule's colour, font and size. So with paragraph { font-colour: … } set, the numbers match the text beside them.
Spec:
- Section 19.6 now describes the
fontsproperties, includingcolour. - Section 19.9 lists text properties for
paragraph(the implementation already accepted them). It now also says they cover paragraphs everywhere, plus clause numbers and list bullets. - The earlier list-spacing note in 19.8 is in the same uncommitted spec change.
I tested this with deliberately clashing colours on a copy of your stylesheet: green for the document, dark red for paragraphs. Paragraph text and bullets came out red, headings stayed #556B80, and headers, footers and defined term names came out green. All 56 tests pass. The new colour test fails if either part of the change is removed.

The uncommitted changes now include the earlier hanging-indent work from someone else and my list-spacing fix. When you commit, I'll split it into a code commit and a spec commit. I'll leave examples/ out unless you want your policy and stylesheet in the repo.
I've fixed the column widths in the document control history table; the fix is uncommitted.
A rule meant to make the labels in the fields table 30% wide was also hitting the history table's header row. So each of its four columns asked for 30%, which squeezed "Changes". Now that rule only applies to the fields table, and the history table is laid out like this:
- Version and Date: shrink to fit their contents.
- Author: about a quarter of the width.
- Changes: gets the rest, roughly half the table.
I also added a gap between the two tables, which were touching.
Before:
After:
The example document with a longer history looks right too. I added a regression test for the widths; it fails without the fix, and all 57 tests pass.
Your earlier changes are committed locally as two commits: 364950e for the renderer and 5d1530f for the spec. Neither is pushed yet, and examples/ stays out of git.
I committed the column-width fix and pushed everything to master on Forgejo:
4c89d63gives the Changes column in the revision history most of the width.5d1530fis the spec update: list spacing, document text colour, and paragraph text properties.364950efixes zero hanging indents, list spacing, and text colours.
examples/ is still untracked and wasn't pushed.
Yes, that makes sense, and both are in now, uncommitted.
header {
spacing: 10pt // gap between the header and the body text
}
footer {
spacing: 10pt // gap between the body text and the footer
}
list {
marker-indent: 0.4cm // bullet or number position, like Word's "bullet position"
indent: 1cm // item text position, like Word's "text indent"
}
Header and footer spacing: the body text stays where your page margins put it, and the header and footer move away from it into the margin. Your top and bottom margins are 1.5cm, so a large spacing will push the header close to the page edge. If that happens, increase the margins too.
List marker-indent: both values are measured from the edge of the surrounding text, so the bullet moves right and the item text stays put. Keep marker-indent smaller than indent, or the bullet will overlap the text. Each list.levelN can set its own.
Here is your policy before and after, with spacing: 10pt on the header and footer and marker-indent: 0.4cm. I used a scratch copy of ripplyn.lms; your own file is unchanged.
Before:
After:
I added both properties to the spec (sections 19.5 and 19.8), with a default of 0. A new test renders the document with and without them. It checks that the header and footer move by exactly the spacing, the body doesn't move, and the bullet sits at marker-indent. It fails without the change, and all 58 tests pass.
Glad it's gone well, thanks.
The header and footer spacing and list marker-indent changes are still uncommitted, in the code (html.py, style.py, tests) and the spec. Everything before that is pushed. When you want them in, I'll commit the code and the spec separately and push.
examples/ is still untracked too, if you ever want your policy and stylesheet in the repo as a sample.
I've committed the header and footer spacing and the list marker indent, and pushed them to master on Forgejo as two commits:
c4a449dis the code: the renderer, the style schema, and the new test.8e0d4adis the spec change, in sections 19.5 and 19.8.
examples/ is still untracked, so it wasn't pushed.