| id | 89 |
|---|---|
| title | TOC generator directive and MDS035 auto-fix |
| status | 🔲 |
| summary | Add a `<?toc?>...<?/toc?>` generated-section directive that emits a nested list of the current document's headings linked to their anchors (MDS037). Upgrade MDS035 (plan 88) to auto-fix each detected renderer-specific TOC token by replacing it with a `<?toc?>` block, which the directive then regenerates on `mdsmith fix`. |
Give mdsmith a native heading-level TOC
generator so MDS035's detection can be followed
by an actual fix. Today MDS035 only flags
[TOC], [[_TOC_]], [[toc]], ${toc} and
points at <?catalog?>, which is a file-index
directive, not a heading-outline replacement.
Plan 88 landed MDS035 as detection-only because
mdsmith had no equivalent for in-document
heading TOCs — the common case of those tokens.
<?catalog?> lists other files matching a
glob, so it is the right replacement only on
index pages, not on the per-document TOCs
authors usually want.
mdsmith already has the generated-section
machinery needed here.
<?catalog?> and
<?include?> each read a directive
body, compute content, and emit it between
<?name ...?> and <?/name?> markers. A rule
keeps the content current on mdsmith fix.
See the generated-section archetype
for the shared mechanics.
The <?toc?> directive is only useful to
MDS035 if MDS035 knows to rewrite its detected
tokens to it; conversely, MDS035's auto-fix is
only possible once <?toc?> exists. Splitting
the work across two plans would leave one half
dead code until the other lands.
<?toc
min-level: 2
max-level: 3
?>
- [First heading](#first-heading)
- [Subheading](#subheading)
- [Second heading](#second-heading)
<?/toc?>
Parameters (all optional):
| Name | Type | Default | Description |
|---|---|---|---|
min-level |
int | 2 |
Lowest heading level to include (1–6) |
max-level |
int | 6 |
Highest heading level to include (1–6, ≥ min-level) |
min-level: 2 excludes the document title by
default, matching what Python-Markdown's [TOC]
produces with default settings.
A nested unordered list, one item per heading in source order. Each item links to the same heading anchor mdsmith already computes for cross-file heading references.
Do not define a TOC-specific slug algorithm.
Reuse the slugify function and the
duplicate-disambiguation logic in
internal/rules/crossfilereferenceintegrity/rule.go
(used by collectHeadingAnchors). Move it
into a shared package. internal/mdtext/ is
the natural home. Both rules then call shared
code so anchors stay consistent. Repeated
headings get -1, -2, … suffixes in source
order, matching the reference-integrity rule.
Indentation uses the same per-level indent as
the MDS016 list-indent rule's spaces
setting (default 2). There is no
<?listindent?> directive; indentation is a
property of the emitted output, not a
configurable parameter of <?toc?>.
List items respect the heading structure, not the raw level. Given H2 → H4 → H2, the tree is:
- [h2a](#h2a)
- [h4](#h4)
- [h2b](#h2b)
…not a flat list keyed on absolute level.
Note on numbering: plan 89 originally claimed
MDS036, but plan 51 shipped first and took
MDS036 for max-section-length. MDS034 remains
reserved for plan 86. Plan 89 takes MDS037.
- ID:
MDS037 - Name:
toc - Category:
meta - Default: enabled (generated sections are enabled by default for the project; users opt out by removing the directive)
- Fixable: yes
Check diff-compares the body between the
markers against the regenerated output.
Fix rewrites the body.
Use the shared internal/archetype/gensection
engine that catalog already uses. New
directive logic lives in
internal/rules/toc/.
MDS035 becomes a FixableRule. On Fix, for
each matched directive line inside a paragraph:
-
[TOC](unresolved),[[_TOC_]],[[toc]],${toc}→ replace the single line with the canonical empty generated-section block:<?toc?> <?/toc?>The empty body means "use defaults". MDS035's Fix must also insert surrounding blank lines when the directive would otherwise fuse into adjacent paragraph text.
-
[TOC]with a matching link reference definition → leave untouched (already suppressed by Check; must stay out of Fix).
The Fix output is plain <?toc?>...<?/toc?>
with an empty body. MDS037 runs in a
subsequent pass of the same mdsmith fix
invocation (mdsmith already supports
multi-pass fix, used by MDS019/MDS021) and
fills in the heading list. If the one-pass
semantics become brittle, an alternative is for
MDS035 to call into gensection directly and
emit populated content; start with multi-pass
and fall back only if needed.
- MDS015 only enforces blank lines around
fenced code blocks, not generated-section
markers. Blank-line padding around inserted
<?toc?>blocks is MDS035's responsibility at Fix time, not MDS015's. - MDS020 (required-structure): no change; TOC blocks are not a required section.
- MDS019 (catalog), MDS021 (include): orthogonal — different directive names.
- Custom link anchor format overrides
(e.g., non-GitHub slugger). Always GitHub-
style for the first release; add a
slugger:parameter later if needed. - Skipping specific headings via frontmatter
or attribute syntax. Use
min-level/max-levelfor now. - Rendering
[TOC]into<?toc?>with preserved non-default parameters. The four renderer-specific tokens have no shared parameter surface; always emit the default<?toc?>.
- Move
slugifyand the duplicate-anchor counter frominternal/rules/crossfilereferenceintegrity/into a shared helper ininternal/mdtext/. Update the reference-integrity rule to call the shared helper. No behavior change. - Create the
<?toc?>directive in a newinternal/rules/toc/package using the sharedinternal/archetype/gensection.Engine(same engine ascatalog) and the shared slug helper from task 1. Register as MDS037 in categorymeta, enabled by default,FixableRule. - Add MDS037 fixtures under
internal/rules/MDS037-toc/:good/with a correct body,bad/with a stale body to verify Check,fixed/with the expected output after Fix. Cover default parameters, custommin-level/max-level, single-level docs, and deeply nested structures. - Upgrade MDS035 to
FixableRule: replace matched directive lines with the canonical empty block<?toc?>\n<?/toc?>, inserting blank lines above and below as needed; leave[TOC]untouched when a link-ref definition suppresses Check; addfixed/fixtures for each of the four variants. - Update MDS035's diagnostic message to point
at
<?toc?>(MDS037) instead of<?catalog?>(MDS019); wording:unsupported TOC directive \{token}`; use `` (MDS037)` - Update MDS035 README (message + examples), plan 88 status/deviation note, and the renderer-portability section in docs/background/markdown-linters.md.
- Update the generated-section archetype doc
to list
tocalongsidecatalogandinclude. - Verify multi-pass
mdsmith fixend-to-end: starting from a document containing[TOC], a singlefixrun must yield a populated<?toc?>...<?/toc?>block. Add an integration test asserting this.
-
<?toc?>...<?/toc?>in a document regenerates onmdsmith fixwith a nested list of headings linked to GitHub-style slugs -
min-levelandmax-levelparameters gate which headings appear -
<?toc?>with a stale body produces an MDS037 diagnostic oncheck - MDS035
fixrewrites[TOC],[[_TOC_]],[[toc]], and${toc}on their own line to<?toc?>\n<?/toc?>blocks - MDS035
fixleaves[TOC]untouched when a matching link reference definition is present - A single
mdsmith fixrun converts a source containing[TOC]into a source containing a populated<?toc?>block - Merge driver regenerates
<?toc?>bodies on conflict, same as<?catalog?> - MDS035 diagnostic message names
<?toc?>(MDS037) as the replacement - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues