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.