Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 blocks must contain exactly one prose block.
  • A part with no optional is required.
  • A table with no columns-open contains 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 only or guidance marker in a document is an error.
  • TLMD_40 and TLMD_116: no blocks means 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.