tramlines
tramlines validates Markdown files against a schema that you write in YAML. A schema is a set of rules. The rules describe which frontmatter fields, headings, lists, and paragraphs a file must contain, and the form of each line. Each rule includes an example line. When a line is wrong, the error shows that example, so you can see what to write.
Features
- Checks frontmatter, headings, lists, paragraphs, and individual lines
- Accepts line rules written as sample lines with named blanks, or as regular expressions
- Lets schemas share line formats through a formats file
- Applies some rules only in certain document states, such as
draftorfinal - Shows an example of a correct line in each error message
- Checks the schema itself, including its example lines
- Checks the HTML anchors of the IDs in headings and lines
- Repairs the headings and anchors that a schema derives, and writes a starter document from a schema
- Prints its report as text, as JSON, or as GitHub annotations
- Includes a Rust library that does all of this, and accepts extra schema keys from other tools
Installation
Prerequisites
- Node.js 18 or later, or a Rust toolchain
Install
With npm:
npm install -g @six5536/tramlines
With Cargo, from crates.io:
cargo install tramlines
Usage
The following schema describes a shopping list: a title in the frontmatter, a level-1 heading with the same text as the title, and a bullet list. Each item of the list consists of a word, a space, x, and a number.
list.yaml
line-limit: 50
frontmatter:
title:
example: Groceries
sections:
- heading: { from: title }
level: 1
blocks:
- kind: bullet-list
each: { template: "${item:word} x${n:int}", example: Milk x2 }
The following file follows the schema:
groceries.md
---
title: Groceries
---
# Groceries
- Milk x2
- Bread x1
$ tramlines validate --schema list.yaml groceries.md
0 errors, 0 warnings
In the following file, the heading differs from the title, and the last item has no count:
shopping.md
---
title: Groceries
---
# Shopping
- Milk x2
- Bread
$ tramlines validate --schema list.yaml shopping.md
shopping.md:5: error[TLMD_165]: heading `Shopping` must equal the frontmatter `title`: `Groceries` (schema list, section "<title>")
shopping.md:8: error[TL_74]: `Bread` fails at column 6: expected ` x`, found the end; an item of this kind reads Milk x2 (schema list, section "<title>" block 1)
2 errors, 0 warnings
[exit 1]
Each error line takes the form path:line: severity[KEY]: message. KEY names the rule of the tramlines specification that the file breaks. The documentation explains each key. The end of the message names the rule in your schema that the line breaks. The error for the list item also shows the example line of that rule, Milk x2.
The command exits with one of three codes:
- 0 when it finds no errors
- 1 when it finds errors
- 2 when the command line is invalid or the command can’t read a file
Documentation
The tramlines documentation includes tutorials, a guide for each part of a schema, a reference for every schema key and every error, and the specifications.
Contributing
See CONTRIBUTING.md.
Licence
Elastic License 2.0. See LICENSE.
Your first schema
In this tutorial you write a schema for a recipe, one rule at a time, and check a recipe against it after each step. At the end you can write a schema for a document of your own, read the errors that tramlines prints, and make tramlines validate the examples in a schema.
A recipe consists of a title, a short introduction, a list of ingredients, a numbered method, and sometimes a few notes. The schema describes each of these parts.
Before you begin
- tramlines installed, as the README shows. Run
tramlines --helpto check that the command works. - An empty directory to work in. You create every file in this tutorial in that directory.
Step 1: write the title heading
A schema is a YAML file. Every schema declares line-limit, the maximum number of lines in a document. This schema also declares the frontmatter and the sections of a recipe.
Create the schema. The frontmatter contains one key, title. The first section starts with a level-1 heading whose text comes from that title, written as { from: title }. A prose block of at most one paragraph follows the heading:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
Each rule includes an example: a line that a real recipe would contain. tramlines validates each example against its rule, and shows the example in every error from that rule.
Create a recipe:
pancakes.md
---
title: Pancakes
---
# Pancakes
Thin pancakes for a weekend breakfast.
Check the recipe against the schema:
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
0 errors, 0 warningsmeans that the recipe follows the schema.- tramlines validates the schema first. If the schema is wrong, the command prints the errors in the schema and stops before it checks the recipe.
Step 2: add the required sections
A recipe needs its ingredients and its method. Add a level-2 section Ingredients with a bullet list, and a level-2 section Method with a numbered list. A schema requires every section that it declares, unless the section is marked optional:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
example: 200 g flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
Check the recipe from step 1 again. The recipe doesn’t contain either section yet:
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md: error[TLMD_18]: section `## Ingredients` is missing (schema recipe, section "Ingredients")
pancakes.md: error[TLMD_18]: section `## Method` is missing (schema recipe, section "Method")
2 errors, 0 warnings
[exit 1]
- Each error line names the file, the error key in brackets, and a message.
- The end of each message names the schema rule that the recipe breaks:
section "Ingredients". - The exit code is 1, because the check found errors.
Add both sections to the recipe:
pancakes.md
---
title: Pancakes
---
# Pancakes
Thin pancakes for a weekend breakfast.
## Ingredients
- 200 g flour
- 2 medium eggs
- 300 ml milk
## Method
1. Whisk the flour, eggs and milk into a smooth batter.
2. Fry a ladle of batter for a minute on each side.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
Step 3: give each ingredient a shape
The schema accepts any bullet item as an ingredient. Make each item an amount, a unit, and the food. A template is a sample line with named holes where the text varies. In the following schema, ${amount:int} is a whole number, ${unit:word} is one word, and ${item} is any text. The each key applies the rule to every item of the list. Its example must match the template:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${amount:int} ${unit:word} ${item}'
example: 200 g flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
- Every ingredient of the recipe matches the template, so the check passes.
- Built-in types describes the types that a hole can take, and how to define your own.
Step 4: add an optional section
Some recipes include notes. Add a section Notes with optional: true. A recipe can omit the section. A recipe that includes the section must follow its rule:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${amount:int} ${unit:word} ${item}'
example: 200 g flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
- heading: Notes
level: 2
optional: true
blocks:
- kind: prose
example: The batter keeps for a day in the fridge.
The pancake recipe has no notes and still passes:
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
Write a second recipe that includes notes:
porridge.md
---
title: Porridge
---
# Porridge
Oats cooked slowly in milk.
## Ingredients
- 50 g oats
- 300 ml milk
## Method
1. Simmer the oats in the milk for five minutes, stirring often.
## Notes
Use water instead of milk for a lighter porridge.
Check both recipes in one run:
$ tramlines validate --schema recipe.yaml pancakes.md porridge.md
0 errors, 0 warnings
Step 5: fix a recipe that breaks the schema
Write a recipe with three mistakes: an ingredient with no amount, a Steps section that the schema doesn’t declare, and no Method section:
cake.md
---
title: Lemon cake
---
# Lemon cake
A light sponge with a sharp lemon glaze.
## Ingredients
- 200 g flour
- a pinch of salt
## Steps
1. Beat the butter and sugar until pale.
$ tramlines validate --schema recipe.yaml cake.md
cake.md: error[TLMD_18]: section `## Method` is missing (schema recipe, section "Method")
cake.md:12: error[TL_74]: `a pinch of salt` fails at column 1: expected an `int` (`\d+`), found `a pinch of salt`; an item of this kind reads 200 g flour (schema recipe, section "Ingredients" block 1)
cake.md:14: error[TLMD_19]: heading `## Steps` is not declared (schema recipe)
3 errors, 0 warnings
[exit 1]
cake.md:12points to the ingredient’s line in the recipe. The error names three things: the column where the line stops matching the template, what the template expects there, and the example from the schema,200 g flour.TLMD_19reports that the schema declares no headingSteps. A heading that no rule declares is an error.- The error reference explains each key, such as TL_74 and TLMD_19.
Give the salt an amount and a unit, and rename Steps to Method:
cake.md
---
title: Lemon cake
---
# Lemon cake
A light sponge with a sharp lemon glaze.
## Ingredients
- 200 g flour
- 1 pinch salt
## Method
1. Beat the butter and sugar until pale.
$ tramlines validate --schema recipe.yaml cake.md
0 errors, 0 warnings
Step 6: check the examples in the schema
tramlines validates the schema before it checks any recipe. It checks each example against its rule, so that every example shows a line that its rule accepts.
Change the ingredient example to flour, which has no amount:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${amount:int} ${unit:word} ${item}'
example: flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
- heading: Notes
level: 2
optional: true
blocks:
- kind: prose
example: The batter keeps for a day in the fridge.
$ tramlines validate --schema recipe.yaml pancakes.md
recipe.yaml:17: error[TL_64]: the example `flour` fails at column 1: expected an `int` (`\d+`), found `flour` (schema recipe, section "Ingredients" block 1)
1 error, 0 warnings
[exit 1]
TL_64points to the line of the example in the schema and explains why the example fails its rule.- The command reports the errors in the schema and stops. It checks
pancakes.mdonly after the schema is right.
A schema can also contain whole documents of its type, under examples. tramlines validates each one as a document. Restore the ingredient example, and add the porridge recipe as an example, with one ingredient that breaks the template:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${amount:int} ${unit:word} ${item}'
example: 200 g flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
- heading: Notes
level: 2
optional: true
blocks:
- kind: prose
example: The batter keeps for a day in the fridge.
examples:
- document: |
---
title: Porridge
---
# Porridge
Oats cooked slowly in milk.
## Ingredients
- 50 g oats
- a pinch of salt
## Method
1. Simmer the oats in the milk for five minutes, stirring often.
$ tramlines validate --schema recipe.yaml pancakes.md
recipe.yaml:31: error[TL_69]: example 1 fails: line 12: [TL_74] `a pinch of salt` fails at column 1: expected an `int` (`\d+`), found `a pinch of salt`; an item of this kind reads 200 g flour (schema recipe, section "Ingredients" block 1)
1 error, 0 warnings
[exit 1]
example 1is the first entry ofexamples, andline 12is the line inside that example document.
Fix the example document in the schema, as you fixed the cake:
recipe.yaml
line-limit: 100
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast, ready in twenty minutes.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${amount:int} ${unit:word} ${item}'
example: 200 g flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
- heading: Notes
level: 2
optional: true
blocks:
- kind: prose
example: The batter keeps for a day in the fridge.
examples:
- document: |
---
title: Porridge
---
# Porridge
Oats cooked slowly in milk.
## Ingredients
- 50 g oats
- 1 pinch salt
## Method
1. Simmer the oats in the milk for five minutes, stirring often.
$ tramlines validate --schema recipe.yaml pancakes.md porridge.md cake.md
0 errors, 0 warnings
- The schema, its example document, and all three recipes pass.
What’s next
- Check documents in CI shows how to run this check on every change to a repository.
- Write the shape of a line shows templates, patterns, and exact text.
- Describe sections and headings and Describe blocks and their units cover the rest of a document’s body.
- Write examples and guidance shows how to write examples that help the writer of a document.
- Strict by default explains why a schema refuses what it doesn’t declare.
- Schema keys lists every key that a schema can contain, such as the keys of a section and a block.
Check documents in CI
In this tutorial you add a schema for meeting notes to a repository and check the notes with tramlines validate. You then add a GitHub Actions workflow that runs the same check on each push and pull request. At the end you know what each exit code of the command means for a continuous integration (CI) job. You also know how to add a rule without breaking the build.
A CI workflow reads the exit code of the command: the workflow fails when the command exits with a code other than 0.
Before you begin
- tramlines installed, as the README shows, to run the check on your own computer.
- A GitHub repository that you can push to. The CI job installs tramlines itself, with
npx. - The steps of Your first schema, or some experience reading a schema.
Step 1: add a schema and a note
Keep the schema in the repository, beside the documents it checks, so that reviewers see a change to the schema and a change to the notes together. This tutorial keeps schemas in schemas/ and notes in notes/.
The meeting-note schema requires a title and a date in the frontmatter, a heading that equals the title, and three sections. Each item under Actions starts with the name of its owner:
schemas/meeting.yaml
line-limit: 200
frontmatter:
title:
example: Planning meeting
date:
pattern: '^\d{4}-\d{2}-\d{2}$'
example: '2026-09-14'
sections:
- heading: { from: title }
level: 1
blocks: []
- heading: Attendees
level: 2
blocks:
- kind: bullet-list
example: Ana Lima
- heading: Decisions
level: 2
blocks:
- kind: bullet-list
example: Ship the beta on 1 October.
- heading: Actions
level: 2
blocks:
- kind: bullet-list
each:
template: '${owner:word}: ${}'
example: 'Ana: book the room for the launch'
Add the first note:
notes/2026-09-14.md
---
title: Planning meeting
date: 2026-09-14
---
# Planning meeting
## Attendees
- Ana Lima
- Ben Okafor
## Decisions
- Ship the beta on 1 October.
## Actions
- Ana: book the room for the launch
- Ben: write the release notes
Run the check from the root of the repository, with paths relative to it:
$ tramlines validate --schema schemas/meeting.yaml notes/2026-09-14.md
0 errors, 0 warnings
- The command finds no errors, so it exits with code 0, and a CI job that runs it passes.
Step 2: see a failing check
Add a second note with two mistakes: the date isn’t in the form YYYY-MM-DD, and the action names no owner:
notes/2026-09-21.md
---
title: Launch review
date: 21 September
---
# Launch review
## Attendees
- Ana Lima
## Decisions
- Move the launch to 8 October.
## Actions
- book a second room
Check both notes in one run. The command checks every document that you name, and reports all the errors before it exits:
$ tramlines validate --schema schemas/meeting.yaml notes/2026-09-14.md notes/2026-09-21.md
notes/2026-09-21.md:3: error[TL_56]: `date` `21 September` does not match `^\d{4}-\d{2}-\d{2}$`; a value of this kind reads 2026-09-14 (schema meeting, key `date`)
notes/2026-09-21.md:18: error[TL_74]: `book a second room` fails at column 5: expected `: `, found ` a second room`; an item of this kind reads Ana: book the room for the launch (schema meeting, section "Actions" block 1)
2 errors, 0 warnings
[exit 1]
- Exit code 1 means that at least one document breaks the schema. A CI job that runs this command fails, and its log shows these error lines.
- Each error line starts with
path:line, so you can open the note at the line that’s wrong.
Fix the note:
notes/2026-09-21.md
---
title: Launch review
date: 2026-09-21
---
# Launch review
## Attendees
- Ana Lima
## Decisions
- Move the launch to 8 October.
## Actions
- Ana: book a second room
$ tramlines validate --schema schemas/meeting.yaml notes/2026-09-14.md notes/2026-09-21.md
0 errors, 0 warnings
Step 3: see a check that can’t run
Exit code 2 means that the command didn’t check the documents at all: its command line is wrong, or it can’t read a file. A CI job fails on code 2 too. To fix the failure, change the workflow rather than the notes.
Name a note that doesn’t exist:
$ tramlines validate --schema schemas/meeting.yaml notes/2026-09-28.md
error: notes/2026-09-28.md: No such file or directory (os error 2)
[exit 2]
Leave out --schema:
$ tramlines validate notes/2026-09-14.md
error: the following required arguments were not provided:
--schema <FILE>
Usage: tramlines validate --schema <FILE> <DOC>...
For more information, try '--help'.
[exit 2]
- The command picks the format of each document by its extension, so a document with a
.txtextension stops the command with code 2 too. The command lists the formats, and Errors: the command lists each error that stops the command.
Step 4: add the workflow
Add a workflow that installs Node.js and runs the check with npx. The npx command downloads the @six5536/tramlines package and runs its tramlines command. The job runs on each push and each pull request:
.github/workflows/check-notes.yml
name: Check notes
on:
push:
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npx @six5536/tramlines validate --github --warnings --schema schemas/meeting.yaml notes/*.md
The job’s shell expands notes/*.md to every note, so the job checks each added note with no change to the workflow. The --github option prints each error and warning as a GitHub workflow command, so that a pull request shows it on the line of the note. The --warnings option lists the warnings as well as the errors. To pin the version of tramlines, add it to the package name, as in @six5536/tramlines@0.1.0.
To run the same check on your computer, name each note:
$ tramlines validate --schema schemas/meeting.yaml notes/2026-09-14.md notes/2026-09-21.md
0 errors, 0 warnings
Commit the schema, the notes, and the workflow, and push. The job passes when the command exits with code 0, and fails on code 1 or 2.
Step 5: add a rule as a warning first
An added rule can break the notes already in the repository, and so break the build for everyone. Add the rule with severity: warn instead. A failure of the rule is then a warning, and warnings don’t change the exit code.
Require each decision to end with a full stop, as a warning:
schemas/meeting.yaml
line-limit: 200
frontmatter:
title:
example: Planning meeting
date:
pattern: '^\d{4}-\d{2}-\d{2}$'
example: '2026-09-14'
sections:
- heading: { from: title }
level: 1
blocks: []
- heading: Attendees
level: 2
blocks:
- kind: bullet-list
example: Ana Lima
- heading: Decisions
level: 2
blocks:
- kind: bullet-list
each:
pattern: '\.$'
severity: warn
example: Ship the beta on 1 October.
- heading: Actions
level: 2
blocks:
- kind: bullet-list
each:
template: '${owner:word}: ${}'
example: 'Ana: book the room for the launch'
A note written after the change leaves out the full stop:
notes/2026-09-28.md
---
title: Launch retrospective
date: 2026-09-28
---
# Launch retrospective
## Attendees
- Ben Okafor
## Decisions
- Keep the weekly release call
## Actions
- Ben: send the survey to the beta users
$ tramlines validate --warnings --schema schemas/meeting.yaml notes/2026-09-14.md notes/2026-09-21.md notes/2026-09-28.md
notes/2026-09-28.md:14: warning[TL_74]: `Keep the weekly release call` does not match `\.$`; an item of this kind reads Ship the beta on 1 October. (schema meeting, section "Decisions" block 1)
0 errors, 1 warning
- With
--warnings, the command lists the warning and exits with code 0, so the CI job passes. The workflow runs with--warningstoo, so the pull request shows the decision to fix. Without--warnings, the command only counts the warning. - When every note follows the rule, remove
severity: warn. From then on, a decision with no full stop is an error, and the job fails on it.
What’s next
- Read an error and fix the document shows how to read each part of an error line.
- Share line formats across schemas shows how to keep the line formats of several schemas in one file that you pass with
--formats. - The command lists every option, the output, and the exit codes.
- Errors explains each key that an error line can name.
Write the shape of a line
When a rule must specify the text of one line of a document, give it a shape: text for a line that never varies, template for a line with parts that vary, or pattern for what a template can’t express. At the end, each line has the shape that you meant, and the error for a wrong line shows the example of a correct one.
A shape fits any rule that checks one unit: a heading, a paragraph, a list item, a table row, a line of a fenced block, or a frontmatter value. This guide uses list items and a paragraph of a recipe.
Before you begin
- A schema with a section and a block, as in Your first schema.
- The
tramlinescommand, from the installation steps.
Steps
-
Pick one shape for each rule. A rule declares exactly one shape:
text,template,pattern, or a boundformat. Share line formats across schemas describes formats. Two shapes on one rule are an error. -
For a line that never varies, write it as
text. In this recipe schema, the paragraph of the first block must be exactlyMakes eight pancakes. -
For a line with parts that vary, write a
template: the line itself, with a hole at each part that varies. A template matches the whole line. In this recipe, every ingredient is a quantity, a unit, and a food. Give every template anexample, a real line that it matches. A hole has a name, a type, both, or neither:${name:type}matches text of the type and captures it asname.${name}matches any text, as the typetext, and captures it asname.${:type}matches text of the type and captures nothing.${}matches any text and captures nothing.
A hole name contains only letters, digits, and
_, and appears only once in its template. A template has at least one hole: write a line with none astext.$${is a literal${, and any other$is a literal$. Thekeyof a rule names a captured hole. Capture keys and anchor them shows how. -
For what a template can’t express, write a
pattern, a regular expression in the Rust dialect. The command doesn’t anchor a pattern, so write^and$to match the whole line. In this recipe, each step of the method starts with a capital letter and ends with its only full stop. Give every pattern anexampletoo. -
When a hole accepts one of a few words, or text that no built-in type matches, define your own type under
types. A type is a list of words, matched as written, or a regular expression with no anchor and no capture group. The typeunitin the following schema is a list. Put alternatives and optional parts in a type, so that a template never needs its own regular expression syntax.The built-in types are
text,word,int,slug, andseq. Built-in types lists what each one matches. Your own type can’t have the name of a built-in type. The types of a formats file rank below the types of the schema, so a type in the schema replaces a type of the same name in the file. Share line formats across schemas describes formats files.
recipe.yaml
line-limit: 100
types:
unit: [g, ml, tsp, tbsp, whole]
sections:
- heading: Pancakes
level: 1
blocks:
- kind: prose
each: { text: Makes eight pancakes. }
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${qty:int} ${unit:unit} ${food}'
example: 200 g plain flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
each:
pattern: '^[A-Z][^.]*\.$'
example: Whisk the flour, eggs and milk into a smooth batter.
- Check a document. When the command matches a shape, it reads each run of spaces and line breaks in the text as one space. A paragraph wrapped over two lines therefore matches as one line, and a double space matches a single one:
pancakes.md
# Pancakes
Makes eight
pancakes.
## Ingredients
- 200 g plain flour
- 2 whole eggs
- 300 ml milk
## Method
1. Whisk the flour, eggs and milk into a smooth batter.
2. Fry each pancake for a minute on each side.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
- Read a failure. The error for a template names the column where the text stops matching, what the template requires there, and the example of the rule. The error for a pattern names the pattern and the example. In this document,
eggsisn’t a unit, and the second step starts with a lowercase letter:
crepes.md
# Pancakes
Makes eight pancakes.
## Ingredients
- 200 g plain flour
- 2 eggs
## Method
1. Whisk the flour, eggs and milk into a smooth batter.
2. fry each pancake for a minute on each side.
$ tramlines validate --schema recipe.yaml crepes.md
crepes.md:8: error[TL_74]: `2 eggs` fails at column 3: expected a `unit` (one of `g`, `ml`, `tsp`, `tbsp`, `whole`), found `eggs`; an item of this kind reads 200 g plain flour (schema recipe, section "Ingredients" block 1)
crepes.md:13: error[TL_74]: `fry each pancake for a minute on each side.` does not match `^[A-Z][^.]*\.$`; an item of this kind reads Whisk the flour, eggs and milk into a smooth batter. (schema recipe, section "Method" block 1)
2 errors, 0 warnings
[exit 1]
Troubleshooting
TL_22: a rule declares two shapes. Keep one oftext,template,pattern, andformat. See TL_22.TL_25: a template has no hole. Write it astext. See TL_25.TL_119: a template names one hole twice. Rename one of them. See TL_119.TL_120: a hole names a type that is unknown to the schema. Define the type, or correct its name. See TL_120.TL_29: a type in the schema uses the name of a built-in type. See TL_29.TL_109: the regular expression of a type contains an anchor or a capture group. See TL_109.TL_66: a rule with atemplateor apatternlacks anexample. Add one. See TL_66.TL_74: a line fails its shape. Compare it with the example in the message, from the column that the message names. See TL_74.
See also
- Schema keys:
unit - Schema keys:
heading - Built-in types
- Share line formats across schemas
- Write examples and guidance
- Section 4 of the core specification
Share line formats across schemas
When two schemas check the same kind of line, write its shape once as a named format and bind it from each rule with format. At the end, one formats file defines the line. Every schema that names the format checks the line in the same way, and a schema that needs a different line can change the format for itself alone.
In this guide, a meeting note and a project plan share the line of an action. An action is either open or done, and a done action includes its date.
Before you begin
- A schema with a list, as in Your first schema.
- An understanding of the shapes of a line, from Write the shape of a line.
Steps
-
Write the formats file. It contains a
registrymap of formats and an optionaltypesmap of the types that the formats use. A format defines one shape and anexample, as a rule does. For a line that comes in more than one form, a format declaresvariantsinstead: named shapes, each with its ownexample. The command tries the variants on a line in order, and the line conforms when one variant matches it.Write the example of each variant so that it matches only its own variant. Put a word that distinguishes the variants in each shape, such as
[open]and[done]in the following file.
formats.yaml
types:
action-id: 'A-\d+'
date: '\d{4}-\d{2}-\d{2}'
registry:
action:
variants:
open:
template: '${:action-id} [open]: ${}'
example: 'A-1 [open]: book the venue'
done:
template: '${:action-id} [done] ${:date}: ${}'
example: 'A-2 [done] 2026-09-30: send the invitations'
- Bind the format from a rule of each schema with
format: <name>. The rule takes its shape and example from the format, so it declares no shape of its own.
meeting.yaml
line-limit: 0
sections:
- heading: Actions
level: 2
blocks:
- kind: bullet-list
each: { format: action }
plan.yaml
line-limit: 0
sections:
- heading: Plan
level: 1
blocks:
- kind: numbered-list
each: { format: action }
- Pass the formats file with
--formatswhen you check a document.
meeting.md
## Actions
- A-1 [open]: book the venue
- A-2 [done] 2026-09-30: send the invitations
plan.md
# Plan
1. A-3 [open]: draft the budget
2. A-4 [open]: ask the speakers
$ tramlines validate --schema meeting.yaml --formats formats.yaml meeting.md
0 errors, 0 warnings
$ tramlines validate --schema plan.yaml --formats formats.yaml plan.md
0 errors, 0 warnings
Without --formats, the schema names a format that the command can’t find:
$ tramlines validate --schema meeting.yaml meeting.md
meeting.yaml:7: error[TL_97]: `format` names no format `action`
1 error, 0 warnings
[exit 1]
- Read the error for a line that fails a format. The command reports the line against the variant that matches it furthest, with the column where the match stops and the example of that variant. When the match stops at the same column in every variant, the command reports the line against all of them, with the example of the first:
meeting.md
## Actions
- A-1 [open]: book the venue
- A-2 [done]: send the invitations
- Ask Sam about the budget
$ tramlines validate --schema meeting.yaml --formats formats.yaml meeting.md
meeting.md:4: error[TL_74]: `A-2 [done]: send the invitations` does not match the `done` form: it fails at column 11: expected ` `, found `: send the invitations`; an item of this kind reads A-2 [done] 2026-09-30: send the invitations (schema meeting, section "Actions" block 1)
meeting.md:5: error[TL_74]: `Ask Sam about the budget` matches none of the forms `open`, `done`; an item of this kind reads A-1 [open]: book the venue or A-2 [done] 2026-09-30: send the invitations (schema meeting, section "Actions" block 1)
2 errors, 0 warnings
[exit 1]
-
To change a format for one schema, define a format of the same name under the
formatsof the schema. The nearest layer wins: theformatsof the schema first, then the formats file. An entry in the schema replaces the whole entry in the file and keeps nothing from it.To state only a difference, set
patch: trueon the entry of the schema. The command merges the patch into the entry of the file with JSON Merge Patch (RFC 7386). A key that the patch sets replaces the key of the file, and a key that the patch leaves out is kept. A key that the patch sets tonullis dropped. A list replaces the whole list of the file. A patch needs noexampleof its own, but the merged entry needs one for each shape. This patch changes only thedonevariant, so that a done action in a plan needs no date:
plan.yaml
line-limit: 0
formats:
action:
patch: true
variants:
done:
template: '${:action-id} [done]: ${}'
example: 'A-2 [done]: send the invitations'
sections:
- heading: Plan
level: 1
blocks:
- kind: numbered-list
each: { format: action }
plan.md
# Plan
1. A-3 [open]: draft the budget
2. A-4 [done]: ask the speakers
$ tramlines validate --schema plan.yaml --formats formats.yaml plan.md
0 errors, 0 warnings
- To change a type that a format uses, define the type under the
typesof the schema. Types resolve through the same layers as formats. The command compiles each format available to a schema against the types available to that schema. In this schema, an action id readsACT-1. The shared examples no longer match, so a patch gives each variant a new example and keeps its template:
renumbered.yaml
line-limit: 0
types:
action-id: 'ACT-\d+'
formats:
action:
patch: true
variants:
open:
example: 'ACT-1 [open]: book the venue'
done:
example: 'ACT-2 [done] 2026-09-30: send the invitations'
sections:
- heading: Actions
level: 2
blocks:
- kind: bullet-list
each: { format: action }
renumbered.md
## Actions
- ACT-1 [open]: book the venue
- ACT-2 [done] 2026-09-30: send the invitations
$ tramlines validate --schema renumbered.yaml --formats formats.yaml renumbered.md
0 errors, 0 warnings
- To capture a key from every line of a format, set
keyon the format to the name of a hole or a named group that every shape of the format contains. A key in a markdown document needs an anchor before it. Capture keys and anchor them explains anchors. Don’t declarekeyon a rule that binds a format with a key.
formats.yaml
types:
action-id: 'A-\d+'
date: '\d{4}-\d{2}-\d{2}'
registry:
action:
key: id
variants:
open:
template: '${id:action-id} [open]: ${}'
example: 'A-1 [open]: book the venue'
done:
template: '${id:action-id} [done] ${:date}: ${}'
example: 'A-2 [done] 2026-09-30: send the invitations'
meeting.md
## Actions
- <a id="a-1"></a>A-1 [open]: book the venue
- <a id="a-2"></a>A-2 [done] 2026-09-30: send the invitations
$ tramlines validate --schema meeting.yaml --formats formats.yaml meeting.md
0 errors, 0 warnings
- When the key is optional in the pattern of the format, set
key-optional: trueon the rule. Without it, a line that matches the shape but contains no key is an error. The same holds when the rule declares the key itself. This format describes a note that can start with an action id:
formats.yaml
registry:
note:
key: id
pattern: '^(?:(?P<id>A-\d+): )?[A-Z].*$'
example: 'A-1: Book the venue'
notes.yaml
line-limit: 0
sections:
- heading: Notes
level: 2
blocks:
- kind: bullet-list
each: { format: note }
notes.md
## Notes
- <a id="a-1"></a>A-1: Book the venue
- Ask Sam about the budget
$ tramlines validate --schema notes.yaml --formats formats.yaml notes.md
notes.md:4: error[TL_44]: `Ask Sam about the budget` carries no key: the format `note` captures `id` (schema notes, section "Notes" block 1)
1 error, 0 warnings
[exit 1]
With key-optional: true, the second line passes as a note without a key:
notes.yaml
line-limit: 0
sections:
- heading: Notes
level: 2
blocks:
- kind: bullet-list
each: { format: note, key-optional: true }
$ tramlines validate --schema notes.yaml --formats formats.yaml notes.md
0 errors, 0 warnings
A format can also ban text in every line bound to it, with prohibited-patterns, as Ban text in a document shows.
Troubleshooting
TL_97: a rule binds a format that no layer defines. Pass the formats file with--formats, or check the name. See TL_97.TL_87: the formats file doesn’t parse, or has noregistrymap. The command stops with exit code 2. A fault inside the registry, such as a type that isn’t valid, stops the command with the key of that fault. See TL_87.TL_40: a format with one shape has noexample. See TL_40.TL_110: a format withvariantsalso declares anexamplebeside them. See TL_110.TL_41: an entry withpatch: truenames a format that no farther layer defines. Removepatch, or fix the name. See TL_41.TL_43: the example of one variant also matches another variant. Change the example or the shapes. See TL_43.TL_44: a line matches the shape of a format with a key but contains no key. Add the key, or setkey-optionalwhere the key is optional. See TL_44.TL_65: the example of a format fails its own shape, for example when a schema changes a type that the format uses. See TL_65.TL_113: a rule declareskeybeside a format that captures a key. Remove thekeyof the rule. See TL_113.
See also
- Schema keys:
format - Schema keys:
format-variant - The command
- How the grammar layers fit together
- Section 5 of the core specification
Apply a rule in some states only
When a document changes as its work progresses, declare its states in a lifecycle. An issue, for example, gains an outcome when it closes. Then gate each rule that applies only in some states with only. At the end, one schema describes the document in every state, and the check requires the parts that each state needs.
In this guide, you write a schema for an issue, which is open or closed, and a bug or a feature. Only a bug includes a reproduction, and only a closed issue includes an outcome and a date.
Before you begin
- A schema with sections and frontmatter, as in Your first schema.
Steps
-
Declare each dimension of the lifecycle under
lifecycle, in order, with its frontmatterkeyand itsstates. Each dimension is a required frontmatter key whose value is one of its states. Don’t declare the key again underfrontmatter. A dimension can takeguidance, a note that explains the states to a writer. -
Set
onlyon each rule that applies only in some states.onlymaps a dimension to a state, or to a list of states. A rule withonlyapplies when the state of the document is in the set for every dimension thatonlynames. Any rule can takeonly: a section, a block, a frontmatter key, or any other rule of a schema.
issue.yaml
line-limit: 0
lifecycle:
- key: state
states: [open, closed]
guidance: Close the issue when its work is merged.
- { key: kind, states: [bug, feature] }
frontmatter:
title:
example: The parser rejects a tab
closed-on:
pattern: '^\d{4}-\d{2}-\d{2}$'
example: '2026-09-30'
only: { state: closed }
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
example: A list item indented with a tab is read as a code block.
- heading: Reproduction
level: 2
only: { kind: bug }
blocks:
- kind: numbered-list
example: Indent a list item with a tab.
- heading: Outcome
level: 2
only: { state: closed, kind: [bug, feature] }
blocks:
- kind: prose
example: The parser now reads a tab as four spaces.
- Check an open bug. This open bug includes a reproduction but lacks an outcome and a date:
tab.md
---
title: The parser rejects a tab
state: open
kind: bug
---
# The parser rejects a tab
A list item indented with a tab is read as a code block.
## Reproduction
1. Indent a list item with a tab.
2. Run the check.
$ tramlines validate --schema issue.yaml tab.md
0 errors, 0 warnings
- Change the state, and check again. After the bug is closed, the check requires the outcome and the date:
tab.md
---
title: The parser rejects a tab
state: closed
kind: bug
---
# The parser rejects a tab
A list item indented with a tab is read as a code block.
## Reproduction
1. Indent a list item with a tab.
2. Run the check.
$ tramlines validate --schema issue.yaml tab.md
tab.md: error[TLMD_18]: section `## Outcome` is missing (schema issue, section "Outcome")
tab.md:2: error[TL_54]: frontmatter key `closed-on` is missing (schema issue, key `closed-on`)
2 errors, 0 warnings
[exit 1]
The check reports a part that the state of the document doesn’t allow as undeclared. A feature can’t include a reproduction:
tabs-as-spaces.md
---
title: Read a tab as four spaces
state: open
kind: feature
---
# Read a tab as four spaces
A list item indented with a tab is read as a code block.
## Reproduction
1. Indent a list item with a tab.
$ tramlines validate --schema issue.yaml tabs-as-spaces.md
tabs-as-spaces.md:11: error[TLMD_19]: heading `## Reproduction` is not declared (schema issue)
1 error, 0 warnings
[exit 1]
- Add real documents of the type under
examples, each as onedocument. The command checks each example at the states of its own frontmatter, as it checks a document, and never copies it. An example can’t contain markers.
issue.yaml
line-limit: 0
lifecycle:
- key: state
states: [open, closed]
guidance: Close the issue when its work is merged.
- { key: kind, states: [bug, feature] }
frontmatter:
title:
example: The parser rejects a tab
closed-on:
pattern: '^\d{4}-\d{2}-\d{2}$'
example: '2026-09-30'
only: { state: closed }
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
example: A list item indented with a tab is read as a code block.
- heading: Reproduction
level: 2
only: { kind: bug }
blocks:
- kind: numbered-list
example: Indent a list item with a tab.
- heading: Outcome
level: 2
only: { state: closed, kind: [bug, feature] }
blocks:
- kind: prose
example: The parser now reads a tab as four spaces.
examples:
- document: |
---
title: Read a tab as four spaces
state: closed
kind: feature
closed-on: '2026-09-30'
---
# Read a tab as four spaces
A list item indented with a tab is read as a code block.
## Outcome
The parser now reads a tab as four spaces.
tab.md
---
title: The parser rejects a tab
state: closed
kind: bug
closed-on: '2026-09-30'
---
# The parser rejects a tab
A list item indented with a tab is read as a code block.
## Reproduction
1. Indent a list item with a tab.
2. Run the check.
## Outcome
The parser now reads a tab as four spaces.
$ tramlines validate --schema issue.yaml tab.md
0 errors, 0 warnings
- Start a new document with
tramlines new. For each combination of states, the engine derives one scaffold, the document that a new document starts from. The commandtramlines validatechecks each scaffold. The commandtramlines newprints the scaffold of the states that--statenames. For each dimension that--statedoesn’t name, it takes the first state. The scaffold contains the parts that those states need, and aguidanceline before each part that has guidance. Read eachguidanceline, act on it, and then delete it:
$ tramlines new --schema issue.yaml --state state=closed --state kind=feature
---
title: The parser rejects a tab
closed-on: 2026-09-30
# tramlines:guidance Close the issue when its work is merged.
state: closed
kind: feature
---
<!-- tramlines:guidance The writer replaces each of the following examples with their own text. -->
# The parser rejects a tab
A list item indented with a tab is read as a code block.
## Outcome
The parser now reads a tab as four spaces.
Troubleshooting
TL_62: a lifecycle key is missing. Add the key. See TL_62.TL_108: the value of a lifecycle key isn’t one of its states. Set the key to one of the states that the message lists. See TL_108.TL_20:onlynames a dimension or a state that the lifecycle doesn’t declare. Fix the name. See TL_20.TL_111: a rule declaresonly, and the schema has no lifecycle. Declare the lifecycle. See TL_111.TL_95: the lifecycle declares one dimension twice. Remove one. See TL_95.TL_112:frontmatterdeclares the key of a lifecycle dimension. Remove it fromfrontmatter. See TL_112.TL_69: an entry ofexamplesfails the schema at its own states. Fix the example. See TL_69.
See also
- Schema keys:
lifecycle-dimension - Schema keys:
example - Constrain frontmatter and ids, to put a state in the id of a document
- Strict by default
- How a schema derives its scaffold
- Section 6 of the core specification
Constrain frontmatter and ids
When the frontmatter of a document must have certain keys with certain values, declare each key under frontmatter and constrain its value. At the end, a document with a missing key, an unknown key, or a value of the wrong shape fails with an error that names the key.
In this guide, you write the frontmatter of a requirement list: a fixed type, a numbered id, a code in capitals, a status, one or two links, and optional tags.
Before you begin
- A schema with a section, as in Your first schema.
- The shapes of a line, from Write the shape of a line. A frontmatter value takes the same shapes.
Steps
-
Declare each key under
frontmatter. The check requires every declared key unless you setoptional: trueon it. A key that the schema doesn’t declare is an error. -
Constrain each value with one of these keys:
const: the value must equal the given value.enum: the value must be one of the listed values.templateorpattern: the value must match the shape. Give the key anexample, as on any rule.format:urlfor an absolute URL, orslugfor lowercase letters and digits joined by single hyphens.
-
For a key whose value is a list, set
list: true. The constraint then applies to every element, andminandmaxbound the number of elements. A list where no list is declared, or a single value where a list is declared, is an error. -
Give
ida template. The template uses only the holes that the engine fills when it writes a new document:${:seq}: a sequence number, unique within the kind of document. A template can contain at most one. The number is three digits wide unless you setseq-widthbeside the template.${:slug}: one or more slug words.${<key>}: a hole with no type, named for a lifecycle key. It holds the state of the document for that key. Step 7 shows an example.
An
idtemplate takes noexample. For a kind that has only one document, set the id withconstinstead.
requirements.yaml
line-limit: 0
types:
code: '[A-Z]{2,6}'
frontmatter:
type:
const: Requirements
id:
template: 'req-${:seq}-${:slug}'
code:
template: '${:code}'
example: LINK
status:
enum: [draft, approved]
homepages:
list: true
format: url
min: 1
max: 2
example: https://example.com/link-checker
tags:
list: true
format: slug
optional: true
example: parsing
sections:
- heading: Link checker
level: 1
blocks:
- kind: prose
example: The link checker follows every link in a document.
- Check a document. Each error names the frontmatter key that fails:
link-checker.md
---
type: Requirements
id: req-7-link-checker
code: Link
status: final
homepages: https://example.com/link-checker
tags: [Parsing, links]
owner: Sam
---
# Link checker
The link checker follows every link in a document.
$ tramlines validate --schema requirements.yaml link-checker.md
link-checker.md:3: error[TL_56]: id `req-7-link-checker` does not match the template `req-${:seq}-${:slug}` (schema requirements, key `id`)
link-checker.md:4: error[TL_56]: `code` `Link` fails at column 1: expected a `code` (`[A-Z]{2,6}`), found `Link`; a value of this kind reads LINK (schema requirements, key `code`)
link-checker.md:5: error[TL_56]: `status` must be one of `draft`, `approved`; `final` is not (schema requirements, key `status`)
link-checker.md:6: error[TL_57]: `homepages` must be a list (schema requirements, key `homepages`)
link-checker.md:7: error[TL_56]: `tags` must be a slug; `Parsing` is not (schema requirements, key `tags`)
link-checker.md:8: error[TL_55]: frontmatter key `owner` is not declared (schema requirements)
6 errors, 0 warnings
[exit 1]
The id has a one-digit sequence number where the template needs three, code isn’t in capitals, final isn’t a status, homepages isn’t a list, Parsing isn’t a slug, and owner isn’t declared. This version of the document corrects each one:
link-checker.md
---
type: Requirements
id: req-007-link-checker
code: LINK
status: draft
homepages: [https://example.com/link-checker]
tags: [parsing, links]
---
# Link checker
The link checker follows every link in a document.
$ tramlines validate --schema requirements.yaml link-checker.md
0 errors, 0 warnings
- To accept keys that the schema doesn’t declare, set
frontmatter-open: trueon the schema. The command still checks the declared keys. Withfrontmatter-open: true, the command acceptsowner:
requirements.yaml
line-limit: 0
frontmatter-open: true
types:
code: '[A-Z]{2,6}'
frontmatter:
type:
const: Requirements
id:
template: 'req-${:seq}-${:slug}'
code:
template: '${:code}'
example: LINK
status:
enum: [draft, approved]
homepages:
list: true
format: url
min: 1
max: 2
example: https://example.com/link-checker
tags:
list: true
format: slug
optional: true
example: parsing
sections:
- heading: Link checker
level: 1
blocks:
- kind: prose
example: The link checker follows every link in a document.
owned.md
---
type: Requirements
id: req-008-owned-links
code: LINK
status: draft
homepages: [https://example.com/link-checker]
owner: Sam
---
# Link checker
The link checker follows every link in a document.
$ tramlines validate --schema requirements.yaml owned.md
0 errors, 0 warnings
- To put a state of the document in its id, name a hole of the
idtemplate after a lifecycle key. Apply a rule in some states only shows how to declare one. The id must then contain the state of the document for that key. This schema requires a four-digit number in the id of each issue:
issue.yaml
line-limit: 0
lifecycle:
- { key: kind, states: [bug, feature] }
frontmatter:
id:
template: 'issue-${:seq}-${kind}-${:slug}'
seq-width: 4
sections:
- heading: Issue
level: 1
blocks:
- kind: prose
example: A list item indented with a tab is read as a code block.
tab.md
---
id: issue-0007-feature-tab
kind: bug
---
# Issue
A list item indented with a tab is read as a code block.
$ tramlines validate --schema issue.yaml tab.md
tab.md:2: error[TL_59]: id `issue-0007-feature-tab` holds `feature` for `kind`, but `kind` is `bug` (schema issue, key `id`)
1 error, 0 warnings
[exit 1]
The command reserves no frontmatter key. A program that embeds the engine can reserve its own keys. The check never reports a reserved key as undeclared. A schema can still declare a reserved key to require it or to narrow its value.
Troubleshooting
TL_54: a declared key is missing. Add it, or setoptional: trueon the key. See TL_54.TL_55: a key isn’t declared. Remove it, declare it, or setfrontmatter-open: true. See TL_55.TL_56: a value fails itsconst,template,pattern,enum, orformat. See TL_56.TL_106,TL_107: a list has fewer elements than itsmin, or more than itsmax. See TL_106 and TL_107.TL_57: a list is declared and the value is a single value, or the other way round. See TL_57.TL_58: theidtemplate contains a hole that the engine can’t fill for a new document. See TL_58.TL_114: theidtemplate contains more than one${:seq}. See TL_114.TL_115: theiddeclares another key beside its template. See TL_115.TL_59: the id contains a state other than the state of the document for that key. See TL_59.TL_53: the frontmatter isn’t valid YAML. See TL_53.TL_123: the frontmatter contains an anchor, an alias, or a tag. See TL_123.
See also
- Schema keys:
frontmatter-constraint - Built-in types
- Frontmatter as the authority over the body
- Section 6 of the core specification
Ban text in a document
When some text must never appear, list a regular expression for it under prohibited-patterns, with a message that states the rule. Such text might be a TODO left in a finished document, or a vague word in a changelog. Each line that matches the expression then fails, and its error quotes the matched text and gives the message. The scaffold of a new document shows the message too, so that its writer reads the rule before writing.
You can set the key at three levels. A ban on the schema applies to every line of the body. A ban on a rule applies to the text of each unit that the rule checks. A ban on a format applies to every line bound to the format.
Before you begin
- A schema with a list, as in Your first schema.
- To ban text in a format, a format that you defined, as in Share line formats across schemas.
Steps
-
To ban text anywhere in the body, list it under
prohibited-patternson the schema. Each entry has apatternand amessage. Thepatternis an unanchored regular expression in the Rust dialect. Themessageis one sentence that states the rule as a fact about the document. Use\bto match whole words, and(?i)to ignore case. -
To ban text in one kind of unit, put
prohibited-patternson the rule that checks it. -
To ban text in every line of a format, put
prohibited-patternson the format. The command checks the shape of a line first, then the bans of its rule and of its format.This changelog schema bans
TODOeverywhere, the wordvariousin a list item, and...in every line of the formatchange:
changelog.yaml
line-limit: 0
prohibited-patterns:
- pattern: '\bTODO\b'
message: 'A finished changelog contains no TODO.'
types:
change: [Added, Changed, Fixed, Removed]
formats:
change:
template: '${:change}: ${}'
example: 'Fixed: the parser reads a tab as four spaces.'
prohibited-patterns: [{pattern: '\.\.\.', message: 'A change ends with a full stop, not with an ellipsis.'}]
sections:
- heading: Changelog
level: 1
blocks:
- kind: bullet-list
each:
format: change
prohibited-patterns: [{pattern: '(?i)\bvarious\b', message: 'An item names its change rather than calling it various.'}]
- kind: code
lang: text
optional: true
example: tramlines validate --schema list.yaml groceries.md
- Check a document. The command applies no ban inside fenced blocks or code spans, so text that you quote as code passes:
changelog.md
# Changelog
- Added: the `--formats` option. TODO document it
- Fixed: various bugs.
- Changed: the `TODO` marker in a code span is fine...
```text
TODO in a fence is fine
```
$ tramlines validate --schema changelog.yaml changelog.md
changelog.md:3: error[TL_75]: `TODO` is refused: A finished changelog contains no TODO. (schema changelog)
changelog.md:4: error[TL_75]: `various` is refused: An item names its change rather than calling it various. An item of this kind reads Fixed: the parser reads a tab as four spaces. (schema changelog, section "Changelog" block 1)
changelog.md:5: error[TL_75]: `...` is refused: A change ends with a full stop, not with an ellipsis. An item of this kind reads Fixed: the parser reads a tab as four spaces. (schema changelog, section "Changelog" block 1)
3 errors, 0 warnings
[exit 1]
Each finding quotes the span that the pattern matched, and gives the message of the ban, not its pattern. When the ban is on a rule or a format, the finding also shows the example of that rule or format, so that you see a line that passes. The scaffold of the schema writes the message of each ban after the guidance of the part that the ban governs. It writes the message of a ban on the schema in its opening line.
Troubleshooting
TL_75: a line of the document matches a banned pattern. Rewrite the span that the message quotes. If the span is a name, not prose, quote it as code instead. See TL_75.TL_118: a pattern isn’t a valid regular expression in the Rust dialect. For example, the dialect has no lookarounds and no backreferences. See TL_118.
See also
Describe sections and headings
When every document of a kind has the same headings, list them under sections in the schema. At the end, a document with a missing heading, an extra heading, or a heading in the wrong place fails. The error names the section of the schema that the document breaks.
In this guide, you build a schema for recipes. Each entry under sections describes one heading and its body, which runs to the next heading. Describe blocks and their units shows how to describe a body.
Before you begin
- A schema with frontmatter and a list, as in Your first schema.
- The
tramlinescommand, from the installation steps.
Steps
-
Give each section a
headingand alevelfrom1to6. Write the heading in one of three forms:- A string, for a heading that never changes, such as
Ingredients. { from: title }, for a heading that must equal a frontmatter value. Here the level-1 heading is thetitleof the recipe.- A heading rule, for a heading that varies: a
templateor apatternwith anexampleof a heading that it accepts. A string can’t contain a hole, so a heading that varies needs a heading rule. Write the shape of a line explains templates and patterns.
When a string and a template both match a heading, the string wins.
- A string, for a heading that never changes, such as
-
Set
optional: trueon a section that a document can leave out. Setrepeat: trueon a section that can occur more than once. By default, a section must occur exactly once. In this schema, a recipe contains one or more steps and can contain notes:
recipe.yaml
line-limit: 60
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
example: 200 g flour
- heading: { template: "Step ${:int}: ${}", example: "Step 1: Mix the batter" }
level: 2
repeat: true
blocks:
- kind: prose
example: Whisk the flour, eggs and milk until smooth.
- heading: Notes
level: 2
optional: true
blocks:
- kind: prose
example: The batter keeps for a day in the fridge.
- Check a document. The sections must appear in the order in which the schema lists them. To accept them in any order, set
sections-ordered: falseat the top of the schema.
pancakes.md
---
title: Pancakes
---
# Pancakes
Thin pancakes for a weekend breakfast.
## Ingredients
- 200 g flour
- 2 eggs
## Step 1: Mix the batter
Whisk the flour, eggs and milk until smooth.
## Step 2: Cook
Fry each pancake for a minute on each side.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
- Read the errors for headings. The error for a missing required section shows no line number. A heading that no section matches is an error. In this document the
Ingredientssection is missing, and the number inStep oneis a word:
crepes.md
---
title: Crepes
---
# Crepes
Thin pancakes for a weekend breakfast.
## Step one: Mix
Whisk the flour, eggs and milk until smooth.
$ tramlines validate --schema recipe.yaml crepes.md
crepes.md: error[TLMD_18]: section `## Ingredients` is missing (schema recipe, section "Ingredients")
crepes.md: error[TLMD_18]: section `## Step ${:int}: ${}` is missing (schema recipe, section "Step ${:int}: ${}")
crepes.md:9: error[TLMD_19]: heading `## Step one: Mix` is not declared (schema recipe)
3 errors, 0 warnings
[exit 1]
-
To let writers add headings that the schema doesn’t list, open a region after a section:
subheadings-open: trueaccepts undeclared headings deeper than the level of the section.headings-open: trueaccepts undeclared headings at the level of the section or deeper.
The region runs to the next heading that the schema declares. Only the rules that apply to the whole document, such as
line-limit, check the bodies inside it. -
To keep some headings out of every open region, list them under
sections-prohibited. Here writers can add any subheading underNotes, exceptDraft:
recipe.yaml
line-limit: 60
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
example: 200 g flour
- heading: { template: "Step ${:int}: ${}", example: "Step 1: Mix the batter" }
level: 2
repeat: true
blocks:
- kind: prose
example: Whisk the flour, eggs and milk until smooth.
- heading: Notes
level: 2
optional: true
subheadings-open: true
blocks:
- kind: prose
example: The batter keeps for a day in the fridge.
sections-prohibited:
- heading: Draft
pancakes.md
---
title: Pancakes
---
# Pancakes
Thin pancakes for a weekend breakfast.
## Ingredients
- 200 g flour
## Step 1: Mix the batter
Whisk the flour, eggs and milk until smooth.
## Notes
The batter keeps for a day in the fridge.
### Serving
Serve with lemon and sugar.
### Draft
Try oat milk.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:25: error[TLMD_26]: heading `### Draft` is prohibited (schema recipe)
1 error, 0 warnings
[exit 1]
-
To nest a section inside each occurrence of a repeating one, give the outer section a
rule-idand name that id as theparentof the inner section. The inner section must have a larger level and come after its parent, with only the other subsections of the parent between them. Theoptionalandrepeatof the inner section then apply within each occurrence of the parent.To require the inner heading to start with the number of the parent heading, a dot, and a further number, set
number-from-parent: true. Under2, for example, the inner heading starts with2.1. The number of a heading is the text before its first space: digits separated by dots. -
To describe text before the first heading, add a
preamblewith its ownblocks. Without apreamble, any content before the first heading is an error. -
When you rename a fixed heading, list its old text under
renamed-from. A document that still contains the old heading fails with an error that names the new one. Change the heading in the document to the new text.This menu schema describes a date line before the heading, courses that repeat, and an optional allergen list in each course:
menu.yaml
line-limit: 60
preamble:
blocks:
- kind: prose
max: 1
example: "Updated: 2026-09-30"
sections:
- heading: Menu
level: 1
renamed-from: Dishes
blocks: []
- heading: { template: "${:int} ${}", example: "1 Starters" }
level: 2
repeat: true
rule-id: course
blocks:
- kind: bullet-list
example: Tomato soup
- heading: { template: "${:int}.${:int} Allergens", example: "1.1 Allergens" }
level: 3
parent: course
number-from-parent: true
optional: true
blocks:
- kind: bullet-list
example: The soup contains celery.
The menu still uses its old heading, and the Allergens heading of the second course uses the number of the first course:
menu.md
Updated: 2026-09-30
# Dishes
## 1 Starters
- Tomato soup
### 1.1 Allergens
- The soup contains celery.
## 2 Mains
- Fish pie
### 1.1 Allergens
- The pie contains milk.
$ tramlines validate --schema menu.yaml menu.md
menu.md:3: error[TLMD_137]: heading `Dishes` was renamed; it is now `Menu` (schema menu, section "Menu")
menu.md:17: error[TLMD_35]: heading `### 1.1 Allergens` must be numbered `2.<n>`, under `## 2 Mains` (schema menu, section "${:int}.${:int} Allergens")
2 errors, 0 warnings
[exit 1]
- Fix the document: rename
DishestoMenu, and renumber the secondAllergensheading2.1.
menu.md
Updated: 2026-09-30
# Menu
## 1 Starters
- Tomato soup
### 1.1 Allergens
- The soup contains celery.
## 2 Mains
- Fish pie
### 2.1 Allergens
- The pie contains milk.
$ tramlines validate --schema menu.yaml menu.md
0 errors, 0 warnings
Troubleshooting
TLMD_18: a required section is missing. Add the section, or setoptionalon it. See TLMD_18.TLMD_151: a section that doesn’t repeat occurs twice. Remove one, or setrepeaton it. See TLMD_151.TLMD_19: no section matches the heading. Correct the heading, declare it, or open a region before it. See TLMD_19.TLMD_20: a heading is more than one level deeper than the heading before it, such as a level-3 heading directly after a level-1 heading. See TLMD_20.TLMD_22: a section comes before a section that the schema lists ahead of it. Move it, or setsections-ordered: false. See TLMD_22.TLMD_144: a heading matches the text of a section at another level. See TLMD_144.TLMD_38: content comes before the first heading of the document, and the schema declares nopreamble. See TLMD_38.TLMD_31: a nested section isn’t inside its parent. See Sections inside sections.
See also
- Schema keys:
section - Schema keys:
heading - Schema keys:
preamble - Schema keys:
prohibited-heading - Errors: headings and sections
- Strict by default
- Capture keys and anchor them
Describe blocks and their units
When the body of a section must contain certain paragraphs, lists, tables, or code, declare them under the blocks of the section. At the end, a body with a missing block, an extra block, or a unit of the wrong shape fails. The error names the line of the document where the problem is.
Before you begin
- A schema with sections. Describe sections and headings shows how to write one.
- Templates and patterns, as in Write the shape of a line.
Steps
-
List the blocks of the body in order, each with a
kind:prose,bullet-list,numbered-list,table,code,blockquote,image,html,thematic-break,references(link reference definitions), orgroup. tramlines reads the body as CommonMark blocks. Consecutive paragraphs form oneproseblock, and a paragraph of only images forms animageblock. tramlines skips HTML comments, footnote definitions, and generated blocks when it matches blocks.If you leave out
blocks, the body must be one prose block. Writeblocks: []for an empty body. -
Bound the units of each block. A unit is a paragraph of a prose block or a blockquote, an item of a list, a row of a table, or one repetition of a group. By default, a block needs at least one unit and has no upper limit. Set
minormaxto change these bounds. -
Set
optional: trueon a block that a body can leave out. Setrepeat: trueon a block that can occur more than once in a row. To accept the blocks of a body in any order, setblocks-ordered: falseon the section. -
Give the units a shape:
eachapplies one rule to every unit.sequenceapplies one rule to each position, in order, and so sets the number of units. Setoptionalorrepeaton an entry to let its position be absent or recur.textsets the whole text of a unit.{ from: description }requires the unit to equal the frontmatter value, asheading: { from: title }does for a heading.
A block takes only one of
each,sequence, orrules. Select and count units and lines describesrules. -
Describe what an item contains besides its own text. tramlines matches the text of an item without its nested blocks. To allow a nested list or code block, describe it under
blockson the rule of the item. Withoutblockson the rule, an item can contain only its text. -
Add the keys of each kind:
- A
codeblock needslang: a tag, a list of tags, orany. A fence without a tag always fails.fencesets the minimum number of backticks. The default is three. - A
tableneedscolumns, the cells of its header row in order.columns-open: trueaccepts more columns after them. A unit of a table is a row, and a template matches the row as its cells separated by|. - An
imageblock takessource-pattern, a regular expression that each image path must match. - Any block takes
inline-html: true. Without it, HTML other than a comment fails.
A key that doesn’t apply to the kind of its block is an error in the schema. This schema describes the notes of a meeting:
- A
meeting.yaml
line-limit: 80
frontmatter:
title:
example: Weekly sync
description:
example: The team agreed the release date.
types:
date: '\d{4}-\d{2}-\d{2}'
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
each: { text: { from: description } }
- kind: bullet-list
sequence:
- { template: "Date: ${:date}", example: "Date: 2026-09-30" }
- { template: "Chair: ${}", example: "Chair: Ana" }
- heading: Attendees
level: 2
blocks:
- kind: table
columns: [Name, Role]
each: { template: "${} | ${}", example: "Ana | Chair" }
- heading: Decisions
level: 2
blocks:
- kind: numbered-list
max: 5
each:
template: "${}."
example: "Ship version 2 on Friday."
blocks:
- kind: blockquote
optional: true
example: Ana asked for one more day of testing.
- kind: code
lang: [text, console]
optional: true
example: npm run release
- heading: Photos
level: 2
optional: true
blocks:
- kind: image
source-pattern: '^images/.+\.png$'
example: ""
- Check a document:
sync.md
---
title: Weekly sync
description: The team agreed the release date.
---
# Weekly sync
The team agreed the release date.
- Date: 2026-09-30
- Chair: Ana
## Attendees
| Name | Role |
| ---- | ----- |
| Ana | Chair |
| Ben | Notes |
## Decisions
1. Ship version 2 on Friday.
> Ana asked for one more day of testing.
2. Move the retro to Monday.
```text
npm run release
```
## Photos

$ tramlines validate --schema meeting.yaml sync.md
0 errors, 0 warnings
- Read the errors for blocks. In this document, the second line of the sequence is missing, a paragraph stands before the table, the table has a different header row, a decision contains HTML, the code block has a tag that the schema doesn’t list, and the photo is in the wrong folder:
sync.md
---
title: Weekly sync
description: The team agreed the release date.
---
# Weekly sync
The team agreed the release date.
- Date: 2026-09-30
## Attendees
Everyone came.
| Name | Team |
| ---- | ---- |
| Ana | Web |
## Decisions
1. Move the retro to <b>Monday</b>.
```sh
npm run release
```
## Photos

$ tramlines validate --schema meeting.yaml sync.md
sync.md:10: error[TLMD_147]: position 2 of the sequence is missing (schema meeting, section "<title>" block 2)
sync.md:14: error[TLMD_39]: unexpected prose block (schema meeting, section "Attendees")
sync.md:16: error[TLMD_44]: the table header is `Name | Team`; expected `Name | Role` (schema meeting, section "Attendees" block 1)
sync.md:22: error[TLMD_45]: inline HTML is not accepted here (schema meeting, section "Decisions" block 1)
sync.md:24: error[TLMD_43]: the code block is tagged `sh`; expected `text` or `console` (schema meeting, section "Decisions" block 2)
sync.md:30: error[TLMD_163]: the image `board.png` does not match `^images/.+\.png$`; a target of this kind reads  (schema meeting, section "Photos" block 1)
6 errors, 0 warnings
[exit 1]
A block that the section doesn’t declare causes one error, and tramlines still checks the next block against its rule. Here the paragraph under Attendees fails, and tramlines validates the table as usual. Each error names a block by its position in the section. The name "<title>" stands for a heading taken from the frontmatter.
- To repeat a run of blocks as one unit, wrap them in a
groupwith its ownblocks. A group always repeats, sominandmaxbound the number of repetitions. Here each repetition is a paragraph with a question, followed by a paragraph with its answer:
faq.yaml
line-limit: 60
sections:
- heading: FAQ
level: 1
blocks:
- kind: group
min: 2
blocks:
- kind: prose
max: 1
each: { template: "Q: ${}?", example: "Q: Can I bring a guest?" }
- kind: prose
max: 1
example: Yes, tell the host a day before.
faq.md
# FAQ
Q: Can I bring a guest?
Yes, tell the host a day before.
$ tramlines validate --schema faq.yaml faq.md
faq.md:3: error[TLMD_146]: the group block needs at least 2 repetitions; it has 1 (schema faq, section "FAQ" block 1)
1 error, 0 warnings
[exit 1]
- Add a second question and answer:
faq.md
# FAQ
Q: Can I bring a guest?
Yes, tell the host a day before.
Q: Is there parking?
No, take the bus.
$ tramlines validate --schema faq.yaml faq.md
0 errors, 0 warnings
The two questions and two answers form four paragraphs in a row. tramlines divides them among the rules. When two rules of the same kind follow each other, the first takes as many units as its max allows and its shape accepts. The next rule takes the rest. How blocks are matched to a schema explains how tramlines matches each block to a rule.
Troubleshooting
TLMD_39: the body contains a block that the section doesn’t declare. See Errors: blocks.TLMD_152: the body lacks a block that the section declares. See TLMD_152.TLMD_69: the body contains a block of another kind where the schema expects a declared block. See TLMD_69.TLMD_146,TLMD_160: a block has fewer units than itsmin, or more than itsmax. See TLMD_146 and TLMD_160.TLMD_147,TLMD_161: a block has fewer units than itssequencehas entries, or more. See TLMD_147 and TLMD_161.TLMD_162: an item contains a nested list or code block that its rule doesn’t declare underblocks. See TLMD_162.TLMD_42: a block declares a key that doesn’t apply to its kind. See TLMD_42.TLMD_153: a block lacks a key that its kind requires. See TLMD_153.TLMD_47: a block declaresrulesbesideeachorsequence. Keep one. See TLMD_47.
See also
- Schema keys:
block - Schema keys:
unit - Errors: kinds of block
- How blocks are matched to a schema
- Select and count units and lines
Select and count units and lines
When one list mixes items of several kinds, or a code block must contain certain lines, write the grammar of the block with rules or lines. At the end, the rule of its kind takes each unit or line, and each kind stays within the count that you set. Anything else fails at its line.
Before you begin
- A schema with blocks. Describe blocks and their units shows how to write one.
- Templates and patterns, as in Write the shape of a line.
Steps
-
Replace
eachorsequencewithrules, a list of unit rules. A rule must take every unit. A rule acts in one of four ways:- A rule with a shape and no
select-patternis a production. It takes the units that its shape matches. Each unit goes to the first production that matches it, so list the narrowest production first. - A rule with a
select-patternis a guard. It takes the units whose text the regular expression matches, and checks them. Use a guard to pick units by a label rather than by their shape, such as an item whose nested blocks you check. - A rule with only a
select-patternand a count is a counter. It counts the units that it matches and takes none, so a production still checks them. - A rule with neither a shape nor a
select-patterntakes every unit.
- A rule with a shape and no
-
Bound each rule with
minandmax. By default, a production requires at least one unit, andmin: 0allows none. A rule with aselect-patternhas no default, so you must set itsmin. A counter withmax: 0bans what it matches.A guard selects units independently of other rules: two guards can select one unit, and both check it. A counter counts a unit that a production takes.
This changelog schema accepts added and fixed entries, at most one breaking entry with a migration command under it, and no
TODO:
changelog.yaml
line-limit: 80
sections:
- heading: { template: "${:int}.${:int}.${:int}", example: "2.1.0" }
level: 2
repeat: true
blocks:
- kind: bullet-list
rules:
- template: "Added: ${}."
min: 0
example: "Added: a dark theme."
- template: "Fixed: ${}."
min: 0
example: "Fixed: the crash on start."
- select-pattern: "^Breaking: "
min: 0
max: 1
example: "Breaking: the config file moved."
blocks:
- kind: code
lang: console
example: "$ app migrate"
- select-pattern: "TODO"
min: 0
max: 0
- Check a document:
CHANGELOG.md
## 2.1.0
- Added: a dark theme.
- Fixed: the crash on start.
- Breaking: the config file moved.
```console
$ app migrate
```
$ tramlines validate --schema changelog.yaml CHANGELOG.md
0 errors, 0 warnings
- Read the errors for rules. A unit that no rule takes fails against the production that it matches furthest, at the column where it stops matching. A unit that matches no production at all fails with the text of the unit. The error for a counter outside its bounds names its pattern and the count:
CHANGELOG.md
## 2.1.0
- Added: a dark theme.
- Fixed: the slow search
- Added: export to PDF (TODO: docs).
- Removed: the old menu.
$ tramlines validate --schema changelog.yaml CHANGELOG.md
CHANGELOG.md:4: error[TLMD_57]: `Fixed: the slow search` matches no rule of the block; the nearest fails at column 23: expected `.`, found the end; an item of this kind reads Fixed: the crash on start. (schema changelog, section "${:int}.${:int}.${:int}" block 1)
CHANGELOG.md:5: error[TLMD_159]: the items matching `TODO`: at most 0 are allowed; there is 1 (schema changelog, section "${:int}.${:int}.${:int}" block 1)
CHANGELOG.md:6: error[TLMD_53]: `Removed: the old menu.` matches no rule of the block; declare it or set `units-open` (schema changelog, section "${:int}.${:int}.${:int}" block 1)
3 errors, 0 warnings
[exit 1]
-
To accept units that no rule takes, set
units-open: trueon the block. The rules still check the units that they take, and still apply their counts. -
To check the lines of a code or
htmlblock, setlineson theeachof the block. A code block has one unit, its text. The keylinesis the grammar of that text, asrulesis the grammar of the units of a block. The same productions, guards, and counts apply to each line. tramlines ignores the indent of each line and skips blank lines. A line rule can’t declarelinesorblocks.To accept lines that no rule takes, set
lines-open: truebesidelines. This schema requires a settings file with a[server]line, one to three settings, and no comments:
settings.yaml
line-limit: 60
sections:
- heading: Settings
level: 1
blocks:
- kind: code
lang: ini
each:
lines:
- text: "[server]"
- template: "${name:slug} = ${}"
min: 1
max: 3
example: "port = 8080"
- select-pattern: "^#"
min: 0
max: 0
settings.md
# Settings
```ini
[server]
port = 8080
host = example.org
# TODO
debug: true
```
$ tramlines validate --schema settings.yaml settings.md
settings.md:8: error[TLMD_87]: `# TODO` matches no rule of the fence; declare it or set `lines-open` (schema settings, section "Settings" block 1)
settings.md:8: error[TLMD_159]: the lines matching `^#`: at most 0 are allowed; there is 1 (schema settings, section "Settings" block 1)
settings.md:9: error[TLMD_57]: `debug: true` matches no rule of the fence; the nearest fails at column 6: expected ` = `, found `: true`; a line of this kind reads port = 8080 (schema settings, section "Settings" block 1)
3 errors, 0 warnings
[exit 1]
The indented host line passes, and the comment fails twice: the counter counts it but doesn’t take it, and no other rule takes it.
- Fix the document: remove the comment and write
debug = true.
settings.md
# Settings
```ini
[server]
port = 8080
host = example.org
debug = true
```
$ tramlines validate --schema settings.yaml settings.md
0 errors, 0 warnings
- Order the productions from narrowest to widest. If an earlier production takes the example of a later one, the later one can take nothing, and the schema fails:
tasks.yaml
line-limit: 60
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
rules:
- template: "${}."
min: 0
example: "Water the plants."
- template: "Urgent: ${}."
min: 0
example: "Urgent: pay the rent."
tasks.md
# Tasks
- Water the plants.
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:11: error[TLMD_58]: the example `Urgent: pay the rent.` is taken by the rule at line 8 first, so this rule takes nothing: order the rules from the narrowest (schema tasks, section "Tasks" block 1)
1 error, 0 warnings
[exit 1]
Move the Urgent rule before the other, and the schema passes.
Troubleshooting
TLMD_53,TLMD_57: no rule takes a unit or a line. Correct the unit or line from the column that the message names, add a rule for it, or setunits-openorlines-open. See TLMD_53 and TLMD_57.TLMD_87: no rule oflinestakes a line. See TLMD_87.TLMD_93: a rule takes or counts fewer units than itsmin. See TLMD_93.TLMD_159: a rule takes or counts more units than itsmax. See TLMD_159.TLMD_92: a rule with aselect-patternlacks amin. Addmin: 0to check the units without requiring any. See TLMD_92.TLMD_91: a unit rule declaresselect-pattern,min, ormaxoutsiderulesandlines. See TLMD_91.TLMD_142: a rule declarestextbesideselect-pattern. A repair oftextrewrites the whole unit and would drop what the guard selected on. See TLMD_142.TLMD_90: a rule declareslinesunder a block that isn’t a code block or anhtmlblock. See TLMD_90.
See also
- Schema keys:
unit - Schema keys:
block - Errors: units
- Errors: lines and pairings
- Ban text in a document
- Pitfalls in writing a schema
Capture keys and anchor them
When the parts of a document have ids, such as requirements named CART-1 and CART-2, capture each id as a key. At the end, every key is unique and has an anchor that a link can open. The document also contains the sections or items that the schema pairs with each key.
Before you begin
- A schema with sections and blocks. Describe blocks and their units shows how to write one.
- A template with a named hole, as in Write the shape of a line.
Steps
-
Set
keyon the rule that matches the id, beside its shape. The value ofkeynames a hole of the template, or a named group of the pattern. A heading rule captures the key from the heading. A unit rule captures the key from each item, row, or line that it matches. A unit can match the shape without a key, as with a format whose variants don’t all capture one. To accept such a unit as plain text, setkey-optional: true. -
Give the block a
rule-id, so that other rules can refer to the keys that it captures. Each key must be unique within the document. A second capture of a key is an error. Theuniqueschema key names the scope. Its default,document, is the only scope that the command checks. A program that embeds the engine can add wider scopes and check them itself. -
To require that each key starts with a known prefix, add
key-derives-frombesidekey.{ from: code, separator: "-" }requires the part before the first-to equal the frontmatter value ofcode.{ rule: reqs, separator: "_" }requires the part before the first_to be a key that the rulereqscaptured. With it,CART-1_AC-1derives fromCART-1. This schema describes the list of requirements of one area:
requirements.yaml
line-limit: 80
frontmatter:
code:
example: CART
sections:
- heading: Requirements
level: 1
blocks:
- kind: bullet-list
rule-id: reqs
each:
template: "${id}: ${}"
key: id
key-derives-from: { from: code, separator: "-" }
example: "CART-1: The cart keeps its items for a week."
- Check a document. Every key needs an anchor,
<a id="slug"></a>. The error for a missing anchor shows the anchor to write. In this document, the first item has its anchor, the second has none, the third has the wrong prefix, and the fourth repeats the key of the second:
cart.md
---
code: CART
---
# Requirements
- <a id="cart-1"></a>CART-1: The cart keeps its items for a week.
- CART-2: The cart shows the total price.
- SHOP-1: The shop opens at nine.
- CART-2: The cart shows the tax.
$ tramlines validate --schema requirements.yaml cart.md
cart.md:8: error[TLMD_109]: key `CART-2` has no anchor; its repair writes `<a id="cart-2"></a>` before it
cart.md:9: error[TL_33]: key `SHOP-1` derives from `SHOP`, which is not `CART`, the value of the frontmatter `code` (schema requirements, rule `reqs`)
cart.md:9: error[TLMD_109]: key `SHOP-1` has no anchor; its repair writes `<a id="shop-1"></a>` before it
cart.md:10: error[TLMD_148]: key `CART-2` is captured more than once; it was first captured at line 8 (schema requirements, rule `reqs`)
4 errors, 0 warnings
[exit 1]
-
Write each anchor that the messages name. The command
tramlines validate --fixwrites every anchor that a key needs, then reports what is left. To write an anchor by hand, give it the slug of the key as its id. The slug is the key in lowercase, with letters, digits,-, and_kept, spaces changed to-, and every other character dropped. Put the anchor in the place that fits its key:- In an item, a row, or a paragraph: directly before the text that the rule matches, or before the code span or link that contains the key.
- In a heading: at the start of the text of the heading, after the
#marks. A heading whose own slug is the slug of the key needs no anchor. - For a line of a code or
htmlblock: on a line of its own before the block, one line for each key in the order of the lines, then a blank line.
No rule matches an anchor, because tramlines removes each anchor before it matches the text.
-
Fix the document: add the anchor of
CART-2, give the third requirement its own id, and remove the duplicate.
cart.md
---
code: CART
---
# Requirements
- <a id="cart-1"></a>CART-1: The cart keeps its items for a week.
- <a id="cart-2"></a>CART-2: The cart shows the total price.
- <a id="cart-3"></a>CART-3: The cart opens at nine.
$ tramlines validate --schema requirements.yaml cart.md
0 errors, 0 warnings
- To require a section for each key, give the section three keys: a heading rule that captures a key,
repeat: true, andfor-each-key-of. The value offor-each-key-ofis therule-idof the rule that captured the keys. A key with no section is an error. A section whose key the rule didn’t capture is an error too. When a heading captures a key, the anchor belongs on the heading instead of the item:
requirements.yaml
line-limit: 80
frontmatter:
code:
example: CART
sections:
- heading: Requirements
level: 1
blocks:
- kind: bullet-list
rule-id: reqs
each:
template: "${id}: ${}"
key: id
key-derives-from: { from: code, separator: "-" }
example: "CART-1: The cart keeps its items for a week."
- heading: { template: "${id} tests", key: id, example: "CART-1 tests" }
level: 2
repeat: true
for-each-key-of: reqs
blocks:
- kind: bullet-list
example: Add an item and reopen the cart a day later.
cart.md
---
code: CART
---
# Requirements
- CART-1: The cart keeps its items for a week.
- CART-2: The cart shows the total price.
## <a id="cart-1"></a>CART-1 tests
- Add an item and reopen the cart a day later.
## <a id="cart-3"></a>CART-3 tests
- Check the total.
$ tramlines validate --schema requirements.yaml cart.md
cart.md:8: error[TLMD_109]: key `CART-2` has no anchor; its repair writes `<a id="cart-2"></a>` before it
cart.md:8: error[TLMD_95]: no `${id} tests` section for key `CART-2` (schema requirements, section "${id} tests")
cart.md:14: error[TLMD_96]: section names key `CART-3`, which rule `reqs` did not capture (schema requirements, section "${id} tests")
3 errors, 0 warnings
[exit 1]
CART-2 has no section, so its anchor still belongs on its item. The last section describes a test of the total, so it belongs to CART-2. Change its heading to <a id="cart-2"></a>CART-2 tests, and the document passes.
-
To require an item for each key, set
for-each-key-ofon the unit rule instead. The rule must capture a key itself. A key with no item is an error, even when the whole block is absent.A key captured from a line of a code block belongs to the
rule-idof its line rule. When the line rule has norule-id, the key belongs to therule-idof the block. When two kinds of line in one block each pair with a different rule, give the unit rule or the line rule its ownrule-id. In this example, each service of a configuration file needs an owner:
services.yaml
line-limit: 60
sections:
- heading: Services
level: 1
blocks:
- kind: code
lang: yaml
rule-id: services
each:
lines:
- text: "services:"
- template: "${name:slug}:"
key: name
example: "web:"
- template: "image: ${}"
example: "image: nginx"
- heading: Owners
level: 2
blocks:
- kind: bullet-list
each:
template: "${name:slug}: ${}"
key: name
for-each-key-of: services
example: "web: Ana"
services.md
# Services
<a id="web"></a>
<a id="db"></a>
```yaml
services:
web:
image: nginx
db:
image: postgres
```
## Owners
- web: Ana
- db: Ben
$ tramlines validate --schema services.yaml services.md
0 errors, 0 warnings
-
In a section that declares a
parent, a pairing uses only the keys captured in the same occurrence of the parent, so each occurrence pairs with its own keys. Elsewhere it uses the keys of the whole document. Describe sections and headings showsparent. -
To pair with the keys of several rules, give
for-each-key-ofa list ofrule-ids. Each distinct key of the rules needs one section or item. A rule that names one key on several units, such as the owner of each item, declareskey-repeats: true. None of its captures then counts as a second capture. In this map, each group needs a design, and so does each block that provides a contract. The guarded ruleprovidersreads the block from the line that the rulecontractstakes.Loadingis both a group and a provider, and needs one design:
map.yaml
line-limit: 60
sections:
- heading: Map
level: 1
blocks:
- kind: bullet-list
rule-id: groups
each:
template: "Group ${name:word}"
key: name
example: "Group Loading"
- kind: numbered-list
rules:
- template: "${name:slug} (${:word})"
key: name
rule-id: contracts
example: "format (Loading)"
- select-pattern: '\('
pattern: '\((?P<block>[A-Z][a-z]+)\)$'
key: block
key-repeats: true
rule-id: providers
min: 0
example: "format (Loading)"
- heading: Designs
level: 2
blocks:
- kind: bullet-list
each:
template: "${name:word} design"
key: name
for-each-key-of: [groups, providers]
example: "Loading design"
map.md
# Map
- <a id="loading"></a>Group Loading
1. <a id="format"></a>format (Loading)
2. <a id="rows"></a>rows <a id="cache"></a>(Cache)
3. <a id="keys"></a>keys (Cache)
## Designs
- Loading design
- Cache design
$ tramlines validate --schema map.yaml map.md
0 errors, 0 warnings
The anchor of Cache sits on its first capture. The anchor of Loading sits on the group, because a rule without key-repeats defines a key at the place where it captures the key.
Troubleshooting
TLMD_109: a key has no anchor, an anchor isn’t where its key is, an anchor names no key, or an anchor is written twice. Write or move the anchor as the message says. See TLMD_109.TLMD_101: two keys, or a key and a heading, have the same slug and would share one anchor. Change one of them. See TLMD_101.TLMD_148: a rule captures one key twice. See TLMD_148.TL_33: the prefix of a key isn’t one that itskey-derives-fromallows. See TL_33.TL_44: a line has the shape of its rule but no key. Add the key, or setkey-optionalon the rule. See TL_44.TL_31: thekeynames no hole or named group of the shape. See TL_31.TLMD_95,TLMD_96: a key has no section or item that pairs with it, or a section or item names a key that the rule didn’t capture. See Errors: lines and pairings.TLMD_98: a rule withfor-each-key-ofcaptures no key. See TLMD_98.TLMD_167: the rule that afor-each-key-ofnames captures no key. See TLMD_167.
See also
- Schema keys:
unit - Schema keys:
heading - Schema keys:
key-derivation - Errors: keys, markers, and scaffolds
- Section 7 of the markdown specification
Write examples and guidance
When you write a rule, give it an example, a real line of the kind that the rule requires, and add guidance for what no example can show. At the end, the error for every failure of the rule shows a line that passes, and a new document starts from text that a writer can imitate.
The example of a rule is the only part of a schema that shows the text that the rule requires. The command checks the example against its rule and prints it in the error for every failure of the rule. The scaffold, the document that a new document starts from, copies the example. A schema never describes the same text twice, so two wordings can’t drift apart.
Before you begin
- A schema with rules that have shapes, as in Write the shape of a line.
Steps
-
Write each
exampleas a line that a real document of the kind would contain, about a real subject. Never write a placeholder such as<decision>, an instruction such asDescribe the decision., or filler such asOne item.A writer, a person or an agent, imitates the examples in a new document. An example written as a prompt therefore comes back word for word. -
Give an
exampleto every rule with atemplateor apattern, unless the rule binds a format that has one. A rule with onlytext, a count, or aselect-patternneeds none, because its error is clear without one.The command checks every example against its rule and reports each one that fails as an error in the schema. This example lacks the full stop that its template requires:
minutes.yaml
line-limit: 0
sections:
- heading: Decisions
level: 2
blocks:
- kind: bullet-list
each:
template: 'Decided: ${}.'
example: 'Decided: the release moves to 3 November'
minutes.md
## Decisions
- Decided: the release moves to 3 November.
$ tramlines validate --schema minutes.yaml minutes.md
minutes.yaml:8: error[TL_64]: the example `Decided: the release moves to 3 November` fails at column 41: expected `.`, found the end (schema minutes, section "Decisions" block 1)
1 error, 0 warnings
[exit 1]
-
Add
guidancefor what an example can’t show, such as a choice, a limit, or a reason. Guidance also covers what a writer must avoid where no rule enforces it. Phrase it as what to do instead: a schema contains no wrong text for a writer to imitate. The schema, a section, a preamble, a block, a frontmatter key, and a lifecycle dimension each accept guidance. The scaffold places it before the part that it guides, and the command never prints it in an error. -
To name a rule in its failures, give it a
rule-id. Without one, the error names the rule by its place in the schema, its key, or its pattern.
meeting.yaml
line-limit: 0
guidance: One meeting, with what it decided and who attended.
sections:
- heading: Decisions
level: 2
guidance: Record what the meeting settled, not what it discussed.
blocks:
- kind: bullet-list
rule-id: decisions
each:
template: 'Decided: ${}.'
example: 'Decided: the release moves to 3 November.'
- heading: Attendees
level: 2
blocks:
- kind: bullet-list
each:
pattern: '^[A-Z]\w+ [A-Z]\w+$'
example: Ana Silva
- Check a document, and read each failure. An error starts with the key of the criterion that the line breaks, such as
TL_74, and ends with the rule that the line fails. The error for a shape quotes the text, then either the column where a template stops matching or the pattern that the text fails, then the example. The command doesn’t print the guidance of the section:
planning.md
## Decisions
- Decided: the release moves to 3 November.
- We talked about the budget
## Attendees
- Ana Silva
- sam
$ tramlines validate --schema meeting.yaml planning.md
planning.md:4: error[TL_74]: `We talked about the budget` fails at column 1: expected `Decided: `, found `We talked about the bud…`; an item of this kind reads Decided: the release moves to 3 November. (schema meeting, rule `decisions`)
planning.md:9: error[TL_74]: `sam` does not match `^[A-Z]\w+ [A-Z]\w+$`; an item of this kind reads Ana Silva (schema meeting, section "Attendees" block 1)
2 errors, 0 warnings
[exit 1]
- To bring in a rule that existing documents don’t follow yet, set
severity: warnon it. Its failures are warnings, which don’t change the exit code. Set it on the rule that fails: here, the unit rule undereach. Aseverityon a block or a section reaches every rule inside it. On a section, it also covers the faults of the heading of the section, such as a heading that is missing, repeated, or out of place. For this reason, set it as close to the failing rule as you can. After the documents comply, set the severity toerror, or remove the key.
meeting.yaml
line-limit: 0
guidance: One meeting, with what it decided and who attended.
sections:
- heading: Decisions
level: 2
guidance: Record what the meeting settled, not what it discussed.
blocks:
- kind: bullet-list
rule-id: decisions
each:
template: 'Decided: ${}.'
example: 'Decided: the release moves to 3 November.'
- heading: Attendees
level: 2
blocks:
- kind: bullet-list
each:
severity: warn
pattern: '^[A-Z]\w+ [A-Z]\w+$'
example: Ana Silva
planning.md
## Decisions
- Decided: the release moves to 3 November.
## Attendees
- Ana Silva
- sam
$ tramlines validate --warnings --schema meeting.yaml planning.md
planning.md:8: warning[TL_74]: `sam` does not match `^[A-Z]\w+ [A-Z]\w+$`; an item of this kind reads Ana Silva (schema meeting, section "Attendees" block 1)
0 errors, 1 warning
- When you rename a heading, list its former text under
renamed-fromon the section. A document with the old heading fails with an error that names the new one, andtramlines validate --fixrewrites the heading.
meeting.yaml
line-limit: 0
guidance: One meeting, with what it decided and who attended.
sections:
- heading: Decisions
level: 2
renamed-from: Outcomes
guidance: Record what the meeting settled, not what it discussed.
blocks:
- kind: bullet-list
rule-id: decisions
each:
template: 'Decided: ${}.'
example: 'Decided: the release moves to 3 November.'
- heading: Attendees
level: 2
blocks:
- kind: bullet-list
each:
severity: warn
pattern: '^[A-Z]\w+ [A-Z]\w+$'
example: Ana Silva
- After you write the part that a guidance line guides, delete the line from the new document. A scaffold contains each piece of guidance as a
<!-- tramlines:guidance … -->line before a part of the body, or a# tramlines:guidance …comment before a frontmatter key. A document that still contains one fails at that line:
review.md
## Outcomes
<!-- tramlines:guidance Record what the meeting settled, not what it discussed. -->
- Decided: the venue is the town hall.
## Attendees
- Ana Silva
$ tramlines validate --schema meeting.yaml review.md
review.md:1: error[TLMD_137]: heading `Outcomes` was renamed; it is now `Decisions` (schema meeting, section "Decisions")
review.md:3: error[TLMD_158]: a `tramlines:guidance` line is left here: write the part it guides, then delete the line
2 errors, 0 warnings
[exit 1]
Troubleshooting
TL_64: an example fails its own rule. Fix the example, or the shape if the example is right. See TL_64.TL_65: the example of a format fails the shape, the bans, or the key of the format. See TL_65.TL_66: a rule with atemplateor apatternhas noexample. Add one. See TL_66.TL_68: theguidanceof a rule repeats itsexample. Write what the example can’t show, or remove the guidance. See TL_68.TLMD_158: a guidance line is left in a document. Write the part that it guides, then delete the line. See TLMD_158.TLMD_164: the scaffold that the schema derives from its examples fails the schema, though each example passes its own rule. See TLMD_164.TLMD_137: a heading still uses a former text, listed underrenamed-from. Change it to the heading that the message names. See TLMD_137.
See also
- Schema keys
- Errors
- Read an error and fix the document
- Strict by default
- How a schema derives its scaffold
- Section 7 of the core specification
Read an error and fix the document
When tramlines validate reports errors in a document that you wrote, read each error from the left: its location, which rule of the specification the document breaks, and which rule of the schema the document fails. At the end, the document passes without any change to the schema.
Before you begin
- The
tramlinescommand, from the installation steps. - The schema that your documents follow, and the command that checks them. The command reference lists the options.
Steps
- Check the document. This schema describes a recipe: a title, a list of ingredients, and a method.
recipe.yaml
line-limit: 60
frontmatter:
title:
example: Pancakes
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: Thin pancakes for a weekend breakfast.
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: "${:int} ${unit:word} ${}"
example: 200 g plain flour
- heading: Method
level: 2
blocks:
- kind: numbered-list
example: Whisk the flour, eggs and milk into a smooth batter.
crepes.md
---
title: Crepes
---
# Crepes
Thin pancakes from Brittany.
## Ingredients
- 250 g flour
- a pinch of salt
$ tramlines validate --schema recipe.yaml crepes.md
crepes.md: error[TLMD_18]: section `## Method` is missing (schema recipe, section "Method")
crepes.md:12: error[TL_74]: `a pinch of salt` fails at column 1: expected an `int` (`\d+`), found `a pinch of salt`; an item of this kind reads 200 g plain flour (schema recipe, section "Ingredients" block 1)
2 errors, 0 warnings
[exit 1]
-
Read the parts of each error. An error has the form
path:line: severity[KEY]: message (schema id, place):crepes.md:12is the file and the line number in it. An error with no line number, such as a missing section, applies to the whole document.erroris the severity. Anerrorfails the check, and the command exits with1. Awarningdoesn’t fail the check.TL_74is the key of the rule that the document breaks.- The message describes the fault. A message about the shape of a line names the column where the text stops matching, what the rule expects there, and an example of a correct line: here,
200 g plain flour. schema recipeis the id of the schema, the name of its file without the extension.- The place names the rule of the schema:
section "Ingredients" block 1is the first block under theIngredientsheading. A place can also name a rule by itsrule-id, as inrule `reqs`. The placesection "<title>"names a section whose heading the schema takes from the frontmatter.
-
Find the key on the error reference. A key that starts with
TL_comes from the core specification, and one that starts withTLMD_from the markdown specification. The Errors page lists the page for each group of keys. Each entry lists the message, its cause, the fix, and an example.TL_74is on Errors: lines and length, andTLMD_18is on Errors: headings and sections. -
Find the rule in the schema. In
recipe.yaml, the section withheading: Ingredientscontains one block, abullet-list. Its template${:int} ${unit:word} ${}matches a number, a word, and any text. The section withheading: Methoddoesn’t setoptional: true, so every recipe needs it. -
Fix the document, not the schema. Start the salt with a number and a word, as the example
200 g plain flourdoes, and add the method:
crepes.md
---
title: Crepes
---
# Crepes
Thin pancakes from Brittany.
## Ingredients
- 250 g flour
- 1 pinch salt
## Method
1. Whisk the flour, eggs and milk into a smooth batter.
2. Fry each crepe for a minute on each side.
$ tramlines validate --schema recipe.yaml crepes.md
0 errors, 0 warnings
- If the document is right and the rule is wrong for it, ask the author of the schema to change the rule. An error in the schema file itself names
recipe.yamland no place. Such an error is for the author of the schema too. The command reports it before it checks a document.
Troubleshooting
- Every error names the schema file: the schema is wrong, and no document can pass until its author fixes it. See Errors: the schema.
- The command stops with exit code
2and one line of output: the command line is wrong, or the command can’t read a file. See The command. - A message mentions a repair: run the command again with
--fix, and it makes the repair in the file. See Repair a document and start a new one.
See also
Repair a document and start a new one
When a schema derives part of a document, such as a heading that repeats the title or the anchor of a key, let the command write that part. At the end, you have a document that started from its scaffold. You write only its content, and tramlines validate --fix keeps the derived parts in step with it.
Before you begin
- A schema with frontmatter and sections, as in Your first schema.
- An understanding of keys and their anchors, from Capture keys and anchor them.
Steps
- Write the schema. This one describes a decision: an id built from a slug, a title that the level-1 heading repeats, and a list of consequences, each with a key.
decision.yaml
line-limit: 0
types:
consequence-id: 'C-\d+'
frontmatter:
id:
template: 'decision-${:slug}'
title:
example: Store the cart for a week
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
example: The cart keeps its items for seven days, and then it empties.
- heading: Consequences
level: 2
blocks:
- kind: bullet-list
each:
template: '${id:consequence-id}: ${}'
key: id
example: 'C-1: A returning customer finds the cart as they left it.'
- Print the scaffold of a new document. The option
--slugfills theslughole of the id. The title takes the words of the slug unless you set--title. The heading repeats the title, as the schema requires. Save the output as the new document:
$ tramlines new --schema decision.yaml --slug cart-lifetime
---
id: decision-cart-lifetime
title: Cart lifetime
---
<!-- tramlines:guidance The writer replaces each of the following examples with their own text. -->
# Cart lifetime
The cart keeps its items for seven days, and then it empties.
## Consequences
- C-1: A returning customer finds the cart as they left it.
- Write the content. Replace each example with your own text, and delete the
guidanceline. In this document, the writer also changed the title, and added a second consequence:
cart.md
---
id: decision-cart-lifetime
title: Keep the cart for a week
---
# Cart lifetime
A cart keeps its items for seven days after the last change, and then it empties.
## Consequences
- C-1: A returning customer finds the cart as they left it.
- C-2: The store keeps a week of carts for each customer.
- Check the document. The heading no longer repeats the title, and neither key has its anchor:
$ tramlines validate --schema decision.yaml cart.md
cart.md:6: error[TLMD_165]: heading `Cart lifetime` must equal the frontmatter `title`: `Keep the cart for a week` (schema decision, section "<title>")
cart.md:12: error[TLMD_109]: key `C-1` has no anchor; its repair writes `<a id="c-1"></a>` before it
cart.md:13: error[TLMD_109]: key `C-2` has no anchor; its repair writes `<a id="c-2"></a>` before it
3 errors, 0 warnings
[exit 1]
- Repair the document with
--fix. The command rewrites the heading from the title and writes each anchor, then reports what is left. A second run finds nothing to repair:
$ tramlines validate --schema decision.yaml --fix cart.md
rewrote cart.md
0 errors, 0 warnings
$ tramlines validate --schema decision.yaml --fix cart.md
0 errors, 0 warnings
To repair a document that an editor or a hook pipes in, name it - and its format with --format, as in tramlines validate --schema decision.yaml --format md --fix -. The command prints the repaired document on stdout and the report on stderr.
Troubleshooting
the id template has a seq hole: the number of a new document depends on the documents around it, which the command doesn’t read. Name the whole id with--id.is no state of: a--statenames a state that its dimension doesn’t declare. Use one of the states that the message lists.TLMD_158: aguidanceline is left in the document. Write the part that it guides, and then delete the line. See TLMD_158.
See also
- The command
- Apply a rule in some states only
- How a schema derives its scaffold
- Why the frontmatter is the authority
Embed the engine in a Rust program
When your program owns the documents, as an editor plugin or a documentation tool does, call the engine as a library instead of running tramlines validate. The library compiles a schema, checks each document, applies the repairs that the frontmatter and the schema require, and derives the scaffold that a new document starts from. Your program is the host. It chooses the schema for each document, and it can add its own keys to the grammar.
Before you begin
- A Rust project that depends on the library. Add it with
cargo add tramlines-md. The crate re-exports everything fromtramlines-corethat a host needs. - A schema that the command accepts, as in Your first schema.
- The library documentation on docs.rs, which describes every type that this guide names.
Steps
The following code comes from the test crates/lib/tramlines-md/tests/it/embed.rs of the repository, so it compiles and runs as shown.
- Load the grammar and compile the schema. The function
load_grammarreturns the markdown grammar merged onto the core grammar. The library has both grammars built in. The functioncompilechecks the schema against the grammar, and returns findings when the schema breaks the grammar. The functioncheck_schemathen checks the schema against its own examples and scaffolds, as the command does.
#![allow(unused)]
fn main() {
/// Compile a schema, written in YAML, against the markdown grammar. The
/// findings, when there are any, are on the schema.
fn compile_schema(grammar: &Grammar, text: &str) -> Result<Schema, Vec<Finding>> {
let written = yaml::parse(text, 1).expect("the schema is YAML");
let schema = compile(
&written,
grammar,
&FormatRegistry::default(),
&RegexCache::new(),
&RepoPath::parse("/list.yaml").expect("a path"),
"list",
&Dialect::default(),
)?;
// A schema is checked against its own examples and scaffolds too.
let findings = check_schema(&schema, &NullHost, &RepoPath::root(), "list");
if findings.is_empty() {
Ok(schema)
} else {
Err(findings)
}
}
}
- Check a document. The function
Document::parsereads the frontmatter and the body. The methodSchema::statesreads the lifecycle states from the frontmatter. The functioncheck_againstreturns the findings, with the repairs and the anchors that the document needs. EachFindingholds a path, a line, a severity, the key of its criterion, and a message.
#![allow(unused)]
fn main() {
/// Check one document against a compiled schema, at the states of its
/// frontmatter, and return each finding as the command prints it.
fn check_document(schema: &Schema, path: &str, text: &str) -> Vec<String> {
let dialect = Dialect::default();
let document = Document::parse(RepoPath::parse(path).expect("a path"), text, &dialect);
let states = schema.states(document.frontmatter.as_ref());
let out = check_against(
document.view(),
schema,
&states,
&NullHost,
CheckOptions::default(),
Locator::schema("list"),
);
document
.problem
.iter()
.chain(&out.findings)
.map(|f| {
format!(
"{}:{}: [{}] {}",
f.path,
f.line.unwrap_or(0),
f.criterion,
f.message
)
})
.collect()
}
}
- Repair the document, if your program writes files. The repairs rewrite a heading or a line that is bound to a frontmatter value, a renamed heading, and a setext heading. They also write, move, and remove the anchors of keys. The function
line_editscollects them, and the functionapplyapplies them in one pass.
#![allow(unused)]
fn main() {
/// The document with every repair of its check applied: headings bound to
/// frontmatter, renamed and setext headings, and the anchors of its keys.
fn repair(schema: &Schema, text: &str) -> String {
let dialect = Dialect::default();
let document = Document::parse(RepoPath::root(), text, &dialect);
let states = schema.states(document.frontmatter.as_ref());
let out = check_against(
document.view(),
schema,
&states,
&NullHost,
CheckOptions::default(),
Locator::schema("list"),
);
let anchors = AnchorEdit {
body: &document.body,
placements: &out.anchors,
text: &document.text,
renames: &[],
};
let edits = line_edits(
&document.text,
&document.segments,
&out.repairs,
Some(&anchors),
);
apply(&document.text, &edits).expect("the edits of one check never clash")
}
}
- Derive the scaffold of a new document. How a schema derives its scaffold explains what a scaffold contains.
#![allow(unused)]
fn main() {
// The scaffold a new document starts from: one for each combination
// of lifecycle states, each with its frontmatter and its examples.
let derived = derive(&schema, &NullHost).expect("the schema derives");
let scaffold = &derived.scaffolds[0].document;
}
- Add keys of your own with a host extension. An extension names the format grammar that it extends, and it can only add to that grammar: keys, rule maps, enum values, and
doctext. The engine keeps each key that an extension adds unchanged. Your program reads and checks the key itself.
#![allow(unused)]
fn main() {
// A host extension names the format grammar it extends, and only adds
// to it: here a schema key `owner` that the host reads itself.
let mut grammar = load_grammar().expect("the grammar is built in");
grammar
.extend(
"extension: shop\nextends: tramlines-md\nschema:\n keys:\n owner:\n type: string\n doc: The team that owns the documents of this schema.\n",
)
.expect("the extension only adds");
}
A host also supplies the facts that lie outside a document. Implement the Host trait of the check module, in place of NullHost. A host can then own frontmatter keys, add a frontmatter format, resolve what an image or a region names, and spell markers in its own namespace. How the grammar layers fit together explains what a host can change.
Troubleshooting
TL_12on a key that your extension adds: you compiled the schema with a grammar that lacks the extension. Compile with the grammar thatextendchanged.extendreturns an error: the extension changes something that the grammar under it defines, or names another grammar inextends. An extension can only add to a grammar.
See also
The command
This page lists the subcommands of tramlines, then the options of tramlines validate, how it picks the format of a document, the forms of its report, and its exit codes. It ends with tramlines new, tramlines info, tramlines completions, and the JSON Schema of a schema.
Commands
$ tramlines --help
Checks documents against a tramlines schema
Usage: tramlines <COMMAND>
Commands:
validate Check a schema, then each document against it, repairing with --fix
new Print the scaffold of a new document from a schema
info Print the schema language, or the formats and types a schema sees
completions Print a shell completion script to stdout
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
Exit codes: 0 no error, 1 errors found, 2 usage error or a file that cannot be read.
Docs: https://six5536.github.io/tramlines/
tramlines validate
tramlines validate checks the schema first, then each document against the schema. With no document, it checks the schema alone: its keys, its examples, and the scaffold of each combination of states.
$ tramlines validate --help
Check a schema, then each document against it, repairing with --fix
Usage: tramlines validate [OPTIONS] --schema <FILE> [DOC]...
Arguments:
[DOC]... The documents to check; `-` reads one from standard input. Without a document, the command checks the schema alone
Options:
--schema <FILE> The schema file, in YAML
--formats <FILE> A file of line formats and types that the schema can use
--format <NAME> Read every document in this format, whatever its extension: `md`
--fix Repair the headings and anchors that the schema derives, then check
--json One JSON object on stdout instead of text
--query <EXPR> A JMESPath expression applied to the JSON object. Requires --json
--github GitHub workflow commands on stdout instead of text
--warnings List each warning. Default: their count
-h, --help Print help
--schema <FILE>: the schema. The filename without its extension is the schema id, which each error message includes.--formats <FILE>: a file with aregistrymap of line formats and an optionaltypesmap. A format or type that the schema defines replaces one of the same name in this file.--format <NAME>: the format of every document. Without it, the command picks the format of each document from its extension.<DOC>: a document to check.-reads one document from standard input. It needs--format, and the report names the document-. The command reads standard input once, so-can appear only once.--fix: the command repairs the parts that the schema derives before it checks a document. These parts are a heading that repeats a frontmatter value, a heading listed inrenamed-from, a paragraph or item bound to a frontmatter key, and the anchor of each key. The command writes a file only when the repairs change it. It keeps the file’s line endings and its byte order mark. It reads every document before it writes any, so a file that can’t be read stops the run with no file changed. With-, the command prints the repaired document on stdout and the report on stderr. It refuses--jsonand--githubwith-.--warnings: list each warning in the report. Without it, the count line counts the warnings, and no line lists them.--json,--query <EXPR>,--github: the form of the report, which Report forms describes.
Formats
| Format | Extensions |
|---|---|
md | .md, .markdown |
If a document’s extension matches no format, the command stops with error TL_90, unless --format names one. Standard input has no extension. The command reads a file that starts with a UTF-8 byte order mark as the text after the mark. If --format names an unknown format, the command stops with error TL_91.
Output
The command prints a rewrote <path> line for each file that --fix changed, one line for each error and listed warning, then a count:
rewrote <path>
<path>:<line>: <severity>[<KEY>]: <message>
<n> errors, <m> warnings
<path>is the document path as you gave it on the command line, or the schema path.<line>is the line number in the file. A finding with no line number has no<line>:part.<severity>iserrororwarning. A rule withseverity: warnraises a warning instead of an error.<KEY>is the key of the criterion that the document or the schema breaks, such asTL_54. The error reference explains each one.- When a schema rule raised the finding,
<message>ends with the place of the rule in the schema, such as(schema list, section "<title>" block 1).
The command sorts the findings by path, then by line number. When an error stops the command, the command prints one line on stderr for each fault, in the form error[<KEY>]: <message>. A fault that no criterion covers, such as a file that the command can’t read, prints error: <message>. A fault inside the --formats file keeps the key that it has in the formats of a schema.
Report forms
--jsonprints one JSON object on stdout:errorsandwarnings, each a list of findings, andfixes, the files that--fixchanged. A finding haspath,linewhen it has a line,key, andmessage. A fix is{ "op": "rewrote", "path": <path> }. The object lists every warning, with or without--warnings.--query <EXPR>prints the result of a JMESPath expression over the JSON object, as one line of JSON. It requires--json. An expression that doesn’t compile stops the command.--githubprints each error and listed warning as a GitHub Actions workflow command:::error file=<path>,line=<line>,title=<KEY>::<message>. GitHub shows each command on the line of the file in a pull request. Therewrotelines and the count line follow as plain lines of the log.--githubcan’t be combined with--json.
list.yaml
line-limit: 50
frontmatter:
title:
example: Groceries
sections:
- heading: { from: title }
level: 1
blocks:
- kind: bullet-list
each: { template: "${item:word} x${n:int}", example: Milk x2 }
shopping.md
---
title: Groceries
---
# Shopping
- Milk x2
- Bread
$ tramlines validate --schema list.yaml shopping.md --github
::error file=shopping.md,line=5,title=TLMD_165::heading `Shopping` must equal the frontmatter `title`: `Groceries` (schema list, section "<title>")
::error file=shopping.md,line=8,title=TL_74::`Bread` fails at column 6: expected ` x`, found the end; an item of this kind reads Milk x2 (schema list, section "<title>" block 1)
2 errors, 0 warnings
[exit 1]
$ tramlines validate --schema list.yaml shopping.md --json
{"errors":[{"path":"shopping.md","line":5,"key":"TLMD_165","message":"heading `Shopping` must equal the frontmatter `title`: `Groceries` (schema list, section \"<title>\")"},{"path":"shopping.md","line":8,"key":"TL_74","message":"`Bread` fails at column 6: expected ` x`, found the end; an item of this kind reads Milk x2 (schema list, section \"<title>\" block 1)"}],"warnings":[],"fixes":[]}
[exit 1]
$ tramlines validate --schema list.yaml shopping.md --json --query 'errors[].key'
["TLMD_165","TL_74"]
[exit 1]
Exit codes
| Code | Meaning |
|---|---|
0 | No errors. Warnings don’t change the code. |
1 | At least one error. |
2 | The command line is wrong, a file is unreadable, or the command stopped on an error before it checked the documents. |
tramlines new
tramlines new prints the scaffold of a schema: the document that a new document of the schema starts from. Repair a document and start a new one shows it at work.
$ tramlines new --help
Print the scaffold of a new document from a schema
Usage: tramlines new [OPTIONS] --schema <FILE>
Options:
--schema <FILE> The schema file, in YAML
--formats <FILE> A file of line formats and types that the schema can use
--slug <SLUG> Fills the `slug` hole of the id template
--id <ID> The id, for a template with a `seq` hole or for no template
--title <TEXT> The title. Default: the words of the slug, or of the id
--state <KEY=STATE> A lifecycle key at a state, as KEY=STATE. Default: each dimension's first state
--json One object with the id and the document instead of the document
-h, --help Print help
--state <KEY>=<STATE>: the state of one lifecycle dimension, repeated for each dimension that you name. A dimension that you don’t name takes its first state. The scaffold contains the parts that the states need, and noonlymarker.--slug <SLUG>: fills theslughole of theidtemplate. A template with aseqhole needs the whole id from--id, because the number depends on the documents around the new one.--id <ID>: the id, which must match theidtemplate when the schema has one.--title <TEXT>: the title. Without it, the title is the words of the slug, or of the id, when the schema declarestitle.--json: one JSON object, with theidand thedocument, instead of the document.
The command rewrites each heading, paragraph, and item that the schema binds to the id or the title. It stops with exit code 2 if the new document fails the schema.
tramlines info
tramlines info grammar prints the schema language verbatim: the core grammar, then the markdown grammar, as two YAML documents.
tramlines info formats prints, as YAML, the line formats and types that a schema can use, after the layers resolve. Each entry has a source: built-in, the path of the formats file, or the id of the schema. With --schema, the command prints what that schema sees. Without it, the command prints the formats file with the built-in types.
tramlines completions
tramlines completions <SHELL> prints a completion script for bash, elvish, fish, powershell, or zsh. For example, tramlines completions bash > ~/.local/share/bash-completion/completions/tramlines installs it for bash.
The JSON Schema of a schema
tramlines-md.schema.json is a JSON Schema of the YAML of a markdown schema. A test generates it from the grammar. An editor with YAML support reads it to complete and check the keys of a schema. To use it in an editor that runs the YAML language server, put the following line at the top of the schema:
# yaml-language-server: $schema=https://six5536.github.io/tramlines/tramlines-md.schema.json
The JSON Schema checks the keys and the types of their values. The command checks the rest, such as the regular expressions, the examples, and the references between rules.
Schema keys
This page lists every key that a markdown schema can use. Each section is one part of a schema, starting with the schema itself, and lists its keys in the order of the grammar. A test writes this page from the grammar, so change the grammar and not this page.
schema
What a schema can state about the documents that it governs. A schema is strict by default: whatever it declares is required, whatever it doesn’t declare is rejected, and each requirement is exclusive. You relax a rule only with a key that you write on purpose. A key that ends in -open opens a set that is otherwise rejected, and optional and repeat loosen a count. Regular expressions use the Rust dialect, with no lookaround or backreferences, and aren’t anchored. The shape of one unit, such as a line or a frontmatter value, is exactly one of these: text, the text itself; template, the text with typed holes ${name:type} where it varies, matched whole; or pattern, a regular expression. Every shape reads the unit’s text squashed, with each run of whitespace read as one space. Each rule has an example, which is text of the kind that the rule requires. The engine checks the example against the rule, shows it with every failure that the rule raises, and writes it into the scaffold. The scaffold is the document that the schema derives and that a new document starts from. A rule that declares a template or a pattern must have an example. For what an example can’t show, use guidance. The engine writes guidance into the scaffold before the part that it governs, and no failure prints it. In a markdown document, a unit is a heading, a paragraph, an item, a row, or a line of a fence. The body is the document’s sections, in heading order, and the preamble before the first heading.
A schema has at least one of preamble, sections.
line-limit(an integer, required). The most lines that a document can have. A longer document is an error, and you split it. A value of zero or less sets no limit.guidance(a string). What the schema states that no example can show. The engine writes it into the scaffold after the sentence that every scaffold opens with. No failure prints it.lifecycle(a list oflifecycle-dimension). The states that the documents of the schema move through, with one entry per dimension, in order. Each dimension is a required frontmatter enum. Don’t repeat it underfrontmatter. A dimension is whatonlycan name. The engine derives one scaffold for each combination of states.frontmatter(a map offrontmatter-constraint). The frontmatter keys, with one entry per key. A declared key is required unless it setsoptional. The engine never rejects the keys that the host reserves. Declare a reserved key to require it or to narrow its value. An undeclared key that is neither reserved nor a lifecycle key is an error unlessfrontmatter-openis set.frontmatter-open(trueorfalse). Set totrueto accept undeclared frontmatter keys. When absent, an undeclared key that isn’t reserved or a lifecycle key is an error.prohibited-patterns(a list ofprohibition). Patterns that no body line may match, each with the message that states its rule. Fenced blocks, code spans, and the interiors of regions are excluded. A unit rule and a format take the same key, which then applies to the text that they govern instead.formats(a map offormat). Line formats local to the schema, in the shape of the shared registry. The nearest layer wins: an entry here replaces an entry of the same name in a farther layer entirely.types(a map of strings or lists of strings). Hole types local to the schema, in the shape of the shared registry. Each entry has a slug as its name and, as its value, either a regular expression with no anchor and no capture group or a list of literal choices. The nearest layer wins by name, and the engine compiles every format that the schema sees against the result. The built-in types are reserved:text(.+),word,int,slug, andseq.examples(a list ofexample). Complete documents that satisfy this schema. The engine checks each one as it checks any other document, at the states that its own frontmatter gives, so a marker in an example is an error. Examples show what a document of the type looks like, and the engine never copies them.preamble(preamble). The content between the frontmatter and the first heading. Content there is an error when no preamble is declared. A document with no headings is all preamble.sections(a list ofsection). The document’s headings, in document order. A heading that no entry matches is an error unless an open region admits it. A heading more than one level deeper than the heading before it is an error. The engine reports an ordered schema whose entries skip a level on the schema.sections-ordered(trueorfalse). Set tofalseto accept the declared sections in any order. When absent, the sections must follow the listed order, and a section out of place is an error that names the first such heading.sections-prohibited(a list ofprohibited-heading). Headings that are banned inside open regions.
lifecycle-dimension
One ordered dimension of a lifecycle.
key(a slug, required). The frontmatter key that contains the state.states(a list of strings, required). The states, in lifecycle order. The key’s value must be one of them.guidance(a string). What the states mean. The engine writes it into the scaffold as a YAML comment line before the key.
frontmatter-constraint
What a schema can state about one frontmatter key. The key is required unless it sets optional.
A frontmatter-constraint also accepts the keys of rule-common.
const(a string). The value must equal this.template(a string). The value written with typed holes where it varies, matched whole. Onid, the template can contain only the holes that the engine fills in a new document. A${:seq}hole is unique within the kind, and the template can contain at most one. A${:slug}hole is one or more slug words. A${<key>}hole has no type and is named for a lifecycle key whose states are slugs. It contains the document’s state for that key. Replacing each hole withxmust yield a slug, and onlyseq-widthcan sit beside the template.pattern(a regular expression). A regular expression that the value must match, for what a template can’t express.enum(a list of strings). The allowed values. The value must be one of them.format(a string). The format that the value must have.urlrequires an absolute URL.slugrequires lowercase letters and digits joined by single hyphens. Values:url,slug.seq-width(an integer). Onid, the number of digits of theseqhole. When absent, the width is three. Requirestemplatebeside it.list(trueorfalse). Set totruewhen the value is a list. Thenconst,pattern,enum, andformatapply to every element, andminandmaxbound the number of elements. A scalar where a list is declared, or a list where none is declared, is an error.min(an integer). The minimum number of elements. When absent, there is no lower bound. Requireslistbeside it.max(an integer). The maximum number of elements. When absent, there is no upper bound. Requireslistbeside it.optional(trueorfalse). Set totrueto let the key be absent. When absent, the key is required.only(a map of strings or lists of strings). Maps lifecycle dimensions to states, as on every rule that takesonly.example(a string). The key’s value, or one element of it whenlistis declared.guidance(a string). What an example can’t show about this key. The engine writes it into the scaffold as a YAML comment line before the key.
prohibition
One banned pattern and the message that states its rule. A finding gives the message and the span that the pattern matched, and a scaffold writes the message after the guidance of the part that the pattern governs.
pattern(a regular expression, required). The regular expression that no governed text may match.message(a string, required). One sentence that states the rule as a fact about the part, in the voice of the document, such as “An interface’s summary contains no modal verb.”
format
One named line grammar, in a schema’s formats or in a registry. A format has either one shape and its example, or variants, and each variant has a shape and an example. The engine shows an example beside every failure of its shape. An entry replaces the entry of the same name in a farther layer entirely, unless it declares patch.
A format has exactly one of text, template, pattern, variants.
patch(trueorfalse). Set totruewhen this entry states only how it differs from the entry of the same name in a farther layer. The engine merges the two by JSON Merge Patch (RFC 7386). A key stated here replaces the farther one, an omitted key is inherited, a key set to null is dropped, and a list replaces the farther list entirely. The keys below apply to the merged entry, so a patch contains only what it changes. The engine reports a patch that shadows no farther entry on the schema. When absent, the entry replaces the farther entry entirely.text(a string). The text that every bound line must equal.template(a string). The line written with typed holes where it varies, matched whole.pattern(a regular expression). A regular expression that every bound line must match, for what a template can’t express.key(a string). The hole or named group, present in every shape of the format, whose capture is the line’s key.variants(a map offormat-variant). The named shapes of the format, in the order in which a line tries them. A line conforms when one of them matches. When none matches, the failure names the variant that the line follows furthest, the place where the line fails, and that variant’s example. The example of each variant must match only its own variant.prohibited-patterns(a list ofprohibition). Patterns that no bound line may match, checked after the shape, each with its message.example(a string). One conforming line, for a format with one shape. In a format with variants, each variant has its own example.
example
One complete document that satisfies the schema at its own lifecycle states. The engine checks it as it checks any other document, and reports a marker in it on the schema.
document(a string, required). The complete document.
preamble
The content before the first heading. A preamble has no heading, level, or repeat, only blocks.
A preamble also accepts the keys of rule-common.
blocks(a list ofblock). The preamble’s blocks, as for a section’sblocks.blocks-ordered(trueorfalse). As for a section’sblocks-ordered.guidance(a string). What an example can’t show about the preamble. The engine writes it into the scaffold before the preamble.
section
One heading of the document. A section is required unless it sets optional, and occurs once unless it sets repeat. Its body runs to the next heading of any level. Nesting is by level, never by containment, except that a section that declares parent lies inside its parent. When an exact heading and a varying heading both match, the exact heading wins.
A section also accepts the keys of rule-common.
heading(a string,{ from: <frontmatter key> }, orheading, required). The heading. It is the exact text,{ from: <key> }for the frontmatter value, or aheadingrule for a heading that varies or captures a key. A string contains no hole. Write a heading that varies as a template, and give the template an example.renamed-from(a string, or a list of them). Former heading text, beside an exact heading. The engine reports a heading that has an old text, and its repair rewrites the heading.rule-id(a slug). The name by which other rules reference this section. It must be unique within the schema.level(an integer, required). The heading depth. Values:1,2,3,4,5,6.optional(trueorfalse). Set totrueto let the section be absent. When absent, the section is required.repeat(trueorfalse). Set totrueto let the heading occur more than once. When absent, it occurs once.headings-open(trueorfalse). Set totrueto accept undeclared headings of this level or deeper after this section, up to the next declared heading. Only document-scope rules apply to their bodies, andsections-prohibitedstill applies.subheadings-open(trueorfalse). Set totrueto act asheadings-opendoes, but only for undeclared headings deeper than this level.for-each-key-of(therule-idof a rule, or a list of them). Requires one occurrence of this section for each key that the referenced rule captured, each key once. A list of rules pairs with the keys that any of them captured, and a key that two of them captured needs one occurrence. The engine reads the key of each occurrence by the heading’skey, which the heading must declare. The referenced rule must capture a key. A missing occurrence and an orphan are each an error that names the key. Under aparent, the keys are those captured inside the same occurrence of the parent.parent(therule-idof a rule). The section that this one lies inside. The parent must have a smallerleveland be listed before this section, with only its own subsections between them. Each occurrence must follow an occurrence of the parent, before the next heading of the parent’s level or higher.optionalandrepeatcount within each occurrence of the parent. A body still runs to the next heading.number-from-parent(trueorfalse). Set totruewhen the heading must begin with the number of its parent’s heading, a dot, and a number, such as4.2.1under4.2. A number is the text before the first space: digits separated by dots. A parent heading with no number is an error. Requiresparentbeside it.blocks(a list ofblock). The body, as an ordered list of blocks. When absent, the body is one prose block of one or more paragraphs.[]means an empty body. The body must be exactly these blocks, in order unlessblocks-orderedisfalse.blocks-ordered(trueorfalse). Set tofalseto accept the declared blocks in any order. When absent, the blocks must appear in the declared order.only(a map of strings or lists of strings). Maps lifecycle dimensions to states. The rule applies when the document’s state is in the set for every dimension named. When absent, the rule always applies. The engine reports on the schema a dimension or state that the lifecycle doesn’t declare, oronlywithout a lifecycle.guidance(a string). What an example can’t show about this section. The engine writes it into the scaffold before the section’s heading.examples(a list of strings). Further occurrences of a section that declaresrepeat: true. Each is the whole markdown text of one occurrence: a heading of the section’s level, its body, and any sections inside it. The scaffold writes the occurrence that it derives from the rules, then each text, in order and unchanged. A section that declaresfor-each-key-ofoccurs once for each key of another rule: the scaffold writes the occurrence of the first key from the rules, the occurrence of each later key from the next text, and the occurrences that remain after the last text from the rules. The engine checks each text as part of the scaffold.
prohibited-heading
One heading that is banned inside open regions.
A prohibited-heading also accepts the keys of rule-common.
heading(a string, required). The exact heading text.only(a map of strings or lists of strings). As for a section’sonly.
format-variant
One named shape of a variant format.
A format-variant has exactly one of text, template, pattern.
text(a string). The text that a line of this variant equals.template(a string). The line written with typed holes where it varies, matched whole.pattern(a regular expression). A regular expression that a line of this variant matches.example(a string, required). One conforming line that matches only this variant.
block
One part of a body, read from the document’s CommonMark blocks. Consecutive paragraphs form one prose block, and a paragraph of images only forms an image block. A block has a kind and units: the paragraphs of prose or a blockquote, the items of a list, the rows of a table, or the repetitions of a group. each applies one unit rule to every unit. sequence applies one rule per position and fixes the count. Otherwise, min and max bound the count. Generated blocks, region interiors, footnote definitions, and HTML comments are exempt from matching. Every rule reads the text with its generated anchors removed. The engine reports on the schema a key that doesn’t apply to the block’s kind.
A block also accepts the keys of rule-common.
kind(a string, required). The kind of block. Aproseblock is paragraphs. Abullet-listornumbered-listblock is items. Atableblock is a GFM table. Acodeblock is a tagged fence. Ablockquoteblock is quoted paragraphs. Animageblock is a paragraph of images only. Anhtmlblock is an HTML block. Athematic-breakblock is a horizontal rule. Areferencesblock is link reference definitions outside the generated block. Agroupblock is a repeating sequence of blocks. Values:prose,bullet-list,numbered-list,table,code,blockquote,image,html,thematic-break,references,group.optional(trueorfalse). Set totrueto let the block be absent. When absent, the block is required.repeat(trueorfalse). Set totrueto let the block occur more than once in a row. When absent, it occurs once.min(an integer). The minimum number of units. When absent, the minimum is one.max(an integer). The maximum number of units. When absent, there is no upper bound.each(unit). A rule that every unit must satisfy.sequence(a list ofunit). One rule per unit, by position. The count is the length of the sequence unless an entry is optional or repeats. A unit must satisfy botheachand the rule for its position.rules(a list ofunit). The grammar of the block’s units, an alternative toeachandsequence. An entry with a shape and noselect-patternis a production. A production takes the units that its shape matches, and each unit goes to the first such entry in order. An entry with aselect-patterntakes the units that the guard selects and checks them. If it states only a guard and a count, it counts those units and takes nothing. An entry with neither takes every unit.minandmaxbound what each entry took or counted. A unit that no entry takes is an error unlessunits-openis set. The engine reports it against the production that it follows furthest.units-open(trueorfalse). Set totrueto accept a unit that no entry ofrulestakes. When absent, such a unit is an error. Requiresrulesbeside it.lang(a string, or a list of them). The tag or tags that the fence must have, orany. A code block requires this key. An untagged fence is always an error. Applies to a block of kindcode.fence(an integer). The minimum number of backticks in the fence. When absent, the minimum is three. Applies to a block of kindcode.columns(a list of strings). The header row, in order. A table block requires this key. A differing header is an error. Applies to a block of kindtable.columns-open(trueorfalse). Set totrueto accept further columns after the declared ones. Applies to a block of kindtable.source-pattern(a regular expression). A regular expression that each image path must match. The engine matches an image in the host’s reference scheme by what the host reports that it shows, and any other image as written. Applies to a block of kindimage.inline-html(trueorfalse). Set totrueto accept inline HTML in this block’s units. When absent, any HTML other than a comment is an error.blocks(a list ofblock). The blocks of one repetition of the group. Applies to a block of kindgroup.rule-id(a slug). The name by which other rules reference this block. Its keys are the keys that its units capture, except the keys of a unit rule that declares its ownrule-id.only(a map of strings or lists of strings). As for a section’sonly.example(a string, or a list of them). The text of one of this block’s units, or a list of such texts. The engine uses it for every unit whose rule has no text or example of its own. A list gives those units in order, as the list of a unit rule does.guidance(a string). What an example can’t show about this block. The engine writes it into the scaffold before the block.
heading
A section’s heading as a unit rule. It defines the heading’s shape, the key that it captures, and an example of the heading. A repeated section is cited by its key.
A heading has exactly one of text, template, pattern.
text(a string, or{ from: <frontmatter key> }). The exact heading text, or{ from: <key> }for the frontmatter value.template(a string). The heading written with typed holes where it varies, matched whole.pattern(a regular expression). A regular expression that the heading must match, for what a template can’t express. Write^and$to bind the ends.key(a string). The hole of the template, or the named group of the pattern, whose capture is the section’s key.unique(a string). The scope within which the key is unique. When absent, the scope isdocument. Values:document. Requireskeybeside it.key-derives-from(key-derivation). Where the key’s prefix must come from. The engine splits the key at the first separator, and the prefix must be one that the derivation allows. Requireskeybeside it.example(a string). A heading of the kind that the template or the pattern requires.
unit
A rule on one unit of a block: a paragraph, an item, a row, a line of a fence, or a repetition of a group. The engine matches a paragraph with its lines joined by one space, and a line of a fence without its indent. It matches an item’s own text. Anything more that the item contains, such as a nested list or a fence, must match the rule’s blocks. A rule has at most one shape: text, template, pattern, or the format that it binds.
A unit also accepts the keys of rule-common.
text(a string, or{ from: <frontmatter key> }). The text that the unit must equal: a string, or{ from: <key> }for the frontmatter value. It applies to a paragraph, an item, or a line.template(a string). The unit’s text written with typed holes where it varies, matched whole.pattern(a regular expression). A regular expression that the unit’s text must match, for what a template can’t express. Write^and$to bind the ends.format(the name of a format). The name of a line format that the unit’s text must match.key(a string). The hole of the template, the named group of the pattern, or the hole or group of every shape of the format, whose capture is the unit’s key. The key must be unique within its scope by its slug, and the slug names the key’s generated anchor. Don’t declarekeybeside a format that captures a key.key-derives-from(key-derivation). Where the key’s prefix must come from. The engine splits the key at the first separator, and the prefix must be one that the derivation allows.key-optional(trueorfalse). Set totrueto treat a unit that contains no key as plain rather than as an error. The key is the rule’s ownkeyor the one that itsformatdeclares. The engine reports a rule that has neither. When absent, a unit with no key is an error.key-repeats(trueorfalse). Set totrueto let the rule capture one key on several units, and a key that another rule captures, as a list that names the owner of each item names one owner many times. Each capture by this rule refers to the key, which a capture by a rule withoutkey-repeatsdefines where there is one. When absent, a second capture of a key is an error.unique(a string). The scope within which the key is unique. When absent, the scope isdocument. The engine reports a clash on both documents, and names the key and the other document. Values:document.rule-id(a slug). The name by which other rules reference this rule. The keys that it captures belong to it, not to the enclosing block.for-each-key-of(therule-idof a rule, or a list of them). Requires one unit that captures each key that the referenced rule captured, each key once, as a section’sfor-each-key-ofpairs headings with keys. A list of rules pairs with the keys that any of them captured, each key once. This rule must capture a key, bykeyor by its format, and so must the referenced rule. A missing key and an orphan are each an error that names the key. When the section declares aparent, the keys are those captured inside the same occurrence of the parent. Otherwise, they are the keys of the document. The engine reports a missing key even where the block that would contain it is absent.prohibited-patterns(a list ofprohibition). Patterns that the unit’s text must not match, each with its message.optional(trueorfalse). In a sequence, set totrueto let this position be absent. When absent, the position is required.repeat(trueorfalse). In a sequence, set totrueto let this position occur more than once. When absent, it occurs at most once.blocks(a list ofblock). The blocks that a list item contains beyond its own text. Each block is required in every item unless it setsoptional. When absent, an item contains nothing beyond its text.lines(a list ofunit). The grammar of the content lines of a code orhtmlblock, asrulesis the grammar of a block’s units. Each line, without its indent, is a unit of its own. Blank lines don’t count. A line that no rule takes is an error unlesslines-openis set. A rule here can’t containlinesorblocks. A key captured from a line belongs to the line rule’s ownrule-id, or else to the enclosing block’s. A rule that containslinescan also state anexample. That example is the whole fence: the scaffold copies it, and these rules read its keys out of it.lines-open(trueorfalse). Set totrueto accept a line that no rule oflinestakes. When absent, such a line is an error. Requireslinesbeside it.select-pattern(a regular expression). Underrulesorlines, a regular expression that selects the units or lines that this rule governs by their text, for a rule that picks them by something other than its shape. A rule that states only a guard and a count counts what it matches and takes nothing.min(an integer). Underrulesorlines, the fewest units or lines that the rule may take or count. A rule that declaresselect-patternmust statemin, because it has no default. Writemin: 0to govern such units without requiring one. When absent on a rule without a guard, the minimum is one.max(an integer). Underrulesorlines, the most units or lines that the rule may take or count. When absent, there is no upper bound.example(a string, or a list of them). The text of a unit that this rule governs, or a list of such texts. A rule that binds a format and states no example of its own uses the format’s example. The engine checks each text of a list against the rule, and a failure quotes the first. The scaffold writes one unit for each text of a list, in order, and copies of the last text whilemindemands more, but never more units than themaxof the rule or of the block. A rule underruleswrites its list even withmin: 0. In asequence, a position that declaresrepeatwrites the whole list, and any other position writes the first text. In acodeblock, each text is one fence, so the block declaresrepeat: true.
key-derivation
Where the prefixes of a key-derives-from come from, and the separator that ends them.
A key-derivation has exactly one of rule, from.
rule(therule-idof a rule). The rule whose keys are the allowed prefixes.from(a string). The frontmatter key whose value is the only allowed prefix.separator(a string, required). The literal text that ends the prefix. The engine splits the key at its first occurrence.
rule-common
Keys that every rule map with common: true accepts.
severity(a string). How the engine reports a failure of the rule:errorreports an error, andwarnreports a warning. A rule inside this one, such as a block’s unit rule, takes this severity unless it declares its own. When absent, the rule takes the severity of the rule it sits in, and a failure of a rule in no other is an error. Values:error,warn.
Errors
Every error and warning that tramlines prints contains a key, such as TL_54 in error[TL_54]. The key names the rule of the specification that the document or the schema breaks. Keys that start with TL_ come from the core specification, which every format shares. Keys that start with TLMD_ come from the Markdown specification. Each of the following pages explains the errors of one part of a schema or a document. To find a key, search for it on the site.
Pages
- The schema: faults in the YAML, keys, values, and regular expressions of the schema itself.
- Shapes and templates: the one shape of each rule, and the holes of a template.
- Schema types and keys: the types, keys, key derivations, and bounds of a schema.
- Lifecycle and ids: the lifecycle of a schema, the rules that it gates, and the
idtemplate. - Formats: line formats, and a line that lacks the key of its format.
- Examples and guidance: the examples of the rules and formats of a schema, and its
examples. - Frontmatter: the frontmatter of a document.
- Lines and length: the shape of the lines of a document, its bans, and its length.
- Headings and sections: the headings of a document and the sections they open.
- Sections inside sections: sections that declare a
parent, and their numbers. - Blocks: the blocks that a section contains, and the number of units in each block.
- Kinds of block: the keys, tags, columns, and HTML that each kind of block allows.
- Units: the paragraphs and items that the rules of a block take, select, and count.
- Lines and pairings: the lines of a code block, and the items that pair with the keys of another rule.
- Markers: the markers of a scaffold that a document keeps.
- Scaffolds: the scaffold that a schema derives for a new document.
- Keys and repairs: keys and their anchors, and the text that a repair rewrites.
- The command: errors that stop
tramlines validatebefore it checks a document.
How an error reads
<file>:<line>: error[<KEY>]: <message> (schema <id>, <place of the rule>)
The end of the message names the rule in your schema that the document breaks, by its rule-id, or by its section and its position in the section. A message that quotes an example line shows what the rule expects. An error in the schema file itself names no rule, because the fault is in the schema.
Errors: the schema
These errors come from the schema file itself: its YAML, its keys and their values, and its regular expressions. Section 3 of the core specification defines these errors. A separate page lists the errors for the shapes and the templates of its rules.
TL_12
The schema contains a key that the grammar doesn’t define for the map that holds it (TL_12).
Message
<file>:<line>: error[TL_12]: unknown key `<key>` in `<rule map>`
<file>:<line>: error[TL_12]: unknown key `heading-from` in `section` (`heading-from: <key>` became `heading: { from: <key> }`)
Cause
The command checks the schema against the grammar before it uses the schema. Each map of a schema accepts only the keys that the grammar defines for it. A misspelled key is unknown, and so is a key that belongs to another kind of map. In the message, <rule map> names the kind of map that holds the key, such as schema, section, block, or format.
Fix
Change the schema. Correct the spelling of the key, move it to the map where it belongs, or remove it. The schema keys reference lists each key and the maps that accept it.
Example
This schema misspells optional:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
- heading: Links
level: 2
optinal: true
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TL_12]: unknown key `optinal` in `section`
1 error, 0 warnings
[exit 1]
Spell the key correctly:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
- heading: Links
level: 2
optional: true
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_101
A value of the schema doesn’t have the type that the grammar defines for its key (TL_101).
Message
<file>:<line>: error[TL_101]: `<rule map>` must be a map, not a <type>
<file>:<line>: error[TL_101]: `<key>` must be an integer
<file>:<line>: error[TL_101]: `<key>` must be true or false
<file>:<line>: error[TL_101]: `<key>` must be a list
<file>:<line>: error[TL_101]: `<key>` must be a map
<file>:<line>: error[TL_101]: `<key>` must be a string, not a <type>
<file>:<line>: error[TL_101]: `<key>` must be a string or a list
<file>:<line>: error[TL_101]: `<key>` must be a string or a map
<file>:<line>: error[TL_101]: `<key>` must be a string or `{ from: <frontmatter key> }`
<file>:<line>: error[TL_101]: `<key>` must be a string, `{ from: <frontmatter key> }` or a `<rule map>` map
<file>:<line>: error[TL_101]: `<key>` items must be strings
<file>:<line>: error[TL_101]: `<key>` items must be integers
<file>:<line>: error[TL_101]: `<key>` items must be booleans
<file>:<line>: error[TL_101]: `<key>` values must be strings or lists of strings
<file>:<line>: error[TL_101]: `types` must be a map of types
<file>:<line>: error[TL_101]: type `<name>` must be a regex or a list of choices
<file>:<line>: error[TL_101]: type `<name>`: a list of choices holds one or more non-empty strings
<file>:<line>: error[TL_101]: the format registry must be a map of formats
Cause
The grammar gives each key a type: a number, true or false, a string, a list, or a map. The value in the schema is of another type, such as a word where the key takes a number, or a single string where the key takes a list.
Fix
Change the schema. Write the value in the type that the message states. The schema keys reference lists the type of each key.
Example
This schema sets level to a word instead of a number:
note.yaml
line-limit: 0
sections:
- heading: Note
level: one
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_101]: `level` must be an integer
1 error, 0 warnings
[exit 1]
Write the level as a number:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_102
A value of the schema has the right type, but it isn’t one of the values that its key allows (TL_102).
Message
<file>:<line>: error[TL_102]: `<key>` must be one of <values>; `<value>` is not
<file>:<line>: error[TL_102]: `<key>` must be a slug: `<value>` is not
<file>:<line>: error[TL_102]: type `<name>` must be named by a slug
<file>:<line>: error[TL_102]: template `<template>`: `seq-width` must be positive
Cause
Some keys take only a value from a list, such as kind, which names a kind of block. Others, such as rule-id and the name of a type, take only a slug: a string of lowercase letters, digits, and hyphens. A seq-width must be 1 or more.
Fix
Change the schema. Write one of the values that the message lists, write the value or the name of the type as a slug, or set seq-width to 1 or more.
Example
This schema names a kind of block that doesn’t exist:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: paragraph
example: Buy milk.
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TL_102]: `kind` must be one of `prose`, `bullet-list`, `numbered-list`, `table`, `code`, `blockquote`, `image`, `html`, `thematic-break`, `references`, `group`; `paragraph` is not
1 error, 0 warnings
[exit 1]
Use the kind prose:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_103
A map of the schema lacks a key that the grammar requires (TL_103).
Message
<file>:<line>: error[TL_103]: `<rule map>` requires `<key>`
<file>:<line>: error[TL_103]: `<key>` requires `<other key>` beside it
<file>:<line>: error[TL_103]: `<rule map>` needs at least one of <keys>
<file>:<line>: error[TL_103]: `<rule map>` needs exactly one of <keys>
<file>:<line>: error[TL_103]: `lines-open` without `lines`
Cause
The grammar requires some keys in every map of a kind, such as line-limit in the schema and level in a section. Some keys need another key beside them, such as number-from-parent, which needs parent. Some maps need one key of a group, such as a key-derives-from, which takes rule or from. The map in the schema lacks the key, or has no key of the group.
Fix
Change the schema. Add the key that the message names, or one key of the group that it lists. If the schema doesn’t need a key that requires another, remove that key.
Example
This schema has no line-limit:
note.yaml
sections:
- heading: Note
level: 1
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:1: error[TL_103]: `schema` requires `line-limit`
1 error, 0 warnings
[exit 1]
Add the limit. A limit of 0 sets none:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_104
A map of the schema contains more than one key of a group that allows only one (TL_104).
Message
<file>:<line>: error[TL_104]: `<rule map>` needs exactly one of <keys>
Cause
Some maps take exactly one key of a group. A key-derives-from, for example, takes its prefixes either from a rule, with rule, or from a frontmatter key, with from. The map in the schema contains two keys of that group. Two shapes on one rule cause a separate error, TL_22.
Fix
Change the schema. Keep one key of the group that the message lists, and remove the others.
Example
This key-derives-from names both a rule and a frontmatter key:
note.yaml
line-limit: 0
types:
code: '[A-Z]+-[0-9]+'
frontmatter:
code:
example: NOTE
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:code} ${}'
key: id
key-derives-from: { rule: other, from: code, separator: '-' }
example: NOTE-1 Buy milk
note.md
---
code: NOTE
---
# Note
- <a id="note-1"></a>NOTE-1 Buy milk
$ tramlines validate --schema note.yaml note.md
note.yaml:15: error[TL_104]: `key-derivation` needs exactly one of `rule`, `from`
1 error, 0 warnings
[exit 1]
Keep from:
note.yaml
line-limit: 0
types:
code: '[A-Z]+-[0-9]+'
frontmatter:
code:
example: NOTE
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:code} ${}'
key: id
key-derives-from: { from: code, separator: '-' }
example: NOTE-1 Buy milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_13
The schema file isn’t valid YAML, so the command can’t read it and stops (TL_13).
Message
error[TL_13]: <file>: YAML does not parse at line <line>: <reason>
error[TL_13]: <file>: YAML mapping key at line <line> is not a string
Cause
The command reads the schema as YAML 1.2 with the core schema. A syntax error, such as a quote that isn’t closed or a key that appears twice in one map, stops the command with exit code 2 before it checks anything. A map key that isn’t a string stops it the same way. An anchor, an alias, or a tag causes a separate error, TL_14.
Fix
Fix the YAML of the schema at the line that the message names.
Example
This schema opens a quote and doesn’t close it:
note.yaml
line-limit: 0
sections:
- heading: "Note
level: 1
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
error[TL_13]: note.yaml: YAML does not parse at line 3: invalid indentation in quoted scalar
[exit 2]
Close the quote:
note.yaml
line-limit: 0
sections:
- heading: "Note"
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_14
The schema or the formats file uses a YAML anchor, alias, or tag, which neither file can contain, so the command stops (TL_14).
Message
error[TL_14]: <file>: YAML anchor at line <line>: anchors are not read
error[TL_14]: <file>: YAML alias at line <line>: aliases are not read
error[TL_14]: <file>: YAML tag at line <line>: tags are not read
Cause
An anchor (&name) names a value, and an alias (*name) repeats it. A tag (!name) gives a value a type. A schema, and the formats file that --formats names, can’t use any of them: each value must appear in full where it’s written. The command stops with exit code 2 before it checks anything.
Fix
Change the file that the message names. Write out in full each value that an alias repeats, and remove the anchor and any tag.
Example
This schema anchors the level of one section and repeats it in another with an alias:
note.yaml
line-limit: 0
sections:
- heading: Note
level: &top 1
blocks: []
- heading: Summary
level: *top
blocks: []
note.md
# Note
# Summary
$ tramlines validate --schema note.yaml note.md
error[TL_14]: note.yaml: YAML anchor at line 4: anchors are not read
[exit 2]
Write the level out each time:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
- heading: Summary
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_15
Two rules of the schema declare the same rule-id (TL_15).
Message
<file>:<line>: error[TL_15]: `rule-id` `<id>` is declared twice
Cause
Other rules refer to a rule by its rule-id, so each rule-id must name exactly one rule. The message names the id, on the line of the second rule that declares it.
Fix
Change the schema. Give one of the two rules another rule-id, and update each reference to that rule.
Example
Both sections of this schema declare rule-id: note:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
rule-id: note
blocks: []
- heading: Notes
level: 2
rule-id: note
optional: true
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:7: error[TL_15]: `rule-id` `note` is declared twice
1 error, 0 warnings
[exit 1]
Rename the second one:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
rule-id: note
blocks: []
- heading: Notes
level: 2
rule-id: notes
optional: true
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_118
A regular expression in the schema, or the expression that a template compiles to, doesn’t compile in the regular expression dialect of tramlines (TL_118).
Message
<file>:<line>: error[TL_118]: `<key>` is not a valid regex: <reason>
<file>:<line>: error[TL_118]: type `<name>`: not a valid regex: <reason>
<file>:<line>: error[TL_118]: template `<template>` does not compile: <reason>
Cause
The command reads each regular expression in the Rust dialect. That dialect has no lookaround, such as (?=…) or (?<!…), and no backreference, such as \1. A pattern, a select-pattern, a source-pattern, an entry of prohibited-patterns, or a type doesn’t compile if it uses either of these or isn’t a valid regular expression at all. The <reason> comes from the regular expression library. For a template or a pattern of a format, the message starts with format `<format>`: , and for a variant, with format `<format>`: variant `<variant>`: .
Fix
Change the schema. Correct the syntax that the <reason> names. Rewrite lookaround or a backreference without it: a word boundary \b or a character class often does the same work.
Example
This schema bans TODO with a lookaround:
note.yaml
line-limit: 0
prohibited-patterns: [{pattern: '(?<![A-Za-z])TODO', message: 'A note contains no TODO.'}]
sections:
- heading: Note
level: 1
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:2: error[TL_118]: `pattern` is not a valid regex: look-around, including look-ahead and look-behind, is not supported
1 error, 0 warnings
[exit 1]
Use a word boundary instead:
note.yaml
line-limit: 0
prohibited-patterns: [{pattern: '\bTODO', message: 'A note contains no TODO.'}]
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: shapes and templates
These errors come from the shapes of the rules of a schema: the one shape that each rule declares, and the holes of a template. Section 4 of the core specification defines these errors. For a template or a pattern of a format, a message starts with format `<format>`: , and for a variant, with format `<format>`: variant `<variant>`: .
TL_22
A rule of the schema declares more than one shape, or a format declares a shape beside variants (TL_22).
Message
<file>:<line>: error[TL_22]: `<shape>` and `<shape>` beside each other: a rule has one shape
<file>:<line>: error[TL_22]: a shape beside `format`: a rule has one shape, its own or the format's
<file>:<line>: error[TL_22]: `<rule map>` needs exactly one of <keys>
Cause
A shape is text, template, or pattern, and a rule that binds a format takes the shape of the format. A rule has more than one shape when it declares two shapes, or a shape beside a format. A frontmatter constraint has more than one shape when it declares two shapes. Its format, such as url, checks the value beside a shape and isn’t a shape of its own. The last message reports a format, a variant, or a heading with two shapes, or a format that declares a shape beside variants. A format, a variant, or a heading with no shape causes a separate error, TL_105.
Fix
Change the schema. Keep one shape on the rule and remove the others. To bind a format, remove the rule’s own shape. A format declares either one shape or variants.
Example
This frontmatter constraint declares both a template and a pattern:
note.yaml
line-limit: 0
frontmatter:
title:
template: '${}'
pattern: '.+'
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
note.md
---
title: Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_22]: `template` and `pattern` beside each other: a rule has one shape
1 error, 0 warnings
[exit 1]
Keep the pattern:
note.yaml
line-limit: 0
frontmatter:
title:
pattern: '.+'
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_105
A format declares no shape and no variants, or a variant or a heading rule declares no shape (TL_105).
Message
<file>:<line>: error[TL_105]: `<rule map>` needs exactly one of <keys>
Cause
A format names the shape of one line. It must declare a text, a template, or a pattern, or else variants, each with a shape of its own. A variant, and a heading written as a map, must declare a shape too. The map in the schema declares none of them.
Fix
Change the schema. Add the shape that the line must have: text for fixed text, template for text with holes, or pattern for a regular expression. A format can declare variants instead.
Example
This format has an example and no shape:
note.yaml
line-limit: 0
formats:
day:
example: Monday
sections:
- heading: Note
level: 1
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_105]: `format` needs exactly one of `text`, `template`, `pattern`, `variants`
1 error, 0 warnings
[exit 1]
Add a pattern:
note.yaml
line-limit: 0
formats:
day:
pattern: '^[A-Z][a-z]+day$'
example: Monday
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_25
A template of the schema contains no hole (TL_25).
Message
<file>:<line>: error[TL_25]: template `<template>` has no hole; write it as `text`
Cause
A template is text with holes, written ${name:type}, at the places where the text varies. A template with no hole is fixed text, and fixed text belongs in text. The sequence $${ writes a literal ${, so a template that contains only $${ has no hole.
Fix
Change the schema. Write the fixed text as text, or add a hole where the text varies.
Example
This heading template contains fixed text only:
note.yaml
line-limit: 0
sections:
- heading: { template: Note, example: Note }
level: 1
blocks: []
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:3: error[TL_25]: template `Note` has no hole; write it as `text`
1 error, 0 warnings
[exit 1]
Write the heading as text:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_119
A template of the schema uses one hole name twice (TL_119).
Message
<file>:<line>: error[TL_119]: template `<template>` names the hole `<name>` twice
Cause
Each named hole of a template captures its own part of the text, and a key can name that part. Each name must therefore be unique within the template. A hole with no name, ${} or ${:type}, captures nothing and can appear any number of times.
Fix
Change the schema. Rename one of the holes, or remove the name from a hole that nothing refers to.
Example
This template names the hole w twice:
note.yaml
line-limit: 0
frontmatter:
title:
template: '${w:word} ${w:word}'
example: Weekly groceries
sections:
- heading: Note
level: 1
blocks: []
note.md
---
title: Weekly groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_119]: template `${w:word} ${w:word}` names the hole `w` twice
1 error, 0 warnings
[exit 1]
Give each hole its own name:
note.yaml
line-limit: 0
frontmatter:
title:
template: '${first:word} ${rest:word}'
example: Weekly groceries
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_120
A hole of a template names a type that isn’t available to the schema (TL_120).
Message
<file>:<line>: error[TL_120]: template `<template>`: no type `<type>`
Cause
Each hole matches the regular expression of its type: a built-in type such as slug or int, or a type under types in the schema or in the --formats file. The type that the hole names is none of these. A misspelt type name often causes this error.
Fix
Change the schema. Correct the name of the type, use a built-in type, or declare the type under types.
Example
This template uses a type name that the schema doesn’t declare:
note.yaml
line-limit: 0
frontmatter:
title:
template: '${:name}'
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
note.md
---
title: Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_120]: template `${:name}`: no type `name`
1 error, 0 warnings
[exit 1]
Declare the type:
note.yaml
line-limit: 0
types:
name: '[A-Z][a-z]+'
frontmatter:
title:
template: '${:name}'
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_121
A hole of a template isn’t closed, or its name isn’t valid (TL_121).
Message
<file>:<line>: error[TL_121]: template `<template>` has an unclosed `${`
<file>:<line>: error[TL_121]: template `<template>`: hole name `<name>` must be letters, digits and `_`, not starting with a digit
Cause
A hole opens with ${ and closes with }. Its name, the part before the :, contains only letters, digits, and _, and doesn’t start with a digit. To write ${ as text in a template, write $${.
Fix
Change the schema. Close the hole, or rename it with letters, digits, and _.
Example
This hole name contains a hyphen:
note.yaml
line-limit: 0
frontmatter:
title:
template: '${first-word} ${}'
example: Weekly groceries
sections:
- heading: Note
level: 1
blocks: []
note.md
---
title: Weekly groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_121]: template `${first-word} ${}`: hole name `first-word` must be letters, digits and `_`, not starting with a digit
1 error, 0 warnings
[exit 1]
Use an underscore:
note.yaml
line-limit: 0
frontmatter:
title:
template: '${first_word} ${}'
example: Weekly groceries
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: schema types and keys
These errors come from the types, the keys, and the counts that a schema declares. Sections 3, 4, and 5 of the core specification define them. One error, TL_33, reports a problem in a document: a key that breaks one of the schema’s key derivations.
TL_29
A type under types reuses the name of a built-in type (TL_29).
Message
<file>:<line>: error[TL_29]: type `<name>` is built in and reserved; name the type otherwise
Cause
tramlines reserves the names of the built-in types text, word, int, slug, and seq, so a schema can’t define them again.
Fix
Change the schema. Remove the type and use the built-in one, or give the type another name and use that name in each template.
Example
This schema defines a type int, which is built in:
note.yaml
line-limit: 0
types:
int: '[0-9]+'
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${:int} ${}'
example: 2 apples
note.md
# Note
- 2 apples
$ tramlines validate --schema note.yaml note.md
note.yaml:3: error[TL_29]: type `int` is built in and reserved; name the type otherwise
1 error, 0 warnings
[exit 1]
Remove the type, and use the built-in int:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${:int} ${}'
example: 2 apples
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_109
The regular expression of a type under types contains an anchor or a capture group (TL_109).
Message
<file>:<line>: error[TL_109]: type `<name>`: `<regex>` holds an anchor; a template matches the whole line itself
<file>:<line>: error[TL_109]: type `<name>`: `<regex>` holds a capture group; write `(?:…)`, since a template names its own groups
Cause
The regular expression of a type matches one hole of a template. The template anchors the whole line and names its own groups, so the type can’t contain a ^ or $ anchor or a capture group. The command reports each refused type once. No template that names a refused type reports TL_120.
Fix
Change the schema. Remove the anchors from the regular expression of the type, and write each group as (?:…).
Example
The type code of this schema starts with ^ and ends with $:
note.yaml
line-limit: 0
types:
code: '^[A-Z]+$'
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${:code} ${}'
example: SHOP milk
note.md
# Note
- SHOP milk
$ tramlines validate --schema note.yaml note.md
note.yaml:3: error[TL_109]: type `code`: `^[A-Z]+$` holds an anchor; a template matches the whole line itself
1 error, 0 warnings
[exit 1]
Remove the anchors:
note.yaml
line-limit: 0
types:
code: '[A-Z]+'
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${:code} ${}'
example: SHOP milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_31
The key of a rule or a format names no hole or named group of its shape (TL_31).
Message
<file>:<line>: error[TL_31]: `key` `<key>` names no hole or named group of the rule's shape
<file>:<line>: error[TL_31]: `key` `<key>` names a hole of a shape, and the rule declares none
<file>:<line>: error[TL_31]: format `<format>`: `key` `<key>` names no hole or named group of the shape
<file>:<line>: error[TL_31]: format `<format>`: `key` `<key>` names no hole or named group of variant `<variant>`
Cause
The key key holds the name of a hole of the template, such as item in ${item:slug}, or of a named group of the pattern. The rule captures the key from that hole. When key names something else, or when the rule declares no shape, the rule finds no hole to read the key from. The key of a format names a hole or a named group of the shape of the format, and of each of its variants.
Fix
Change the schema. Set key to the name of a hole or a named group of the shape, or give the rule the shape that holds the key.
Example
This rule names its key name, but its template calls the hole item:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${item:slug}: ${:int}'
key: name
example: 'milk: 2'
note.md
# Note
- <a id="milk"></a>milk: 2
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TL_31]: `key` `name` names no hole or named group of the rule's shape
1 error, 0 warnings
[exit 1]
Name the hole:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${item:slug}: ${:int}'
key: item
example: 'milk: 2'
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_113
A rule declares key and binds a format that captures a key itself (TL_113).
Message
<file>:<line>: error[TL_113]: `key` beside format `<format>`, which captures a key itself
Cause
A format that declares key captures that key from every line that it matches. A rule that binds the format takes the key of the format, so a key of its own would name a second key for the same line.
Fix
Change the schema. Remove key from the rule, or bind a format that declares no key.
Example
The format item captures the key id, and the rule that binds it declares key too:
note.yaml
line-limit: 0
formats:
item:
template: '${id:slug}: ${:int}'
key: id
example: 'milk: 2'
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
format: item
key: id
note.md
# Note
- <a id="milk"></a>milk: 2
$ tramlines validate --schema note.yaml note.md
note.yaml:13: error[TL_113]: `key` beside format `item`, which captures a key itself
1 error, 0 warnings
[exit 1]
Remove key from the rule:
note.yaml
line-limit: 0
formats:
item:
template: '${id:slug}: ${:int}'
key: id
example: 'milk: 2'
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
format: item
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_33
A key in the document starts with a prefix that the key-derives-from of its rule doesn’t allow, or the key lacks the separator (TL_33).
Message
<file>:<line>: error[TL_33]: key `<key>` derives from `<prefix>`, which rule `<rule-id>` did not capture (schema <id>, <place of the rule>)
<file>:<line>: error[TL_33]: key `<key>` derives from `<prefix>`, which is not `<value>`, the value of the frontmatter `<frontmatter key>` (schema <id>, <place of the rule>)
<file>:<line>: error[TL_33]: key `<key>` derives from `<prefix>`, and the frontmatter `<frontmatter key>` that it derives from is absent (schema <id>, <place of the rule>)
<file>:<line>: error[TL_33]: key `<key>` has no `<separator>` separator to derive from (schema <id>, <place of the rule>)
Cause
key-derives-from: { rule: <rule-id>, separator: <separator> } requires the part of each key before the separator to be a key that the named rule captured. key-derives-from: { from: <frontmatter key>, separator: <separator> } requires it to be the value of that frontmatter key. The message shows that value after the frontmatter key’s name. The error means that a key in the document starts with another prefix, or contains no separator at all.
Fix
Change the key in the document so that its prefix is a key of the named rule or the frontmatter value. If the schema names the wrong rule, frontmatter key, or separator, fix key-derives-from in the schema instead.
Example
This schema requires the key of each item to start with the value of the frontmatter key code:
note.yaml
line-limit: 0
types:
item-id: '[A-Z]+-[0-9]+'
frontmatter:
code:
pattern: '[A-Z]+'
example: SHOP
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:item-id}: ${}'
key: id
key-derives-from: { from: code, separator: "-" }
example: 'SHOP-1: milk'
The second item of this document starts with HOME:
note.md
---
code: SHOP
---
# Note
- <a id="shop-1"></a>SHOP-1: milk
- <a id="home-2"></a>HOME-2: soap
$ tramlines validate --schema note.yaml note.md
note.md:8: error[TL_33]: key `HOME-2` derives from `HOME`, which is not `SHOP`, the value of the frontmatter `code` (schema note, section "Note")
1 error, 0 warnings
[exit 1]
Start the key with the code:
note.md
---
code: SHOP
---
# Note
- <a id="shop-1"></a>SHOP-1: milk
- <a id="shop-2"></a>SHOP-2: soap
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_96
A unit rule that captures no key declares key-derives-from (TL_96).
Message
<file>:<line>: error[TL_96]: `key-derives-from` on a rule that captures no key
Cause
key-derives-from checks the key that its rule captures against a frontmatter value or against the keys of another rule. The rule declares no key, and it binds either no format or a format that declares none. It therefore captures no key for key-derives-from to check. On a heading, a key-derives-from with no key reports TL_103 instead, because the grammar requires key beside it.
Fix
Change the schema. Give the rule a key that names a hole of its shape, or remove key-derives-from.
Example
This rule derives its keys from the frontmatter key code, but declares no key:
note.yaml
line-limit: 0
types:
item-id: '[A-Z]+-[0-9]+'
frontmatter:
code:
pattern: '[A-Z]+'
example: SHOP
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:item-id}: ${}'
key-derives-from: { from: code, separator: "-" }
example: 'SHOP-1: milk'
note.md
---
code: SHOP
---
# Note
- <a id="shop-1"></a>SHOP-1: milk
$ tramlines validate --schema note.yaml note.md
note.yaml:14: error[TL_96]: `key-derives-from` on a rule that captures no key
1 error, 0 warnings
[exit 1]
Declare the key:
note.yaml
line-limit: 0
types:
item-id: '[A-Z]+-[0-9]+'
frontmatter:
code:
pattern: '[A-Z]+'
example: SHOP
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:item-id}: ${}'
key: id
key-derives-from: { from: code, separator: "-" }
example: 'SHOP-1: milk'
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_117
A key-derives-from names a rule or a frontmatter key that the schema doesn’t declare (TL_117).
Message
<file>:<line>: error[TL_117]: `key-derives-from` names no rule `<rule-id>`
<file>:<line>: error[TL_117]: `key-derives-from` names no frontmatter key `<key>`
Cause
key-derives-from: { rule: <rule-id>, separator: <separator> } checks each key against the keys of the rule with that rule-id. key-derives-from: { from: <frontmatter key>, separator: <separator> } checks it against the value of that frontmatter key. The schema declares no rule or frontmatter key of that name.
Fix
Change the schema. Correct the name in key-derives-from, or declare the rule or the frontmatter key that it names.
Example
This rule derives its keys from a frontmatter key code, which the schema doesn’t declare:
note.yaml
line-limit: 0
types:
item-id: '[A-Z]+-[0-9]+'
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:item-id}: ${}'
key: id
key-derives-from: { from: code, separator: "-" }
example: 'SHOP-1: milk'
note.md
---
code: SHOP
---
# Note
- <a id="shop-1"></a>SHOP-1: milk
$ tramlines validate --schema note.yaml note.md
note.yaml:10: error[TL_117]: `key-derives-from` names no frontmatter key `code`
1 error, 0 warnings
[exit 1]
Declare code:
note.yaml
line-limit: 0
types:
item-id: '[A-Z]+-[0-9]+'
frontmatter:
code:
pattern: '[A-Z]+'
example: SHOP
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${id:item-id}: ${}'
key: id
key-derives-from: { from: code, separator: "-" }
example: 'SHOP-1: milk'
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_47
A rule sets key-optional but captures no key (TL_47).
Message
<file>:<line>: error[TL_47]: `key-optional` without a `key` or a `format` that declares one
Cause
key-optional: true lets a unit leave out the key that its rule captures. The rule declares no key and binds no format that declares one. The rule therefore captures no key that a unit could leave out.
Fix
Change the schema. Remove key-optional, or declare the key that it makes optional.
Example
This rule sets key-optional and captures no key:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${:slug}'
key-optional: true
example: milk
note.md
# Note
- milk
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TL_47]: `key-optional` without a `key` or a `format` that declares one
1 error, 0 warnings
[exit 1]
Remove key-optional:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: '${:slug}'
example: milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_98
A rule or a frontmatter key declares a min greater than its max (TL_98).
Message
<file>:<line>: error[TL_98]: `min` <min> is above `max` <max>
Cause
min and max bound how many units a block or a rule takes or counts, or how many elements a frontmatter list contains. When min is larger than max, no count meets both bounds. No document can then satisfy the rule.
Fix
Change the schema. Swap the two numbers, or correct the one that’s wrong.
Example
This rule requires at least two items and allows at most one:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
rules:
- template: '${:slug}'
min: 2
max: 1
example: milk
note.md
# Note
- milk
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TL_98]: `min` 2 is above `max` 1
1 error, 0 warnings
[exit 1]
Set min to a value no higher than max:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
rules:
- template: '${:slug}'
min: 1
max: 2
example: milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: lifecycle and ids
These errors come from the lifecycle of a schema, from the only of a rule, and from the template of the id key. Sections 3 and 6 of the core specification define these errors.
TL_20
The only of a rule names a lifecycle dimension or a state that the schema doesn’t declare (TL_20).
Message
<file>:<line>: error[TL_20]: `only` names a dimension the lifecycle does not declare: `<dimension>`
<file>:<line>: error[TL_20]: `only` names `<state>`, which is no state of `<dimension>`
Cause
The key only restricts a rule to some states of a document. Each key of only is the key of a dimension under lifecycle, and each value is one of the states of that dimension.
Fix
Change the schema. Correct the dimension or the state in only, or add it to the lifecycle.
Example
This rule names a state drafted, which the lifecycle doesn’t declare:
note.yaml
line-limit: 0
lifecycle:
- key: status
states: [draft, done]
sections:
- heading: Note
level: 1
blocks: []
- heading: Open questions
level: 2
only: { status: [drafted] }
blocks: []
note.md
---
status: draft
---
# Note
## Open questions
$ tramlines validate --schema note.yaml note.md
note.yaml:11: error[TL_20]: `only` names `drafted`, which is no state of `status`
1 error, 0 warnings
[exit 1]
Name the state as the lifecycle does:
note.yaml
line-limit: 0
lifecycle:
- key: status
states: [draft, done]
sections:
- heading: Note
level: 1
blocks: []
- heading: Open questions
level: 2
only: { status: [draft] }
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_111
A rule declares only, and the schema declares no lifecycle (TL_111).
Message
<file>:<line>: error[TL_111]: `only` without a lifecycle
Cause
The key only restricts a rule to some states of a document, and the lifecycle of a schema declares those states. A schema without a lifecycle declares no states for only to name.
Fix
Change the schema. Declare the dimension that only names under lifecycle, or remove only from the rule.
Example
This schema gates a section on status, but declares no lifecycle:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
- heading: Open questions
level: 2
only: { status: [draft] }
blocks: []
note.md
---
status: draft
---
# Note
## Open questions
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TL_111]: `only` without a lifecycle
1 error, 0 warnings
[exit 1]
Declare the lifecycle:
note.yaml
line-limit: 0
lifecycle:
- key: status
states: [draft, done]
sections:
- heading: Note
level: 1
blocks: []
- heading: Open questions
level: 2
only: { status: [draft] }
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_58
The template of the id key contains a hole that the command can’t fill for a new document (TL_58).
Message
<file>:<line>: error[TL_58]: template `<template>`: an id holds only the holes a new document is filled with, `${:seq}`, `${:slug}` and `${<lifecycle key>}`, not a `<type>`
<file>:<line>: error[TL_58]: template `<template>`: the hole `${<name>}` names no lifecycle key
Cause
The command also fills the template of id to write the id of a new document. The template therefore contains only the holes that the command can fill: ${:seq}, ${:slug}, and a hole with no type named for a key of the lifecycle.
Fix
Change the schema. Replace a hole of another type with ${:seq} or ${:slug}. For a hole with no type, name a key that lifecycle declares, or declare that key under lifecycle.
Example
This id template numbers documents with an int hole:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:int}'
example: note-1
sections:
- heading: Note
level: 1
blocks: []
note.md
---
id: note-1
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_58]: template `note-${:int}`: an id holds only the holes a new document is filled with, `${:seq}`, `${:slug}` and `${<lifecycle key>}`, not an `int`
1 error, 0 warnings
[exit 1]
Number them with a seq hole, and set seq-width to the number of digits (the default is three):
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:seq}'
seq-width: 1
example: note-1
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_114
The template of the id key contains more than one seq hole (TL_114).
Message
<file>:<line>: error[TL_114]: template `<template>` has more than one `seq` hole
Cause
The command fills the seq hole of the id template with the next free number when it writes a new document. A template with two seq holes has no single number to fill.
Fix
Change the schema. Keep one seq hole, and write the other part of the id with another hole, such as ${:slug}.
Example
This id template contains two seq holes:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:seq}-${:seq}'
sections:
- heading: Note
level: 1
blocks: []
note.md
---
id: note-001-002
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_114]: template `note-${:seq}-${:seq}` has more than one `seq` hole
1 error, 0 warnings
[exit 1]
Write the second part as a slug:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:seq}-${:slug}'
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_115
The id key declares a template and a key beside it that id doesn’t accept (TL_115).
Message
<file>:<line>: error[TL_115]: `<key>` beside the `id` template: `id` takes `template` alone
Cause
The template of id defines every id that a document can have, and the id that the command writes for a new document. A const, enum, format, list, min, or max beside it would accept ids that the template writes differently.
Fix
Change the schema. Remove the key beside the template, and write what it says into the template.
Example
This id declares an enum beside its template:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:seq}'
enum: [note-001, note-002]
sections:
- heading: Note
level: 1
blocks: []
note.md
---
id: note-001
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:5: error[TL_115]: `enum` beside the `id` template: `id` takes `template` alone
1 error, 0 warnings
[exit 1]
Remove the enum:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:seq}'
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_116
The key seq-width sits beside the template of a key other than id, or beside an id template with no seq hole (TL_116).
Message
<file>:<line>: error[TL_116]: `seq-width` beside a template with no `seq` hole
<file>:<line>: error[TL_116]: `seq-width` applies to the `id` template alone
Cause
The key seq-width sets the number of digits of the seq hole of the id template. Beside a template with no seq hole, or beside the template of another key, seq-width has no hole to size. For a seq-width beside no template at all, the command reports TL_103 instead, because the key requires template beside it.
Fix
Change the schema. Remove seq-width, or add a seq hole to the id template.
Example
This id template has no seq hole, and declares seq-width:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:slug}'
seq-width: 3
sections:
- heading: Note
level: 1
blocks: []
note.md
---
id: note-groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_116]: `seq-width` beside a template with no `seq` hole
1 error, 0 warnings
[exit 1]
Remove seq-width:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:slug}'
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_122
The template or the const of the id key admits an id that isn’t a slug (TL_122).
Message
<file>:<line>: error[TL_122]: template `<template>` admits a non-slug: `<sample>` is not a slug
<file>:<line>: error[TL_122]: template `<template>`: the state `<state>` of `<key>` is not a slug, so no id can hold it
<file>:<line>: error[TL_122]: the id `<id>` is not a slug
Cause
An id is a slug: lowercase ASCII letters and digits, in words joined by single hyphens, such as note-001. The command writes the literal text of the template and the state of each lifecycle hole into the id, so the text and the states are slugs too. A fixed id, the const of id, is a slug.
Fix
Change the schema. Write the text of the template or the const as a slug, and name each state of a dimension that the template holds as a slug.
Example
This id template starts with a capital letter:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'Note-${:seq}'
sections:
- heading: Note
level: 1
blocks: []
note.md
---
id: note-001
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_122]: template `Note-${:seq}` admits a non-slug: `Note-x` is not a slug
1 error, 0 warnings
[exit 1]
Write the template in lowercase:
note.yaml
line-limit: 0
frontmatter:
id:
template: 'note-${:seq}'
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_59
The id of a document contains a lifecycle state other than the state of the document (TL_59).
Message
<file>:<line>: error[TL_59]: id `<id>` holds `<state>` for `<key>`, but `<key>` is `<other state>` (schema <id>, key `id`)
Cause
The template of id contains a hole named for a lifecycle key, such as ${status}. The id of each document therefore repeats the state of the document for that key. The state of the document changed, and its id still contains the old state.
Fix
Change the document. Write the state of the document into its id, or set the state back to the one in the id.
Example
This schema puts the status of a note into its id:
note.yaml
line-limit: 0
lifecycle:
- { key: status, states: [open, closed] }
frontmatter:
id:
template: 'note-${status}-${:seq}'
sections:
- heading: Note
level: 1
blocks: []
The status of this note is closed, but its id contains open:
note.md
---
id: note-open-001
status: closed
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:2: error[TL_59]: id `note-open-001` holds `open` for `status`, but `status` is `closed` (schema note, key `id`)
1 error, 0 warnings
[exit 1]
Write the state into the id:
note.md
---
id: note-closed-001
status: closed
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_95
The lifecycle declares one dimension twice (TL_95).
Message
<file>:<line>: error[TL_95]: dimension `<key>` is declared twice
Cause
Each dimension under lifecycle declares one frontmatter key, and its states list the values that the key can take. Two dimensions with the same key would give that key two lists of states.
Fix
Change the schema. Merge the two dimensions into one, or give one of them another key.
Example
This lifecycle declares status twice:
note.yaml
line-limit: 0
lifecycle:
- { key: status, states: [draft, done] }
- { key: status, states: [open, closed] }
sections:
- heading: Note
level: 1
blocks: []
note.md
---
status: draft
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:4: error[TL_95]: dimension `status` is declared twice
1 error, 0 warnings
[exit 1]
Keep one dimension:
note.yaml
line-limit: 0
lifecycle:
- { key: status, states: [draft, done] }
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_112
The frontmatter of the schema declares the key of a lifecycle dimension (TL_112).
Message
<file>:<line>: error[TL_112]: `<key>` is a lifecycle dimension; it is not repeated under `frontmatter`
Cause
A dimension under lifecycle declares its frontmatter key, and its states list the values that the key can take. Because the lifecycle declares the key, frontmatter can’t declare it again.
Fix
Change the schema. Remove the key of the dimension from frontmatter.
Example
This schema declares status both as a lifecycle dimension and under frontmatter:
note.yaml
line-limit: 0
lifecycle:
- key: status
states: [draft, done]
frontmatter:
status:
enum: [draft, done]
sections:
- heading: Note
level: 1
blocks: []
note.md
---
status: draft
---
# Note
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TL_112]: `status` is a lifecycle dimension; it is not repeated under `frontmatter`
1 error, 0 warnings
[exit 1]
Remove status from frontmatter:
note.yaml
line-limit: 0
lifecycle:
- key: status
states: [draft, done]
sections:
- heading: Note
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: formats
These errors concern the line formats that a schema can use. They also cover a line that lacks the key captured by its rule or format. Section 5 of the core specification defines them. A separate page lists the errors for the examples of a schema.
TL_40
A format with one shape declares no example (TL_40).
Message
<file>:<line>: error[TL_40]: format `<name>`: a format with one shape carries an `example`
Cause
A format with one shape (a text, a template, or a pattern) declares no example. The engine shows the example beside every failure of the shape, so each such format needs one. A patch, an entry with patch: true, needs no example of its own. You can define a format in the formats map of the schema or in a formats file that you pass with --formats. For a format in a formats file, the command stops with the same key and message.
Fix
Add an example to the format in the schema or in the formats file.
Example
This schema defines a format task with no example:
tasks.yaml
line-limit: 0
formats:
task:
template: 'TODO ${}'
sections:
- heading: Tasks
level: 1
blocks: []
tasks.md
# Tasks
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:4: error[TL_40]: format `task`: a format with one shape carries an `example`
1 error, 0 warnings
[exit 1]
Add an example of the format:
tasks.yaml
line-limit: 0
formats:
task:
template: 'TODO ${}'
example: TODO buy milk
sections:
- heading: Tasks
level: 1
blocks: []
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_110
A format with variants declares an example of its own (TL_110).
Message
<file>:<line>: error[TL_110]: format `<name>`: a format with `variants` has no `example` of its own: each variant's example speaks for its shape
Cause
Each variant of a format has a shape and an example of its own. A failure shows the example of the variant that the line comes nearest to. An example beside variants belongs to no shape. For a format in a formats file that you pass with --formats, the command stops with the same key and message.
Fix
Remove the example from the format. If the example shows a line that no variant accepts, add a variant with that example.
Example
This format declares an example beside its variants:
tasks.yaml
line-limit: 0
formats:
task:
example: TODO buy milk
variants:
open:
template: 'TODO ${}'
example: TODO buy milk
done:
template: 'DONE ${}'
example: DONE buy bread
sections:
- heading: Tasks
level: 1
blocks: []
tasks.md
# Tasks
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:4: error[TL_110]: format `task`: a format with `variants` has no `example` of its own: each variant's example speaks for its shape
1 error, 0 warnings
[exit 1]
Remove the example of the format:
tasks.yaml
line-limit: 0
formats:
task:
variants:
open:
template: 'TODO ${}'
example: TODO buy milk
done:
template: 'DONE ${}'
example: DONE buy bread
sections:
- heading: Tasks
level: 1
blocks: []
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_41
A format entry declares patch: true, but no farther layer defines a format of that name for it to patch (TL_41).
Message
<file>:<line>: error[TL_41]: format `<name>` patches no farther entry
Cause
A patch states only how it differs from the entry that it shadows. The formats map of a schema patches a format of a formats file that you pass with --formats. When you don’t pass that file, or the file has no format of that name, the patch has nothing to merge onto. A patch in the formats file itself has no farther layer, and the command stops with the same key and message.
Fix
Change the schema or the command. Pass the formats file that defines the entry with --formats. If no such file exists, remove patch: true and write the whole format in the schema. For a patch in the formats file, remove patch: true there and write the whole format.
Example
This schema patches a format task that nothing defines:
tasks.yaml
line-limit: 0
formats:
task:
patch: true
example: TODO buy bread
sections:
- heading: Tasks
level: 1
blocks: []
tasks.md
# Tasks
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:4: error[TL_41]: format `task` patches no farther entry
1 error, 0 warnings
[exit 1]
Write the whole format:
tasks.yaml
line-limit: 0
formats:
task:
template: 'TODO ${}'
example: TODO buy bread
sections:
- heading: Tasks
level: 1
blocks: []
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_97
A rule binds a format by a name that no layer declares (TL_97).
Message
<file>:<line>: error[TL_97]: `format` names no format `<name>`
Cause
A rule sets format to a name that is neither in the formats map of the schema nor in a formats file that you pass with --formats. A format of the schema can fail to load, for example when it has no example. That format has its own error, and the rules that bind it report nothing more.
Fix
Change the schema or the command. Correct the name, define the format in the formats map of the schema, or pass the formats file that defines it with --formats.
Example
This schema binds a format task that it doesn’t define:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
format: task
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:8: error[TL_97]: `format` names no format `task`
1 error, 0 warnings
[exit 1]
Define the format:
tasks.yaml
line-limit: 0
formats:
task:
template: 'TODO ${}'
example: TODO buy milk
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
format: task
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_43
The example of one variant of a format also matches another variant of the same format (TL_43).
Message
<file>:<line>: error[TL_43]: the example of variant `<variant>` of format `<name>` also matches variant `<other>`: each variant's example matches its own variant only
<file>:<line>: error[TL_43]: the example of variant `<variant>` of format `<name>` (<layer>) also matches variant `<other>`: each variant's example matches its own variant only
Cause
The example of a variant distinguishes that variant from the others, so the example must match its own variant and no other. A variant with a broad shape, such as ${}, matches the examples of the other variants too. The command reports an example that doesn’t match its own variant as TL_65 instead. For a format of a formats file that you pass with --formats, the command reports the error on that file, at the line of the format. The message with (<layer>) names a format that a program adds when it uses tramlines as a library. The program reports that error on the first line of the schema.
Fix
Change the schema or the formats file. Narrow the shape of the variant that matches too much, or change the example so that only its own variant matches it.
Example
The variant any of this format matches every line, so it matches the example of open:
tasks.yaml
line-limit: 0
formats:
task:
variants:
open:
template: 'TODO ${}'
example: TODO buy milk
any:
template: '${}'
example: buy bread
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
format: task
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:6: error[TL_43]: the example of variant `open` of format `task` also matches variant `any`: each variant's example matches its own variant only
1 error, 0 warnings
[exit 1]
Narrow the second variant to lines that start with DONE:
tasks.yaml
line-limit: 0
formats:
task:
variants:
open:
template: 'TODO ${}'
example: TODO buy milk
done:
template: 'DONE ${}'
example: DONE buy bread
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
format: task
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_44
A line matches the shape of its rule but lacks the key that the rule or its format captures (TL_44).
Message
<file>:<line>: error[TL_44]: `<text>` carries no key: `<key>` captures nothing; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
<file>:<line>: error[TL_44]: `<text>` carries no key: the format `<name>` captures `<key>` (schema <id>, <place of the rule>)
Cause
The rule declares key, or binds a format that declares one. The shape makes the capture group of the key optional, as in a pattern with (?<id>…)?. The line matches the shape but contains no key. A rule that binds a format and declares its own key reads the key in the shape of the format. The second message reports a key that the format declares.
Fix
Add the key to the line in the document. If the key is optional in lines of this kind, set key-optional: true on the rule in the schema.
Example
This rule captures an optional key id at the start of each item:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
pattern: '^(?:(?<id>T-\d+): )?.+$'
key: id
example: 'T-1: buy milk'
This item has no key:
tasks.md
# Tasks
- buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.md:3: error[TL_44]: `buy milk` carries no key: `id` captures nothing; an item of this kind reads T-1: buy milk (schema tasks, section "Tasks" block 1)
1 error, 0 warnings
[exit 1]
Add the key, and the anchor that must precede every key:
tasks.md
# Tasks
- <a id="t-1"></a>T-1: buy milk
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
Errors: examples and guidance
These errors come from the examples of a schema: the example of each rule and format, the examples that the schema lists, and the examples of a section. Section 7 of the core specification defines the TL errors, and section 9 of the markdown specification defines the TLMD errors.
TL_64
The example of a rule fails the rule that declares it (TL_64).
Message
<file>:<line>: error[TL_64]: the example of `<key>` <reason> (schema <id>, key `<key>`)
<file>:<line>: error[TL_64]: the example `<example>` <reason> (schema <id>, <place of the rule>)
Cause
The example of a rule shows the text that the rule requires, so the command checks it against the rule. The first message reports a frontmatter key. The second reports a heading or a unit rule. The reason names what the example fails, such as the shape, a const, an enum, a select-pattern, a prohibited pattern, or a key that it doesn’t capture.
Fix
In the schema, change the example so that the rule accepts it, or correct the rule if the example shows what you meant.
Example
The template of this rule starts with TODO, and its example doesn’t:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: buy milk
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:8: error[TL_64]: the example `buy milk` fails at column 1: expected `TODO `, found `buy milk` (schema tasks, section "Tasks" block 1)
1 error, 0 warnings
[exit 1]
Make the example match the template:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_65
The example of a format fails the shape, the prohibited patterns, or the key of that format (TL_65).
Message
<file>:<line>: error[TL_65]: the example `<example>` of format `<name>` <reason>
<file>:<line>: error[TL_65]: the example `<example>` of variant `<variant>` of format `<name>` does not match its own variant: it <reason>
<file>:<line>: error[TL_65]: the example `<example>` of format `<name>` (<layer>) <reason>
Cause
The command checks the example of each format that the schema can use against that format: its shape, its bans, and its key. An example that matches the shape of a format with a key, but captures none, fails too. The command checks the example of a variant against that variant. For a format of a formats file that you pass with --formats, the command reports the error on that file, at the line of the format. The message with (<layer>) names a format that a program adds when it uses tramlines as a library. The program reports that error on the first line of the schema.
Fix
In the schema or the formats file that defines the format, change the example so that the format accepts it, or correct the format if the example shows what you meant.
Example
The template of this format starts with TODO, and its example doesn’t:
tasks.yaml
line-limit: 0
formats:
task:
template: 'TODO ${}'
example: buy milk
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
format: task
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:4: error[TL_65]: the example `buy milk` of format `task` fails at column 1: expected `TODO `, found `buy milk`
1 error, 0 warnings
[exit 1]
Make the example match the template:
tasks.yaml
line-limit: 0
formats:
task:
template: 'TODO ${}'
example: TODO buy milk
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
format: task
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_66
A rule declares a shape but no example of it (TL_66).
Message
<file>:<line>: error[TL_66]: a rule that declares `<key>` carries an `example` of it: a failure shows the example, and the scaffold writes it
<file>:<line>: error[TL_66]: heading `<heading>` holds a hole: write `heading: { template: …, example: … }`, since a template carries an example
Cause
A rule declares a template, a pattern, or a source-pattern, declares no example, and binds no format that declares one. A frontmatter format such as url checks the value and brings no example. The id key of the frontmatter is the one exception. The second message reports a heading written as a plain string with a hole such as ${:int}. A plain string leaves no place for an example.
Fix
Change the schema. Add an example to the rule. Write a heading with a hole as a map with template and example.
Example
This rule declares a template and no example:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:8: error[TL_66]: a rule that declares `template` carries an `example` of it: a failure shows the example, and the scaffold writes it
1 error, 0 warnings
[exit 1]
Add an example:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_68
The guidance and the example of a rule contain the same words (TL_68).
Message
<file>:<line>: error[TL_68]: `guidance` and `example` hold the same words: guidance says what the example cannot
Cause
The example shows the text that a rule requires, and the guidance describes what the example can’t show. When the two contain the same words, the guidance adds nothing. The command reads the guidance of a section against the example of its heading. It reads the guidance of a block against the example of each of its unit rules. It pairs them this way because the scaffold writes each guidance immediately before that example.
Fix
Change the schema. Rewrite the guidance to describe what the example can’t show, such as when to write the part or what to leave out. If there is nothing to add, remove the guidance.
Example
The guidance of this block repeats its example:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
guidance: TODO buy milk
example: TODO buy milk
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:6: error[TL_68]: `guidance` and `example` hold the same words: guidance says what the example cannot
1 error, 0 warnings
[exit 1]
Describe what the example can’t show:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
guidance: List one task per item, in the order you plan to do them.
example: TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TL_69
A document in the examples of the schema fails the schema (TL_69).
Message
<file>:<line>: error[TL_69]: example <n> fails: line <line>: [<KEY>] <message>
<file>:<line>: error[TL_69]: example <n> fails: [<KEY>] <message>
Cause
The command checks each entry of examples as a complete document at its own states, with no markers. The message names the number of the example, and the key and message of the error that the document raises. When the error has a line, the message also names the line of the example document that fails. Look up that key to find the rule that the line breaks.
Fix
Change the schema. Correct the example document so that the schema accepts it, or correct the schema if the example shows what you meant.
Example
The example document of this schema contains an item without TODO:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: TODO buy milk
examples:
- document: |
# Tasks
- buy milk
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:11: error[TL_69]: example 1 fails: line 3: [TL_74] `buy milk` fails at column 1: expected `TODO `, found `buy milk`; an item of this kind reads TODO buy milk (schema tasks, section "Tasks" block 1)
1 error, 0 warnings
[exit 1]
Correct the example document:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: TODO buy milk
examples:
- document: |
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TLMD_180
A section declares examples, but it doesn’t repeat (TLMD_180).
Message
<file>:<line>: error[TLMD_180]: `examples` holds further occurrences of a section, so the section declares `repeat: true`
Cause
The examples of a section are further occurrences of the section. The scaffold writes them after the occurrence that it derives. Only a section that repeats can occur more than once.
Fix
In the schema, declare repeat: true on the section, or remove examples.
Example
This section lists a further occurrence but doesn’t repeat:
notes.yaml
line-limit: 0
sections:
- heading: Notes
level: 1
blocks: []
- heading: { template: '${}', example: Milk }
level: 2
examples:
- |
## Bread
Buy a loaf.
blocks:
- kind: prose
example: Buy two litres.
notes.md
# Notes
## Milk
Buy two litres.
$ tramlines validate --schema notes.yaml notes.md
notes.yaml:9: error[TLMD_180]: `examples` holds further occurrences of a section, so the section declares `repeat: true`
1 error, 0 warnings
[exit 1]
Let the section repeat:
notes.yaml
line-limit: 0
sections:
- heading: Notes
level: 1
blocks: []
- heading: { template: '${}', example: Milk }
level: 2
repeat: true
examples:
- |
## Bread
Buy a loaf.
blocks:
- kind: prose
example: Buy two litres.
$ tramlines validate --schema notes.yaml notes.md
0 errors, 0 warnings
TLMD_181
A text of the examples of a section doesn’t begin with a heading of the level of the section (TLMD_181).
Message
<file>:<line>: error[TLMD_181]: item <n> of `examples` begins with `<line>`, but each occurrence begins with a heading of level <level>
Cause
The scaffold writes each text of examples as a whole occurrence of the section, so the text starts with the heading of that occurrence.
Fix
In the schema, start the text with a heading of the level of the section.
Example
The further occurrence of this section starts with its body:
notes.yaml
line-limit: 0
sections:
- heading: Notes
level: 1
blocks: []
- heading: { template: '${}', example: Milk }
level: 2
repeat: true
examples:
- Buy a loaf.
blocks:
- kind: prose
example: Buy two litres.
notes.md
# Notes
## Milk
Buy two litres.
$ tramlines validate --schema notes.yaml notes.md
notes.yaml:10: error[TLMD_181]: item 1 of `examples` begins with `Buy a loaf.`, but each occurrence begins with a heading of level 2
1 error, 0 warnings
[exit 1]
Start the text with its heading:
notes.yaml
line-limit: 0
sections:
- heading: Notes
level: 1
blocks: []
- heading: { template: '${}', example: Milk }
level: 2
repeat: true
examples:
- |
## Bread
Buy a loaf.
blocks:
- kind: prose
example: Buy two litres.
$ tramlines validate --schema notes.yaml notes.md
0 errors, 0 warnings
TLMD_182
The example of a block or a unit rule is an empty list (TLMD_182).
Message
<file>:<line>: error[TLMD_182]: `example` is an empty list: write at least one text, or leave `example` out
Cause
The scaffold writes one unit for each text of a list example. An empty list gives the scaffold nothing to write, and a failure has no example to show.
Fix
In the schema, write at least one text in the list, or remove example.
Example
The example of this rule is an empty list:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: []
tasks.md
# Tasks
- TODO buy milk
$ tramlines validate --schema tasks.yaml tasks.md
tasks.yaml:9: error[TLMD_182]: `example` is an empty list: write at least one text, or leave `example` out
1 error, 0 warnings
[exit 1]
Write the texts of the list:
tasks.yaml
line-limit: 0
sections:
- heading: Tasks
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example:
- TODO buy milk
- TODO call the plumber
$ tramlines validate --schema tasks.yaml tasks.md
0 errors, 0 warnings
TLMD_183
A code block that doesn’t repeat carries a list example of more than one text (TLMD_183).
Message
<file>:<line>: error[TLMD_183]: a list `example` on a `code` block writes one fence for each text, so the block declares `repeat: true`
Cause
The scaffold writes each text of a list example of a code block as one fence of its own. A block that doesn’t repeat holds one fence.
Fix
In the schema, declare repeat: true on the block, or give the block one text.
Example
This block lists two fences but doesn’t repeat:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks:
- kind: code
lang: sh
example:
- make bread
- make toast
recipe.md
# Recipe
```sh
make bread
```
$ tramlines validate --schema recipe.yaml recipe.md
recipe.yaml:6: error[TLMD_183]: a list `example` on a `code` block writes one fence for each text, so the block declares `repeat: true`
1 error, 0 warnings
[exit 1]
Let the block repeat:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks:
- kind: code
lang: sh
repeat: true
example:
- make bread
- make toast
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
Errors: frontmatter
These errors come from the frontmatter of a document. The frontmatter can fail to parse, or it can hold keys and values that break their constraints. Section 6 of the core specification defines these errors. A separate page lists the errors for the lines of a document and its length.
TL_53
The frontmatter of the document isn’t valid YAML (TL_53).
Message
<file>:<line>: error[TL_53]: the frontmatter does not read: <reason>
Cause
The frontmatter doesn’t parse as YAML 1.2. A common cause is a plain value that contains : , which YAML reads as the start of a map. A map key that isn’t a string, such as [a]: b, is also an error. The error reports the first line of the frontmatter, and the command skips every frontmatter rule.
Fix
Correct the YAML of the frontmatter in the document. Quote a value that contains : , and write each key as plain text.
Example
This schema requires a title:
note.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
The title of this document contains : :
note.md
---
title: Groceries: weekly
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:1: error[TL_53]: the frontmatter does not read: YAML does not parse at line 2: mapping values are not allowed in this context
1 error, 0 warnings
[exit 1]
Quote the value:
note.md
---
title: 'Groceries: weekly'
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_123
The frontmatter of the document contains a YAML anchor, an alias, or a tag (TL_123).
Message
<file>:<line>: error[TL_123]: the frontmatter does not read: <reason>
Cause
The frontmatter uses an anchor (&name), an alias (*name), or a tag (!tag). Frontmatter is plain YAML: each value is written where it applies. The error reports the first line of the frontmatter, and the command skips every frontmatter rule.
Fix
In the document, replace each alias with the value that its anchor names, written out in full, then remove each anchor and each tag.
Example
This schema requires a title and a subtitle:
note.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
subtitle:
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
The subtitle of this document repeats the title through an alias:
note.md
---
title: &name Groceries
subtitle: *name
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:1: error[TL_123]: the frontmatter does not read: YAML anchor at line 2: anchors are not read
1 error, 0 warnings
[exit 1]
Write the value out in full:
note.md
---
title: Groceries
subtitle: Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_54
A key that the schema requires is missing from the frontmatter of the document (TL_54).
Message
<file>:<line>: error[TL_54]: frontmatter key `<key>` is missing (schema <id>, key `<key>`)
Cause
The schema declares the key under frontmatter without optional: true, and the document leaves it out. A document with no frontmatter at all is missing every key that the schema requires.
Fix
Add the key to the frontmatter of the document. If documents of this kind can leave the key out, set optional: true on the key in the schema instead.
Example
This schema requires a title:
note.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
This document leaves out title:
note.md
---
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:1: error[TL_54]: frontmatter key `title` is missing (schema note, key `title`)
1 error, 0 warnings
[exit 1]
Add the key:
note.md
---
title: Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_55
The frontmatter of the document contains a key that the schema doesn’t declare (TL_55).
Message
<file>:<line>: error[TL_55]: frontmatter key `<key>` is not declared (schema <id>)
Cause
A schema accepts only the frontmatter keys that it declares under frontmatter, the key of each dimension of its lifecycle, and the keys that a host reserves. The document contains another key, and the schema doesn’t set frontmatter-open.
Fix
Remove the key from the frontmatter of the document, or correct its spelling. If documents of this kind need the key, declare it under frontmatter in the schema. To accept any key, set frontmatter-open: true in the schema.
Example
This schema declares only title:
note.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
This document also contains an author:
note.md
---
title: Groceries
author: Ada
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TL_55]: frontmatter key `author` is not declared (schema note)
1 error, 0 warnings
[exit 1]
Remove the key:
note.md
---
title: Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_56
A frontmatter value, or an element of a list, doesn’t meet the const, the shape, the enum, or the format of its key (TL_56).
Message
<file>:<line>: error[TL_56]: `<key>` must be `<const>`; it is `<value>` (schema <id>, key `<key>`)
<file>:<line>: error[TL_56]: `<key>` `<value>` <failure>; a value of this kind reads <example> (schema <id>, key `<key>`)
<file>:<line>: error[TL_56]: `<key>` must be one of <values>; `<value>` is not (schema <id>, key `<key>`)
<file>:<line>: error[TL_56]: `<key>` must be an absolute URL; `<value>` is not (schema <id>, key `<key>`)
<file>:<line>: error[TL_56]: `<key>` must be a slug; `<value>` is not (schema <id>, key `<key>`)
<file>:<line>: error[TL_56]: id `<value>` does not match the template `<template>` (schema <id>, key `id`)
Cause
The key declares a const, a template, a pattern, an enum, or a format (url or slug), and the value doesn’t meet it. For a key that declares list, the command checks each element. For an id key with a template, the command reports a message of its own. A program that uses tramlines as a library can add formats of its own. Such a program reports a value that fails one of them under this key too, in its own words.
Fix
Change the value in the frontmatter of the document so that it meets the constraint. If the constraint is wrong for documents of this kind, correct it in the schema.
Example
This schema allows two values of status:
note.yaml
line-limit: 0
frontmatter:
status:
enum: [draft, done]
example: draft
sections:
- heading: Note
level: 1
blocks: []
This document uses another:
note.md
---
status: finished
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:2: error[TL_56]: `status` must be one of `draft`, `done`; `finished` is not (schema note, key `status`)
1 error, 0 warnings
[exit 1]
Use one of the values:
note.md
---
status: done
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_106
A list in the frontmatter has fewer elements than the min of its key (TL_106).
Message
<file>:<line>: error[TL_106]: `<key>` needs at least <min> entries; it has <n> (schema <id>, key `<key>`)
Cause
The key declares list and a min, and the list in the document is shorter. An empty list, [], has no elements.
Fix
Add elements to the list in the document. If documents of this kind can hold fewer, lower min on the key in the schema.
Example
This schema requires at least one tag:
note.yaml
line-limit: 0
frontmatter:
tags:
list: true
min: 1
example: shopping
sections:
- heading: Note
level: 1
blocks: []
This document has none:
note.md
---
tags: []
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:2: error[TL_106]: `tags` needs at least 1 entry; it has 0 (schema note, key `tags`)
1 error, 0 warnings
[exit 1]
Add a tag:
note.md
---
tags: [shopping]
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_107
A list in the frontmatter has more elements than the max of its key (TL_107).
Message
<file>:<line>: error[TL_107]: `<key>` allows at most <max> entries; it has <n> (schema <id>, key `<key>`)
Cause
The key declares list and a max, and the list in the document is longer.
Fix
Remove elements from the list in the document. If documents of this kind need more, raise max on the key in the schema.
Example
This schema allows two tags:
note.yaml
line-limit: 0
frontmatter:
tags:
list: true
max: 2
example: shopping
sections:
- heading: Note
level: 1
blocks: []
This document has three:
note.md
---
tags: [shopping, weekly, food]
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:2: error[TL_107]: `tags` allows at most 2 entries; it has 3 (schema note, key `tags`)
1 error, 0 warnings
[exit 1]
Keep two of them:
note.md
---
tags: [shopping, weekly]
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_57
A frontmatter value, or an element of a list, is a list or a map where the schema expects one value, or one value where the schema expects a list (TL_57).
Message
<file>:<line>: error[TL_57]: `<key>` must be a list (schema <id>, key `<key>`)
<file>:<line>: error[TL_57]: `<key>` must be a scalar, not a list (schema <id>, key `<key>`)
<file>:<line>: error[TL_57]: `<key>` must be a scalar (schema <id>, key `<key>`)
Cause
The key declares list: true, and its value isn’t a list. Or the key doesn’t declare list, and its value is a list or a map. The last message also reports an element of a list that is itself a list or a map.
Fix
In the frontmatter of the document, write the value in the shape that the schema expects. If the key must hold a list, set list: true on it in the schema.
Example
This schema expects one title:
note.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
sections:
- heading: Note
level: 1
blocks: []
The title of this document is a list:
note.md
---
title:
- Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TL_57]: `title` must be a scalar, not a list (schema note, key `title`)
1 error, 0 warnings
[exit 1]
Write one value:
note.md
---
title: Groceries
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_62
The key of a lifecycle dimension is missing from the frontmatter (TL_62).
Message
<file>:<line>: error[TL_62]: lifecycle key `<key>` is missing (schema <id>)
Cause
The schema declares a lifecycle, and each of its dimensions declares a key that the frontmatter of every document must contain. The document leaves the key out.
Fix
Add the key to the frontmatter of the document, set to one of the states of the dimension.
Example
This schema declares a lifecycle with the key status:
note.yaml
line-limit: 0
lifecycle:
- { key: status, states: [open, closed] }
sections:
- heading: Note
level: 1
blocks: []
This document leaves out status:
note.md
# Note
$ tramlines validate --schema note.yaml note.md
note.md:1: error[TL_62]: lifecycle key `status` is missing (schema note)
1 error, 0 warnings
[exit 1]
Add the key with a state:
note.md
---
status: open
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_108
The value of the key of a lifecycle dimension isn’t one of the states of the dimension (TL_108).
Message
<file>:<line>: error[TL_108]: `<key>` must be one of <states>; `<value>` is not (schema <id>)
Cause
The schema declares a lifecycle, and the dimension lists its states. The document sets the key of the dimension to a value that isn’t in that list. The command compares the text exactly, so Open isn’t open.
Fix
In the frontmatter of the document, set the key to one of the states that the message lists. If documents need another state, add it to states in the schema.
Example
This schema declares a lifecycle with the key status:
note.yaml
line-limit: 0
lifecycle:
- { key: status, states: [open, closed] }
sections:
- heading: Note
level: 1
blocks: []
This document sets status to a value that isn’t a state:
note.md
---
status: done
---
# Note
$ tramlines validate --schema note.yaml note.md
note.md:2: error[TL_108]: `status` must be one of `open`, `closed`; `done` is not (schema note)
1 error, 0 warnings
[exit 1]
Use one of the states:
note.md
---
status: closed
---
# Note
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: lines and length
These errors come from the text of a document: a line that doesn’t match the shape of its rule, a line that a ban matches, and a document longer than its limit. Sections 3 and 8 of the core specification define these errors.
TL_21
The document contains more lines than the line-limit of its schema allows (TL_21).
Message
<file>: error[TL_21]: the document has <lines> lines; the limit is <limit>; split it (schema <id>)
Cause
The schema sets line-limit to a number greater than zero, and the document is longer. The command doesn’t count the marker lines of a scaffold, such as a guidance line or the ends of an optional region. It doesn’t count the blank line that sets each marker apart either. A line-limit of 0 sets no limit.
Fix
Split the document into shorter documents, or remove what it doesn’t need. If documents of this kind need more room, raise line-limit in the schema.
Example
This schema allows three lines:
note.yaml
line-limit: 3
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
This document contains five:
note.md
# Note
Buy milk.
Buy bread.
$ tramlines validate --schema note.yaml note.md
note.md: error[TL_21]: the document has 5 lines; the limit is 3; split it (schema note)
1 error, 0 warnings
[exit 1]
Remove a paragraph:
note.md
# Note
Buy milk and bread.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_74
A line of the document doesn’t match the shape that its rule or its format requires (TL_74).
Message
<file>:<line>: error[TL_74]: `<text>` fails at column <n>: expected <expected>, found <found>; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
<file>:<line>: error[TL_74]: `<text>` does not match `<pattern>`; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
<file>:<line>: error[TL_74]: `<text>` must read `<expected>` (schema <id>, <place of the rule>)
<file>:<line>: error[TL_74]: `<text>` does not match the `<variant>` form: it <failure>; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
<file>:<line>: error[TL_74]: `<text>` matches none of the forms <variants>[: each fails at column <n>]; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
Cause
The rule declares a template, a pattern, or a text, or binds a format, and the line doesn’t match it. For a template, the message gives the column where the line stops matching and what the template expects there. A line that lacks only the end of the template stops matching at its own end. When a hole doesn’t take the value that the line gives it, the message names the column where the value starts. For a format with variants, the message names the variant that matches the line furthest, or every such variant when several stop matching at the same column. The message ends with the example of the shape, when the rule declares one, or of each variant that it names. <unit> is what the line is: an item, a paragraph, a row, or a line of a fence. Brackets mark a part that appears only sometimes.
Fix
Rewrite the line in the document to match the example. If the rule is wrong for documents of this kind, correct its shape in the schema.
Example
This rule requires each item to start with TODO:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
template: 'TODO ${}'
example: TODO buy milk
This item doesn’t:
note.md
# Note
- buy milk
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TL_74]: `buy milk` fails at column 1: expected `TODO `, found `buy milk`; an item of this kind reads TODO buy milk (schema note, section "Note" block 1)
1 error, 0 warnings
[exit 1]
Start the item with TODO:
note.md
# Note
- TODO buy milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TL_75
A line of the document matches one of the prohibited-patterns of the schema or of its rule (TL_75).
Message
<file>:<line>: error[TL_75]: `<span>` is refused: <message> (schema <id>)
<file>:<line>: error[TL_75]: `<span>` is refused: <message>[ A <unit> of this kind reads <example>] (schema <id>, <place of the rule>)
Cause
The schema, or the rule that applies to the line, declares prohibited-patterns, and the line matches one of them. The finding quotes the part of the line that matched. It also gives the message of the ban, which states the ban’s rule. The first message reports a schema-wide pattern, which applies to every line of the body outside code. The second message reports a pattern on a rule or a format. It ends with the example of the rule when the rule has one, and the brackets in the message mark that part.
Fix
Rewrite the line in the document so that it doesn’t match the pattern. If the pattern refuses text that documents of this kind need, correct it in the schema.
Example
This schema refuses TBD anywhere in the body:
note.yaml
line-limit: 0
prohibited-patterns: [{pattern: '\bTBD\b', message: 'A note decides everything that it states.'}]
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
This document leaves a gap:
note.md
# Note
Buy TBD.
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TL_75]: `TBD` is refused: A note decides everything that it states. (schema note)
1 error, 0 warnings
[exit 1]
Fill the gap:
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: headings and sections
These errors concern the headings of a document and the sections that the schema declares for those headings. Section 4 of the Markdown specification defines these errors. Errors: sections inside sections lists the errors for a section inside a parent section. Errors: keys and repairs lists the headings that a repair rewrites.
TLMD_18
A section that the schema requires is missing from the document, or from an occurrence of its parent (TLMD_18).
Message
<file>: error[TLMD_18]: section `<heading>` is missing (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_18]: section `<heading>` is missing under `<parent heading>` (schema <id>, <place of the rule>)
Cause
The schema declares the section without optional: true, and the document contains no heading for it. For a section that declares parent, optional applies within each occurrence of the parent: the second message names the parent heading that lacks the section.
Fix
Add the missing heading to the document. If the message names a parent heading, put the missing heading under it. If documents of this kind can leave the section out, set optional: true on the section in the schema.
Example
This schema requires a Steps section:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Steps
level: 2
blocks: []
This document lacks a Steps heading:
recipe.md
# Recipe
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md: error[TLMD_18]: section `## Steps` is missing (schema recipe, section "Steps")
1 error, 0 warnings
[exit 1]
Add the heading:
recipe.md
# Recipe
## Steps
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_151
The heading of a section that doesn’t repeat occurs more than once in the document, or under one occurrence of its parent (TLMD_151).
Message
<file>:<line>: error[TLMD_151]: heading `<heading>` occurs more than once (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_151]: heading `<heading>` occurs more than once under `<parent heading>` (schema <id>, <place of the rule>)
Cause
The schema declares the section without repeat: true, and the document contains its heading more than once. For a section that declares parent, repeat applies within each occurrence of the parent: the second message names the parent heading that contains the section more than once. A repeated heading stands after the sections that follow its first occurrence. The command reports it with this error, not as out of place (TLMD_22). The body of a repeated heading can capture a key that the first body captured. The command reports that key with this error too, not as captured twice (TLMD_29).
Fix
Remove the second heading, or merge the two sections into one. If the section can occur more than once, set repeat: true on the section in the schema.
Example
In this schema, each step has one Check:
guide.yaml
line-limit: 0
sections-ordered: false
sections:
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: Check
level: 2
parent: step
blocks: []
This document contains two Check headings under Step 1:
guide.md
# Step 1
## Check
## Check
$ tramlines validate --schema guide.yaml guide.md
guide.md:5: error[TLMD_151]: heading `## Check` occurs more than once under `# Step 1` (schema guide, section "Check")
1 error, 0 warnings
[exit 1]
Remove the second heading:
guide.md
# Step 1
## Check
$ tramlines validate --schema guide.yaml guide.md
0 errors, 0 warnings
TLMD_19
A heading matches no section of the schema, and no open region allows it (TLMD_19).
Message
<file>:<line>: error[TLMD_19]: heading `<heading>` is not declared (schema <id>)
Cause
The document contains a heading that no section of the schema matches, outside the open region of any section that sets headings-open or subheadings-open.
Fix
Remove the heading from the document, or change it to a heading that the schema declares. If the heading belongs in documents of this kind, declare a section for it in the schema, or open a region for it with headings-open or subheadings-open on the section before it.
Example
This schema declares Recipe and Steps:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Steps
level: 2
blocks: []
This document adds a Tips heading:
recipe.md
# Recipe
## Steps
## Tips
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:5: error[TLMD_19]: heading `## Tips` is not declared (schema recipe)
1 error, 0 warnings
[exit 1]
Remove the heading:
recipe.md
# Recipe
## Steps
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_20
A heading is more than one level deeper than the heading before it (TLMD_20).
Message
<file>:<line>: error[TLMD_20]: heading `<heading>` is more than one level deeper than `<heading before>` (schema <id>)
Cause
The document skips a level, such as a level 3 heading directly after a level 1 heading. tramlines reports this error for every schema, whatever its sections declare. For a heading that matches a section at a level other than the declared one, the command reports TLMD_144 instead. For a subsection outside every occurrence of its parent, it reports TLMD_31.
Fix
Change the level of the heading in the document, so that it’s at most one level deeper than the heading before it.
Example
This schema lets any deeper heading follow Recipe:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
subheadings-open: true
blocks: []
This document puts a level 3 heading after the level 1 heading:
recipe.md
# Recipe
### Tips
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:3: error[TLMD_20]: heading `### Tips` is more than one level deeper than `# Recipe` (schema recipe)
1 error, 0 warnings
[exit 1]
Make it a level 2 heading:
recipe.md
# Recipe
## Tips
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_21
In a schema that orders its sections, a section is more than one level deeper than the section before it (TLMD_21).
Message
<file>:<line>: error[TLMD_21]: section `<section>` skips a level after `<section before>`
Cause
The schema lists a section more than one level deeper than the section before it, and doesn’t set sections-ordered: false. A document that follows the schema would skip a level too.
Fix
Change the level of the section in the schema, so that it’s at most one level deeper than the section before it.
Example
In this schema, Steps is level 3 after the level 1 Recipe:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Steps
level: 3
blocks: []
recipe.md
# Recipe
### Steps
$ tramlines validate --schema recipe.yaml recipe.md
recipe.yaml:6: error[TLMD_21]: section `Steps` skips a level after `Recipe`
1 error, 0 warnings
[exit 1]
Make Steps level 2 in the schema and in the document:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Steps
level: 2
blocks: []
recipe.md
# Recipe
## Steps
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_22
A section appears before another section that the schema lists earlier (TLMD_22).
Message
<file>:<line>: error[TLMD_22]: heading `<heading>` is out of place; the sections are in the order the schema lists them (schema <id>, <place of the rule>)
Cause
The schema doesn’t set sections-ordered: false, so its sections must appear in the order that it lists them. The document places them in another order. The error names only the first heading that is out of place. For a heading that occurs again where its section doesn’t declare repeat, the command reports TLMD_151 instead.
Fix
Move the sections of the document into the order in which the schema lists them. If the sections of documents of this kind can appear in any order, set sections-ordered: false in the schema.
Example
This schema lists Recipe, then Steps:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Steps
level: 2
blocks: []
This document puts Steps first:
recipe.md
## Steps
# Recipe
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:3: error[TLMD_22]: heading `# Recipe` is out of place; the sections are in the order the schema lists them (schema recipe, section "Recipe")
1 error, 0 warnings
[exit 1]
Put the sections in order:
recipe.md
# Recipe
## Steps
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_144
A heading matches a section, but its level differs from the level that the section declares (TLMD_144).
Message
<file>:<line>: error[TLMD_144]: heading `<heading>` must be level <level> (schema <id>, <place of the rule>)
Cause
The text of the heading matches a section of the schema, and the number of # signs differs from the level of that section.
Fix
Change the level of the heading in the document to the level that the message names. If the other level is correct for documents of this kind, change the level of the section in the schema.
Example
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Steps
level: 2
blocks: []
This document writes Steps at level 1:
recipe.md
# Recipe
# Steps
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:3: error[TLMD_144]: heading `# Steps` must be level 2 (schema recipe, section "Steps")
1 error, 0 warnings
[exit 1]
Make it level 2:
recipe.md
# Recipe
## Steps
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_26
A heading in an open region matches the text of a heading that the schema prohibits (TLMD_26).
Message
<file>:<line>: error[TLMD_26]: heading `<heading>` is prohibited (schema <id>)
Cause
A section sets headings-open or subheadings-open, so undeclared headings can follow it. The schema lists the text of this heading under sections-prohibited.
Fix
Remove the heading from the document, or change its text. If documents of this kind can contain the heading, remove its text from sections-prohibited in the schema.
Example
This schema lets any deeper heading follow Recipe, except Draft:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
subheadings-open: true
blocks: []
sections-prohibited:
- heading: Draft
This document contains a Draft heading:
recipe.md
# Recipe
## Draft
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:3: error[TLMD_26]: heading `## Draft` is prohibited (schema recipe)
1 error, 0 warnings
[exit 1]
Rename the heading:
recipe.md
# Recipe
## Tips
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_170
The schema prohibits a heading that one of its sections declares (TLMD_170).
Message
<file>:<line>: error[TLMD_170]: `sections-prohibited` names `<heading>`, which a section declares
Cause
A heading of sections-prohibited matches the heading of a declared section, and the two apply in the same lifecycle states. The engine matches such a heading to its section, even inside an open region, so the prohibition never applies.
Fix
If documents of this kind can contain the section, remove the heading from sections-prohibited. Otherwise, remove the section from the schema. If the two apply in different states, give each an only for its states.
Example
This schema declares a Tips section and also prohibits a Tips heading:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
subheadings-open: true
blocks: []
- heading: Tips
level: 2
optional: true
blocks: []
sections-prohibited:
- heading: Tips
recipe.md
# Recipe
$ tramlines validate --schema recipe.yaml recipe.md
recipe.yaml:12: error[TLMD_170]: `sections-prohibited` names `Tips`, which a section declares
1 error, 0 warnings
[exit 1]
Remove the prohibition:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
subheadings-open: true
blocks: []
- heading: Tips
level: 2
optional: true
blocks: []
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_29
Two sections, or two units, paired with one rule share a key (TLMD_29).
Message
<file>:<line>: error[TLMD_29]: key `<key>` is captured more than once; it was first captured at line <line> (schema <id>, <place of the rule>)
Cause
A section or a unit rule sets for-each-key-of, so the document must contain one section or unit for each key that the named rule captures. The document contains two with the same key, in one scope. A heading can repeat although its section doesn’t declare repeat. If the body of the repeated heading captures a key that the first body captured, the command reports the key with the heading, as TLMD_151.
Fix
Remove the second section from the document, or merge it into the first one.
Example
In this schema, a notes section follows for each part in the list:
bike.yaml
line-limit: 0
sections:
- heading: Parts
level: 1
blocks:
- kind: bullet-list
rule-id: parts
each:
template: "${name:slug}"
key: name
example: wheel
- heading: { template: "${name:slug} notes", key: name, example: wheel notes }
level: 2
repeat: true
for-each-key-of: parts
blocks: []
This document contains two sections for wheel:
bike.md
# Parts
- wheel
- brake
## <a id="wheel"></a>wheel notes
## <a id="brake"></a>brake notes
## wheel notes
$ tramlines validate --schema bike.yaml bike.md
bike.md:10: error[TLMD_29]: key `wheel` is captured more than once; it was first captured at line 6 (schema bike, section "${name:slug} notes")
1 error, 0 warnings
[exit 1]
Remove the second section:
bike.md
# Parts
- wheel
- brake
## <a id="wheel"></a>wheel notes
## <a id="brake"></a>brake notes
$ tramlines validate --schema bike.yaml bike.md
0 errors, 0 warnings
TLMD_38
The document contains content before its first heading, and the schema declares no preamble (TLMD_38).
Message
<file>:<line>: error[TLMD_38]: content before the first heading, with no preamble declared (schema <id>)
Cause
The body of the document begins with a block, such as a paragraph, before its first heading. Without a preamble, the schema declares no rule for that block.
Fix
Remove the content from the document, or move it under a heading. If documents of this kind begin with content, declare a preamble with its blocks in the schema.
Example
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
This document begins with a paragraph:
recipe.md
Serves two.
# Recipe
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:1: error[TLMD_38]: content before the first heading, with no preamble declared (schema recipe)
1 error, 0 warnings
[exit 1]
Remove the paragraph:
recipe.md
# Recipe
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
Errors: sections inside sections
These errors come from a section that declares parent, and from its place in the schema and in the document. Section 4 of the Markdown specification defines them.
TLMD_31
A heading of a section that declares parent doesn’t lie inside a heading of its parent (TLMD_31).
Message
<file>:<line>: error[TLMD_31]: heading `<heading>` lies outside every `<parent heading>` section; it belongs inside one (schema <id>, <place of the rule>)
Cause
The section names its parent with parent, and the nearest preceding heading at the parent’s level or a smaller level isn’t a heading of the parent.
Fix
Move the heading in the document under a heading of its parent, or add a heading of the parent before it.
Example
In this schema, Lessons lies inside Outcome:
bug.yaml
line-limit: 0
sections:
- heading: Bug
level: 1
blocks: []
- heading: Outcome
level: 1
optional: true
rule-id: outcome
blocks: []
- heading: Lessons
level: 2
parent: outcome
optional: true
blocks: []
This document places Lessons under Bug:
bug.md
# Bug
## Lessons
$ tramlines validate --schema bug.yaml bug.md
bug.md:3: error[TLMD_31]: heading `## Lessons` lies outside every `# Outcome` section; it belongs inside one (schema bug, section "Lessons")
1 error, 0 warnings
[exit 1]
Add the Outcome heading before Lessons:
bug.md
# Bug
# Outcome
## Lessons
$ tramlines validate --schema bug.yaml bug.md
0 errors, 0 warnings
TLMD_166
A heading matches a section whose parent doesn’t apply in the lifecycle state of the document (TLMD_166).
Message
<file>:<line>: error[TLMD_166]: heading `<heading>` lies inside `<rule-id>`, which does not apply in this document's states (schema <id>, <place of the rule>)
Cause
A section declares parent, and the parent section sets only to states that exclude the document’s state. The parent section doesn’t apply, so the document can’t contain a heading for the child section inside the parent.
Fix
Remove the heading from the document, or move the document to a state in which the parent applies and add the parent’s heading. If the section belongs in every state, change the only of its parent in the schema.
Example
In this schema, Outcome applies only to a closed bug, and Lessons lies inside it:
bug.yaml
line-limit: 0
lifecycle:
- { key: state, states: [open, closed] }
sections:
- heading: Bug
level: 1
blocks: []
- heading: Outcome
level: 1
only: { state: closed }
rule-id: outcome
blocks: []
- heading: Lessons
level: 2
parent: outcome
optional: true
blocks: []
This bug is open and contains a Lessons heading:
bug.md
---
state: open
---
# Bug
## Lessons
$ tramlines validate --schema bug.yaml bug.md
bug.md:7: error[TLMD_166]: heading `## Lessons` lies inside `outcome`, which does not apply in this document's states (schema bug, section "Lessons")
1 error, 0 warnings
[exit 1]
Close the bug and add its outcome:
bug.md
---
state: closed
---
# Bug
# Outcome
## Lessons
$ tramlines validate --schema bug.yaml bug.md
0 errors, 0 warnings
TLMD_34
The parent of a section names no section of the schema (TLMD_34).
Message
<file>:<line>: error[TLMD_34]: `parent` names no rule `<rule-id>`
<file>:<line>: error[TLMD_34]: `parent` names `<rule-id>`, which is not a section
Cause
The parent of a section in the schema names a rule-id that no rule declares, or the rule-id of a block instead of a section.
Fix
Set parent to the rule-id of the section that contains this one. If that section has no rule-id, give it one.
Example
In this schema, parent names steps, but the rule-id of the section is step:
guide.yaml
line-limit: 0
sections:
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: Check
level: 2
parent: steps
blocks: []
guide.md
# Step 1
## Check
$ tramlines validate --schema guide.yaml guide.md
guide.yaml:8: error[TLMD_34]: `parent` names no rule `steps`
1 error, 0 warnings
[exit 1]
Name the rule-id of the parent:
guide.yaml
line-limit: 0
sections:
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: Check
level: 2
parent: step
blocks: []
$ tramlines validate --schema guide.yaml guide.md
0 errors, 0 warnings
TLMD_156
The parent of a section names a section whose level isn’t smaller than its own (TLMD_156).
Message
<file>:<line>: error[TLMD_156]: `parent` `<rule-id>` is level <parent level>; a section lies inside one of a smaller level, and this one is level <level>
Cause
A section lies inside its parent only when the parent’s heading has a smaller level, as ## lies inside #. The schema gives the section a level equal to or smaller than the level of the section that its parent names.
Fix
Give the section a level greater than the level of its parent, or set parent to a section of a smaller level.
Example
In this schema, Check and its parent Step are both level 1:
guide.yaml
line-limit: 0
sections:
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: Check
level: 1
parent: step
blocks: []
guide.md
# Step 1
## Check
$ tramlines validate --schema guide.yaml guide.md
guide.yaml:8: error[TLMD_156]: `parent` `step` is level 1; a section lies inside one of a smaller level, and this one is level 1
1 error, 0 warnings
[exit 1]
Make Check level 2:
guide.yaml
line-limit: 0
sections:
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: Check
level: 2
parent: step
blocks: []
$ tramlines validate --schema guide.yaml guide.md
0 errors, 0 warnings
TLMD_157
The schema doesn’t list a section directly after its parent, with only the parent’s other subsections between them (TLMD_157).
Message
<file>:<line>: error[TLMD_157]: section `<section>` is not listed after its `parent` `<rule-id>` with only its subsections between
Cause
The schema lists the sections in document order, so a section inside a parent comes after the parent. The schema lists the section before its parent, or lists another section at the parent’s level or a smaller level between them.
Fix
Move the section in the schema so that it follows its parent, with only the parent’s other subsections between them.
Example
This schema lists Check before its parent:
guide.yaml
line-limit: 0
sections:
- heading: Check
level: 2
parent: step
blocks: []
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
guide.md
# Step 1
## Check
$ tramlines validate --schema guide.yaml guide.md
guide.yaml:3: error[TLMD_157]: section `Check` is not listed after its `parent` `step` with only its subsections between
1 error, 0 warnings
[exit 1]
List Check after Step:
guide.yaml
line-limit: 0
sections:
- heading: { template: "Step ${:int}", example: Step 1 }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: Check
level: 2
parent: step
blocks: []
$ tramlines validate --schema guide.yaml guide.md
0 errors, 0 warnings
TLMD_35
The heading of a section that sets number-from-parent doesn’t extend the number of its parent’s heading (TLMD_35).
Message
<file>:<line>: error[TLMD_35]: heading `<heading>` must be numbered `<number>.<n>`, under `<parent heading>` (schema <id>, <place of the rule>)
Cause
The section sets number-from-parent: true, so its heading must begin with the number of its parent’s heading, a dot, and a number. The heading in the document begins with another number, or with none.
Fix
Number the heading in the document as the message shows: the number of the parent heading, a dot, and a number.
Example
In this schema, each Check extends the number of its step:
guide.yaml
line-limit: 0
sections:
- heading: { template: "${:int} Step", example: 1 Step }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: { template: "${:int}.${:int} Check", example: 1.1 Check }
level: 2
parent: step
number-from-parent: true
blocks: []
This document numbers the second Check from the first step:
guide.md
# 1 Step
## 1.1 Check
# 2 Step
## 1.1 Check
$ tramlines validate --schema guide.yaml guide.md
guide.md:7: error[TLMD_35]: heading `## 1.1 Check` must be numbered `2.<n>`, under `# 2 Step` (schema guide, section "${:int}.${:int} Check")
1 error, 0 warnings
[exit 1]
Number the second Check from its own step:
guide.md
# 1 Step
## 1.1 Check
# 2 Step
## 2.1 Check
$ tramlines validate --schema guide.yaml guide.md
0 errors, 0 warnings
TLMD_36
The heading of the parent of a section that sets number-from-parent has no number (TLMD_36).
Message
<file>:<line>: error[TLMD_36]: heading `<parent heading>` carries no number for `<heading>` to extend (schema <id>, <place of the rule>)
Cause
The section sets number-from-parent: true, and the heading of its parent in the document doesn’t begin with a number. The error reports the line of the parent heading.
Fix
Begin the parent heading in the document with a number, such as 1 or 2.3. If the parent heading carries no number in documents of this kind, remove number-from-parent from the section in the schema.
Example
In this schema, the heading of a step can begin with any text:
guide.yaml
line-limit: 0
sections:
- heading: { template: "${} Step", example: 1 Step }
level: 1
repeat: true
rule-id: step
blocks: []
- heading: { template: "${:int}.${:int} Check", example: 1.1 Check }
level: 2
parent: step
number-from-parent: true
blocks: []
This document begins the heading of its step with a word instead of a number:
guide.md
# First Step
## 1.1 Check
$ tramlines validate --schema guide.yaml guide.md
guide.md:1: error[TLMD_36]: heading `# First Step` carries no number for `## 1.1 Check` to extend (schema guide, section "${:int}.${:int} Check")
1 error, 0 warnings
[exit 1]
Number the step:
guide.md
# 1 Step
## 1.1 Check
$ tramlines validate --schema guide.yaml guide.md
0 errors, 0 warnings
Errors: blocks
These errors come from the blocks of a section: which blocks the section contains, their order, and the number of units in each block. Sections 2 and 5 of the markdown specification define them. Those sections cover documents and blocks.
TLMD_162
An item of a list contains a block beyond its text, such as a nested list, a code block, or a second paragraph, and the rule of the item declares no blocks (TLMD_162).
Message
<file>:<line>: error[TLMD_162]: the item carries content beyond its text (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_162]: the item's text reads as a <kind> block (schema <id>, <place of the rule>)
Cause
The rule of the item, under each, sequence, or rules, declares no blocks, so the item can contain only its own text. The item in the document contains a block after its first paragraph, such as a nested list, a code block, a blockquote, a heading, or a further paragraph. The error reports the line of the document where that block starts. The second message reports an item whose text Markdown reads as a block. For example, - 1. starts a numbered list, and - > note starts a blockquote. The command then checks no text of the item against its rule. No item can meet a rule whose shape asks for such text, and the check of the schema’s scaffold reports that rule.
Fix
Remove the nested content from the item in the document. If items of this kind contain nested content, declare that content under blocks on the rule of the item in the schema.
Example
This schema gives each item a rule with no blocks, and the document nests a list inside an item:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
each: { template: '${}.', example: Open the lid. }
note.md
# Steps
- Open the lid.
- Use both hands.
$ tramlines validate --schema note.yaml note.md
note.md:4: error[TLMD_162]: the item carries content beyond its text (schema note, section "Steps" block 1)
1 error, 0 warnings
[exit 1]
Remove the nested list:
note.md
# Steps
- Open the lid.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_39
The body of a section contains a block that none of the blocks of the section claims (TLMD_39).
Message
<file>:<line>: error[TLMD_39]: unexpected <kind> block (schema <id>, <place of the rule>)
Cause
The engine checks the body of a section against exactly the blocks that the section declares under blocks, in order unless the section sets blocks-ordered: false. The block in the document comes after the blocks that the schema declares, or no declared block takes its kind. A stray block can also come before a declared block, when no later block of the section declares its kind. The engine reports the stray block once, then checks the following declared block as usual.
Fix
Remove the extra block from the document. If the section can contain the block, declare it under blocks in the schema, at its place among the other blocks. If the section can also leave the block out, set optional: true on it.
Example
This schema declares one prose block, and the document contains a code block after the prose:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- { kind: prose, example: Buy milk. }
note.md
# Note
Buy milk.
```text
milk
```
$ tramlines validate --schema note.yaml note.md
note.md:5: error[TLMD_39]: unexpected code block (schema note, section "Note")
1 error, 0 warnings
[exit 1]
Remove the code block:
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_152
The body of a section lacks a block that the schema requires (TLMD_152).
Message
<file>:<line>: error[TLMD_152]: expected a <kind> block (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_152]: a <kind> block is missing (schema <id>, <place of the rule>)
Cause
The section declares a block that doesn’t set optional: true, and no block of the document is left to fill it. Either the body contains no block of that kind, or the blocks before it took them all. The first message reports that the body ends before the block. The second reports the same fault in a section that sets blocks-ordered: false. Where a block of another kind stands in its place, the engine reports TLMD_69 instead.
Fix
Add the missing block to the document. If the section can leave the block out, set optional: true on the block in the schema.
Example
This schema declares a paragraph and then a list, and the document contains only the paragraph:
note.yaml
line-limit: 0
sections:
- heading: Groceries
level: 1
blocks:
- { kind: prose, example: For the week. }
- { kind: bullet-list, example: Milk }
note.md
# Groceries
For the week.
$ tramlines validate --schema note.yaml note.md
note.md:1: error[TLMD_152]: expected a bullet-list block (schema note, section "Groceries" block 2)
1 error, 0 warnings
[exit 1]
Add the list:
note.md
# Groceries
For the week.
- Milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_146
A block contains fewer units than its min (TLMD_146).
Message
<file>:<line>: error[TLMD_146]: the <kind> block needs at least <min> <units>; it has <count> (schema <id>, <place of the rule>)
Cause
The block sets min, the fewest units that it holds. A unit is a paragraph, an item, a row, an image, a definition, or a repetition, depending on the kind of block. The block in the document contains fewer units. A block with no min needs at least one unit.
Fix
Add units to the block in the document. If the block can hold fewer units, lower min in the schema.
Example
This schema requires at least two items, and the document lists one:
note.yaml
line-limit: 0
sections:
- heading: Groceries
level: 1
blocks:
- { kind: bullet-list, min: 2, example: Milk }
note.md
# Groceries
- Milk
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_146]: the bullet-list block needs at least 2 items; it has 1 (schema note, section "Groceries" block 1)
1 error, 0 warnings
[exit 1]
Add an item:
note.md
# Groceries
- Milk
- Eggs
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_160
A block contains more units than its max (TLMD_160).
Message
<file>:<line>: error[TLMD_160]: the <kind> block allows at most <max> <units>; it has <count> (schema <id>, <place of the rule>)
Cause
The block sets max, the most units that it holds, and the block in the document contains more. A unit is a paragraph, an item, a row, an image, a definition, or a repetition, depending on the kind of block.
Fix
Remove units from the block in the document. If the block can hold more units, raise max in the schema, or remove it.
Example
This schema allows at most two items, and the document lists three:
note.yaml
line-limit: 0
sections:
- heading: Groceries
level: 1
blocks:
- { kind: bullet-list, max: 2, example: Milk }
note.md
# Groceries
- Milk
- Eggs
- Bread
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_160]: the bullet-list block allows at most 2 items; it has 3 (schema note, section "Groceries" block 1)
1 error, 0 warnings
[exit 1]
Remove an item:
note.md
# Groceries
- Milk
- Eggs
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_147
A block that declares sequence ends before a position of the sequence (TLMD_147).
Message
<file>:<line>: error[TLMD_147]: position <position> of the sequence is missing (schema <id>, <place of the rule>)
Cause
The sequence of a block gives one rule for each position, in order. A position that isn’t optional takes the next unit and reports what the unit lacks. When the unit has the shape of a later position, the engine treats this position as missing, and the error names it. The block in the document can also end before the position.
Fix
Add the missing unit to the document. If the block can leave the position out, set optional: true on that position in the schema.
Example
This schema declares a sequence of two items, and the document contains only the first:
note.yaml
line-limit: 0
sections:
- heading: Contact
level: 1
blocks:
- kind: bullet-list
sequence:
- { template: 'Name: ${}', example: 'Name: Ada' }
- { template: 'Email: ${}', example: 'Email: ada@example.com' }
note.md
# Contact
- Name: Ada
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_147]: position 2 of the sequence is missing (schema note, section "Contact" block 1)
1 error, 0 warnings
[exit 1]
Add the second item:
note.md
# Contact
- Name: Ada
- Email: ada@example.com
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_161
A block that declares sequence contains a unit after the last position of the sequence (TLMD_161).
Message
<file>:<line>: error[TLMD_161]: `<unit>` is beyond the sequence (schema <id>, <place of the rule>)
Cause
The sequence of a block gives one rule for each position, in order, and each position takes one unit unless it sets repeat. The block in the document contains more units than the positions take. The engine reports each unit beyond the last position.
Fix
Remove the extra unit from the document. If the last position can take more than one unit, set repeat: true on it in the schema.
Example
This schema declares a sequence of two items, and the document contains three:
note.yaml
line-limit: 0
sections:
- heading: Contact
level: 1
blocks:
- kind: bullet-list
sequence:
- { template: 'Name: ${}', example: 'Name: Ada' }
- { template: 'Email: ${}', example: 'Email: ada@example.com' }
note.md
# Contact
- Name: Ada
- Email: ada@example.com
- Phone: 555 0100
$ tramlines validate --schema note.yaml note.md
note.md:5: error[TLMD_161]: `Phone: 555 0100` is beyond the sequence (schema note, section "Contact" block 1)
1 error, 0 warnings
[exit 1]
Remove the third item:
note.md
# Contact
- Name: Ada
- Email: ada@example.com
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_50
Paragraphs or items remain after the last block of their kind that takes part of the run (TLMD_50).
Message
<file>:<line>: error[TLMD_50]: `<unit>` remains after the last <kind> position that takes the run (schema <id>, <place of the rule>)
Cause
The section, or a group, lists two or more blocks of one kind in order: prose, bullet list, or numbered list. The engine divides one run of that kind among those blocks. Each block takes what its min and max allow. A section that sets blocks-ordered: false divides a run only inside a group. The run in the document holds more units than those blocks take together. The error reports the first unit left over.
Fix
Remove the units left over from the document. If the section can hold more, raise the max of the last block of that kind in the schema.
Example
This schema declares two paragraphs, and the document contains three:
note.yaml
line-limit: 0
sections:
- heading: Pancakes
level: 1
blocks:
- { kind: prose, max: 1, example: Thin pancakes for four people. }
- { kind: prose, max: 1, example: Mix the batter and fry it. }
note.md
# Pancakes
Thin pancakes for four people.
Mix the batter and fry it.
Serve warm.
$ tramlines validate --schema note.yaml note.md
note.md:7: error[TLMD_50]: `Serve warm.` remains after the last prose position that takes the run (schema note, section "Pancakes")
1 error, 0 warnings
[exit 1]
Remove the third paragraph:
note.md
# Pancakes
Thin pancakes for four people.
Mix the batter and fry it.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_169
A block in the document stands after a block that the schema declares later (TLMD_169).
Message
<file>:<line>: error[TLMD_169]: the <kind> block is out of place; the blocks are in the order the schema lists them (schema <id>, <place of the rule>)
Cause
The section lists its blocks in order and doesn’t set blocks-ordered: false. A required block of the section comes after a block that the section lists later. No later block of the section has its kind. The engine reports the block once, at its own line, and checks it against the rule that declares it.
Fix
Move the block in the document to its place in the order of the schema. If the blocks of the section can come in any order, set blocks-ordered: false on the section in the schema.
Example
This schema declares a paragraph, then a code block:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- { kind: prose, example: The port to serve on. }
- { kind: code, lang: yaml, example: 'port: 80' }
The document puts the code block first:
note.md
# Config
```yaml
port: 80
```
The port to serve on.
$ tramlines validate --schema note.yaml note.md
note.md:7: error[TLMD_169]: the prose block is out of place; the blocks are in the order the schema lists them (schema note, section "Config" block 1)
1 error, 0 warnings
[exit 1]
Put the paragraph first:
note.md
# Config
The port to serve on.
```yaml
port: 80
```
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_69
The schema expects a block of one kind where the document has a block of another kind, and no block of the expected kind follows (TLMD_69).
Message
<file>:<line>: error[TLMD_69]: expected a <kind> block; found a <kind> block (schema <id>, <place of the rule>)
Cause
The section declares a block of one kind. Where that block belongs, the document contains a block of another kind that no later rule declares, and no block of the declared kind comes after it. The engine names both kinds in one error and doesn’t report the found block again.
Fix
Replace the block in the document with a block of the expected kind. If the other kind belongs in the section, change kind in the schema.
Example
This schema declares one code block, and the document contains a paragraph in its place:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- { kind: code, lang: yaml, example: 'port: 80' }
note.md
# Config
The port to serve on.
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_69]: expected a code block; found a prose block (schema note, section "Config" block 1)
1 error, 0 warnings
[exit 1]
Replace the paragraph with a code block:
note.md
# Config
```yaml
port: 80
```
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: kinds of block
These errors come from what a block of each kind must declare in the schema and contain in the document. Section 5 of the markdown specification, on blocks, defines them.
TLMD_42
A block of the schema declares a key that doesn’t apply to its kind, or contains a unit rule with a key out of place for that rule (TLMD_42).
Message
<file>:<line>: error[TLMD_42]: `<key>` does not apply to a `<kind>` block; it needs kind <required kind>
<file>:<line>: error[TLMD_42]: `text` applies to a paragraph, an item or a line, not a `<kind>` block
<file>:<line>: error[TLMD_42]: `blocks` applies to an item, not to a line
Cause
The error is on the schema. Some keys of a block apply to one kind only: lang and fence to code, columns and columns-open to table, and source-pattern to image. The error also occurs when a unit rule with text sits under a block other than prose, a blockquote, or a list. It occurs too when a rule under lines declares blocks.
Fix
In the schema, remove the key that doesn’t apply, or change the kind of the block to a kind that accepts the key.
Example
This schema declares lang on a prose block, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- { kind: prose, lang: text, example: Buy milk. }
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_42]: `lang` does not apply to a `prose` block; it needs kind code
1 error, 0 warnings
[exit 1]
Remove lang:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- { kind: prose, example: Buy milk. }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_153
A block of the schema lacks a key that its kind requires (TLMD_153).
Message
<file>:<line>: error[TLMD_153]: a `code` block requires `lang`
<file>:<line>: error[TLMD_153]: a `table` block requires `columns`
<file>:<line>: error[TLMD_153]: a `group` block requires `blocks`
Cause
The error is on the schema. The engine can’t check a block of some kinds without one key. A code block needs lang, the tags that its fence can carry. A table block needs columns, its header row. A group block needs blocks, the blocks of one repetition. The block declares the kind and leaves the key out.
Fix
Add the key that the message names to the block in the schema.
Example
This schema declares a code block with no lang:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- { kind: code, example: 'port: 80' }
note.md
# Config
```yaml
port: 80
```
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_153]: a `code` block requires `lang`
1 error, 0 warnings
[exit 1]
Add lang:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- { kind: code, lang: yaml, example: 'port: 80' }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_43
A code block lacks a language tag, or carries a tag that its block doesn’t list in lang (TLMD_43).
Message
<file>:<line>: error[TLMD_43]: the code block has no tag (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_43]: the code block is tagged `<tag>`; expected `<tag>` or `<tag>` (schema <id>, <place of the rule>)
Cause
The block of kind code lists its tags under lang. The opening fence of the code block in the document carries no tag, or a tag that isn’t in the list. A lang of any accepts every tag, but the fence still needs one.
Fix
In the document, tag the fence with one of the tags in lang. If the block can contain that language, add the tag to lang in the schema.
Example
This schema accepts a yaml code block, and the document tags the block json:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- { kind: code, lang: yaml, example: 'port: 80' }
note.md
# Config
```json
{"port": 80}
```
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_43]: the code block is tagged `json`; expected `yaml` (schema note, section "Config" block 1)
1 error, 0 warnings
[exit 1]
Write the block in YAML and tag it yaml:
note.md
# Config
```yaml
port: 80
```
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_145
A code block opens with fewer backticks than the fence of its block requires (TLMD_145).
Message
<file>:<line>: error[TLMD_145]: the fence has <count> backticks; at least <fence> are needed (schema <id>, <place of the rule>)
Cause
The block of kind code sets fence, the minimum number of backticks in its fences. The fence of the code block in the document is shorter. A longer fence lets the block contain lines of three backticks, such as a Markdown example with its own code block.
Fix
Open and close the code block in the document with at least as many backticks as fence sets. If a shorter fence is enough, lower fence in the schema, or remove it to use the default of three.
Example
This schema expects Markdown in a fence of four backticks, and the document uses three:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- { kind: code, lang: markdown, fence: 4, example: '# Title' }
note.md
# Config
```markdown
# Title
```
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_145]: the fence has 3 backticks; at least 4 are needed (schema note, section "Config" block 1)
1 error, 0 warnings
[exit 1]
Use four backticks:
note.md
# Config
````markdown
# Title
````
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_44
The header row of a table differs from the columns that its block declares (TLMD_44).
Message
<file>:<line>: error[TLMD_44]: the table header is `<column> | <column>`; expected `<column> | <column>` (schema <id>, <place of the rule>)
Cause
The block of kind table declares its header under columns. The header row of the table in the document differs in its columns, their names, or their order. With columns-open: true, the header can contain more columns after the declared ones, but it must start with them.
Fix
Change the header row of the table in the document to the declared columns. If the table needs other columns, change columns in the schema, or set columns-open: true to accept more columns at the end.
Example
This schema declares the columns Item and Price, and the document names the second column Cost:
note.yaml
line-limit: 0
sections:
- heading: Prices
level: 1
blocks:
- { kind: table, columns: [Item, Price], example: Milk | 1.20 }
note.md
# Prices
| Item | Cost |
| ---- | ---- |
| Milk | 1.20 |
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_44]: the table header is `Item | Cost`; expected `Item | Price` (schema note, section "Prices" block 1)
1 error, 0 warnings
[exit 1]
Rename the column:
note.md
# Prices
| Item | Price |
| ---- | ----- |
| Milk | 1.20 |
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_45
A block contains HTML, and its rule doesn’t accept HTML (TLMD_45).
Message
<file>:<line>: error[TLMD_45]: inline HTML is not accepted here (schema <id>, <place of the rule>)
Cause
A unit of the block contains an HTML tag, and the block doesn’t set inline-html: true. The check doesn’t count HTML comments or the anchors of keys as HTML. The error points at the first line of the block with HTML.
Fix
Replace the HTML in the document with Markdown, or remove it. If blocks of this kind can contain HTML, set inline-html: true on the block in the schema.
Example
This schema declares a prose block, and the document makes a word bold with HTML:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- { kind: prose, example: Buy milk. }
note.md
# Note
Buy <b>milk</b>.
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_45]: inline HTML is not accepted here (schema note, section "Note" block 1)
1 error, 0 warnings
[exit 1]
Use Markdown for the bold word:
note.md
# Note
Buy **milk**.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_163
The path of an image doesn’t match the source-pattern of its block (TLMD_163).
Message
<file>:<line>: error[TLMD_163]: the image `<path>` does not match `<pattern>`; a target of this kind reads <example> (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_163]: the image `<reference>` names a document; this rule takes what matches `<pattern>`, a file or an asset by its path; a target of this kind reads <example> (schema <id>, <place of the rule>)
Cause
The block of kind image declares source-pattern, a regular expression, and the path of an image in the document doesn’t match it. The engine matches the path as written. Under a host, the engine matches a path in the host’s scheme, and the target of a region, by what the host resolves each one to (TLMD_46). When the host reports that an image or the target of a region names a document, the engine reports it whatever the pattern, because a pattern describes a path and a document has none (TLMD_200). The second message is for that case. tramlines validate runs without a host, so it reports only the first message, for an image.
Fix
Change the path of the image in the document to one that the pattern accepts. If the block can contain images from other places, widen source-pattern in the schema. If the image or the region names a document, point it at a file or an asset instead.
Example
This schema accepts SVG images from the images folder, and the document shows a PNG file from another folder:
note.yaml
line-limit: 0
sections:
- heading: Diagram
level: 1
blocks:
- kind: image
source-pattern: '^images/.+\.svg$'
example: ''
note.md
# Diagram

$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_163]: the image `flow.png` does not match `^images/.+\.svg$`; a target of this kind reads  (schema note, section "Diagram" block 1)
1 error, 0 warnings
[exit 1]
Point the image at an SVG file in images:
note.md
# Diagram

$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_47
A block of the schema declares rules beside each or sequence (TLMD_47).
Message
<file>:<line>: error[TLMD_47]: `rules` beside `each` or `sequence`: a block takes one of the three
Cause
The error is on the schema. A block applies rules to its units in one of three ways: each applies one rule to every unit, sequence applies one rule to each position, and rules applies a list of rules as a grammar. The block declares rules and one of the others.
Fix
In the schema, keep one of the three keys on the block. To keep the rule of each beside other rules, move it into rules.
Example
This schema declares each and rules on one block, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
each: { template: 'STEP ${}', example: STEP Open the lid. }
rules:
- { template: 'NOTE ${}', min: 0, example: NOTE Use both hands. }
note.md
# Steps
- STEP Open the lid.
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_47]: `rules` beside `each` or `sequence`: a block takes one of the three
1 error, 0 warnings
[exit 1]
Move the rule of each into rules:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: 'STEP ${}', example: STEP Open the lid. }
- { template: 'NOTE ${}', min: 0, example: NOTE Use both hands. }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_172
A blockquote contains a heading (TLMD_172).
Message
<file>:<line>: error[TLMD_172]: heading `<heading>` is inside a blockquote; a heading of the document stands outside every quote (schema <id>, <place of the rule>)
Cause
A blockquote reads only its paragraphs. A heading of the document stands outside every quote, so a heading inside a quote starts no section. A heading inside a quote or a list within the quote counts too. A quote that holds only a heading reports this error and no missing paragraph. A heading inside a list item is a block of the item. TLMD_162 reports such a heading where the rule of the item declares no blocks.
Fix
Move the heading out of the quote, or write it as a paragraph of the quote.
Example
This schema declares a quote and then a list:
note.yaml
line-limit: 0
sections:
- heading: Notes
level: 1
blocks:
- kind: blockquote
example: Keep the lid closed.
- kind: bullet-list
example: Open the lid.
This document puts a heading inside the quote:
note.md
# Notes
> Keep the lid closed.
>
> ## Why
- Open the lid.
$ tramlines validate --schema note.yaml note.md
note.md:5: error[TLMD_172]: heading `## Why` is inside a blockquote; a heading of the document stands outside every quote (schema note, section "Notes" block 1)
1 error, 0 warnings
[exit 1]
Write the heading as a paragraph of the quote:
note.md
# Notes
> Keep the lid closed.
>
> Why: the steam escapes.
- Open the lid.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: units
These errors come from the rules that a block applies to its units. Sections 5 and 6 of the markdown specification define these errors: section 5 covers blocks, and section 6 covers units and lines.
TLMD_53
A unit of a block matches none of the rules of the block (TLMD_53).
Message
<file>:<line>: error[TLMD_53]: `<unit>` matches no rule of the block; declare it or set `units-open` (schema <id>, <place of the rule>)
Cause
The block declares rules, and these rules work as a grammar: every unit must go to a rule. No rule takes this unit, and the unit doesn’t start to match any rule with a shape. A rule with only select-pattern and a count takes no unit. The engine therefore also reports a unit that only such a rule selects. When a unit partly matches a rule, the engine reports TLMD_57 instead.
Fix
Change the unit in the document so that a rule takes it, or remove it. If the block can contain units of this kind, add a rule for them in the schema. To accept any unit that no rule takes, set units-open: true on the block instead.
Example
This schema accepts items that start with STEP, and the document contains an item that doesn’t:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: 'STEP ${}', example: STEP Open the lid. }
note.md
# Steps
- STEP Open the lid.
- Close it.
$ tramlines validate --schema note.yaml note.md
note.md:4: error[TLMD_53]: `Close it.` matches no rule of the block; declare it or set `units-open` (schema note, section "Steps" block 1)
1 error, 0 warnings
[exit 1]
Start the item with STEP:
note.md
# Steps
- STEP Open the lid.
- STEP Close it.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_57
A unit or a line matches part of a rule and then stops, and no rule takes it (TLMD_57).
Message
<file>:<line>: error[TLMD_57]: `<unit>` matches no rule of the block; the nearest <failure>; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_57]: `<unit>` matches no rule of the fence; the nearest <failure>; a <unit> of this kind reads <example> (schema <id>, <place of the rule>)
Cause
The block declares rules, or a rule of a code block declares lines, and no rule takes the unit or the line. The engine finds the rule whose shape the text matches furthest. It reports where the text stops matching that rule, and it gives the example of the rule. The message names the block for a unit and the fence for a line of a code block.
Fix
Change the text in the document so that it matches the rule, as the example shows. Start at the column that the message names. If the text is right, add a rule to the schema that takes it, or set units-open or lines-open.
Example
This schema accepts lines of the form name=value in a code block, and the document contains a line with a colon:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- kind: code
lang: text
each:
lines:
- { template: '${:word}=${}', example: port=80 }
note.md
# Config
```text
port=80
host: example.com
```
$ tramlines validate --schema note.yaml note.md
note.md:5: error[TLMD_57]: `host: example.com` matches no rule of the fence; the nearest fails at column 5: expected `=`, found `: example.com`; a line of this kind reads port=80 (schema note, section "Config" block 1)
1 error, 0 warnings
[exit 1]
Write the line in the form of the example:
note.md
# Config
```text
port=80
host=example.com
```
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_58
A rule under rules or lines can take nothing, because an earlier rule takes its example (TLMD_58).
Message
<file>:<line>: error[TLMD_58]: the example `<example>` is taken by the rule at line <n> first, so this rule takes nothing: order the rules from the narrowest (schema <id>, <place of the rule>)
Cause
The error is on the schema. Under rules or lines, a rule with a shape and no select-pattern takes the units that its shape matches, and each unit goes to the first such rule. An earlier rule of this kind matches the example of a later one, so the later rule never takes a unit.
Fix
In the schema, put the narrower rule first, or narrow the shape of the earlier rule so that it doesn’t match the example of the later one.
Example
In this schema, a rule that takes any item comes before a rule for notes, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: '${}', example: Open the lid. }
- { template: 'NOTE: ${}', min: 0, example: 'NOTE: Use both hands.' }
note.md
# Steps
- Open the lid.
$ tramlines validate --schema note.yaml note.md
note.yaml:9: error[TLMD_58]: the example `NOTE: Use both hands.` is taken by the rule at line 8 first, so this rule takes nothing: order the rules from the narrowest (schema note, section "Steps" block 1)
1 error, 0 warnings
[exit 1]
Put the rule for notes first:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: 'NOTE: ${}', min: 0, example: 'NOTE: Use both hands.' }
- { template: '${}', example: Open the lid. }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_91
A unit rule declares select-pattern, min, or max where these keys don’t apply (TLMD_91).
Message
<file>:<line>: error[TLMD_91]: `select-pattern` applies to a rule under `rules` or `lines`, which chooses what it governs
<file>:<line>: error[TLMD_91]: `min` applies to a rule under `rules` or `lines`; elsewhere the block bounds its units
<file>:<line>: error[TLMD_91]: `max` applies to a rule under `rules` or `lines`; elsewhere the block bounds its units
Cause
The error is on the schema. A rule under rules or lines selects or counts units, so it accepts a guard, min, and max. A rule under each or sequence applies to units that the block already bounds, so none of these keys apply to it.
Fix
In the schema, move min or max from the unit rule to the block. To select units by a guard, move the rule under rules.
Example
This schema sets max on the rule of each, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Groceries
level: 1
blocks:
- kind: bullet-list
each: { template: '${}', max: 3, example: Milk }
note.md
# Groceries
- Milk
$ tramlines validate --schema note.yaml note.md
note.yaml:7: error[TLMD_91]: `max` applies to a rule under `rules` or `lines`; elsewhere the block bounds its units
1 error, 0 warnings
[exit 1]
Move max to the block:
note.yaml
line-limit: 0
sections:
- heading: Groceries
level: 1
blocks:
- kind: bullet-list
max: 3
each: { template: '${}', example: Milk }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_92
A rule with a select-pattern doesn’t state its min (TLMD_92).
Message
<file>:<line>: error[TLMD_92]: a guarded rule states `min`: `min: 0` governs such units without demanding one, and a number demands that many
Cause
The error is on the schema. A rule under rules or lines has no default min when it declares select-pattern. A guard selects the units that the rule applies to, but doesn’t require that any such unit exists.
Fix
In the schema, add min to the rule: min: 0 to apply the rule to the units that the guard selects without requiring any, or a number to require that many.
Example
This schema bans items that start with TODO but doesn’t state min, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: '${}', example: Open the lid. }
- { select-pattern: '^TODO', max: 0 }
note.md
# Steps
- Open the lid.
$ tramlines validate --schema note.yaml note.md
note.yaml:9: error[TLMD_92]: a guarded rule states `min`: `min: 0` governs such units without demanding one, and a number demands that many
1 error, 0 warnings
[exit 1]
Add min: 0:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: '${}', example: Open the lid. }
- { select-pattern: '^TODO', min: 0, max: 0 }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_142
A rule declares text beside select-pattern (TLMD_142).
Message
<file>:<line>: error[TLMD_142]: `text` beside `select-pattern`: the repair rewrites the unit whole and would drop what the guard selected on
Cause
The error is on the schema. The key text specifies the exact text of a unit, and a repair rewrites the unit to match it. A guard selects units by how they start. A repair would therefore drop the text that the guard matched.
Fix
In the schema, remove select-pattern from the rule, or replace text with a template or a pattern.
Example
This schema selects the item that starts with Done and specifies its exact text, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { select-pattern: '^Done', min: 1, text: Done. }
note.md
# Steps
- Done.
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TLMD_142]: `text` beside `select-pattern`: the repair rewrites the unit whole and would drop what the guard selected on
1 error, 0 warnings
[exit 1]
Remove select-pattern:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { text: Done. }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_93
A rule under rules or lines takes or counts fewer units than its min (TLMD_93).
Message
<file>:<line>: error[TLMD_93]: <selector>: at least <min> are needed; there are <count> (schema <id>, <place of the rule>)
Cause
The rule sets min, the fewest units that it takes or counts, and the block contains fewer. The selector names the units by the guard or the shape of the rule, such as the items of the template `NOTE: ${}` or the lines of the template `${:word}=${}` .
Fix
Add units of this kind to the document. If the block can contain fewer, lower min on the rule in the schema.
Example
This schema requires at least two items of the shape NOTE: ${}, and the document contains one:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: 'NOTE: ${}', min: 2, example: 'NOTE: The lid is hot.' }
- { template: '${}', example: Open the lid. }
note.md
# Steps
- Open the lid.
- NOTE: The lid is hot.
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_93]: the items of the template `NOTE: ${}`: at least 2 are needed; there is 1 (schema note, section "Steps" block 1)
1 error, 0 warnings
[exit 1]
Add a second note:
note.md
# Steps
- Open the lid.
- NOTE: The lid is hot.
- NOTE: Wear gloves.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_159
A rule under rules or lines takes or counts more units than its max (TLMD_159).
Message
<file>:<line>: error[TLMD_159]: <selector>: at most <max> are allowed; there are <count> (schema <id>, <place of the rule>)
Cause
The rule sets max, the most units that it takes or counts, and the block contains more. The selector names the units by the guard or the shape of the rule. The error points at the first unit over the limit. A rule with max: 0 bans the units that it selects.
Fix
Remove units of this kind from the document. If the block can contain more, raise max on the rule in the schema.
Example
This schema bans items that start with TODO, and the document contains one:
note.yaml
line-limit: 0
sections:
- heading: Steps
level: 1
blocks:
- kind: bullet-list
rules:
- { template: '${}', example: Open the lid. }
- { select-pattern: '^TODO', min: 0, max: 0 }
note.md
# Steps
- Open the lid.
- TODO close it
$ tramlines validate --schema note.yaml note.md
note.md:4: error[TLMD_159]: the items matching `^TODO`: at most 0 are allowed; there is 1 (schema note, section "Steps" block 1)
1 error, 0 warnings
[exit 1]
Remove the item:
note.md
# Steps
- Open the lid.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: lines and pairings
These errors come from two kinds of rule. The lines rules check each line of a code block. The pairing rules pair a unit with each key of another rule. Section 6 of the Markdown specification defines them, with the other rules for units and lines.
TLMD_87
A line of a code block matches none of the lines rules of its block (TLMD_87).
Message
<file>:<line>: error[TLMD_87]: `<line>` matches no rule of the fence; declare it or set `lines-open` (schema <id>, <place of the rule>)
Cause
The unit rule of a code block declares lines, a grammar for the lines of the block. None of its rules with a shape matches this line, in whole or in part. The engine skips blank lines and ignores the indent of each line. When a line partly matches a rule, the engine reports TLMD_57 instead.
Fix
Change the line in the document so that a rule matches it, or remove the line. If the block can contain lines of this kind, add a rule for them under lines in the schema, or set lines-open: true on the rule that declares lines.
Example
This schema accepts lines of the form name=value, and the document contains a comment line:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- kind: code
lang: text
each:
lines:
- { template: '${:word}=${}', example: port=80 }
note.md
# Config
```text
port=80
# the default
```
$ tramlines validate --schema note.yaml note.md
note.md:5: error[TLMD_87]: `# the default` matches no rule of the fence; declare it or set `lines-open` (schema note, section "Config" block 1)
1 error, 0 warnings
[exit 1]
Remove the comment:
note.md
# Config
```text
port=80
```
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_90
A unit rule declares lines under a block that isn’t a code block or an HTML block, or a line rule declares lines of its own (TLMD_90).
Message
<file>:<line>: error[TLMD_90]: `lines` applies to a `code` or `html` block, not a `<kind>` block
<file>:<line>: error[TLMD_90]: `lines` does not nest
Cause
The error is on the schema. Only a code block or an HTML block contains raw lines for lines to check. The first message reports that a unit rule under a block of another kind declares lines. The second reports that a rule under lines declares lines again.
Fix
In the schema, describe the units of the block with a shape, such as template or pattern, in place of lines. If the block contains raw lines, make it a code block with a lang. Under lines, remove the inner lines, and give the line rule a shape.
Example
This schema declares lines on the paragraphs of a blockquote, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- kind: blockquote
each:
lines:
- { template: '${:word}=${}', example: port=80 }
note.md
# Config
> port=80
$ tramlines validate --schema note.yaml note.md
note.yaml:8: error[TLMD_90]: `lines` applies to a `code` or `html` block, not a `blockquote` block
1 error, 0 warnings
[exit 1]
Give each paragraph a template in place of lines:
note.yaml
line-limit: 0
sections:
- heading: Config
level: 1
blocks:
- kind: blockquote
each: { template: '${:word}=${}', example: port=80 }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_95
A rule pairs a unit or a section with each key that another rule captures, and one of those keys has no unit or section (TLMD_95).
Message
<file>:<line>: error[TLMD_95]: no <unit> names key `<key>` of rule `<rule-id>` (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_95]: no `<section>` section for key `<key>` (schema <id>, <place of the rule>)
Cause
A unit rule or a section declares for-each-key-of, which names a rule by its rule-id. The engine expects one unit or section for each key that the named rule captures, and the document contains none for this key. The error is on the line that captures the key. Where the section declares a parent, the engine checks each occurrence of the parent on its own, and ends the message with under `<parent heading>` .
Fix
Add to the document a unit or a section that names the key, or remove the key from the rule that captures it.
Example
This schema pairs each item of the numbered list with a tool of the bullet list, and the document describes hammer but not saw:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name:word}', key: name, example: hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
key: name
for-each-key-of: tools
example: 'hammer: drives nails'
note.md
# Tools
- <a id="hammer"></a>hammer
- <a id="saw"></a>saw
1. hammer: drives nails
$ tramlines validate --schema note.yaml note.md
note.md:4: error[TLMD_95]: no item names key `saw` of rule `tools` (schema note, section "Tools")
1 error, 0 warnings
[exit 1]
Describe saw:
note.md
# Tools
- <a id="hammer"></a>hammer
- <a id="saw"></a>saw
1. hammer: drives nails
2. saw: cuts wood
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_96
A unit or a section names a key that its paired rule doesn’t capture (TLMD_96).
Message
<file>:<line>: error[TLMD_96]: key `<key>` is not one rule `<rule-id>` captured (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_96]: section names key `<key>`, which rule `<rule-id>` did not capture (schema <id>, <place of the rule>)
Cause
A unit rule or a section declares for-each-key-of, which names a rule by its rule-id. The unit or section captures a key that the named rule doesn’t capture in the same scope. Where the section declares a parent, the scope is one occurrence of the parent, and the message ends with under `<parent heading>` .
Fix
Change the key in the document to one that the named rule captures, add the key to that rule, or remove the unit or section.
Example
The schema is the same as in the TLMD_95 example. This document describes drill, which isn’t in the list of tools. A paired item refers to a key, so drill needs no anchor:
note.md
# Tools
- <a id="hammer"></a>hammer
1. hammer: drives nails
2. drill: makes holes
$ tramlines validate --schema note.yaml note.md
note.md:6: error[TLMD_96]: key `drill` is not one rule `tools` captured (schema note, section "Tools")
1 error, 0 warnings
[exit 1]
Remove drill:
note.md
# Tools
- <a id="hammer"></a>hammer
1. hammer: drives nails
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_98
A section or a unit rule declares for-each-key-of, but it captures no key of its own (TLMD_98).
Message
<file>:<line>: error[TLMD_98]: `for-each-key-of` on a rule that captures no key: declare `key`, or bind a `format` that declares one
<file>:<line>: error[TLMD_98]: `for-each-key-of` pairs headings by their key: the heading declares `key`
Cause
The error is on the schema. A pairing matches each key that the pairing rule captures with a key of the named rule. The pairing rule must therefore capture a key of its own. Here, the unit rule or the heading with for-each-key-of declares no key, and binds no format that declares one.
Fix
In the schema, add key to the pairing unit rule or heading, with the name of the hole that holds the key. Or bind a format that declares a key.
Example
This schema pairs the numbered list with the tools, but the rule of the numbered list declares no key, so the run reports the error on the schema:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name:word}', key: name, example: hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
for-each-key-of: tools
example: 'hammer: drives nails'
The document is the fixed document from the TLMD_96 example:
$ tramlines validate --schema note.yaml note.md
note.yaml:11: error[TLMD_98]: `for-each-key-of` on a rule that captures no key: declare `key`, or bind a `format` that declares one
1 error, 0 warnings
[exit 1]
Add key:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name:word}', key: name, example: hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
key: name
for-each-key-of: tools
example: 'hammer: drives nails'
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_167
The for-each-key-of of a section or a unit rule names no rule, or a rule that captures no key (TLMD_167).
Message
<file>:<line>: error[TLMD_167]: `for-each-key-of` names no rule `<rule-id>`
<file>:<line>: error[TLMD_167]: `for-each-key-of` names rule `<rule-id>`, which captures no key
Cause
The error is on the schema. A pairing matches its keys with the keys of the rule that for-each-key-of names by its rule-id. No rule of the schema declares that rule-id, or the rule that declares it captures no key.
Fix
In the schema, set for-each-key-of to the rule-id of a rule that captures a key, or add key to the rule that it names.
Example
This schema pairs the numbered list with tool, but the bullet list declares the rule-id tools:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name:word}', key: name, example: hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
key: name
for-each-key-of: tool
example: 'hammer: drives nails'
The document is the fixed document from the TLMD_96 example:
$ tramlines validate --schema note.yaml note.md
note.yaml:11: error[TLMD_167]: `for-each-key-of` names no rule `tool`
1 error, 0 warnings
[exit 1]
Name the rule by its rule-id:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name:word}', key: name, example: hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
key: name
for-each-key-of: tools
example: 'hammer: drives nails'
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: markers
These errors come from a marker that a scaffold leaves in a document. Section 3 of the Markdown specification defines these errors. A separate page lists the errors for the scaffolds that a schema derives.
TLMD_10
A document still contains an only marker, which belongs only in a scaffold (TLMD_10).
Message
<file>:<line>: error[TLMD_10]: a `tramlines:only` marker belongs in a scaffold, not a governed document
Cause
A scaffold marks each part that applies only in some lifecycle states with a <!-- tramlines:only … --> line, or with a # tramlines:only … comment after a frontmatter key. A document written from the scaffold still contains one of these markers outside a code block.
Fix
Delete the only marker from the document: the whole line for an HTML comment, or only the # tramlines:only … comment after a frontmatter key. The schema already states when the part applies.
Example
In this schema, Outcome applies only to a closed bug:
bug.yaml
line-limit: 0
lifecycle:
- { key: state, states: [open, closed] }
sections:
- heading: Bug
level: 1
blocks: []
- heading: Outcome
level: 1
only: { state: closed }
blocks: []
This document still contains the only line of its scaffold:
bug.md
---
state: closed
---
# Bug
<!-- tramlines:only state=closed -->
# Outcome
$ tramlines validate --schema bug.yaml bug.md
bug.md:7: error[TLMD_10]: a `tramlines:only` marker belongs in a scaffold, not a governed document
1 error, 0 warnings
[exit 1]
Delete the line:
bug.md
---
state: closed
---
# Bug
# Outcome
$ tramlines validate --schema bug.yaml bug.md
0 errors, 0 warnings
TLMD_158
A document still contains a guidance line of its scaffold (TLMD_158).
Message
<file>:<line>: error[TLMD_158]: a `tramlines:guidance` line is left here: write the part it guides, then delete the line
Cause
A scaffold explains each part with a <!-- tramlines:guidance … --> line before it, or a # tramlines:guidance … line before a frontmatter key. A document written from the scaffold still contains one of these lines outside a code block.
Fix
In the document, write the part that the guidance line describes, then delete the line.
Example
This schema requires a paragraph:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
This document still contains the guidance line of its scaffold:
note.md
# Note
<!-- tramlines:guidance Say what to buy. -->
Buy milk.
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_158]: a `tramlines:guidance` line is left here: write the part it guides, then delete the line
1 error, 0 warnings
[exit 1]
Delete the line:
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_11
A document still contains an optional region of its scaffold (TLMD_11).
Message
<file>:<line>: warning[TLMD_11]: a `tramlines:optional` region is left here: keep what it holds and drop both markers, or drop the region
Cause
A scaffold puts each part that the schema doesn’t require between <!-- tramlines:optional --> and <!-- /tramlines:optional -->. The document still contains the opening marker. The finding is a warning, so the command still exits with 0.
Fix
In the document, decide whether to keep the part. To keep it, delete both markers. To leave it out, delete the whole region, markers included.
Example
This example uses the note.yaml of TLMD_158. This document still contains an optional region:
note.md
# Note
<!-- tramlines:optional -->
Buy milk.
<!-- /tramlines:optional -->
$ tramlines validate --warnings --schema note.yaml note.md
note.md:3: warning[TLMD_11]: a `tramlines:optional` region is left here: keep what it holds and drop both markers, or drop the region
0 errors, 1 warning
Keep the paragraph and delete both markers:
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_12
An optional marker has no partner: a region opens and nothing closes it, or it closes and nothing opened it (TLMD_12).
Message
<file>:<line>: error[TLMD_12]: a `tramlines:optional` region opens here and nothing closes it
<file>:<line>: error[TLMD_12]: a `tramlines:optional` region closes here and nothing opened it
Cause
The document contains a <!-- tramlines:optional --> line with no <!-- /tramlines:optional --> line after it, or a closing line with no opening line before it. Regions don’t nest. When a second opening line follows an opening line before any closing line, the first opening line has no partner either. A marker with no partner forms no region, so the command reports no TLMD_11 for it.
Fix
In the document, delete the marker that has no partner, along with any other optional marker that’s left.
Example
This example uses the note.yaml of TLMD_158. This document contains the closing marker of a region but not its opening marker:
note.md
# Note
Buy milk.
<!-- /tramlines:optional -->
$ tramlines validate --schema note.yaml note.md
note.md:5: error[TLMD_12]: a `tramlines:optional` region closes here and nothing opened it
1 error, 0 warnings
[exit 1]
Delete the marker:
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: scaffolds
These errors come from the scaffold of a schema: a part that the scaffold has no text for, a key that it can’t write, two blocks that read as one, a marker that disagrees with its rule, and a scaffold that fails its own schema. Section 9 of the Markdown specification defines these errors.
TLMD_116
tramlines can’t derive a scaffold from the schema, because a part that the scaffold needs has no text in the schema (TLMD_116).
Message
<file>:<line>: error[TLMD_116]: the schema derives no scaffold: <reason>
Cause
Before it checks the documents, the command derives a scaffold from the schema. The scaffold contains only text that the schema specifies in a text or an example. A required block, unit rule, heading, or frontmatter key declares neither, so the scaffold contains no text for it. The error is on the schema file, at the line of that rule. When an example lacks the key that the scaffold writes into it, the error is TLMD_154. When text comes from a frontmatter key that the scaffold doesn’t write, the error is TLMD_155.
Fix
In the schema, give the rule that the error names an example, or a text that sets its exact words.
Example
This schema requires a paragraph but declares no example for it:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_116]: the schema derives no scaffold: a `prose` block has no `example`, and the scaffold writes no text of its own; give the block an `example`
1 error, 0 warnings
[exit 1]
Add an example:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_154
tramlines can’t derive a scaffold from the schema, because the example of a rule doesn’t hold the key that the scaffold writes into it (TLMD_154).
Message
<file>:<line>: error[TLMD_154]: the schema derives no scaffold: a rule demands one unit per key of `<rule-id>`, and its example `<example>` does not carry `<key>` where `<hole>` reads it
<file>:<line>: error[TLMD_154]: the schema derives no scaffold: a section demands one heading per key of `<rule-id>`, and its example `<example>` does not carry `<key>` where `<hole>` reads it
<file>:<line>: error[TLMD_154]: the schema derives no scaffold: a key deriving from <source> cannot be written: `<key>` does not read as `<derived key>` where `<hole>` takes it
Cause
The scaffold writes one unit or one heading for each key of the rule that for-each-key-of names. With key-derives-from, a derived key starts with the key that it derives from. To write each one, the scaffold puts the key into the example of the rule, at the hole that holds the key. The key doesn’t fit that hole. The capturing rule allows keys that this rule’s hole doesn’t read. A key with a space, for example, doesn’t fit a hole of type word. The error is on the schema file, at the line of the rule whose example is written.
Fix
In the schema, give the two holes types that read the same keys. Make the example of the capturing rule a key that both holes read.
Example
The tools of this schema are any text, but the pairing reads a word:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name}', key: name, example: claw hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
key: name
for-each-key-of: tools
example: 'hammer: drives nails'
note.md
# Tools
- <a id="hammer"></a>hammer
1. hammer: drives nails
$ tramlines validate --schema note.yaml note.md
note.yaml:11: error[TLMD_154]: the schema derives no scaffold: a rule demands one unit per key of rule `tools`, and its example `hammer: drives nails` does not carry `claw hammer` where `name` reads it
1 error, 0 warnings
[exit 1]
Read each tool as a word, with an example that is one:
note.yaml
line-limit: 0
sections:
- heading: Tools
level: 1
blocks:
- kind: bullet-list
rule-id: tools
each: { template: '${name:word}', key: name, example: hammer }
- kind: numbered-list
each:
template: '${name:word}: ${}'
key: name
for-each-key-of: tools
example: 'hammer: drives nails'
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_155
tramlines can’t derive a scaffold from the schema, because a heading or a unit takes its text from a frontmatter key that the scaffold doesn’t write (TLMD_155).
Message
<file>:<line>: error[TLMD_155]: the schema derives no scaffold: a section takes its heading from `<key>`, which no frontmatter writes
<file>:<line>: error[TLMD_155]: the schema derives no scaffold: a unit rule takes its text from `<key>`, which no frontmatter writes
Cause
A heading declared as { from: <key> }, or a unit rule with text: { from: <key> }, repeats the value of a frontmatter key. The scaffold writes a frontmatter key from its const, its template, its example, or the first value of its enum. The scaffold leaves the key out in three cases: the frontmatter doesn’t declare it, its only excludes the states of the scaffold, or it’s optional and declares none of them. The heading or the unit then has no text. The error is on the schema file, at the line of the section or the rule.
Fix
In the schema, declare the frontmatter key, give it an example, and gate it no narrower than the rule that reads it.
Example
The heading of this schema repeats the title, which declares no example:
note.yaml
line-limit: 0
frontmatter:
title:
optional: true
sections:
- heading: { from: title }
level: 1
blocks: []
note.md
---
title: Groceries
---
# Groceries
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_155]: the schema derives no scaffold: a section takes its heading from `title`, which no frontmatter writes
1 error, 0 warnings
[exit 1]
Add an example to the key:
note.yaml
line-limit: 0
frontmatter:
title:
optional: true
example: Groceries
sections:
- heading: { from: title }
level: 1
blocks: []
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_122
The only markers of a scaffold don’t agree with the rules that match its parts when the command checks the scaffold against the schema (TLMD_122).
Message
<file>:<line>: error[TLMD_122]: <scaffold>: the `tramlines:only` marker before `<part>` gates nothing: the part it marks is not matched by a gated rule
<file>:<line>: error[TLMD_122]: <scaffold>: the `tramlines:only` marker before `<part>` disagrees with its rule, whose `only` is `<states>`
<file>:<line>: error[TLMD_122]: <scaffold>: the part `<part>` is matched by a rule whose `only` is `<states>` and carries no `tramlines:only` marker
Cause
The scaffold marks each part that a rule with only writes. When the command checks the scaffold, each marked part must match a rule with the same only. Two rules that can match the same part break this agreement, because the check gives the part to the first rule that matches it. In the following example, a repeating section without only takes every Details heading, so the Details section with only never matches.
<scaffold> is the scaffold, or the scaffold derived for `<states>` when the schema has a lifecycle, and <part> is a line of that scaffold. A marker of a frontmatter key reads on instead of before, because it sits on the key’s own line. When the command can tell which rule wrote the part, the error is on the line of that rule in the schema. Otherwise, the error is on the first line of the schema.
Fix
In the schema, make sure that no other rule can match a part that a gated rule writes. For example, give that part a different heading.
Example
The second Details section of this schema applies only to closed notes:
note.yaml
line-limit: 0
lifecycle:
- { key: state, states: [open, closed] }
sections:
- { heading: Details, level: 1, optional: true, repeat: true, blocks: [] }
- { heading: Details, level: 1, optional: true, only: { state: closed }, blocks: [] }
note.md
---
state: open
---
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_122]: the scaffold derived for `state=closed`: the `tramlines:only` marker before `# Details` gates nothing: the part it marks is not matched by a gated rule
1 error, 0 warnings
[exit 1]
Give the gated section a different heading:
note.yaml
line-limit: 0
lifecycle:
- { key: state, states: [open, closed] }
sections:
- { heading: Details, level: 1, optional: true, repeat: true, blocks: [] }
- { heading: Outcome, level: 1, optional: true, only: { state: closed }, blocks: [] }
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_124
A part that the schema doesn’t require has no text, so the scaffold leaves it out (TLMD_124).
Message
<file>:<line>: warning[TLMD_124]: an optional part is left out of the scaffold: <reason>
Cause
The scaffold writes each optional part inside an optional region, from the text or the example of its rule. An optional rule declares neither, so the scaffold leaves the part out and doesn’t show it to the writer. <reason> can also be any fault that refuses a required part, such as a key that the scaffold can’t write (TLMD_154, TLMD_155) or two blocks that read as one (TLMD_149). The warning is on the schema file, at the line of that rule. It’s a warning, so the command still checks the documents and exits with 0 when they pass.
Fix
In the schema, give the optional rule an example, or fix the fault that the reason names.
Example
This schema allows a list but declares no example for it:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
- kind: bullet-list
optional: true
note.md
# Note
Buy milk.
$ tramlines validate --warnings --schema note.yaml note.md
note.yaml:8: warning[TLMD_124]: an optional part is left out of the scaffold: a `bullet-list` block has no `example`, and the scaffold writes no text of its own; give the block an `example`
0 errors, 1 warning
Add an example:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
- kind: bullet-list
optional: true
example: Milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_164
A scaffold derived from the schema fails the schema (TLMD_164).
Message
<file>:<line>: error[TLMD_164]: <scaffold> fails: [<key>] <message>
<file>:<line>: error[TLMD_164]: <scaffold> fails at `<text>`: [<key>] <message>
Cause
Before it checks the documents, the command derives a scaffold for each combination of states and checks it against the schema. The scaffold contains every part of the schema, including each optional part that it has text for, and a rule in the schema rejects it. In the following example, the line-limit is too small for a document with the optional list.
<scaffold> is the scaffold, or the scaffold derived for `<states>` when the schema has a lifecycle. <key> and <message> are those of the error in the scaffold. When that error has a line, <text> is that line of the scaffold. The error is on the line of the rule in the schema that the scaffold fails, or on the first line of the schema where no rule is to blame. The error quotes the key of the error in the scaffold. To find the rule, look up that key.
Fix
In the schema, change the rule that the quoted error names, so that a document with every allowed part can pass it.
Example
This schema allows a list and limits a document to three lines. This example uses the note.md of TLMD_124, which has three lines.
note.yaml
line-limit: 3
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
- kind: bullet-list
optional: true
example: Milk
$ tramlines validate --schema note.yaml note.md
note.yaml:1: error[TLMD_164]: the scaffold fails: [TL_21] the document has 5 lines; the limit is 3; split it (schema note)
1 error, 0 warnings
[exit 1]
Raise the limit:
note.yaml
line-limit: 10
sections:
- heading: Note
level: 1
blocks:
- kind: prose
example: Buy milk.
- kind: bullet-list
optional: true
example: Milk
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
TLMD_149
tramlines can’t derive a scaffold from the schema, because two consecutive blocks of the scaffold read as one block, and the first takes all of it (TLMD_149).
Message
<file>:<line>: error[TLMD_149]: the schema derives no scaffold: a `<kind>` block whose rule admits more units than the scaffold writes is followed by another `<kind>` block, which a document reads as one block, so the first takes all of it; give the first block a `max` of the units it holds
Cause
A document reads paragraphs in a row as one prose block, and two lists of one kind as one list. When two such blocks follow each other, tramlines divides the run between their rules, and each rule takes as many units as it allows. A rule takes the whole run when two things hold: its max is absent or larger than the number of units that the scaffold writes for it, and its units state no shape, format, or prohibited pattern. Nothing is then left for the block after it. This happens between two blocks of a section, and between two repetitions of a group. The error is on the schema file, at the line of the first block.
Fix
In the schema, give the first block a max equal to the number of units that it holds, or a shape that the next block’s text fails. Or put a block of another kind between the two.
Example
This schema repeats a paragraph at least twice, but the paragraph has no max:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: group
min: 2
blocks:
- kind: prose
example: Buy milk.
note.md
# Note
Buy milk.
Buy bread.
$ tramlines validate --schema note.yaml note.md
note.md:3: error[TLMD_146]: the group block needs at least 2 repetitions; it has 1 (schema note, section "Note" block 1)
note.yaml:9: error[TLMD_149]: the schema derives no scaffold: a `prose` block whose rule admits more units than the scaffold writes is followed by another `prose` block, which a document reads as one block, so the first takes all of it; give the first block a `max` of the units it holds
2 errors, 0 warnings
[exit 1]
Give the paragraph a max of 1, so that each repetition takes one paragraph:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: group
min: 2
blocks:
- kind: prose
max: 1
example: Buy milk.
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Errors: keys and repairs
These errors come from keys and their anchors, and from headings and text that must equal a frontmatter value or the current form of a heading. The command tramlines validate --fix repairs the anchors. It also rewrites each heading and text that the frontmatter or the schema determines. Sections 7 and 10 of the Markdown specification define these errors.
TLMD_101
A key has the same slug as another key or as a heading of the document, so the two would share one anchor (TLMD_101).
Message
<file>:<line>: error[TLMD_101]: key `<key>` has the slug `<slug>` of the heading at line <line>; keys and headings share one namespace (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_101]: key `<key>` has the slug `<slug>` of key `<other key>` at line <line>; each key needs a slug of its own (schema <id>, <place of the rule>)
Cause
Each key receives an anchor whose id is the slug of the key. Each heading also receives a slug. The slug is the text in lowercase. Spaces become hyphens, and most punctuation drops out. Two keys that differ only in case, or a key that reads like a heading of the document, produce the same id twice. A key that a rule with for-each-key-of captures refers to a key and receives no anchor. The command doesn’t check its slug for this error.
Fix
In the document, rename the key or the heading so that each slug is different.
Example
This schema captures a key from each item of a list:
pantry.yaml
line-limit: 0
sections:
- heading: Pantry
level: 1
blocks:
- kind: bullet-list
each:
template: "${name:word}: ${}"
key: name
example: "milk: two litres"
The key pantry has the slug of the heading Pantry:
pantry.md
# Pantry
- <a id="pantry"></a>pantry: the top shelf
$ tramlines validate --schema pantry.yaml pantry.md
pantry.md:3: error[TLMD_101]: key `pantry` has the slug `pantry` of the heading at line 1; keys and headings share one namespace (schema pantry, section "Pantry")
1 error, 0 warnings
[exit 1]
Rename the key, and its anchor with it:
pantry.md
# Pantry
- <a id="shelf"></a>shelf: the top shelf
$ tramlines validate --schema pantry.yaml pantry.md
0 errors, 0 warnings
TLMD_148
Rules capture one key twice in the scope that unique declares (TLMD_148).
Message
<file>:<line>: error[TLMD_148]: key `<key>` is captured more than once; it was first captured at line <line> (schema <id>, <place of the rule>)
Cause
A key identifies one part of a document. Each key that the rules capture must therefore be different within its scope, whichever rule captures it. The scope is the document unless unique names a wider one that a host adds. The document contains two items, headings, or lines with the same key. Suppose a heading repeats, its section doesn’t declare repeat, and the body of the repeat captures a key that the first body captured. The command reports that key with the heading, as TLMD_151. For a key that pairs with another rule through for-each-key-of, the command reports TLMD_29 instead.
Fix
In the document, merge the two parts, or rename one of the keys.
Example
This example uses the pantry.yaml of TLMD_101. This document lists milk twice:
pantry.md
# Pantry
- <a id="milk"></a>milk: two litres
- milk: one more
$ tramlines validate --schema pantry.yaml pantry.md
pantry.md:4: error[TLMD_148]: key `milk` is captured more than once; it was first captured at line 3 (schema pantry, section "Pantry")
1 error, 0 warnings
[exit 1]
Merge the two items:
pantry.md
# Pantry
- <a id="milk"></a>milk: three litres
$ tramlines validate --schema pantry.yaml pantry.md
0 errors, 0 warnings
TLMD_109
A key has no anchor, an anchor isn’t where its key is, an anchor names no key, or an anchor is written twice (TLMD_109).
Message
<file>:<line>: error[TLMD_109]: key `<key>` has no anchor; its repair writes `<a id="<slug>"></a>` before it
<file>:<line>: error[TLMD_109]: the anchor `#<slug>` is not where its key `<key>` is; its repair moves it
<file>:<line>: error[TLMD_109]: the anchor `#<slug>` names no key of this document; its repair removes it
<file>:<line>: error[TLMD_109]: the anchor `#<slug>` occurs more than once; its repair keeps one
Cause
Each key that a rule captures needs one anchor, <a id="<slug>"></a>, so that a link can open the page at the key. The anchor goes at the start of the text of a heading, or directly before the text of an item, a paragraph, or a cell. Before a code block, the anchor goes on a separate line. A key that a unit rule with for-each-key-of captures refers to the key and needs no anchor. In the document, the anchor is missing, in the wrong place, or written twice, or it belongs to a key that no longer exists. The command reports an anchor written again beside a second capture of a key as TLMD_148 or TLMD_101 instead. An anchor beside a capture with for-each-key-of goes with the error of that pairing. The repair removes both kinds of anchor.
Fix
In the document, write the anchor that the message names in the place where it belongs, and delete any extra or orphaned anchor. tramlines validate --fix does both.
Example
This example uses the pantry.yaml of TLMD_101. The key milk has no anchor:
pantry.md
# Pantry
- milk: two litres
$ tramlines validate --schema pantry.yaml pantry.md
pantry.md:3: error[TLMD_109]: key `milk` has no anchor; its repair writes `<a id="milk"></a>` before it
1 error, 0 warnings
[exit 1]
Write the anchor before the text of the item:
pantry.md
# Pantry
- <a id="milk"></a>milk: two litres
$ tramlines validate --schema pantry.yaml pantry.md
0 errors, 0 warnings
TLMD_165
A heading or a unit that the schema binds to a frontmatter value doesn’t equal that value (TLMD_165).
Message
<file>:<line>: error[TLMD_165]: heading `<heading>` must equal the frontmatter `<key>`: `<value>` (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_165]: `<text>` must equal the frontmatter `<key>`: `<value>` (schema <id>, <place of the rule>)
Cause
The schema declares heading: { from: <key> } on a section, or text: { from: <key> } on a unit rule. The text in the document must equal the value of that key, and it differs.
Fix
In the document, change the heading or the text to the value of the frontmatter key, or run tramlines validate --fix, which makes the change. If the value is wrong, change the frontmatter instead.
Example
This schema requires the level-1 heading to equal the frontmatter title:
list.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
sections:
- heading: { from: title }
level: 1
blocks: []
The heading of this document differs from its title:
list.md
---
title: Groceries
---
# Shopping
$ tramlines validate --schema list.yaml list.md
list.md:5: error[TLMD_165]: heading `Shopping` must equal the frontmatter `title`: `Groceries` (schema list, section "<title>")
1 error, 0 warnings
[exit 1]
Change the heading:
list.md
---
title: Groceries
---
# Groceries
$ tramlines validate --schema list.yaml list.md
0 errors, 0 warnings
TLMD_168
A heading, a paragraph, or an item is bound to a frontmatter key that the document doesn’t set (TLMD_168).
Message
<file>:<line>: error[TLMD_168]: heading `<heading>` is bound to the frontmatter `<key>`, which is absent (schema <id>, <place of the rule>)
<file>:<line>: error[TLMD_168]: `<text>` is bound to the frontmatter `<key>`, which is absent (schema <id>, <place of the rule>)
Cause
The schema declares { from: <key> } as the heading of a section or as the text of a unit rule. The frontmatter of the document doesn’t contain the key, so the heading or the unit has no value to equal. The key is optional: when the schema requires the key, the command reports it missing as TL_54 and nothing more.
Fix
Add the key to the frontmatter of the document, with the text that the heading or the unit holds as its value. Then tramlines validate --fix keeps the two equal.
Example
This schema binds the first paragraph to the optional frontmatter description:
list.yaml
line-limit: 0
frontmatter:
description:
optional: true
example: What to buy this week.
sections:
- heading: List
level: 1
blocks:
- kind: prose
each:
text: { from: description }
This document has no description:
list.md
# List
What to buy this week.
$ tramlines validate --schema list.yaml list.md
list.md:3: error[TLMD_168]: `What to buy this week.` is bound to the frontmatter `description`, which is absent (schema list, section "List" block 1)
1 error, 0 warnings
[exit 1]
Add the key:
list.md
---
description: What to buy this week.
---
# List
What to buy this week.
$ tramlines validate --schema list.yaml list.md
0 errors, 0 warnings
TLMD_137
A heading matches an old text of its section, which the section lists under renamed-from (TLMD_137).
Message
<file>:<line>: error[TLMD_137]: heading `<old text>` was renamed; it is now `<new text>` (schema <id>, <place of the rule>)
Cause
The schema renamed the section and lists the old text under renamed-from. The document still contains the old heading.
Fix
Change the heading in the document to the text that the message names, or run tramlines validate --fix, which makes the change.
Example
This schema renamed Steps to Method:
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
- heading: Method
level: 2
renamed-from: Steps
blocks: []
This document still contains Steps:
recipe.md
# Recipe
## Steps
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:3: error[TLMD_137]: heading `Steps` was renamed; it is now `Method` (schema recipe, section "Method")
1 error, 0 warnings
[exit 1]
Rename the heading:
recipe.md
# Recipe
## Method
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
TLMD_143
A section declares renamed-from, but its heading is a template or a pattern (TLMD_143).
Message
<file>:<line>: error[TLMD_143]: `renamed-from` beside a heading that varies: a former heading is rewritten to exact text
Cause
renamed-from lists the former text of a heading, so that a document with the old heading is reported and repaired to the current one. A heading with a template or a pattern has no single current text to repair it to. The error is on the schema file.
Fix
In the schema, delete renamed-from from the section, or give the section a literal heading.
Example
This section declares a heading that varies, and a former heading:
plan.yaml
line-limit: 0
sections:
- heading: { template: "Week ${n:int}", example: Week 1 }
renamed-from: Plan
level: 1
blocks: []
plan.md
# Week 1
$ tramlines validate --schema plan.yaml plan.md
plan.yaml:4: error[TLMD_143]: `renamed-from` beside a heading that varies: a former heading is rewritten to exact text
1 error, 0 warnings
[exit 1]
Delete renamed-from:
plan.yaml
line-limit: 0
sections:
- heading: { template: "Week ${n:int}", example: Week 1 }
level: 1
blocks: []
$ tramlines validate --schema plan.yaml plan.md
0 errors, 0 warnings
TLMD_138
A heading is a setext heading, underlined with = or -, where tramlines expects an ATX heading that begins with # (TLMD_138).
Message
<file>:<line>: error[TLMD_138]: heading `<heading>` is setext; write it as ATX (schema <id>)
Cause
The document writes a heading as a line of text with a line of = or - under it. The tramlines Markdown specification accepts only ATX headings, whatever the schema declares.
Fix
In the document, write the heading with # signs: one for level 1, two for level 2. Alternatively, run tramlines validate --fix, which rewrites the heading.
Example
recipe.yaml
line-limit: 0
sections:
- heading: Recipe
level: 1
blocks: []
This document underlines its heading:
recipe.md
Recipe
======
$ tramlines validate --schema recipe.yaml recipe.md
recipe.md:1: error[TLMD_138]: heading `Recipe` is setext; write it as ATX (schema recipe)
1 error, 0 warnings
[exit 1]
Write it with a #:
recipe.md
# Recipe
$ tramlines validate --schema recipe.yaml recipe.md
0 errors, 0 warnings
Errors: the command
These errors stop tramlines validate with exit code 2 before it checks any document. They report a --formats file that the command can’t use, a document whose format it can’t determine, and a --format that it doesn’t know. Section 10 of the core specification defines them.
TL_87
The file that --formats names doesn’t parse as YAML, or has no registry key (TL_87).
Message
error[TL_87]: <file>: <reason>
Cause
The command reads the --formats file as YAML, with a registry map of formats and an optional types map. The command stops when the file doesn’t parse or has no registry key. A fault inside the registry or the types also stops the command, such as a format with no example or a type named slug. Each such fault prints the key that it has in the formats of a schema, with the file and the line (TL_124). A YAML anchor, alias, or tag in the file stops the command under TL_14, as it does in a schema. The command prints each error to stderr and omits the count of errors and warnings.
Fix
Change the --formats file so that it parses as YAML, and put the formats under a registry key. For a fault inside the registry, look up the key that the error prints.
Example
This schema binds a format item:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: bullet-list
each:
format: item
note.md
# Note
- milk
This formats file defines item, but not under a registry key:
formats.yaml
item:
template: '${:slug}'
example: milk
$ tramlines validate --schema note.yaml --formats formats.yaml note.md
error[TL_87]: formats.yaml: no registry
[exit 2]
Put the format under registry:
formats.yaml
registry:
item:
template: '${:slug}'
example: milk
$ tramlines validate --schema note.yaml --formats formats.yaml note.md
0 errors, 0 warnings
A fault inside the registry prints its own key. This format has no example, which TL_40 describes:
formats.yaml
registry:
item:
template: '${:slug}'
$ tramlines validate --schema note.yaml --formats formats.yaml note.md
error[TL_40]: formats.yaml:3: format `item`: a format with one shape carries an `example`
[exit 2]
Add the example back:
formats.yaml
registry:
item:
template: '${:slug}'
example: milk
$ tramlines validate --schema note.yaml --formats formats.yaml note.md
0 errors, 0 warnings
TL_90
The extension of a document matches no format, and the command line includes no --format (TL_90).
Message
error[TL_90]: <file>: no format matches the extension `<extension>`; name one with `--format` (<formats>)
Cause
The command picks the format of each document by its file extension: .md and .markdown select the md format. The command checks the extension of every document before it reads any. One document with another extension, or with none, stops the whole run.
Fix
Name the format of the documents with --format, or give the document a filename with an extension that a format matches.
Example
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
This document is markdown, but its filename ends in .txt:
note.txt
# Note
$ tramlines validate --schema note.yaml note.txt
error[TL_90]: note.txt: no format matches the extension `txt`; name one with `--format` (`md`)
[exit 2]
Name the format:
$ tramlines validate --schema note.yaml --format md note.txt
0 errors, 0 warnings
TL_91
--format names a format that the command doesn’t know (TL_91).
Message
error[TL_91]: no format `<name>`; the formats are <formats>
Cause
--format takes the name of a format, such as md, not the name of a language or an extension.
Fix
Pass --format one of the formats that the message lists.
Example
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks: []
note.txt
# Note
$ tramlines validate --schema note.yaml --format markdown note.txt
error[TL_91]: no format `markdown`; the formats are `md`
[exit 2]
Use the name of the format:
$ tramlines validate --schema note.yaml --format md note.txt
0 errors, 0 warnings
Built-in types
A type defines what a template hole matches: ${amount:int} is a hole of the type int. tramlines defines five types. A schema or a formats file can define more types under names that aren’t reserved. tramlines looks up each type by name in a fixed order.
Write the shape of a line shows how to write a template with holes.
Types that tramlines defines
| Type | Regular expression | Matches |
|---|---|---|
text | .+ | Any text of one or more characters. A hole with no type, such as ${item} or ${}, has this type. |
word | \w+ | One or more letters, digits, or underscores, including letters outside ASCII, such as é. |
int | \d+ | One or more digits. No sign and no decimal point. |
slug | [a-z0-9]+(?:-[a-z0-9]+)* | Lowercase ASCII letters and digits, in words joined by single hyphens, such as plain-flour. |
seq | \d+ | The sequence number of an id template. In the id template of the frontmatter, it matches exactly seq-width digits, or three digits when the schema doesn’t set seq-width. |
The regular expressions use the Rust dialect. A hole matches only its type, because tramlines matches the template against the whole text of the line.
An error names the type and its regular expression, so you can see what the hole accepts:
expected a `slug` (`[a-z0-9]+(?:-[a-z0-9]+)*`), found `Bad-Slug 3`
Holes
| Hole | Type | Captured as |
|---|---|---|
${name:type} | type | name |
${name} | text | name |
${:type} | type | Nothing |
${} | text | Nothing |
A rule’s key can name a captured hole. $${ is a literal ${, and any other $ is a literal $.
Define a type
A schema defines its own types in a types map, at the top level of the schema. Each entry maps a name to one of two kinds of value:
- A regular expression, as a string. It can’t contain an anchor (
^or$) or a capture group: the template already matches the whole line and names its own groups. Write a group as(?:…). - A list of choices, each a non-empty string. A choice matches exactly as written: tramlines escapes every character, so
b.cmatches onlyb.c.
A type name must be a slug and can’t be a reserved name.
Write the alternatives and optional parts of a hole in a type, so that a template never needs regular expression syntax.
The following schema defines unit as a list of choices and amount as a regular expression, then uses both in a template:
recipe.yaml
line-limit: 100
types:
unit: [g, ml, tsp, tbsp]
amount: '\d+(?:\.\d+)?'
sections:
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each:
template: '${:amount} ${:unit} ${food}'
example: 1.5 tsp baking powder
scones.md
## Ingredients
- 250 g flour
- 1.5 tsp baking powder
- 2 cups milk
$ tramlines validate --schema recipe.yaml scones.md
scones.md:5: error[TL_74]: `2 cups milk` fails at column 3: expected a `unit` (one of `g`, `ml`, `tsp`, `tbsp`), found `cups milk`; an item of this kind reads 1.5 tsp baking powder (schema recipe, section "Ingredients" block 1)
1 error, 0 warnings
[exit 1]
An error shows a list of choices as one of, followed by the choices.
Types in a formats file
A formats file, which you pass with tramlines validate --formats <FILE>, contains a registry map of line formats and an optional types map. Its types map takes the same form as the types of a schema. Every schema checked with that file can use its types. Share line formats across schemas shows how to write one.
Reserved names
tramlines reserves the names text, word, int, slug, and seq: a schema or a formats file can’t define them. A program that embeds tramlines can reserve types of its own. The tramlines command reserves only these five.
The check of a schema fails if the schema defines a reserved name, a type with an anchor or a capture group, a name that isn’t a slug, or an empty list of choices:
broken.yaml
line-limit: 100
types:
int: '[0-9]+'
amount: '^\d+$'
unit: '(g|ml)'
Unit: [g]
size: []
sections:
- heading: Ingredients
level: 2
blocks: []
$ tramlines validate --schema broken.yaml scones.md
broken.yaml:3: error[TL_29]: type `int` is built in and reserved; name the type otherwise
broken.yaml:4: error[TL_109]: type `amount`: `^\d+$` holds an anchor; a template matches the whole line itself
broken.yaml:5: error[TL_109]: type `unit`: `(g|ml)` holds a capture group; write `(?:…)`, since a template names its own groups
broken.yaml:6: error[TL_102]: type `Unit` must be named by a slug
broken.yaml:7: error[TL_101]: type `size`: a list of choices holds one or more non-empty strings
5 errors, 0 warnings
[exit 1]
If a formats file defines a reserved type, the command stops with exit code 2. It prints the fault under TL_29, the key that the fault has in a schema, as TL_124 states.
Lookup order
tramlines looks up a type by its name in the following layers, the nearest first, and takes the first entry it finds:
- The
typesof the schema. - The
typesof the formats file, when--formatsnames one. - The built-in types.
An entry in a nearer layer replaces an entry of the same name in a farther one. tramlines builds each format of a schema with the types that the lookup finds for that schema. A schema can therefore redefine a type in a shared format.
In the following example, the formats file defines the format ingredient and the types unit and amount. The schema adds tsp to unit, and uses amount from the formats file:
formats.yaml
registry:
ingredient:
template: '${:amount} ${:unit} ${food}'
example: 250 g flour
types:
unit: [g, ml]
amount: '\d+'
baking.yaml
line-limit: 100
types:
unit: [g, ml, tsp]
sections:
- heading: Ingredients
level: 2
blocks:
- kind: bullet-list
each: { format: ingredient }
$ tramlines validate --schema baking.yaml --formats formats.yaml scones.md
scones.md:4: error[TL_74]: `1.5 tsp baking powder` fails at column 1: expected an `amount` (`\d+`), found `1.5 tsp baking powder`; an item of this kind reads 250 g flour (schema baking, section "Ingredients" block 1)
scones.md:5: error[TL_74]: `2 cups milk` fails at column 3: expected a `unit` (one of `g`, `ml`, `tsp`), found `cups milk`; an item of this kind reads 250 g flour (schema baking, section "Ingredients" block 1)
2 errors, 0 warnings
[exit 1]
- The format uses the
unitof the schema, so it acceptstsp, and the error forcupslists three choices. - The format uses the
amountof the formats file,\d+, which allows no decimal point, so1.5fails.
The specification defines the lookup in TL_27, the reserved names in TL_28, and the checks on a type in TL_29, TL_101, TL_102, TL_109, and TL_118.
How the grammar layers fit together
The keys that a schema can use come from one grammar. tramlines builds that grammar from up to three layers: the core grammar, a format grammar, and a host extension. Each layer adds keys to the layer under it. No layer removes or changes a key.
Why layers
Some parts of a schema depend on the format of the document, and some don’t. The frontmatter, the lifecycle, the line formats, the types, and the prohibited patterns mean the same thing in every format. Headings, blocks, and list items exist only in Markdown. A program that embeds tramlines is called its host. A host can also need keys of its own, such as links between documents.
The layers keep each concern in one place. A second format can reuse the core grammar unchanged, and a host can add what it needs without forking the format grammar. Because a layer only adds keys, a schema that is valid against the core and Markdown grammars means the same thing under every host.
The three layers
The engine reads the layers in order and merges each one onto the result so far.
| Layer | Names itself with | Defines |
|---|---|---|
| core grammar | grammar: tramlines | The vocabulary that every grammar file is written in, and the schema keys that every format shares. |
| format grammar | grammar: tramlines-md, base: tramlines | What the body of a document of its format contains. |
| host extension | extension: <name>, extends: tramlines-md | The keys and rule maps that the host reads and checks itself. |
A grammar file can contain two sections: meta, the vocabulary that grammar files are written in, and schema, the vocabulary that a schema uses. Only the core grammar contains a meta section, so every layer is written in one vocabulary.
The core grammar
The core grammar contains the schema keys that apply to every format: line-limit, guidance, lifecycle, frontmatter, frontmatter-open, prohibited-patterns, formats, types, and examples. It also defines the rule maps behind them: a frontmatter constraint, a lifecycle dimension, an example, a format and its variants, and a key derivation. The schema-keys reference lists them.
In the vocabulary that the meta section defines, each key takes one of these types: string, integer, boolean, list, map, object, string-or-list, string-or-map, text, or text-or-rule. The definition of a key can also specify the type of its elements, whether the key is required, its allowed values, a format, the keys that it requires, and its documentation. A rule map can require that exactly one, or at least one, of a group of keys is present.
The Markdown format grammar
The Markdown grammar, tramlines-md, names the core grammar with base: tramlines. It adds the vocabulary of a Markdown body:
- The schema keys
preamble,sections,sections-ordered, andsections-prohibited, and the rule that a schema declares at least one ofpreambleandsections. - The rule maps
section,heading,preamble,block,unit, andprohibited-heading.
A format grammar can add keys, rule maps, values to an enum, block kinds that a key requires, text to the documentation of a key, and the “exactly one of” and “at least one of” groups. It can’t change what the core grammar defines. It has no meta section, and it changes no type, element type, or required flag of a core key. The engine refuses a format grammar that tries.
Host extensions
A host extension names the format grammar that it extends, with extends. It can add keys, rule maps, values to an enum, block kinds that a key requires, text to the documentation of a key, and sections of its own. It has no meta section and no constraint group, and it changes nothing that the grammar under it defines. The engine refuses an extension that breaks one of these rules. It also refuses an extension whose extends names a format grammar other than the one that the engine reads.
The engine keeps a key that a host adds as written, and leaves the host to read and check it. A host can therefore give schemas keys that the engine doesn’t know, such as links between documents in a store.
What this means for a schema
The tramlines command runs with no host, so it reads only the core and Markdown grammars. A key that a host extension defines is an unknown key to the command.
This schema uses links, a key that a host might add:
list.yaml
line-limit: 0
links:
- rel: depends-on
sections:
- heading: Groceries
level: 1
blocks:
- kind: bullet-list
example: Milk
list.md
# Groceries
- Milk
$ tramlines validate --schema list.yaml list.md
list.yaml:2: error[TL_12]: unknown key `links` in `schema`
1 error, 0 warnings
[exit 1]
The command reports the key as an error in the schema, because none of the layers that the command reads defines the key. Under a host whose extension defines links, the same schema is valid. Write a schema for the layers that its documents are checked under, and validate the schema with the program that checks those documents.
See also
- TL_1: the keys of the core grammar.
- TL_3 and TL_4: what a format grammar can add, and what makes the engine refuse it.
- TL_6, TL_7, and TL_8: what a host extension can add, and what makes the engine refuse it.
- TL_9: the engine keeps a key that a host adds for the host to check.
- TL_12 and TL_101: a key that the grammar doesn’t define, and a value of the wrong type, are reported on the schema.
- Built-in types
- Embed the engine in a Rust program
Strict by default
A schema is strict. What it declares is required, what it doesn’t declare is refused, and a section contains exactly the blocks that the schema declares for it, in order. So tramlines reports a heading that the schema doesn’t mention. You loosen each rule with a key that you set on purpose.
Why strict
A schema exists to keep documents of one kind alike, so that a reader, a script, or an agent can rely on their shape. A lenient checker passes a document with an extra section, a misspelled frontmatter key, or a missing part. The documents then drift apart unnoticed until someone depends on their shape. A strict checker reports each fault at the line where it occurs.
Strictness also keeps a schema complete. Every part that a document can contain is written in the schema. The schema is therefore the full description of the kind, and the scaffold derived from it contains every part that the schema declares.
The three rules
Three rules apply unless the schema relaxes them:
- What is declared is required. A declared section, frontmatter key, or block must be present exactly once.
- What is not declared is refused. A heading, frontmatter key, table column, or block that the schema doesn’t declare is an error.
- A declaration is exclusive. A section’s body contains exactly its declared blocks, in the declared order, and the sections come in the declared order.
This schema describes a shopping list with a title and one list:
list.yaml
line-limit: 0
frontmatter:
title:
example: Groceries
sections:
- heading: { from: title }
level: 1
blocks: []
- heading: Items
level: 2
blocks:
- kind: bullet-list
example: Milk
This document adds a frontmatter key and a section that the schema doesn’t declare:
list.md
---
title: Groceries
store: Corner shop
---
# Groceries
## Items
- Milk
- Bread
## Notes
Pay by card.
$ tramlines validate --schema list.yaml list.md
list.md:3: error[TL_55]: frontmatter key `store` is not declared (schema list)
list.md:13: error[TLMD_19]: heading `## Notes` is not declared (schema list)
2 errors, 0 warnings
[exit 1]
The command reports each undeclared part as one error, at the line of that part.
The relaxations
Each relaxation is a key, and each loosens one rule for one part of the schema. Nothing loosens by default, so a reader of the schema sees every exception where it applies.
| Key | Loosens | Where |
|---|---|---|
optional: true | A declared part can be absent. | A section, a block, a frontmatter key, a position in a sequence. |
repeat: true | A declared part can occur more than once. | A section, a block, a position in a sequence. |
frontmatter-open: true | Undeclared frontmatter keys are accepted. | The schema. |
headings-open: true | Undeclared headings of the section’s level or deeper can follow it. | A section. |
subheadings-open: true | Undeclared headings deeper than the section can follow it. | A section. |
columns-open: true | Further columns after the declared ones are accepted. | A table block. |
units-open: true | A unit that no rule under rules takes is accepted. | A block. |
lines-open: true | A line that no rule under lines takes is accepted. | A unit rule with lines. |
sections-ordered: false | Sections can come in any order. | The schema. |
blocks-ordered: false | Blocks can come in any order. | A section. |
The core grammar defines frontmatter-open, and the Markdown grammar defines the rest. A host extension can add -open keys of its own. The schema-keys reference describes each key in full.
The table leaves out min and max, because they are bounds. They set how many units a block contains, and they default to one and no limit.
Relaxing the example
To accept the extra key and section, the schema relaxes the two rules that the document broke, and nothing else. frontmatter-open accepts any frontmatter key. A Notes section with optional: true accepts the extra section without requiring it in every list:
list.yaml
line-limit: 0
frontmatter-open: true
frontmatter:
title:
example: Groceries
sections:
- heading: { from: title }
level: 1
blocks: []
- heading: Items
level: 2
blocks:
- kind: bullet-list
example: Milk
- heading: Notes
level: 2
optional: true
blocks:
- kind: prose
example: Pay by card.
$ tramlines validate --schema list.yaml list.md
0 errors, 0 warnings
A declaration of store under frontmatter, with optional: true, is the narrower choice. It accepts that one key and still refuses a misspelling of it.
An open region
An open section accepts undeclared headings after it, up to the next declared heading. tramlines checks the bodies under those headings only against the rules that apply to the whole document, such as prohibited-patterns and line-limit. Use an open region for a part whose structure the schema can’t predict, such as free notes, and keep it as small as you can. To ban particular headings inside an open region, list them under sections-prohibited.
Absence is strict too
A key that you leave out never relaxes a rule. A section with no blocks must contain exactly one prose block. A block with no optional is required. A table with no columns-open contains exactly its declared columns. Pitfalls in writing a schema shows the errors that follow.
See also
- TL_54 and TL_55: a declared frontmatter key is required, and an undeclared one is refused unless
frontmatter-openis set. - TLMD_18, TLMD_151, and TLMD_19: a declared section is required once, and an undeclared heading is refused.
- TLMD_22: sections come in the declared order.
- TLMD_24 and TLMD_25: what an open region accepts.
- TLMD_39, TLMD_152, and TLMD_40: a body has exactly its declared blocks, and no
blocksmeans one prose block. - TLMD_44: a table has its declared columns.
- Describe sections and headings
- Constrain frontmatter and ids
Frontmatter as the authority over the body
When a heading and a frontmatter value disagree, the frontmatter is right. A schema can bind a heading or a paragraph to a frontmatter value. The check then requires the body to agree with the frontmatter, never the other way round. The frontmatter also holds the lifecycle state of a document, and that state decides which rules apply to the document.
Why the frontmatter is the authority
Frontmatter is a YAML map. Each value has a name, and a program reads it without parsing prose. Tools that list, index, or move documents read the frontmatter, so its copy of a fact is the one that those tools see. When the same fact appears twice, once in the frontmatter and once in the body, one copy must be the authority. Otherwise the two drift apart, and nobody knows which to trust.
With the frontmatter as the authority, every disagreement has one fix: change the body to match. A program that repairs documents, such as a host that embeds tramlines, can make that fix itself without guessing. The same authority applies to a new document. tramlines derives its body from its frontmatter, so the two start out equal.
Binding body text with from
A schema binds a part of the body to a frontmatter key with { from: <key> }:
heading: { from: title }on a section requires the heading to equal the value oftitle.text: { from: description }on a unit rule requires a paragraph, an item, or a line to equal the value ofdescription.
In this schema, the heading of a recipe is its title, and its first paragraph is its description:
recipe.yaml
line-limit: 0
frontmatter:
title:
example: Pancakes
description:
example: Thin pancakes for four people.
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
each: { text: { from: description } }
The body of this document doesn’t match its frontmatter:
pancakes.md
---
title: Pancakes
description: Thin pancakes for four people.
---
# Crepes
Thin pancakes for two people.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:6: error[TLMD_165]: heading `Crepes` must equal the frontmatter `title`: `Pancakes` (schema recipe, section "<title>")
pancakes.md:8: error[TLMD_165]: `Thin pancakes for two people.` must equal the frontmatter `description`: `Thin pancakes for four people.` (schema recipe, section "<title>" block 1)
2 errors, 0 warnings
[exit 1]
Each error names the frontmatter key and its value. The value is the text to write. tramlines validate reports the disagreement and leaves the file as it is. With --fix, it rewrites the heading and the paragraph to match the frontmatter values, as a host that applies the engine’s repairs does.
A { from: … } binding is the only way to tie body text to frontmatter. A literal text, such as text: RESPONSIBILITIES, sets a label that’s the same in every document.
Lifecycle states live in the frontmatter
A schema can declare a lifecycle. A lifecycle has one or more dimensions, and each dimension is a list of states:
lifecycle:
- { key: state, states: [open, closed] }
Each dimension is a frontmatter key that every document must contain, with one of its states as the value. You don’t declare it again under frontmatter. The state of a document is the value of each of these keys. A rule that sets only applies only in the states that it lists. The frontmatter therefore decides which sections and blocks the body must contain, as well as the text of a heading.
This schema requires an Outcome section when a meeting note is closed:
meeting.yaml
line-limit: 0
lifecycle:
- { key: state, states: [open, closed] }
frontmatter:
title:
example: Release planning
sections:
- heading: { from: title }
level: 1
blocks: []
- heading: Agenda
level: 2
blocks:
- kind: bullet-list
example: Pick the release date.
- heading: Outcome
level: 2
only: { state: closed }
blocks:
- kind: prose
example: The release ships on 3 March.
The same body passes while the note is open and fails when it’s closed:
planning.md
---
title: Release planning
state: closed
---
# Release planning
## Agenda
- Pick the release date.
$ tramlines validate --schema meeting.yaml planning.md
planning.md: error[TLMD_18]: section `## Outcome` is missing (schema meeting, section "Outcome")
1 error, 0 warnings
[exit 1]
With state: open, the Outcome section doesn’t apply:
planning.md
---
title: Release planning
state: open
---
# Release planning
## Agenda
- Pick the release date.
$ tramlines validate --schema meeting.yaml planning.md
0 errors, 0 warnings
To close a note, you change one frontmatter value. The check then reports what the body of the closed note must contain. An id template can include the state too, such as note-${state}-${:seq}. The check then requires the id to agree with the state key. Apply a rule in some states only shows how to write these rules.
A document with no frontmatter
A document doesn’t have to open with frontmatter. When it doesn’t, the check reads it as if its frontmatter were an empty map. The rules stay the same: every key that the schema requires is missing, and every lifecycle key is missing.
notes.md
# Release planning
## Agenda
- Pick the release date.
$ tramlines validate --schema meeting.yaml notes.md
notes.md:1: error[TL_54]: frontmatter key `title` is missing (schema meeting, key `title`)
notes.md:1: error[TL_62]: lifecycle key `state` is missing (schema meeting)
2 errors, 0 warnings
[exit 1]
Each missing key causes one error: the key title and the lifecycle key state. When you add the frontmatter, both errors go away.
A schema that declares no frontmatter key and no lifecycle accepts a document with no frontmatter, because an empty map breaks none of its rules.
See also
- TL_94: a document with no frontmatter is checked as if its frontmatter were an empty map.
- TL_61, TL_62, and TL_108: the state of a document is read from its frontmatter, and a missing or unknown state is reported.
- TL_19: a rule with
onlyapplies only in the listed states. - TL_59: an id that includes a state must agree with the state key.
- TLMD_136 and TLMD_165: a heading or unit bound to a frontmatter value is reported and repaired to that value.
- Constrain frontmatter and ids
- Apply a rule in some states only
How blocks are matched to a schema
An error can name a block that you didn’t expect, or report expected a code block; found a prose block. Such an error follows from how tramlines matches the blocks of a section to the blocks that the schema declares. tramlines matches them one position at a time, in order. Each position takes a block of its own kind. A position gives the block back only when the block fails it and the schema made the position optional, repeating, or a group. With these steps, you can tell which rule an error comes from.
What a block is
tramlines reads the body of a document as CommonMark, with GitHub tables, footnotes, and task lists. It splits each section into blocks. A few conventions decide where one block ends and what counts as a block:
- Consecutive paragraphs are one
proseblock. Each paragraph is one unit of that block. - A paragraph that contains only images is an
imageblock. - Two lists with the same marker, one after the other, are one list.
- Generated blocks, the interior of a host’s region, footnote definitions, and HTML comments aren’t matched at all.
The kinds are prose, bullet-list, numbered-list, table, code, blockquote, image, html, thematic-break, references, and group. A host can add a region kind. Each block consists of units: the paragraphs of prose or a quote, the items of a list, the rows of a table, and the repetitions of a group.
Positions
Each entry under a section’s blocks is a position. The body must contain exactly the blocks that its positions declare, in order, unless the section sets blocks-ordered: false. A section with no blocks has one position, a prose block.
The engine walks the blocks of the section and the positions together. For each block, the kind comes first: a position takes only a block of its own kind.
A block of a kind no position expects
When no later position declares the kind of a block, tramlines reports the block once and carries on past it. The block after it still meets its own position, and tramlines reports no further error for the unexpected block.
This schema declares one code block in a recipe’s method:
recipe.yaml
line-limit: 0
sections:
- heading: Method
level: 1
blocks:
- kind: code
lang: text
example: Whisk the eggs into the flour.
The document contains a paragraph before its code block:
pancakes.md
# Method
Whisk well.
```text
Whisk the eggs into the flour.
```
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:3: error[TLMD_39]: unexpected prose block (schema recipe, section "Method")
1 error, 0 warnings
[exit 1]
When no code block follows, the paragraph is all the position meets. The error then names both kinds in one finding, the kind that the position expected and the kind that it found:
pancakes.md
# Method
Whisk the eggs into the flour.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:3: error[TLMD_69]: expected a code block; found a prose block (schema recipe, section "Method" block 1)
1 error, 0 warnings
[exit 1]
Claims
When a block meets a position of its kind, the position checks it and either claims it or releases it. The choice depends on how the schema declared the position.
A required position that occurs once claims its block, whatever the check reports. The schema requires the block, so tramlines takes a block of that kind in that place as the required block, even when it’s malformed. The errors land on that block, at its lines.
An optional, repeating, or group position claims a block only when the check reports no error. When the check fails, the position releases the block. The engine throws away what the check found and offers the block to the later positions that declare its kind. When the check reports only warnings, the position still claims the block.
The second rule lets a schema declare two positions of one kind that differ by their rules. This schema allows an optional tip before a required warning, and both are blockquotes:
recipe.yaml
line-limit: 0
sections:
- heading: Method
level: 1
blocks:
- kind: blockquote
optional: true
each:
template: 'Tip: ${}'
example: 'Tip: rest the batter for an hour.'
- kind: blockquote
each:
template: 'Warning: ${}'
example: 'Warning: the pan is hot.'
The document contains only the warning. The optional tip position meets the quote first. The quote fails the tip rule, so the tip position releases it. The warning position then receives the quote that the schema wrote it for:
pancakes.md
# Method
> Warning: the pan is hot.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
When a quote fits neither position, the tip releases it and the warning claims it, because the warning is required once. The error comes from the warning’s rule:
pancakes.md
# Method
> Note: the pan is hot.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:3: error[TL_74]: `Note: the pan is hot.` fails at column 1: expected `Warning: `, found `Note: the pan is hot.`; a paragraph of this kind reads Warning: the pan is hot. (schema recipe, section "Method" block 2)
1 error, 0 warnings
[exit 1]
The same release lets a schema declare a repeating yaml code block before a text one, or a group before a block of the group’s first inner kind. Each block goes to the position written for it.
When a released block finds no home
A released block isn’t lost. When no later position claims a released block, the engine reports the error that made the last position release it, at that block. When a position releases a block of its own kind and then ends up with no block, the engine reports why the position refused that block, not only that the block is missing.
The claim test is the same whether the section fixes the block order or sets blocks-ordered: false.
Groups
A group repeats a sequence of blocks as one unit, such as a step and its quote, then the next step and its quote. A group always repeats. It takes every repetition it can, and its min and max bound how many.
When a repetition is incomplete, the engine releases every block that the repetition claimed, so that later positions can take them. When releasing the blocks would hide the cause, the engine keeps them in the group and reports why the repetition failed. That happens in three cases: the group needs the repetition to reach its min, no later position takes the opening block of the repetition, or the group’s own rule accepted that opening block. When a block fits an inner block other than the first, and no later position takes it, the engine reads it as a repetition whose opening blocks are missing. The error names the missing kind.
This schema describes a method as steps. Each step is a paragraph followed by a quote with a tip:
recipe.yaml
line-limit: 0
sections:
- heading: Method
level: 1
blocks:
- kind: group
blocks:
- kind: prose
max: 1
each:
template: 'Step ${:int}: ${}'
example: 'Step 1: whisk the eggs into the flour.'
- kind: blockquote
max: 1
example: Use a fork if you have no whisk.
The second step has no tip. Its paragraph matches the group’s first inner block, so the group keeps it and reports the missing quote. In the error, block 1.2 names the second inner block of the first position:
pancakes.md
# Method
Step 1: whisk the eggs into the flour.
> Use a fork if you have no whisk.
Step 2: fry the batter.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:7: error[TLMD_152]: expected a blockquote block (schema recipe, section "Method" block 1.2)
1 error, 0 warnings
[exit 1]
In this document the second tip has no step. The quote fits the group’s second inner block, so the engine reads it as a repetition whose opening paragraph is missing:
pancakes.md
# Method
Step 1: whisk the eggs into the flour.
> Use a fork if you have no whisk.
> Use butter, not oil.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:7: error[TLMD_69]: expected a prose block; found a blockquote block (schema recipe, section "Method" block 1.1)
1 error, 0 warnings
[exit 1]
Extents: dividing a run between rules
Some runs of units are one block even when the schema declares separate positions for them. Consecutive paragraphs are one prose block, and two consecutive lists with the same marker are one list. When a section declares two or more positions of that kind in a fixed order, the positions divide the run between them. A group that opens and closes with a position of that kind counts its next repetition as one more position, so its repetitions divide a run too. A group that opens with a position of that kind counts as one more position, whatever closes it. Sometimes the block after the run fits such a group only after the group’s opening, such as a code block after the sentence that introduces it. The run then leaves to the group the units that the group’s opening needs.
The first position takes its extent. The extent is the longest run of units that passes the position’s rule with no error, within its min and max. The next position starts where the first one stops. When a unit is left over after the last position that allows it, tramlines reports the unit at its own line. An optional block between the two positions doesn’t change the division of the run. A position with no max and no shape takes the whole run. So tramlines can’t derive a scaffold from a schema that puts another position of the same kind directly after it, and reports TLMD_149.
This schema gives a recipe a paragraph that states how many people the recipe serves, then its method:
recipe.yaml
line-limit: 0
sections:
- heading: Pancakes
level: 1
blocks:
- kind: prose
max: 2
each:
template: 'Serves ${:int}.'
example: Serves 4.
- kind: prose
max: 2
example: Whisk the eggs into the flour.
The first position allows two paragraphs. The second paragraph isn’t a serving line, so the extent of the first position is one paragraph. The second position takes the other two:
pancakes.md
# Pancakes
Serves 4.
Whisk the eggs into the flour.
Fry each pancake for a minute a side.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
A fourth paragraph is beyond the last position’s max. tramlines reports it at its line, as a unit that remains after the last prose position:
pancakes.md
# Pancakes
Serves 4.
Whisk the eggs into the flour.
Fry each pancake for a minute a side.
Serve warm.
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:9: error[TLMD_50]: `Serve warm.` remains after the last prose position that takes the run (schema recipe, section "Pancakes")
1 error, 0 warnings
[exit 1]
A required position that occurs once claims its block, whatever the check reports. When no extent passes such a position, the position takes the longest extent that its bounds allow and reports the errors in that extent. A document that passes under the bounds alone never fails because of this division.
The division applies only to prose, bullet-list, and numbered-list blocks, only where the block order is fixed, and only where the section declares two or more positions of the kind. With blocks-ordered: false, a run is never divided.
Reading a surprising error
When a block error surprises you, ask these questions in order:
- Is the block the kind you think? Two paragraphs with nothing between them are one prose block, and two consecutive lists with the same marker are one list.
- Which position met it first? The positions run in schema order.
- Was that position required once? Then it kept the block, and the error is that position’s.
- Was it optional, repeating, or a group? Then it released the block on an error, and the error comes from a later position, or from the last one that tried the block.
Read an error and fix the document walks through a finding, and the error reference lists each block error with its fix.
See also
- TLMD_5: consecutive paragraphs are one prose block.
- TLMD_39, TLMD_152, and TLMD_41: a body has exactly its declared blocks, and which blocks are exempt.
- TLMD_50: the units of a run that no position takes are reported at the first of them.
- TLMD_169: a required block that stands after a block declared later is reported once, as out of place.
- TLMD_59 to TLMD_67: claims, releases, and what a released block reports.
- TLMD_68 to TLMD_70: a position steps over a stray block, and a block of an unexpected kind is named with the expected kind in one error.
- TLMD_71 to TLMD_73: groups.
- TLMD_48 to TLMD_51 and TLMD_74 to TLMD_80, TLMD_193, and TLMD_194: extents.
- Describe blocks and their units
How a schema derives its scaffold
A scaffold is the document that a new document starts from. The engine builds scaffolds from the rules of a schema, one for each combination of lifecycle states, and checks each one against the schema. tramlines new prints the scaffold of the states that you name. A host program, such as an editor plugin, hands it to the writer of a new document. tramlines validate checks the scaffolds of each schema before it checks a document. A schema author therefore also sees a scaffold through the errors that it causes.
Where the text of a scaffold comes from
The structure of a scaffold comes from the schema: its frontmatter keys, its sections, and its blocks. Its words come from the schema too, and from nowhere else:
- The
textthat a rule fixes, such as a heading that is the same in every document - The
exampleof each rule that requires text - The
exampleof the format that a rule uses, when the rule has noexampleof its own - The
idthat thetemplateofidproduces for the states of the scaffold
The engine writes no text of its own. Filler such as “Write the summary here” would pass the check, tell the writer nothing, and be copied into every new document. So when a required part has no text and no example, the engine can’t derive the scaffold, and the command reports the error in the schema:
note.yaml
line-limit: 0
sections:
- heading: Note
level: 1
blocks:
- kind: prose
note.md
# Note
Buy milk.
$ tramlines validate --schema note.yaml note.md
note.yaml:6: error[TLMD_116]: the schema derives no scaffold: a `prose` block has no `example`, and the scaffold writes no text of its own; give the block an `example`
1 error, 0 warnings
[exit 1]
A rule’s example appears in two places: in every error that the rule reports, as a line of the kind that the rule expects, and in every new document. Write examples and guidance shows how to write one.
Guidance and markers
A scaffold contains three kinds of marker. Each marker is an HTML comment, or a YAML comment in the frontmatter:
- A
guidanceline before a part holds theguidanceof the rule for that part: what no example can show. The first line of every scaffold is a guidance line that tells the writer to replace each part with their own text. The writer reads each guidance line and then deletes it. - An
onlymarker before a part names the states that the part applies in, from theonlyof its rule. A host keeps or drops each part according to the states of the new document. - An
optionalregion wraps a part that the schema doesn’t require. The writer keeps its content and deletes both markers, or deletes the region.
The markers belong only in the scaffold. In a document that a schema checks, a guidance or only marker is an error, and an optional region left in place is a warning. Keys, markers, and scaffolds lists these errors.
Why the scaffold is checked
Checking a scaffold tests the schema against its own rules. When a scaffold fails, every new document starts with an error before its writer types a word. So the engine checks each scaffold against the schema, in the states of that scaffold. It also checks that each only marker agrees with the rule that the marker gates. It reports any failure as an error in the schema, with the key of the criterion that the scaffold breaks.
In this schema, each example passes its own rule, and note.md passes too. The scaffold also writes the optional list, so the scaffold is longer than the line-limit:
note.yaml
line-limit: 3
sections:
- heading: Note
level: 1
blocks:
- kind: prose
each: { pattern: '^[A-Z].*\.$', example: 'Buy milk.' }
- kind: bullet-list
optional: true
example: Eggs
$ tramlines validate --schema note.yaml note.md
note.yaml:1: error[TLMD_164]: the scaffold fails: [TL_21] the document has 5 lines; the limit is 3; split it (schema note)
1 error, 0 warnings
[exit 1]
The engine checks each example against its own rule first. When an example fails its rule, the scaffold line that holds the example fails too. The example check reports that fault, so the scaffold check doesn’t report it again. Raise the limit, and the scaffold passes:
note.yaml
line-limit: 5
sections:
- heading: Note
level: 1
blocks:
- kind: prose
each: { pattern: '^[A-Z].*\.$', example: 'Buy milk.' }
- kind: bullet-list
optional: true
example: Eggs
$ tramlines validate --schema note.yaml note.md
0 errors, 0 warnings
Optional parts
An optional section or block is in the scaffold too, inside an optional region, because the schema author still wants the writer to see it. Each optional section has a region of its own, so the writer keeps one section and deletes the next. Two regions inside one section, with only blank lines between them, become one, so the writer keeps or deletes two optional blocks of a section together. When a part inside a region has no text to write, the scaffold leaves it out, and a warning names the rule that has no example.
A fence with its own text
The engine builds a code block whose rule declares lines from the line rules, one line for each rule. A diagram built this way repeats one line many times. Instead, give the rule an example that holds the whole block. The scaffold copies the example, and the line rules read their keys from the copy.
See also
- TLMD_114: one scaffold for each combination of states, checked against the schema.
- TLMD_115 and TLMD_116: the engine writes no text itself.
- TLMD_122: markers that agree with their rules.
- TLMD_123 to TLMD_125, and TLMD_192: optional regions.
- TLMD_126: a fence with its own text.
- Embed the engine in a Rust program
Pitfalls in writing a schema
Most mistakes in a schema come from two sources: a regular expression that matches more than you meant, and a key that you left out. A key that you leave out keeps its strict default. Each of the following sections shows one pitfall, a short example, and what the check reports.
An unanchored pattern
A pattern isn’t anchored, so a line passes when the pattern matches anywhere in it. A pattern that you meant to match a whole line also accepts any longer text that contains a match.
The author of this schema wants each ingredient to read as a weight and a name:
recipe.yaml
line-limit: 0
sections:
- heading: Ingredients
level: 1
blocks:
- kind: bullet-list
each:
pattern: '[0-9]+ g'
example: 200 g flour
pancakes.md
# Ingredients
- 200 g flour
- flour, about 200 g or so
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
The second item passes, because it contains 200 g. Anchor the ends with ^ and $:
recipe.yaml
line-limit: 0
sections:
- heading: Ingredients
level: 1
blocks:
- kind: bullet-list
each:
pattern: '^[0-9]+ g [a-z]+$'
example: 200 g flour
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:4: error[TL_74]: `flour, about 200 g or so` does not match `^[0-9]+ g [a-z]+$`; an item of this kind reads 200 g flour (schema recipe, section "Ingredients" block 1)
1 error, 0 warnings
[exit 1]
The same applies to a heading: a heading pattern: 'Notes' accepts every heading that contains the word. Write a fixed heading as a string, heading: Notes.
A pattern where a template would do
tramlines matches a template whole, so a template needs no anchors. The error for a template shows where the text stops matching and what the template expects there. The error for a pattern can only quote the pattern. This schema writes the anchored pattern of the preceding section as a template:
recipe.yaml
line-limit: 0
sections:
- heading: Ingredients
level: 1
blocks:
- kind: bullet-list
each:
template: '${:int} g ${}'
example: 200 g flour
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:4: error[TL_74]: `flour, about 200 g or so` fails at column 1: expected an `int` (`\d+`), found `flour, about 200 g or so`; an item of this kind reads 200 g flour (schema recipe, section "Ingredients" block 1)
1 error, 0 warnings
[exit 1]
Compare the two errors: the template error names column 1 and the int that it expected there. Use a pattern only for what a template can’t express. When a part of a line is one of a few choices, name it once as a type, such as unit: [g, kg, ml], and write ${:unit} in the template. Write the shape of a line shows how.
A guard that repeats the rule’s shape
Under rules, a rule with a shape and no select-pattern already takes the units that its shape matches. A select-pattern that restates the shape adds a second copy to keep in step with the first. The check finds the same faults without it.
method.yaml
line-limit: 0
sections:
- heading: Method
level: 1
blocks:
- kind: numbered-list
rules:
- select-pattern: '^Step '
template: 'Step ${:int}: ${}'
min: 1
example: 'Step 1: whisk the eggs.'
pancakes.md
# Method
1. Step 1: whisk the eggs.
2. Fry the batter.
$ tramlines validate --schema method.yaml pancakes.md
pancakes.md:4: error[TLMD_53]: `Fry the batter.` matches no rule of the block; declare it or set `units-open` (schema method, section "Method" block 1)
1 error, 0 warnings
[exit 1]
Drop the guard, and the rule selects units by its template:
method.yaml
line-limit: 0
sections:
- heading: Method
level: 1
blocks:
- kind: numbered-list
rules:
- template: 'Step ${:int}: ${}'
min: 1
example: 'Step 1: whisk the eggs.'
$ tramlines validate --schema method.yaml pancakes.md
pancakes.md:4: error[TLMD_53]: `Fry the batter.` matches no rule of the block; declare it or set `units-open` (schema method, section "Method" block 1)
1 error, 0 warnings
[exit 1]
Keep a guard for a rule that picks its units by something other than its shape, such as a list item picked by its label to check its nested list, or a counter that bans a line.
Lookaround in a regular expression
tramlines reads regular expressions in the Rust dialect, which has no look-ahead, no look-behind, and no backreferences. A pattern that uses one is an error in the schema:
recipe.yaml
line-limit: 0
sections:
- heading: Ingredients
level: 1
blocks:
- kind: bullet-list
each:
pattern: '^(?!TODO).+$'
example: 200 g flour
$ tramlines validate --schema recipe.yaml pancakes.md
recipe.yaml:8: error[TL_118]: `pattern` is not a valid regex: look-around, including look-ahead and look-behind, is not supported
1 error, 0 warnings
[exit 1]
In place of a negative look-ahead, ban the text with prohibited-patterns. Ban text in a document shows how.
Leaving a key out
A key that you leave out never loosens a rule. The rule keeps its strict default.
- A section with no
blocksmust contain exactly one prose block. - A part with no
optionalis required. - A table with no
columns-opencontains exactly its declared columns.
A section with no blocks causes two errors. One is in a document that has a list in that section. The other is in the schema itself, because the scaffold has no text to write for the prose block:
recipe.yaml
line-limit: 0
sections:
- heading: Ingredients
level: 1
pancakes.md
# Ingredients
- 200 g flour
$ tramlines validate --schema recipe.yaml pancakes.md
pancakes.md:3: error[TLMD_69]: expected a prose block; found a bullet-list block (schema recipe, section "Ingredients" block 1)
recipe.yaml:3: error[TLMD_116]: the schema derives no scaffold: a section with no `blocks` holds one prose block, and the scaffold writes no text of its own; declare the block and its `example`
2 errors, 0 warnings
[exit 1]
Declare each block and its example. Strict by default lists every relaxation.
Overriding a shared format in one schema
A format can come from a file of shared formats, passed with --formats, or from a schema’s own formats. The nearer one wins, and an entry in a schema replaces the shared entry whole, for that schema only. When you write into one schema a change that you meant for every schema, the other schemas keep the old format, and nothing reports the difference. Put a change meant for every schema in the shared file, and keep a schema’s own entry for what that schema alone does differently. Share line formats across schemas shows the layers.
Copying markers from a scaffold
A scaffold is the document that a new document starts from. It contains markers: an only marker before each part that some states need, and a guidance line before each part that has guidance. The markers guide the person who fills in the scaffold. A new document is the scaffold gated to its states. It holds the parts that its states need, with no only markers, and it keeps the guidance lines for you to read, act on, and delete. tramlines new prints it. A marker that you copy by hand, or a guidance line that you leave behind, stays in the document. The check then fails at the marker’s line:
meeting.yaml
line-limit: 0
sections:
- heading: Agenda
level: 1
guidance: One item for each decision the meeting must make.
blocks:
- kind: bullet-list
example: Pick the release date.
planning.md
<!-- tramlines:guidance One item for each decision the meeting must make. -->
# Agenda
- Pick the release date.
$ tramlines validate --schema meeting.yaml planning.md
planning.md:1: error[TLMD_158]: a `tramlines:guidance` line is left here: write the part it guides, then delete the line
1 error, 0 warnings
[exit 1]
Write the part that the line guides, then delete the line.
An example shaped like a prompt
The example of a rule is the text that the scaffold writes, so every new document starts with a copy of it. An example written as a slot (<ingredient>), an instruction (Add an ingredient here.), or filler (One item.) passes a loose rule. The check then reports nothing, even when a document keeps the example word for word:
recipe.yaml
line-limit: 0
sections:
- heading: Ingredients
level: 1
blocks:
- kind: bullet-list
each:
template: '${}'
example: Add an ingredient here.
pancakes.md
# Ingredients
- Add an ingredient here.
$ tramlines validate --schema recipe.yaml pancakes.md
0 errors, 0 warnings
A person or an agent who fills the scaffold imitates what it shows, and a prompt-shaped example comes back word for word. Write the line that a real document contains, such as 200 g flour, and put what an example can’t show in guidance. Write examples and guidance shows how.
See also
- TL_16 and TL_118: the regular expression dialect, with no lookaround and no anchor you didn’t write.
- TL_24 and TL_26: a template is matched whole, and its failure names the column.
- TL_36: a nearer format replaces a farther one whole.
- TLMD_10 and TLMD_158: an
onlyorguidancemarker in a document is an error. - TLMD_40 and TLMD_116: no
blocksmeans one prose block, and a scaffold with no text for a part fails the schema. - TLMD_55: a rule with a shape and no guard takes the units that its shape matches.
What tramlines doesn’t do
tramlines validates one document at a time against one schema, and reads only the files that you name. It leaves out the work around that check on purpose: links between documents, an editor integration, and other formats and schema languages. A host program can provide that work, or the project can add it when someone needs it.
Why it stays small
tramlines is an engine that other programs embed, and a command that runs the engine on its own. Each feature that it leaves out would tie it to one way of storing, editing, or serving documents. Without those features, the same engine works with every host, and a check gives the same result for the same files on any machine.
Checks across documents
tramlines doesn’t follow links, resolve ids that another document defines, build a graph of documents, or know about a store of documents. When a rule needs information from outside the document, the engine asks the host program. The tramlines command has no host, so it checks only what’s inside each document. For example, the command checks that a key is unique within one document. A host can check that the key is unique across many documents.
Why: the program that manages a set of documents owns the model of how they relate. The engine leaves that model out, so each host can define it in its own way.
Knowledge of any particular host
tramlines knows nothing of any program that embeds it. A host adds its own schema keys through a grammar extension, and answers the engine’s questions about what’s outside a document. The engine keeps the host’s keys as written and leaves them for the host to check.
Why: an engine that knows nothing of its hosts can serve all of them. How the grammar layers fit together explains how a host adds to the grammar.
An editor integration
tramlines has no language server and no editor extension. It prints its findings, one per line, and exits with a code that shows whether any finding is an error.
Why: the host program owns the files and the editor, and decides where to show the findings. The command reference describes the output format to build on.
Formats other than Markdown
tramlines validates only Markdown documents. The command picks the format of a file by its extension, .md for Markdown. On a file with any other extension, the command stops, unless you name a format with --format:
list.yaml
line-limit: 0
sections:
- heading: Groceries
level: 1
blocks:
- kind: bullet-list
example: Milk
list.txt
Groceries: milk, bread
$ tramlines validate --schema list.yaml list.txt
error[TL_90]: list.txt: no format matches the extension `txt`; name one with `--format` (`md`)
[exit 2]
Why: the core grammar leaves room for a second format, but the project builds a second format only when a user needs one.
Schema languages other than YAML
A schema must be YAML 1.2. tramlines doesn’t read JSON Schema, a schema written in code, or any other schema language.
Why: one schema language means one grammar, one set of error messages, and one guide to learn.
Files you didn’t name
tramlines reads only the files that you, or the program that calls it, name: the schema, a file of shared formats, and the documents. It doesn’t search directories for documents, fetch anything, or open a network connection.
Why: a check must give the same findings for the same files, wherever it runs. Finding the files is the job of whoever runs the check, such as a script in continuous integration (CI) or a host program.
See also
- TL_9: the engine keeps a key that a host adds, for the host to check.
- TL_79: a rule is skipped when the host can’t supply what the rule needs.
- TL_85: the command checks each document that it names, with no host.
- TL_88 and TL_90: the format of a document comes from its file extension.
- TL_13: a schema that does not parse as YAML 1.2 is refused.
- Embed the engine in a Rust program
- Check documents in CI
tramlines core specification
This specification defines tramlines: the grammar that a schema uses, and how the engine checks a document against a schema, whatever the format of the document.
tramlines validates a document against a schema and derives the scaffold that a new document starts from. A format grammar adds what is specific to the documents of one format. The markdown specification defines the first format. The host, not the engine, handles the documents around a document. The host also adds vocabulary of its own.
About this specification
Each normative rule of this specification is an EARS criterion, keyed TL_n.
1. Terminology
engine- The library that reads a grammar, checks a schema against it, checks a document against a schema, and derives a scaffold.document- A text of one format, with optional frontmatter and a body, which a schema governs.frontmatter- The optional map of named values, written in YAML, that opens a document. The format grammar defines where it appears.body- The part of a document that is not its frontmatter, which the format grammar describes.unit- The smallest part of a body that one rule checks, such as a paragraph, an item, or a line. The format grammar defines the units of its format.schema- A YAML map in the vocabulary of a grammar, that defines what a document of one kind contains.grammar- The vocabulary in which a schema is written: the core grammar, one format grammar, and the host extensions, merged.core grammar- The filetramlines.grammar.yaml: the meta vocabulary, and the schema keys that every format shares.format grammar- The grammar file of one format, which names the core grammar as its base, such astramlines-md.grammar.yaml.host extension- The grammar file of a host, which names a format grammar and only adds to it.host- The program that uses the engine. It selects the schema that governs each document, resolves what is outside a document, and optionally extends the grammar.rule- One entry of a schema that a part of a document satisfies or breaks.shape- The required form of the text of one unit: atext, atemplate, or apattern.type- A named regular expression, or a list of choices, that a hole of a template matches.format- A named shape of one line, with its example, that a rule binds by name.registry- One layer of named formats and types: the schema, the host, or the shared layer.example- Text of the kind that a rule demands, which the engine checks against the rule.guidance- What an example cannot express, which the scaffold includes and no finding prints.lifecycle dimension- One ordered list of states, stored in one frontmatter key.key- An identifier that a rule captures from a document.scaffold- The document that a schema derives, which a new document starts from.finding- One failure of a rule, on one line of one file.edit- A replacement of lines of a text: the form of every repair.command- Thetramlinesprogram, which checks documents against one schema with no host.
2. Grammars
A grammar file is YAML. Its meta section is the vocabulary in which a grammar file is written, and its schema section is the vocabulary that a schema uses. The engine builds the grammar of a format from three layers: the core grammar, the format grammar, and the host extensions.
grammar: tramlines-md
base: tramlines
version: "0.1"
- TL_1 [ubiquitous]: the core grammar SHALL contain the
metavocabulary and the schema keys that do not depend on the format of a document:line-limit,guidance,lifecycle,frontmatter,frontmatter-open,prohibited-patterns,formats,types, andexamples. - TL_2 [ubiquitous]: the format grammar SHALL name itself with
grammarand name the core grammar withbase: tramlines. - TL_3 [ubiquitous]: the engine SHALL merge a format grammar onto the core grammar: new keys, new rule maps, values of an enum, block kinds that a key requires, text of a
doc, and the constraint groupsexactly-one-ofandat-least-one-ofof any rule map. - TL_4 [unwanted]: IF a format grammar contains a
metasection, or changes the type, theof, or therequiredof a key of the core grammar, THEN the engine SHALL reject the format grammar. - TL_5 [ubiquitous]: the host extension SHALL name itself with
extensionand name the format grammar that it extends withextends. - TL_6 [ubiquitous]: the engine SHALL merge a host extension onto the grammar of its format: new keys, new rule maps, values of an enum, block kinds that a key requires, text of a
doc, and sections of its own. - TL_7 [unwanted]: IF a host extension contains a
metasection or a constraint group, or changes a definition of the grammar under it, THEN the engine SHALL reject the host extension. - TL_8 [unwanted]: IF the
extendsof a host extension names a grammar other than the format grammar that the engine reads, THEN the engine SHALL reject the host extension. - TL_9 [ubiquitous]: the engine SHALL preserve as written a key that a host extension adds, so that the host reads and checks it.
- TL_10 [ubiquitous]: the grammar SHALL state, in the
docof every keymin, what an absentminmeans.
3. Schemas
A schema is strict: the engine requires what the schema declares, rejects what the schema does not declare, and treats each requirement as exclusive. A relaxation is a key that the schema sets explicitly. The host decides which schema governs each document.
- TL_11 [ubiquitous]: the engine SHALL check a schema against the grammar before it uses the schema.
- TL_12 [unwanted]: IF a schema contains a key that the grammar does not define for the rule map that holds it, THEN the engine SHALL report a finding on the schema that names the key.
- TL_101 [unwanted]: IF a value of a schema does not have the type that the grammar defines for its key, THEN the engine SHALL report a finding on the schema that names the key and the type.
- TL_102 [unwanted]: IF a value of a schema is not one of the values that the grammar lists for its key, does not have the format that the grammar defines for its key, or is a
seq-widthbelow one, or a schema names a type with a string that is not a slug, THEN the engine SHALL report a finding on the schema that names the key. - TL_103 [unwanted]: IF a map of a schema lacks a key that the grammar requires of it, a key that another key of the map requires beside it, or every key of a group of which the grammar requires one, THEN the engine SHALL report a finding on the schema that names the missing key.
- TL_104 [unwanted]: IF a map of a schema contains more than one key of a group of which the grammar admits one, other than a group of shapes, THEN the engine SHALL report a finding on the schema that names the group.
- TL_13 [unwanted]: IF a schema does not parse as YAML 1.2, core schema, THEN the engine SHALL report a finding on the schema.
- TL_14 [unwanted]: IF a schema, or the file that
--formatsnames, contains a YAML anchor, alias, or tag, THEN the engine SHALL report a finding on that file. - TL_15 [unwanted]: IF two rules of one schema declare the same
rule-id, THEN the engine SHALL report a finding on the second of them that names the id. - TL_16 [ubiquitous]: the engine SHALL read each regular expression of a schema in the Rust dialect, with no lookaround and no backreference, and with no anchor that the regular expression does not contain.
- TL_118 [unwanted]: IF a regular expression of a schema, or the expression that a template compiles to, does not compile in that dialect, THEN the engine SHALL report a finding on the schema that names the key and the reason.
- TL_17 [ubiquitous]: the engine SHALL accept
prohibited-patterns, a list of bans, each apatternand amessage, on a schema, a rule that has a shape, and a format. - TL_18 [ubiquitous]: the engine SHALL report each failure of a rule that declares
severity: warn, or of a rule inside one that declares it where the inner rule declares noseverityof its own, as a warning, and each failure of any other rule as an error. - TL_19 [optional]: WHERE a rule declares
only, the engine SHALL apply the rule only when the state of the document is in the listed set for every dimension thatonlynames. - TL_20 [unwanted]: IF
onlynames a dimension that the lifecycle does not declare, or a state that its dimension does not declare, THEN the engine SHALL report a finding on the schema that names it. - TL_111 [unwanted]: IF a rule declares
onlyand the schema declares no lifecycle, THEN the engine SHALL report a finding on the schema. - TL_21 [unwanted]: IF a schema declares a
line-limitgreater than zero and a document has more lines than the limit, THEN the engine SHALL report a finding on the document. - TL_98 [unwanted]: IF a rule declares a
mingreater than itsmax, THEN the engine SHALL report a finding on the schema.
4. Shapes and types
A shape defines the text of one unit, in one of three forms:
text- The text itself.template- The text, with a typed hole${name:type}where it varies.pattern- A regular expression, for what a template cannot say.
Each shape matches the text with each run of whitespace collapsed to one space.
- TL_22 [unwanted]: IF a format, a variant, or a rule of the format grammar declares more than one of
text,template,pattern, andformat, a frontmatter constraint declares more than one oftext,templateandpattern, or a format declares one of them besidevariants, THEN the engine SHALL report a finding on the schema that names them. - TL_23 [ubiquitous]: the engine SHALL read the text of a unit with each run of whitespace as one space, for every shape, key, guard, and ban.
- TL_24 [ubiquitous]: the engine SHALL match a template against the whole text of a unit, each hole against the regular expression of its type.
- TL_105 [unwanted]: IF a format declares none of
text,template,pattern, andvariants, or a variant or a heading rule declares none oftext,templateandpattern, THEN the engine SHALL report a finding on the schema. - TL_25 [unwanted]: IF a template has no hole, THEN the engine SHALL report a finding on the schema that names the template.
- TL_119 [unwanted]: IF a template names one hole twice, THEN the engine SHALL report a finding on the schema that names the hole.
- TL_120 [unwanted]: IF a hole of a template names a type that is not visible to the schema, THEN the engine SHALL report a finding on the schema that names the type.
- TL_121 [unwanted]: IF a template contains a
${that no}closes, or a hole whose name is not letters, digits, and_that do not start with a digit, THEN the engine SHALL report a finding on the schema that names the template. - TL_26 [unwanted]: IF a text fails a template, THEN the engine SHALL report the column where the text stops matching and what the template expects there: the end of a text that lacks only the end of the template, where only literals follow the last hole or the hole does not take the literal that follows it, and the start of a hole whose value runs on to the literal after the hole and that the hole does not take.
- TL_27 [ubiquitous]: the engine SHALL resolve a type by name through the
typesof the schema, of the host, and of the shared registry, and the built-in types, the nearest first. - TL_28 [ubiquitous]: the engine SHALL reserve the built-in types
text,word,int,slug, andseq, and the types that the host reserves. - TL_29 [unwanted]: IF a schema defines a type with the name of a reserved type, THEN the engine SHALL report a finding on the schema that names the type.
- TL_109 [unwanted]: IF the regular expression of a type that a schema defines contains an anchor or a capture group, THEN the engine SHALL report a finding on the schema that names the type.
- TL_30 [ubiquitous]: the engine SHALL read a key as the capture of the hole or the named group that
keynames. - TL_31 [unwanted]: IF the
keyof a rule names no hole or named group of the shape of the rule, or of every shape of the format that the rule binds, or thekeyof a format names none of every shape of the format, THEN the engine SHALL report a finding on the schema that names the key. - TL_113 [unwanted]: IF a rule declares
keybeside a format that declares one, THEN the engine SHALL report a finding on the schema that names the format. - TL_32 [ubiquitous]: the engine SHALL check a
key-derives-fromagainst the key of the rule that declares it. - TL_33 [unwanted]: IF the part of a key before the first separator is not a prefix that its
key-derives-fromallows, THEN the engine SHALL report a finding that names the key. - TL_96 [unwanted]: IF a
key-derives-fromsits on a unit rule that captures no key, THEN the engine SHALL report a finding on the schema. - TL_117 [unwanted]: IF a
key-derives-fromnames a rule or a frontmatter key that the schema does not declare, THEN the engine SHALL report a finding on the schema that names it.
5. Formats
A format names the shape of one line, so that rules in many schemas share it. A format has one shape and an example, or variants, which each have a shape and an example of their own. The registries are layers: the schema, then the host, then the shared formats.
- TL_34 [ubiquitous]: the engine SHALL resolve a format by name through the
formatsof the schema, of the host, and of the shared registry, the nearest first. - TL_35 [ubiquitous]: the engine SHALL compile each format that is visible to a schema against the types that are visible to the schema.
- TL_36 [optional]: WHERE the nearer entry declares no patch, the engine SHALL replace a farther entry whole.
- TL_37 [ubiquitous]: the engine SHALL merge a format entry that declares itself a patch onto the entry that it shadows, by RFC 7386.
- TL_38 [ubiquitous]: the engine SHALL drop a key that a patch states as null.
- TL_39 [ubiquitous]: the engine SHALL replace a list-valued key of a patch whole and not join its elements.
- TL_40 [unwanted]: IF a format that is not a patch has one shape and no
example, THEN the engine SHALL report a finding on the schema that names the format. - TL_110 [unwanted]: IF a format declares
variantsand anexampleof its own, THEN the engine SHALL report a finding on the schema that names the format. - TL_41 [unwanted]: IF a patch names an entry that no farther layer defines, THEN the engine SHALL report a finding that names the entry.
- TL_97 [unwanted]: IF a rule binds a format that no layer declares, THEN the engine SHALL report a finding on the schema that names the format.
- TL_125 [ubiquitous]: the engine SHALL report a type or a format of a layer that does not load on its own declaration, and report no finding on a rule or a template that names it.
- TL_42 [ubiquitous]: the engine SHALL accept a line that conforms to one variant of its format, trying the variants in order, and report a line that conforms to none against the variant that it matches furthest, or each variant that it matches as far, with the example of each.
- TL_43 [unwanted]: IF the example of a variant matches its own variant and another variant of its format, THEN the engine SHALL report a finding at the format.
- TL_44 [unwanted]: IF a unit satisfies a rule or a bound format that declares a key, and the text of the unit contains no such key, THEN the engine SHALL report a finding that names the key that it expects.
- TL_45 [optional]: WHERE a rule sets
key-optional, the engine SHALL accept a unit that contains no key of its bound format. - TL_46 [ubiquitous]: the engine SHALL accept
key-optionalon a rule that declareskeyor binds a format that declares one. - TL_47 [unwanted]: IF a rule sets
key-optionaland neither declares a key nor binds a format that declares one, THEN the engine SHALL report a finding on the schema. - TL_48 [ubiquitous]: the engine SHALL check a unit against the prohibited patterns of the rule and of the format that govern it.
- TL_49 [invariant]: FOR ANY unit bound to a format that declares a key, the engine SHALL either capture the key or report one finding that names it, but never neither and never both.
- TL_50 [equivalent]: FOR ANY format entry and any patch of it, the engine SHALL produce a merged entry equal to RFC 7386 applied to the two as written.
- TL_51 [invariant]: FOR ANY two layers whose nearer entry declares no patch, the engine SHALL take the nearer entry as the effective entry, with nothing of the farther one.
6. Frontmatter and lifecycle
Frontmatter is the authority over the body. A repair rewrites the body to agree with the frontmatter, and the engine fills the body of a new document from its frontmatter. A lifecycle defines the states that a document moves through, each dimension a key of its frontmatter.
- TL_52 [ubiquitous]: the engine SHALL read frontmatter as YAML 1.2, core schema.
- TL_53 [unwanted]: IF frontmatter does not parse, THEN the engine SHALL report a finding on its first line and check no frontmatter rule.
- TL_123 [unwanted]: IF frontmatter contains an anchor, an alias, or a tag, THEN the engine SHALL report a finding on its first line and check no frontmatter rule.
- TL_94 [state]: WHILE a document has no frontmatter, the engine SHALL check it as a document whose frontmatter is an empty map.
- TL_54 [unwanted]: IF a frontmatter key that the schema declares, and does not mark
optional, is missing, THEN the engine SHALL report a finding that names the key. - TL_55 [unwanted]: IF a frontmatter key is not declared, not reserved by the host, and not the key of a lifecycle dimension, and the schema declares no
frontmatter-open, THEN the engine SHALL report a finding that names the key. - TL_56 [unwanted]: IF a frontmatter value, or an element of the value of a key that declares
list, fails itsconst,template,pattern,enum, orformat, THEN the engine SHALL report a finding that names the key. - TL_106 [unwanted]: IF the value of a key that declares
listhas fewer elements than itsmin, THEN the engine SHALL report a finding that names the key, the bound, and the count. - TL_107 [unwanted]: IF the value of a key that declares
listhas more elements than itsmax, THEN the engine SHALL report a finding that names the key, the bound, and the count. - TL_57 [unwanted]: IF a key that declares
listhas a value that is not a list, or a key that does not declarelist, or an element of a list, has a value that is not a scalar, THEN the engine SHALL report a finding that names the key. - TL_58 [unwanted]: IF the template of
idcontains a hole other than aseqhole, aslughole, and a hole with no type named for a lifecycle key, THEN the engine SHALL report a finding on the schema that names the template. - TL_114 [unwanted]: IF the template of
idcontains more than oneseqhole, THEN the engine SHALL report a finding on the schema. - TL_115 [unwanted]: IF the constraint of
iddeclaresconst,enum,format,list,min, ormaxbeside its template, THEN the engine SHALL report a finding on the schema that names the key. - TL_116 [unwanted]: IF
seq-widthsits beside the template of a key other thanid, or beside anidtemplate with noseqhole, THEN the engine SHALL report a finding on the schema. - TL_122 [unwanted]: IF the template or the
constofidadmits an id that is not a slug, THEN the engine SHALL report a finding on the schema that names the id or the state that is not a slug. - TL_59 [unwanted]: IF the template of
idcontains a hole named for a lifecycle key and the id of a document has another state at that hole than the state of the document for that key, THEN the engine SHALL report a finding that names the key. - TL_60 [ubiquitous]: the engine SHALL ask the host whether a value matches a frontmatter format that the host added.
- TL_61 [ubiquitous]: the engine SHALL read the state of a document as the value of the key of each lifecycle dimension in its frontmatter.
- TL_62 [unwanted]: IF the key of a lifecycle dimension is missing, THEN the engine SHALL report a finding that names the key.
- TL_108 [unwanted]: IF the value of the key of a lifecycle dimension is not one of the
statesof its dimension, THEN the engine SHALL report a finding that names the key and the states. - TL_95 [unwanted]: IF a lifecycle declares one dimension twice, THEN the engine SHALL report a finding on the schema that names the dimension.
- TL_112 [unwanted]: IF the frontmatter of a schema declares the key of a lifecycle dimension, THEN the engine SHALL report a finding on the schema that names the key.
7. Examples and guidance
The example of a rule is text of the kind that the rule demands. The example shows the reader of a finding what to write, and the scaffold copies it. What an example cannot say is guidance, which the scaffold places before the part that it guides.
- TL_63 [ubiquitous]: the engine SHALL accept
exampleon a frontmatter constraint and on each rule of the format grammar that has a shape. - TL_64 [unwanted]: IF the
exampleof a rule fails the rule that declares it, THEN the engine SHALL report a finding on the schema that names the rule. - TL_65 [unwanted]: IF the
exampleof a format fails the shape, a prohibited pattern, or the key of that format, or the example of a variant fails that variant, THEN the engine SHALL report a finding at the format that names it. - TL_66 [unwanted]: IF a rule other than the frontmatter constraint of
iddeclarestemplate,pattern, orsource-pattern, and declares noexampleand binds no format that declares one, THEN the engine SHALL report a finding on the schema that names the key that declares the shape. - TL_67 [ubiquitous]: the engine SHALL demand no
exampleof a rule that declares only aselect-pattern, a count, or atext. - TL_68 [unwanted]: IF the
guidanceand theexampleof a rule, or the guidance of a section or a block and the example of its heading or of a unit rule inside it, contain the same words, THEN the engine SHALL report a finding on the schema that names both keys. - TL_69 [unwanted]: IF an entry of
examples, checked as a complete document at its own states and with no markers, fails the schema, THEN the engine SHALL report a finding on the schema that names the entry and the failure. - TL_70 [ubiquitous]: the engine SHALL accept
guidanceon a schema, a frontmatter constraint, a lifecycle dimension, and each rule map of the format grammar that declares the key. - TL_71 [ubiquitous]: the engine SHALL print no
guidancein a finding.
8. Findings
A finding has a file, a line where one applies, a severity, the key of the criterion that it breaks, and a message. The key is how a reader looks the finding up. Where a rule depends on a fact outside the document, the engine asks the host. The null host supplies no fact beyond the document.
- TL_72 [ubiquitous]: the engine SHALL give each finding the key of the most specific criterion that it breaks, the criterion of a format over a criterion of this specification, and end the message of a finding that a schema rule raises with
(schema <id>, <where>). - TL_100 [ubiquitous]: the engine SHALL give each finding the key of an unwanted criterion that describes one fault, which one kind of change to the document or the schema fixes.
- TL_73 [ubiquitous]: the engine SHALL name in each finding that a schema rule raises the rule that it came from, by
rule-idwhere the rule declares one, and by its place in the schema, its key, or its pattern otherwise, a section by its heading as the schema writes it. - TL_74 [unwanted]: IF a text fails the shape of a rule or a format, THEN the engine SHALL report the text, the shape, or the column where a template stops matching, and the example of that shape.
- TL_75 [unwanted]: IF a text matches a prohibited pattern, THEN the engine SHALL report the span that the pattern matched and the message of the ban.
- TL_76 [unwanted]: IF a text matches a prohibited pattern of a rule or a format, THEN the engine SHALL report the example of that rule beside the span and the message.
- TL_77 [ubiquitous]: the engine SHALL include in a finding no text that a schema writes to explain it.
- TL_78 [ubiquitous]: the host SHALL handle a scope, a format, or a block kind that it added.
- TL_79 [unwanted]: IF the host cannot supply what a rule needs to know, THEN the engine SHALL skip the rule.
- TL_80 [invariant]: FOR ANY format and any line that it rejects on shape, the engine SHALL report text that contains the example of the expected shape and nothing else that a schema wrote.
- TL_81 [invariant]: FOR ANY ban and any line that it matches, the engine SHALL report a span that is a substring of the line and that the pattern of the ban matches.
9. Repairs
A repair is an edit: a replacement of lines of the text as the engine read it. The format grammar defines which parts of its documents the engine repairs. A host builds edits of its own, which write its generated content.
- TL_82 [ubiquitous]: the engine SHALL apply all edits of one text in one pass.
- TL_83 [unwanted]: IF two edits target one line, or an edit extends beyond the text, THEN the engine SHALL reject the edits.
- TL_84 [ubiquitous]: the engine SHALL apply an insertion at a line before a replacement at the same line.
10. The command
The command tramlines validate checks documents against one schema, with the null host and the default dialect of each format. Its options are --schema <FILE>, --formats <FILE>, and --format <NAME>, then the documents. This section states what the schema language requires of a check. The command’s repairs, its other subcommands, and the forms of its report are outside the language.
tramlines validate --schema contract.yaml --formats formats.yaml docs/contract-cache.md
- TL_85 [ubiquitous]: the command SHALL check each document that it names against the schema that
--schemanames, with the null host. - TL_86 [event]: WHEN
--formatsnames a file, the command SHALL read it as aregistrymap of formats and an optionaltypesmap, the layer under the schema. - TL_87 [unwanted]: IF the file that
--formatsnames does not parse or has noregistrykey, THEN the command SHALL exit with code 2 and name the file. - TL_124 [unwanted]: IF the registry or the types of the file that
--formatsnames break a criterion that applies to theformatsor thetypesof a schema, THEN the command SHALL exit with code 2 and print each finding with the key of that criterion and the name of the file. - TL_88 [ubiquitous]: the command SHALL select the format of a document by the extension of its file,
.mdfor markdown. - TL_89 [event]: WHEN
--formatnames a format, the command SHALL read every document in that format. - TL_90 [unwanted]: IF a document has an extension that no format matches and the command has no
--format, THEN the command SHALL exit with code 2 and name the document. - TL_91 [unwanted]: IF
--formatnames no format, THEN the command SHALL exit with code 2 and name the formats that it knows. - TL_92 [state]: WHILE the command line asks for no other form of report, the command SHALL print each error, and each warning that the command line asks it to list, as one line
path:line: severity[KEY]: message, then a count of errors and warnings. - TL_93 [ubiquitous]: the command SHALL exit with code 0 when no finding is an error, 1 when a finding is an error, and 2 for a usage error or a file that the command fails to read.
- TL_99 [unwanted]: IF the command stops on a criterion of this specification, THEN the command SHALL print the error as
error[KEY]: message.
tramlines-md markdown format specification
This specification defines tramlines-md: the markdown format of tramlines, what a schema specifies about the body of a markdown document, and how the engine checks it.
The core specification defines the grammar, the schema keys that every format shares, the shapes, the formats, the findings, and the command. This specification adds what only a markdown document has: headings and sections, blocks and their units, anchors, and markers in HTML comments. Its grammar, tramlines-md.grammar.yaml, names the core grammar as its base.
About this specification
Each normative rule of this specification is an EARS criterion, keyed TLMD_n.
1. Terminology
document- A UTF-8 markdown text, with an optional YAML frontmatter block and a body.section- One heading of a body and the blocks under it, to the next heading of any level.preamble- The content of a body before its first heading.block- One part of a body, of one kind: prose, a list, a table, a fence, a quote, images, HTML, a rule, link references, or a group.unit- One paragraph, item, row, line of a fence, or repetition of a group: the part of a block that one unit rule checks.line rule- A rule on one content line of a code or HTML block.position- One entry of the blocks that a section declares, which claims the blocks of its kind.open region- The headings after a section that declaresheadings-openorsubheadings-open, up to the next declared heading.dialect- How a host spells its markers: a namespace, and the regions, trailer, and scheme that it adds.marker- An HTML comment on a line of its own, or a YAML comment in the frontmatter, in the namespace of the dialect.region- A block that the markers of the host open and close, whose interior the host generates.trailer- The generated block of link reference definitions that ends a document.slug- The text in lower case, with letters, digits, hyphens, and underscores kept, other characters dropped, and spaces made hyphens.anchor- The empty HTML link<a id="<slug>"></a>that marks where a key is described.pairing- A section or a unit rule that declaresfor-each-key-of, with one occurrence for each key of another rule.scaffold- The document that a schema derives for one combination of states, which a new document starts from.
2. Documents
A document opens with frontmatter when its first line is ---. The body is CommonMark, with GitHub tables, footnotes, and task lists. The engine reads the body as blocks and units.
- TLMD_1 [event]: WHEN the first line of a document is
---, the engine SHALL read the lines up to the next---line as frontmatter and the lines after it as the body. - TLMD_2 [unwanted]: IF the frontmatter has no closing
---line, THEN the engine SHALL read the frontmatter to the end of the text, with an empty body. - TLMD_3 [ubiquitous]: the engine SHALL read a text with CRLF line endings as LF.
- TLMD_4 [ubiquitous]: the engine SHALL read the body as CommonMark, with GitHub tables, footnotes, and task lists.
- TLMD_5 [ubiquitous]: the engine SHALL read consecutive paragraphs as one prose block, and a paragraph of images only as an image block.
- TLMD_6 [ubiquitous]: the engine SHALL read a paragraph with its lines joined by one space, and a line of a fence without its indent.
- TLMD_7 [ubiquitous]: the engine SHALL match the item’s own text, and check what the item contains beyond that text, a nested list or a fence, against the
blocksof its rule. - TLMD_162 [unwanted]: IF an item contains a block after its first paragraph, such as a nested list, a fence, a blockquote, or another paragraph, or in place of it on the item’s own line, and its rule declares no
blocks, THEN the engine SHALL report a finding on the first line of that block, and hold no text of the item to its rule where the block stands in place of the text. - TLMD_173 [ubiquitous]: the engine SHALL read a heading inside a list item or a blockquote as a block of that item or quote, of a kind that no schema declares, and only a heading outside both as a heading of the document.
- TLMD_172 [unwanted]: IF a blockquote contains a heading, THEN the engine SHALL report a finding that names the heading, and count no paragraph missing from a quote that holds only headings.
- TLMD_8 [ubiquitous]: the engine SHALL record the position of every code span of a body.
- TLMD_9 [invariant]: FOR ANY line, the engine SHALL produce a masked line with the length and the line number of the original, and change no byte outside a recorded span.
3. Dialect and markers
The engine has three markers of its own, each in the namespace <ns> of the dialect:
<!-- <ns>:only k=v,v2 -->- The part after it belongs to those states.<!-- <ns>:guidance … -->- What an example cannot say, before the part that it guides. Before a frontmatter key, the marker is# <ns>:guidance ….<!-- <ns>:optional -->- With<!-- /<ns>:optional -->, a part that the schema does not demand.
A host adds to its dialect a region, a trailer, or a scheme.
- TLMD_10 [unwanted]: IF a governed document contains an
onlymarker, THEN the engine SHALL report a finding on the line of the marker. - TLMD_158 [unwanted]: IF a governed document contains a
guidancemarker, THEN the engine SHALL report a finding on the line of the marker. - TLMD_11 [unwanted]: IF a governed document contains an
optionalregion, both of its markers, THEN the engine SHALL report a warning. - TLMD_12 [unwanted]: IF an
optionalmarker has no partner, THEN the engine SHALL report a finding. - TLMD_171 [ubiquitous]: the engine SHALL count toward
line-limitno marker line outside a fence, and no blank line that sets one apart, so that a scaffold is held to the length of the document that it leaves. - TLMD_13 [optional]: WHERE the dialect names a region, the engine SHALL read the lines from
<!-- <ns>:<region> target -->to<!-- /<ns>:<region> -->as one block of the kind that the dialect names, and check its interior with no rule. - TLMD_14 [optional]: WHERE the dialect names a trailer, the engine SHALL read
<!-- <ns>:<trailer> -->and the link reference definitions after it as a generated block that no rule reads. - TLMD_15 [optional]: WHERE the dialect names a scheme, the engine SHALL preserve the rest of a link label
<scheme>:<rest>as written, for the host to resolve. - TLMD_16 [ubiquitous]: the engine SHALL read a marker inside a fenced code block as text.
- TLMD_17 [ubiquitous]: the engine SHALL assign the default dialect the namespace
tramlines, and no region, trailer, or scheme.
4. Sections and headings
A schema lists the headings of a document under sections, in document order. The body of a section runs to the next heading of any level. Sections nest by level, except that a section that declares parent is inside its parent.
- TLMD_18 [unwanted]: IF a declared section that does not declare
optionalis missing from the document, or from an occurrence of its parent where it declaresparent, THEN the engine SHALL report a finding that names the section. - TLMD_151 [unwanted]: IF the heading of a declared section that does not declare
repeatoccurs more than once in the document, or in one occurrence of its parent where it declaresparent, THEN the engine SHALL report a finding that names the heading. - TLMD_19 [unwanted]: IF a heading matches no section and no open region admits it, THEN the engine SHALL report a finding that names the heading.
- TLMD_20 [unwanted]: IF a heading is more than one level deeper than the heading before it, matches no section or matches a section at the level that the section declares, and does not lie outside every occurrence of the parent of its section, THEN the engine SHALL report a finding that names the heading.
- TLMD_21 [unwanted]: IF the sections of a schema that orders them skip a level, THEN the engine SHALL report a finding on the schema.
- TLMD_22 [unwanted]: IF a declared section is out of the listed order, its heading does not repeat where the section does not declare
repeat, and the schema does not setsections-ordered: false, THEN the engine SHALL report a finding that names the first such heading. - TLMD_144 [unwanted]: IF a heading matches a section at another level than the section declares, THEN the engine SHALL report a finding that names the level.
- TLMD_23 [ubiquitous]: the engine SHALL assign a heading that an exact heading and a varying heading both match to the exact one.
- TLMD_175 [ubiquitous]: the engine SHALL match a heading against the varying headings of the sections of the heading’s own level before it matches the heading against the varying heading of a section of another level.
- TLMD_24 [optional]: WHERE a section declares
headings-open, the engine SHALL accept undeclared headings of its level or deeper after it, up to the next declared heading, and check their bodies against only the rules of the document scope. - TLMD_25 [optional]: WHERE a section declares
subheadings-open, the engine SHALL accept undeclared headings deeper than its level after it, up to the next declared heading. - TLMD_26 [unwanted]: IF a heading in an open region has the exact text of a prohibited heading, THEN the engine SHALL report a finding that names the heading.
- TLMD_170 [unwanted]: IF
sections-prohibitednames a heading that a declared section matches in some states that both admit, THEN the engine SHALL report a finding on the schema that names the heading. - TLMD_27 [ubiquitous]: the engine SHALL accept on the heading of a section the keys that a unit rule accepts for one line: a shape,
key,unique,key-derives-from, andexample. - TLMD_28 [ubiquitous]: the engine SHALL accept a heading key equal to a key that a rule named in the
for-each-key-ofof the section captures. - TLMD_29 [unwanted]: IF two sections or two units that one
for-each-key-ofpairs to a rule capture the same key in one scope, other than a capture in the body of a heading that repeats where its section does not, of a key that the body it repeats captures before it, THEN the engine SHALL report a finding on the second that names the key and the line of the first. - TLMD_30 [ubiquitous]: the engine SHALL check each pairing for a repeated key on its own, in each scope that the engine reads it in.
- TLMD_31 [unwanted]: IF a heading of a section that declares
parentfollows no occurrence of the parent before the next heading of the level of the parent or higher, THEN the engine SHALL report a finding that names the heading. - TLMD_32 [optional]: WHERE a section declares
parent, the engine SHALL count itsoptionaland itsrepeatwithin each occurrence of the parent. - TLMD_166 [unwanted]: IF a heading matches a section whose
parentdoes not apply at the states of the document, THEN the engine SHALL report a finding that names the heading and the parent. - TLMD_33 [ubiquitous]: the engine SHALL accept a heading of a repeating section after the headings of the sections inside it.
- TLMD_34 [unwanted]: IF a
parentnames no section, THEN the engine SHALL report a finding on the schema that names it. - TLMD_156 [unwanted]: IF a
parentnames a section of a level that is not smaller than the level of the section that declares it, THEN the engine SHALL report a finding on the schema that names both levels. - TLMD_157 [unwanted]: IF a section that declares
parentis not listed after its parent with only the other sections inside that parent between, THEN the engine SHALL report a finding on the schema that names the section. - TLMD_35 [unwanted]: IF a section declares
number-from-parentand its heading does not begin with the number of the heading of its parent, a dot and a number, THEN the engine SHALL report a finding that names the expected number. - TLMD_36 [unwanted]: IF the heading of the parent of a section that declares
number-from-parenthas no number, THEN the engine SHALL report a finding that names that heading. - TLMD_37 [optional]: WHERE a schema declares a
preamble, the engine SHALL check the blocks before the first heading of a document against theblocksof the preamble. - TLMD_38 [unwanted]: IF a body has content before its first heading and the schema declares no
preamble, THEN the engine SHALL report a finding.
5. Blocks
A section declares its body as an ordered list of blocks, each of one kind with rules for its units. A position claims the blocks of its kind. Where a position refuses a block, the engine offers the block to the positions after it.
- TLMD_39 [unwanted]: IF no declared position claims a block of a body, in order unless the section sets
blocks-ordered: false, THEN the engine SHALL report a finding that names the kind of the block. - TLMD_152 [unwanted]: IF a position that does not declare
optionalclaims no block and no block of another kind stands where it expects one, THEN the engine SHALL report a finding that names the kind that it expects. - TLMD_40 [optional]: WHERE a section declares no
blocks, the engine SHALL read its body as one prose block of one or more paragraphs. - TLMD_41 [ubiquitous]: the engine SHALL exempt generated blocks, region interiors, footnote definitions, and HTML comments from block matching.
- TLMD_42 [unwanted]: IF a block declares a key that does not apply to its kind, or a unit rule declares a key that does not apply where the rule sits, THEN the engine SHALL report a finding on the schema that names the key.
- TLMD_153 [unwanted]: IF a block lacks a key that its kind requires,
langfor acodeblock,columnsfor atableblock, orblocksfor agroupblock, THEN the engine SHALL report a finding on the schema that names the key. - TLMD_43 [unwanted]: IF a fence has no tag, or a tag that the
langof its block does not list, THEN the engine SHALL report a finding. - TLMD_145 [unwanted]: IF a fence has fewer backticks than the
fenceof its block, THEN the engine SHALL report a finding that names both numbers. - TLMD_44 [unwanted]: IF the header row of a table differs from the declared
columns, beyond further columns thatcolumns-openaccepts, THEN the engine SHALL report a finding. - TLMD_45 [unwanted]: IF a unit contains HTML other than a comment and its block does not set
inline-html, THEN the engine SHALL report a finding. - TLMD_46 [ubiquitous]: the engine SHALL match the path of an image or the target of a region against a
source-patternby what the host reports that it shows where the path is in the scheme of the host, and as written otherwise. - TLMD_163 [unwanted]: IF the path of an image or the target of a region does not match the
source-patternof its block, THEN the engine SHALL report a finding that names the path and the pattern. - TLMD_200 [unwanted]: IF the host reports that an image or the target of a region names a document, THEN the engine SHALL report the finding of TLMD_163 whatever the
source-patternof its block, with a message that says that it names a document. - TLMD_47 [unwanted]: IF a block declares
rulesbesideeachor besidesequence, THEN the engine SHALL report a finding on the schema. - TLMD_146 [unwanted]: IF a block has fewer units than its
min, THEN the engine SHALL report a finding that names the bound and the count. - TLMD_160 [unwanted]: IF a block has more units than its
max, THEN the engine SHALL report a finding that names the bound and the count. - TLMD_147 [unwanted]: IF a position of a
sequencethat does not declareoptionaltakes no unit, THEN the engine SHALL report a finding that names the position. - TLMD_161 [unwanted]: IF units of a block that declares
sequenceremain after its last position, THEN the engine SHALL report a finding on each of them. - TLMD_48 [ubiquitous]: the engine SHALL let consecutive declared prose rules take successive runs of units from one prose block.
- TLMD_49 [ubiquitous]: the engine SHALL bound each run that one of these prose rules takes by the
minand themaxof that rule. - TLMD_50 [unwanted]: IF units of a prose, bullet list, or numbered list block remain after the last position that takes a run of them, THEN the engine SHALL report a finding against the first unit that remains.
- TLMD_51 [ubiquitous]: the engine SHALL divide a prose run only under ordered block matching.
- TLMD_52 [ubiquitous]: the engine SHALL apply each entry of the
rulesof a block that declares a guard to the units that match that guard. - TLMD_53 [unwanted]: IF no entry of
rulestakes a unit, the unit matches no part of a rule with a shape, and the block declares nounits-open, THEN the engine SHALL report a finding that names the unit. - TLMD_54 [ubiquitous]: the engine SHALL bound the units that an entry selected by the
minand themaxof that entry. - TLMD_55 [ubiquitous]: the engine SHALL assign a unit to the first rule with a shape and no guard whose shape the unit has.
- TLMD_56 [ubiquitous]: the engine SHALL count, and not take, the units that a rule matches when that rule declares only a guard and a count.
- TLMD_57 [unwanted]: IF no rule takes a unit or a line that partly matches a rule with a shape, and the block declares no
units-openor its unit rule nolines-open, THEN the engine SHALL report the unit against the rule that it matches furthest, with the column where it stops. - TLMD_58 [unwanted]: IF an earlier rule with a shape and no guard takes the example of a later one, THEN the engine SHALL report a finding on the schema.
- TLMD_59 [ubiquitous]: the engine SHALL claim a block for an optional, repeating, or grouped position only where that position reports no error against the block.
- TLMD_60 [ubiquitous]: the engine SHALL claim a block for a required position that does not repeat, whatever that position reports against the block.
- TLMD_61 [unwanted]: IF the engine cannot complete a group repetition, THEN the engine SHALL release every block that the repetition claimed.
- TLMD_62 [ubiquitous]: the engine SHALL apply one claim test whether a section fixes the block order or frees it.
- TLMD_63 [ubiquitous]: the engine SHALL offer a block that a position released to the positions after it that declare its kind.
- TLMD_64 [ubiquitous]: the engine SHALL discard the findings, captures, repairs, and gates of a position that it releases.
- TLMD_65 [unwanted]: IF a position reports only warnings against a block, THEN the engine SHALL claim the block.
- TLMD_66 [unwanted]: IF a position refuses a block of its own kind and then reports itself missing, THEN the engine SHALL report why the position refused the block.
- TLMD_67 [unwanted]: IF no position claims a block that a position refused, THEN the engine SHALL report why the last position that tried the block refused it.
- TLMD_68 [event]: WHEN a block of a kind that no later position declares appears before the block that a position declares, the engine SHALL move the position past it, as a block that no position claims.
- TLMD_169 [unwanted]: IF the block of a position that is no group and does not declare
optionalstands after a block that a later position declares, and no later position declares the block of the position, THEN the engine SHALL report a finding that names the block, and check the block against its position. - TLMD_69 [unwanted]: IF a position meets a block of another kind and no block of its own kind follows, THEN the engine SHALL name the expected kind and the found kind in one finding.
- TLMD_70 [unwanted]: IF a finding names the found kind, THEN the engine SHALL claim that block for the position that named it.
- TLMD_71 [unwanted]: IF a repetition of a group fails where the group needs it to reach its
min, where no later position takes its opening block, or where the group’s own rule accepted its opening block, THEN the engine SHALL report why that repetition failed and claim its blocks for the group. - TLMD_72 [unwanted]: IF a block fits an inner block of a group after the first and no later position takes it, THEN the engine SHALL read it as a repetition that has no opening blocks and name the expected kind.
- TLMD_73 [ubiquitous]: the engine SHALL claim for a group every block that the group took, whatever the group reports about them.
- TLMD_74 [ubiquitous]: the engine SHALL assign to a position over a run the longest extent of units that its bounds admit and that it reports no error against.
- TLMD_75 [unwanted]: IF no extent of a run satisfies a position that claims a block whatever it reports, THEN the engine SHALL assign to that position the longest extent that its bounds admit.
- TLMD_76 [ubiquitous]: the engine SHALL choose an extent only for a prose, bullet list, or numbered list block.
- TLMD_77 [ubiquitous]: the engine SHALL choose an extent only where a section fixes the block order.
- TLMD_78 [ubiquitous]: the engine SHALL choose an extent only where a section declares two or more positions of the kind of the block, counting a group that opens with a position of that kind as one more position, whatever closes the group, and counting the next repetition of a group that opens and closes with a position of that kind as one more position, where the group’s
maxadmits that repetition. - TLMD_193 [optional]: WHERE a later group opens with a position of the kind of a run, and the block after the run fits that group only after its opening, the engine SHALL leave the units that the group’s opening position demands at the end of the run to that group.
- TLMD_194 [unwanted]: IF the bounds of the position admit no extent that leaves those units, THEN the engine SHALL choose the extent that TLMD_74 chooses.
- TLMD_79 [invariant]: FOR ANY prose run and any ordered prose rules, the engine SHALL assign every unit to exactly one rule or report it exactly once.
- TLMD_80 [invariant]: FOR ANY document and schema, the engine SHALL choose an extent no longer than the extent that the bounds alone produce, and equal to it where that extent reports no error, so that a document with no error under the bounds alone has no error.
- TLMD_81 [invariant]: FOR ANY position that refuses a block, the engine SHALL preserve the findings, captures, repairs, and gates that the check had before the engine tried the position.
6. Units and lines
A unit rule shapes one unit of a block. A code or HTML block also accepts lines, the grammar of its content lines. Each content line, without its indent, is a unit of its own. A blank line is layout, not a line.
- TLMD_82 [ubiquitous]: the engine SHALL apply each rule of the
linesof a unit rule to the content lines of a code or HTML block, and to no fence marker. - TLMD_83 [ubiquitous]: the engine SHALL apply a line rule that declares a guard only to the lines that match that guard.
- TLMD_84 [unwanted]: IF the number of lines that a guard selected falls outside the
minand themaxof the rule, THEN the engine SHALL report a finding that names the number and the bound. - TLMD_85 [ubiquitous]: the engine SHALL read an absent
minas one on a line rule that declares no guard. - TLMD_86 [ubiquitous]: the engine SHALL report a failure of a line rule against the file line that caused it.
- TLMD_87 [unwanted]: IF no rule of the
linesof a fence takes a line, the line matches no part of a rule with a shape, and its unit rule declares nolines-open, THEN the engine SHALL report a finding that names the line. - TLMD_88 [ubiquitous]: the engine SHALL attribute a key that a line rule captures to the
rule-idof the enclosing block. - TLMD_89 [optional]: WHERE a unit rule declares
rule-id, the engine SHALL file the keys that the unit rule captures under that id and not under the id of the block. - TLMD_90 [unwanted]: IF
linesappears on a unit rule of a block that is neither code nor HTML, or on a line rule, THEN the engine SHALL report a finding on the schema. - TLMD_91 [unwanted]: IF a guard,
min, ormaxappears on a unit rule outside therulesof a block and thelinesof a unit rule, THEN the engine SHALL report a finding on the schema. - TLMD_92 [unwanted]: IF a rule under
rulesorlinesdeclares a guard and declares nomin, THEN the engine SHALL report a finding on the schema that names the rule. - TLMD_142 [unwanted]: IF a rule declares
textbesideselect-pattern, THEN the engine SHALL report a finding on the schema. - TLMD_93 [unwanted]: IF a rule under
rulesorlinestakes or counts fewer units than itsmin, THEN the engine SHALL report its selector or its shape, and the count. - TLMD_159 [unwanted]: IF a rule under
rulesorlinestakes or counts more units than itsmax, THEN the engine SHALL report its selector or its shape, and the count. - TLMD_94 [ubiquitous]: the engine SHALL check a line of a fence against the prohibited patterns of the unit rule and the format that govern it.
- TLMD_95 [unwanted]: IF a key of a rule that the
for-each-key-ofof a unit rule or a section names has no unit or section in scope, THEN the engine SHALL report a finding that names the key, on the line that captures it, whether or not the block that contains it is present. - TLMD_96 [unwanted]: IF the key of a unit or a section that declares
for-each-key-ofis not a key that a rule it names captures in scope, THEN the engine SHALL report a finding that names the key. - TLMD_97 [ubiquitous]: the engine SHALL check a pairing in each occurrence of the
parentof the section where the section declares one, and in the whole document otherwise. - TLMD_98 [unwanted]: IF a section or a unit rule declares
for-each-key-ofand captures no key, THEN the engine SHALL report a finding on the schema. - TLMD_167 [unwanted]: IF a rule-id of a
for-each-key-ofnames no rule that captures a key, THEN the engine SHALL report a finding on the schema that names it. - TLMD_197 [ubiquitous]: the engine SHALL read a
for-each-key-ofthat holds a list of rule-ids as a pairing with the union of the keys that those rules capture, each distinct key once. - TLMD_99 [invariant]: FOR ANY fence text and any guard, the engine SHALL check with a rule exactly the lines that match the guard, in file order, and report no other line against that rule, which applies to units and lines alike.
- TLMD_100 [invariant]: FOR ANY failing line rule, the engine SHALL report the finding at the line whose text failed, inside the content of the block.
7. Keys and anchors
A rule captures a key from a heading, an item, a row, or a line: the hole of its template, or the named group of its pattern. Each key has one generated anchor, <a id="cli-1_ac-2"></a>, so that a fragment opens the rendered page at the key. A repair writes, moves, and removes the anchors.
- TLMD_101 [unwanted]: IF the slug of a key that a heading or the capture that defines it holds equals the slug of another such key or of a heading in the same document, THEN the engine SHALL report a finding that names the key.
- TLMD_148 [unwanted]: IF rules that declare no
for-each-key-ofcapture one key twice in the scope thatuniquedeclares, other than a capture in the body of a heading that repeats where its section does not, of a key that the body it repeats captures before it, or a capture by a rule that declareskey-repeats, THEN the engine SHALL report a finding that names the key and the line of its first capture. - TLMD_198 [optional]: WHERE a rule declares
key-repeats, the engine SHALL read each of its captures as a reference to the key, which a capture by a rule withoutkey-repeatsdefines where there is one, and otherwise its first capture. - TLMD_102 [ubiquitous]: the host SHALL check each scope of a key wider than the document that the host adds.
- TLMD_103 [ubiquitous]: the engine SHALL read a key as defined by the one capture of a rule without
for-each-key-of, and every other capture of it as a reference to it, which needs no anchor unless a heading makes it. - TLMD_104 [optional]: WHERE a heading captures the key, the engine SHALL put the anchor at the start of the text of the heading, and none where the slug of the heading is the slug of the key.
- TLMD_105 [optional]: WHERE a paragraph, an item, or a table cell captures the key, the engine SHALL put the anchor directly before the text that the rule matches, and before a code span or a link that contains that point.
- TLMD_106 [optional]: WHERE a line of a code or HTML block captures the key, the engine SHALL put the anchor on a line of its own directly before the block, the anchors of the block in the order of capture, then a blank line.
- TLMD_107 [ubiquitous]: the engine SHALL remove each anchor from the text before any rule reads the text, and form the slug of a heading without it.
- TLMD_108 [ubiquitous]: the engine SHALL read any HTML other than an anchor of this exact form as written.
- TLMD_109 [unwanted]: IF a key has no anchor, an anchor is not where its key is, an anchor names no key, or an anchor is written two times, other than an anchor beside a second capture of its key or of a key with its slug, or beside a capture with
for-each-key-of, THEN the engine SHALL report a finding. - TLMD_110 [ubiquitous]: the engine SHALL report no anchor finding on a scaffold.
8. Checking
The engine checks a document against a schema at the states of the document. The check reads the frontmatter, the sections, the blocks and their units, the keys and their anchors, the line limit, and the bans.
- TLMD_111 [ubiquitous]: the engine SHALL exclude fenced blocks, code spans, and region interiors from every prohibited pattern of the schema.
- TLMD_112 [ubiquitous]: the engine SHALL exclude a code span that opens on one line and closes on a later one.
- TLMD_113 [ubiquitous]: the engine SHALL ask the host what an image in the scheme of the host shows, and what the target of a region names, to match a
source-pattern. - TLMD_174 [ubiquitous]: the engine SHALL check a document against the rules that its host adds, after its own rules, and treat each finding and each gate of the host as its own.
9. Scaffolds
A schema derives one scaffold for each combination of its lifecycle states. The text of a scaffold is the text of the schema: the text that a rule fixes, the example of each rule, the example of the format that a rule binds, and the examples of a section that repeats. The example of a block or a unit rule can be a list of texts. The scaffold then writes one unit for each text, so that a new document shows several units rather than one.
- TLMD_114 [ubiquitous]: the engine SHALL derive one scaffold for each state combination of a schema and check it against that schema.
- TLMD_115 [ubiquitous]: the engine SHALL write into a scaffold no text that neither the schema fixes nor an example states.
- TLMD_116 [unwanted]: IF a scaffold needs a part whose text neither the schema fixes nor an example states, THEN the engine SHALL report a finding on the schema that names the rule that demands the part.
- TLMD_154 [unwanted]: IF the scaffold splices a key into the example of a rule that declares
key-derives-fromorfor-each-key-of, and the example does not carry a key where the rule reads one, THEN the engine SHALL refuse the derivation with a finding on the schema that names the rule and the key. - TLMD_155 [unwanted]: IF a rule takes its text or its heading from a frontmatter key that the scaffold does not write, THEN the engine SHALL refuse the derivation with a finding on the schema that names the rule and the key.
- TLMD_117 [ubiquitous]: the engine SHALL take the
exampleof the bound format for a unit rule that binds a format and has noexample. - TLMD_118 [ubiquitous]: the engine SHALL open every scaffold with one
<ns>:guidanceline before its title heading, whose text startsThe writer replaces each of the following examples with their own text.and continues with theguidanceof the schema. - TLMD_119 [ubiquitous]: the engine SHALL write the
guidanceof a section, a preamble, or a block as one<!-- <ns>:guidance … -->line before that part. - TLMD_195 [ubiquitous]: the engine SHALL write in a scaffold the message of each ban after the guidance of the part that the ban governs, and the message of each ban of the schema in the opening guidance line.
- TLMD_196 [ubiquitous]: the engine SHALL check no guidance line against the prohibited patterns of the schema.
- TLMD_120 [ubiquitous]: the engine SHALL write the
guidanceof a frontmatter constraint or a lifecycle dimension as a# <ns>:guidance …comment line before it. - TLMD_121 [ubiquitous]: the engine SHALL write into a scaffold the
onlymarker of each gated rule that the scaffold satisfies. - TLMD_122 [unwanted]: IF another rule of the schema matches a part that a gated rule writes into a scaffold, so that a marker of the scaffold gates nothing or disagrees with the rule that matches its part, or a gated part has no marker, THEN the engine SHALL report a finding on the schema, at the rule of the part where the engine knows it, that quotes the part.
- TLMD_123 [ubiquitous]: the engine SHALL write a part that the schema does not demand inside an
optionalregion. - TLMD_124 [unwanted]: IF a part inside an
optionalregion has no text to write, THEN the engine SHALL leave it out and report a warning that names the rule. - TLMD_125 [unwanted]: IF two optional regions of a scaffold are adjacent and the second opens no section, THEN the engine SHALL write them as one.
- TLMD_192 [ubiquitous]: the engine SHALL write each optional section of a scaffold in an optional region of its own.
- TLMD_126 [optional]: WHERE a unit rule declares
linesand its fence has an example, the engine SHALL copy that example into the scaffold. - TLMD_127 [optional]: WHERE a scaffold contains the example of a fence, the engine SHALL read the keys of the line rules out of that example.
- TLMD_128 [optional]: WHERE a section declares
parent, the engine SHALL write the section inside each occurrence of its parent in a scaffold. - TLMD_129 [optional]: WHERE a unit rule declares
for-each-key-of, the engine SHALL write one unit for each distinct key of the referenced rules in scope, with the key spliced into the example of the rule. - TLMD_199 [ubiquitous]: the engine SHALL count toward the units that a guarded rule of
ruleswrites into a scaffold the units that earlier rules of the block write and its guard selects, and read the keys of the rule out of those units. - TLMD_130 [optional]: WHERE a section that declares
parentcontains a pairing, the engine SHALL write the section where the pairing has keys, leave it out where the pairing has none, and put it in no optional region. - TLMD_131 [optional]: WHERE a unit rule of a list declares
blocks, the engine SHALL write those blocks under each item that it writes, indented to the text of the item. - TLMD_132 [ubiquitous]: the engine SHALL write the frontmatter keys that the host owns first, and the lines of the host last, and with the null host only the keys that the schema declares.
- TLMD_133 [invariant]: FOR ANY schema and any state combination, the engine SHALL either refuse the derivation with a finding that names a rule, or derive a scaffold that satisfies that schema at those states with an agreeing marker for every gate of that check.
- TLMD_164 [unwanted]: IF a scaffold that the engine derives fails its schema, other than on a line that holds an example that fails its own rule or format, or at a rule whose example fails it, THEN the engine SHALL report a finding on the schema, at the rule that the scaffold fails where the failure has one, that names the states of the scaffold, quotes the line of the scaffold that fails, and names the failure.
- TLMD_134 [invariant]: FOR ANY schema that derives, the engine SHALL write into the scaffold, outside its frontmatter and its markers, only a heading that the schema fixes, text that a rule fixes, or an example that the schema states.
- TLMD_135 [invariant]: FOR ANY schema whose subsection is inside a repeating section, optional or not, paired or not, numbered or not, and gated on any states, the engine SHALL derive a scaffold that satisfies that schema at each state.
- TLMD_149 [unwanted]: IF a scaffold writes a prose, bullet list, or numbered list block directly after a block of the same kind whose rule admits more units than the scaffold writes for it and states no unit shape, format, ban, guard, or sequence, THEN the engine SHALL refuse the derivation with a finding on the rule of the first block.
- TLMD_176 [ubiquitous]: the engine SHALL accept as the
exampleof a block or a unit rule either one string or a list of strings. - TLMD_177 [ubiquitous]: the engine SHALL check each string of a list
exampleagainst its rule as it checks a singleexample. - TLMD_178 [unwanted]: IF a string of a list
exampleof more than one string fails its rule, THEN the engine SHALL report a finding on the schema that names the position of the string in the list. - TLMD_179 [ubiquitous]: the engine SHALL quote the first string of a list
examplewherever a finding shows the example of the rule. - TLMD_180 [unwanted]: IF a section declares
examplesand does not declarerepeat: true, THEN the engine SHALL report a finding on the schema. - TLMD_181 [unwanted]: IF a text of the
examplesof a section does not begin with a heading of the level of the section, THEN the engine SHALL report a finding on the schema that names the position of the text. - TLMD_182 [unwanted]: IF the
exampleof a block or a unit rule is an empty list, THEN the engine SHALL report a finding on the schema. - TLMD_183 [unwanted]: IF a
codeblock that does not declarerepeat: truecarries, on the block or on one of its unit rules, a listexampleof more than one string, THEN the engine SHALL report a finding on the schema. - TLMD_184 [optional]: WHERE the
exampleof a unit rule or of a block is a list, the engine SHALL write one unit for each string of the list, in order, and no more units than themaxof the rule or of the block allows. - TLMD_185 [optional]: WHERE the
minof a rule or of a block demands more units than itsexamplegives strings, the engine SHALL write further copies of the last string. - TLMD_186 [optional]: WHERE a unit rule under
rulesorlinesdeclaresmin: 0and a listexample, the engine SHALL write the strings of the list. - TLMD_187 [optional]: WHERE a position of a
sequencecarries a listexample, the engine SHALL write every string of the list where the position declaresrepeat: true, and only the first string otherwise. - TLMD_188 [optional]: WHERE a
codeblock writes more than one unit, the engine SHALL write one fence for each unit, each after its ownonlymarker. - TLMD_189 [optional]: WHERE a repeating section that declares no
for-each-key-ofdeclaresexamples, the engine SHALL write each text ofexamples, in order and unchanged, after the occurrence that it derives, in the same optional region and after the sameonlymarker. - TLMD_190 [optional]: WHERE a text of the
examplesof a section has a heading that captures a key, the engine SHALL record that key for the rules that refer to the section. - TLMD_191 [optional]: WHERE a section that declares
for-each-key-ofdeclaresexamples, the engine SHALL write the occurrence for each key after the first from the next text ofexamples, unchanged, and write the occurrences for the keys that remain after the last text from the rules. - TLMD_150 [optional]: WHERE a unit rule of a list declares
blockswhose first block is not a list, the engine SHALL write a blank line between the text of each item and its nested blocks.
10. Repairs
The engine repairs what the frontmatter and the schema determine. It never edits what belongs to the host.
- TLMD_136 [ubiquitous]: the engine SHALL repair a heading or a unit bound to a frontmatter value to that value.
- TLMD_165 [unwanted]: IF a heading or a unit bound to a frontmatter value differs from that value, THEN the engine SHALL report a finding that names the value and repair the text to it.
- TLMD_168 [unwanted]: IF a heading or a unit is bound to a frontmatter key that the document does not set and that the schema does not demand in the states of the document, THEN the engine SHALL report a finding that names the key.
- TLMD_137 [unwanted]: IF a heading has the text of a
renamed-fromof its section, THEN the engine SHALL report a finding and repair the heading to the current text. - TLMD_143 [unwanted]: IF a section declares
renamed-fromand its heading is a template or a pattern, THEN the engine SHALL report a finding on the schema. - TLMD_138 [unwanted]: IF a heading is a setext heading, THEN the engine SHALL report a finding and repair the heading to an ATX heading.
- TLMD_139 [ubiquitous]: the engine SHALL repair the anchors of a document as section 7 places them.
- TLMD_140 [ubiquitous]: the engine SHALL edit no interior of a region, no trailer, and no blank line before the trailer.