Skip to content

Commit d65713a

Browse files
committed
Add plan 89: TOC generator directive and MDS035 auto-fix
Follow-up to plan 88. Adds a <?toc?>...<?/toc?> generated-section directive (MDS036) that emits a nested list of the document's headings, and upgrades MDS035 from detection-only to auto-fix by rewriting the four detected renderer-specific TOC tokens into <?toc?> blocks that MDS036 then populates on the next fix pass. Design notes: - New rule MDS036 "toc" (meta, default-enabled, fixable) built on the shared internal/archetype/gensection engine that MDS019 catalog already uses. - Parameters: min-level (default 2, matches Python-Markdown [TOC]) and max-level (default 6). Output is GitHub-slug links. - MDS035 Fix leaves [TOC] untouched when a matching link reference definition makes it resolve to a real link. - Relies on mdsmith's existing multi-pass fix; falls back to emitting populated content directly from MDS035 only if that proves brittle.
1 parent ccd272a commit d65713a

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ footer: |
4646
| 86 | 🔲 | [Markdown flavor validation](plan/86_markdown-flavor-validation.md) |
4747
| 87 | 🔲 | [Flavor validation for GitHub Alerts](plan/87_markdown-flavor-github-alerts.md) |
4848
| 88 | 🔲 | [TOC directive migration aid](plan/88_toc-directive-migration.md) |
49+
| 89 | 🔲 | [TOC generator directive and MDS035 auto-fix](plan/89_toc-generator-directive.md) |
4950
<?/catalog?>

