Custom markup language for legal documents

Medium
Unlisted
Hello! I have spent quite a long time recently writing legal documents (policies, regulatory business plans etc) and it has occurred to me that current mark up languages are woeful for this as the document doesn't really know which bit of text belongs to which section heading etc. So, I am pondering coming up with a simple mark up language which is much more prescriptive about document structure. Would you be able to help me with this and perhaps, once the language is defined, help me write a converter to (for example) HTML and PDF?

This 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:

  1. Explicit hierarchy - sections know their level and parent
  2. Automatic numbering - "Section 3.2.1" computed, not hardcoded
  3. Cross-references - @ref(section:definitions) that update automatically
  4. Definitions - first-class support for defined terms
  5. Schedules/Annexes - separate numbering schemes
  6. Recitals/Whereas clauses - lettered lists (a), (b), (c)
  7. 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?

mostly policy and regulatory business plans - the structure is very regimented and yes, you have multiple layers of sub sections, and bulleting can become a problem quite quickly too, especially in Word when you have bullets that have multiple paragraphs per bullet point. Yes, all sections should be numbered, and the numbering should be handled as part of the conversion / rendering ("complilation?") stage. And yes, some way of managing Appendices and internal references easily would be good, too. There should also be a way of styling, but that can come later (ie choose fonts, change size of each layer of heading, choose indentation of text under headings etc.

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 .lms files

Want me to write up a formal specification document for this syntax, or shall we iterate on the design first?

How would we escape the braces if they are used in normal text? There should also be a way of importing image files (SVG / PNG / JPG), and linking to external documents too, and definitely tables too. All images and tables should be able to have a caption.

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?

That looks great! Yes, please do draft that document.

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:

  1. Review and refine the spec
  2. Build a parser (I'd suggest Python or TypeScript)
  3. Create HTML renderer
  4. Create PDF renderer (via WeasyPrint, Pandoc, or direct)

Shall I make any changes to the spec, or shall we start on the parser?

That's brilliant, I'll review it over the next day. Could you move it into ~src/legalmark for now rather than ~/legalmark?

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.

thank you!

You're welcome! Enjoy reviewing it, and just pick up when you're ready to build the parser.

I've taken some time on this one, apologies for the delay! Here are my suggestions, please shout if you have different ideas about the examples, and if you have any questions! OK so I'm going to split these into release 1 and release 2: # release 1 - There should be a version of the lm file format, and the lms file format included in both files for renderers to understand what version of the standard they are rendering to - Contents page generation (we already need multi pass compilation I think due to the internal references that may refer forward)? - Add ability for other local effects, such as colour, these should these be defined in the lms file and then allow temporary effect setting. See example below. - Allow for striping of rows in tables - Headers and footers including page numbers, document version, dates etc. - Page break controls on section / appendix style level (eg sections and appendicies should / should not start a new page, configurable respetively) - Font feature controls (ligaments, ranged / old style numbers etc) # release 2 - A proper multiline code block with similar code formatting like markdown (eg ```bash) so that renderers can choose to render with syntax colouring if they choose to. - Cover / title page layout and formatting controls - Background images for eg design on cover page, or on page template - Orphan / widow control # Example of local effects Something like: ``` effect red-text { font-colour: #ff0000 } ``` And then inline in the document block something like this: ``` Lorem @effect(red-text){ipsum} doler sic amet ``` Of course they will need to be nestable, too: ``` Lorem @effect(red-text){ @effect(serif-font){ipsum}} doler sic amet ```

Updated SPECIFICATION.md to v0.2.0. Here's what changed:

  • Format versioning (Section 1.3) - legalmark: "1.0" required in both .lm and .lms frontmatter
  • Table of contents (Section 2.2) - toc: true in 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 | none in 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
Thank you! Your assmptions are all correct: I agree Effects should be in lms only, Ligtures I meant OpenType font features, Striping looks good and code blocks looks great, too. I just had one quick thought, when talking about fonts, do you think we need to do something about kerning, or is that a Word legacy (and I don't want to output to Word!) and most platforms will kern properly by default now?

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.

Ah, perfect, sorry I hadn't spotted that yet :). OK great, I will review the new spec and return when done! Thank you again!

No problem at all. Take your time reviewing it, and when you're ready we can start building the parser and renderers. Enjoy!

I've just realised we have missed something from the Frontmatter: document classification, and this should be possible to add to document headers or footers, too. I think it should be freeform text as different entities have different document classification policies. Could you add that and update the version of the Spec to 0.2.1?

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.

Fantastic, thank you! Please commit and push

Committed and pushed to origin/master.

Thank you!

You're welcome! Let me know when you've finished reviewing and we can start on the parser.

Hello! It's been a while, could you review what we have proposed in legalmark (~/src/legalmark/) and see if you would make any changes given you've progressed as much as time!

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:

  1. 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 @clause that shares numbering with sections, plus list items you can reference, so "clause 3.2(a)" works.
  2. 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.
  3. Bugs in the spec's own examples:
    • dpo@example.com would 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.
  4. 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.
  5. 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 @include for 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.

Sorry, where have you put that file? I can't find it in the legalmark dir!
Created 1 file⁠
Ah, nice, thank you!

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've marked my decisions in the review, please commit them to the repository, and then let's start the spec update!

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 write GDPR @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.

Thank you, that's great. My only comment is that it isn't clear to me whether Sections can have paragraphs etc, like a clause, or not - ie that you don't HAVE to use clauses. Is that the case?

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.

Perfect, please commit the new version of the spec!

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.

Yes, please push!

I've pushed both commits to origin/master on Forgejo: the review with your decisions (f6178a9) and spec v0.3.0 (63dcf75).

Yes, let's and let's build test cases as we go. Just a thought - is it possible to create a vim syntax, a VS Code syntax extension, and a Zed syntax extension for this as well?

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 (so dpo@example.com works), 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 with uv run legalmark parse file.lm.

Tests

  • tests/conformance/ holds 13 cases, each a .lm file 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 lm code 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.

Wonderful! please commit that first stage of code, and the spec changes (in a separate commit please!) and carry on with the validation and numbering.

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 @toc and @document-control sit 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
    • @meta values 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 @effect names 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.levelN from 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 z to aa, 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.

Commit and push everything, then carry on with style files and HTML

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 1: document control and contents

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 2: clauses, list items and footnote

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.

Page 4: references and an effect

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.

Commit and push this, thank you!. Next up, let's do the editor syntax extensions so I can generate a first policy document and stylesheet using my preferred editor to test with!

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: x is an option, but https://… 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.

Helix, LegalMark document

Helix, style file

Vim, LegalMark document (habamax)

Things to know:

  • Zed needs one extra step at commit time. It builds the grammar from a pushed commit, so editors/zed/extension.toml still 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_RUNTIME is 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.

I'll use vim for now, but yes, please do fix the HELIX_RUNTIME! Not sure how that happened as I haven't touched it since the install.

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.

thank you, let's commit the editor support too so I can get it installed in Zed on my local laptop (you're sitting on a pi, not on my laptop right now)

The editor support is committed and pushed to master on Forgejo as two commits:

  • c52065e Add editor support for Helix, Zed, VS Code and Vim: the grammars, all four editors' files, the Vim test and the READMEs.
  • a2b4887 Point the Zed extension at the grammar commit: sets both rev values in editors/zed/extension.toml to c52065e.

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:

  1. Clone or pull the repo.

  2. In Zed, run zed: install dev extension and choose editors/zed.

  3. If the grammar fetch fails with an authentication error, point the extension at your local clone instead. In editors/zed/extension.toml, change both grammar repository lines to the clone's path (leave the top-level repository field alone), then reinstall:

    repository = "file:///path/to/your/legalmark"
    

    Keep that edit local rather than committing it. rev can 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.

Just a quick thought, can I refer to fonts that are on the Internet, or would that make things like word and pdf generation more difficult? If that is the case (more difficult), I'll download the fonts and install them myself.

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
PDF 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.

I'll install them myself, I think that makes sense for now, thank you!

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.

OK so a few things to ponder: - I don't want a hanging indent, but when I set that in the section the heading text overwrites the section number - it should just naturally follow after the number - Lists should use the paragraph space after the whole list, at the moment it's using the list gap spacing after the list, which when it's different from paragraph spacing means the gap between the paragraph before the list and the list is different from the one between the list and the paragraph after - How do I change the font-colour of all the body text?

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.

Encryption policy, page 2

Heading gap, zoomed

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.

I agree, do both. I'll fix the lms file re the missing reference, status, classification.

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 fonts properties, including colour.
  • 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.

Colour check: document green, paragraphs dark red

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.

Please commit those changes, thank you! I've spotted one more thing - in the document control section the first three columns are too wide, and the forth column (the one most likely to hold more text) is too thin. Can you have a look at that pleae?

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:
Document control before

After:
Document control 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.

Commit this fix and push everything

I committed the column-width fix and pushed everything to master on Forgejo:

  • 4c89d63 gives the Changes column in the revision history most of the width.
  • 5d1530f is the spec update: list spacing, document text colour, and paragraph text properties.
  • 364950e fixes zero hanging indents, list spacing, and text colours.

examples/ is still untracked and wasn't pushed.

This is great! Thank you! I have two more visual minor issues: - Can we add the ability to define spacing below the header, and above the footer? - Can we add the ability to nudge the bullets to the right a bit? I prefer how it works in Word: the bullets aren't fully left aligned, they are nudged in a bit from the paragraph edge. Does that make sense?

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:
Before

After:
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.

This is great, thank you for all your help on this today, very pleased with how it's gone! There may be a few more tweaks to go, but I'm super happy with where we've got to. Thank you!

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.

Commit and push, code and spec separately

I've committed the header and footer spacing and the list marker indent, and pushed them to master on Forgejo as two commits:

  • c4a449d is the code: the renderer, the style schema, and the new test.
  • 8e0d4ad is the spec change, in sections 19.5 and 19.8.

examples/ is still untracked, so it wasn't pushed.