plan/89_toc-generator-directive.md

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
---
2+
id: 89
3+
title: TOC generator directive and MDS035 auto-fix
4+
status: "🔲"
5+
summary: >-
6+
Add a `<?toc?>...<?/toc?>` generated-section
7+
directive that emits a nested list of the
8+
current document's headings linked to their
9+
anchors (MDS036). Upgrade MDS035 (plan 88) to
10+
auto-fix each detected renderer-specific TOC
11+
token by replacing it with a `<?toc?>` block,
12+
which the directive then regenerates on
13+
`mdsmith fix`.
14+
---
15+
# TOC generator directive and MDS035 auto-fix
16+
17+
## Goal
18+
19+
Give mdsmith a native heading-level TOC
20+
generator so MDS035's detection can be followed
21+
by an actual fix. Today MDS035 only flags
22+
`[TOC]`, `[[_TOC_]]`, `[[toc]]`, `${toc}` and
23+
points at `<?catalog?>`, which is a file-index
24+
directive, not a heading-outline replacement.
25+
26+
## Context
27+
28+
Plan 88 landed MDS035 as detection-only because
29+
mdsmith had no equivalent for in-document
30+
heading TOCs — the common case of those tokens.
31+
`<?catalog?>` lists *other* files matching a
32+
glob, so it is the right replacement only on
33+
index pages, not on the per-document TOCs
34+
authors usually want.
35+
36+
mdsmith already has the generated-section
37+
machinery needed here.
38+
[`<?catalog?>`][catalog] and
39+
[`<?include?>`][include] each read a directive
40+
body, compute content, and emit it between
41+
`<?name ...?>` and `<?/name?>` markers. A rule
42+
keeps the content current on `mdsmith fix`.
43+
See the [generated-section archetype][gensection]
44+
for the shared mechanics.
45+
46+
[catalog]: ../internal/rules/MDS019-catalog/README.md
47+
[include]: ../internal/rules/MDS021-include/README.md
48+
[gensection]: ../docs/background/archetypes/generated-section/README.md
49+
50+
### Why both pieces in one plan
51+
52+
The `<?toc?>` directive is only useful to
53+
MDS035 if MDS035 knows to rewrite its detected
54+
tokens to it; conversely, MDS035's auto-fix is
55+
only possible once `<?toc?>` exists. Splitting
56+
the work across two plans would leave one half
57+
dead code until the other lands.
58+
59+
## Design
60+
61+
### Directive syntax
62+
63+
```text
64+
<?toc
65+
min-level: 2
66+
max-level: 3
67+
?>
68+
- [First heading](#first-heading)
69+
- [Subheading](#subheading)
70+
- [Second heading](#second-heading)
71+
<?/toc?>
72+
```
73+
74+
Parameters (all optional):
75+
76+
| Name | Type | Default | Description |
77+
|-------------|------|---------|-----------------------------------------------------|
78+
| `min-level` | int | `2` | Lowest heading level to include (1–6) |
79+
| `max-level` | int | `6` | Highest heading level to include (1–6, ≥ min-level) |
80+
81+
`min-level: 2` excludes the document title by
82+
default, matching what Python-Markdown's `[TOC]`
83+
produces with default settings.
84+
85+
### Generated content
86+
87+
A nested unordered list, one item per heading
88+
in source order. Each item links to a
89+
GitHub-flavor slug of the heading text:
90+
91+
- Lowercase the heading plain text.
92+
- Replace spaces with `-`.
93+
- Strip characters outside `[a-z0-9-_]`.
94+
- Disambiguate repeats by appending `-1`, `-2`, …
95+
in source order (matching goldmark-meta /
96+
GitHub's behavior).
97+
98+
Indentation is `<?listindent?>`-aware: use the
99+
same per-level indent as MDS016 (two spaces by
100+
default).
101+
102+
List items respect the heading structure, not
103+
the raw level. Given H2 → H4 → H2, the tree is:
104+
105+
```text
106+
- [h2a](#h2a)
107+
- [h4](#h4)
108+
- [h2b](#h2b)
109+
```
110+
111+
…not a flat list keyed on absolute level.
112+
113+
### Rule: MDS036 (toc)
114+
115+
- ID: `MDS036`
116+
- Name: `toc`
117+
- Category: `meta`
118+
- Default: enabled (generated sections are
119+
enabled by default for the project; users opt
120+
out by removing the directive)
121+
- Fixable: yes
122+
123+
`Check` diff-compares the body between the
124+
markers against the regenerated output.
125+
`Fix` rewrites the body.
126+
127+
Use the shared `internal/archetype/gensection`
128+
engine that `catalog` already uses. New
129+
directive logic lives in
130+
`internal/rules/toc/`.
131+
132+
### MDS035 auto-fix
133+
134+
MDS035 becomes a `FixableRule`. On `Fix`, for
135+
each matched directive line inside a paragraph:
136+
137+
- `[TOC]` (unresolved), `[[_TOC_]]`, `[[toc]]`,
138+
`${toc}` → replace the single line with:
139+
140+
```text
141+
<?toc
142+
?>
143+
<?/toc?>
144+
```
145+
146+
The empty YAML body means "use defaults". A
147+
trailing newline separator is inserted if the
148+
surrounding paragraph would otherwise fuse
149+
the directive into adjacent text.
150+
151+
- `[TOC]` with a matching link reference
152+
definition → leave untouched (already
153+
suppressed by Check; must stay out of Fix).
154+
155+
The Fix output is plain `<?toc?>...<?/toc?>`
156+
with an **empty** body. MDS036 runs in a
157+
subsequent pass of the same `mdsmith fix`
158+
invocation (mdsmith already supports
159+
multi-pass fix, used by MDS019/MDS021) and
160+
fills in the heading list. If the one-pass
161+
semantics become brittle, an alternative is for
162+
MDS035 to call into `gensection` directly and
163+
emit populated content; start with multi-pass
164+
and fall back only if needed.
165+
166+
### Interaction with existing rules
167+
168+
- MDS015 (blank line around fenced code /
169+
generated sections): the Fix output adds the
170+
required blank-line padding around the
171+
inserted block.
172+
- MDS020 (required-structure): no change; TOC
173+
blocks are not a required section.
174+
- MDS019 (catalog), MDS021 (include): orthogonal
175+
— different directive names.
176+
177+
### Out of scope
178+
179+
- Custom link anchor format overrides
180+
(e.g., non-GitHub slugger). Always GitHub-
181+
style for the first release; add a
182+
`slugger:` parameter later if needed.
183+
- Skipping specific headings via frontmatter
184+
or attribute syntax. Use `min-level` /
185+
`max-level` for now.
186+
- Rendering `[TOC]` into `<?toc?>` with
187+
preserved non-default parameters. The four
188+
renderer-specific tokens have no shared
189+
parameter surface; always emit the default
190+
`<?toc?>`.
191+
192+
## Tasks
193+
194+
1. Create the `<?toc?>` directive in a new
195+
`internal/rules/toc/` package using the
196+
shared `internal/archetype/gensection.Engine`
197+
(same engine as `catalog`). Add a slug
198+
helper in `internal/mdtext/` or the toc
199+
package. Register as MDS036 in category
200+
`meta`, enabled by default, `FixableRule`.
201+
2. Add MDS036 fixtures under
202+
`internal/rules/MDS036-toc/`: `good/` with a
203+
correct body, `bad/` with a stale body to
204+
verify Check, `fixed/` with the expected
205+
output after Fix. Cover default parameters,
206+
custom `min-level`/`max-level`, single-level
207+
docs, and deeply nested structures.
208+
3. Upgrade MDS035 to `FixableRule`: replace
209+
matched directive lines with
210+
`<?toc?>\n<?/toc?>` blocks preserving
211+
surrounding blank lines; leave `[TOC]`
212+
untouched when a link-ref definition
213+
suppresses Check; add `fixed/` fixtures for
214+
each of the four variants.
215+
4. Update MDS035's diagnostic message to point
216+
at `<?toc?>` (MDS036) instead of
217+
`<?catalog?>` (MDS019); wording:
218+
`unsupported TOC directive \`{token}\`; use \`<?toc?>\` (MDS036)`
219+
5. Update MDS035 README (message + examples),
220+
plan 88 status/deviation note, and the
221+
renderer-portability section in
222+
[docs/background/markdown-linters.md][lnt].
223+
6. Update the generated-section archetype doc
224+
to list `toc` alongside `catalog` and
225+
`include`.
226+
7. Verify multi-pass `mdsmith fix` end-to-end:
227+
starting from a document containing `[TOC]`,
228+
a single `fix` run must yield a populated
229+
`<?toc?>...<?/toc?>` block. Add an
230+
integration test asserting this.
231+
232+
[lnt]: ../docs/background/markdown-linters.md
233+
234+
## Acceptance Criteria
235+
236+
- [ ] `<?toc?>...<?/toc?>` in a document
237+
regenerates on `mdsmith fix` with a
238+
nested list of headings linked to
239+
GitHub-style slugs
240+
- [ ] `min-level` and `max-level` parameters
241+
gate which headings appear
242+
- [ ] `<?toc?>` with a stale body produces
243+
an MDS036 diagnostic on `check`
244+
- [ ] MDS035 `fix` rewrites `[TOC]`,
245+
`[[_TOC_]]`, `[[toc]]`, and `${toc}`
246+
on their own line to
247+
`<?toc?>\n<?/toc?>` blocks
248+
- [ ] MDS035 `fix` leaves `[TOC]` untouched
249+
when a matching link reference
250+
definition is present
251+
- [ ] A single `mdsmith fix` run converts a
252+
source containing `[TOC]` into a source
253+
containing a populated `<?toc?>` block
254+
- [ ] Merge driver regenerates `<?toc?>`
255+
bodies on conflict, same as `<?catalog?>`
256+
- [ ] MDS035 diagnostic message names
257+
`<?toc?>` (MDS036) as the replacement
258+
- [ ] All tests pass: `go test ./...`
259+
- [ ] `go tool golangci-lint run` reports
260+
no issues

0 commit comments

Comments
 (0)