diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index 8e4ee9433..a66328b83 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -34,7 +34,7 @@ jobs: actions: write issues: write steps: - - uses: jeduden/merge-queue-action@10aad589472d158972acc028efbfbcd2ee579af2 # v0.4.1 + - uses: jeduden/merge-queue-action@fd94568d534e31322ce59898030b340a7c36202d # v0.5.0 with: token: ${{ secrets.MERGE_QUEUE_TOKEN }} ci_workflow: .github/workflows/ci.yml diff --git a/PLAN.md b/PLAN.md index 91f86a38a..1d4221faa 100644 --- a/PLAN.md +++ b/PLAN.md @@ -44,4 +44,6 @@ footer: | | 84 | 🔲 | [Symlink default-deny for file discovery](plan/84_symlink-default-deny.md) | | 85 | 🔲 | [Increase test coverage to 95% by extracting shared rule helpers](plan/85_coverage-to-95-percent.md) | | 86 | 🔲 | [Markdown flavor validation](plan/86_markdown-flavor-validation.md) | +| 87 | 🔲 | [Flavor validation for GitHub Alerts](plan/87_markdown-flavor-github-alerts.md) | +| 88 | 🔲 | [TOC directive migration aid](plan/88_toc-directive-migration.md) | diff --git a/plan/87_markdown-flavor-github-alerts.md b/plan/87_markdown-flavor-github-alerts.md new file mode 100644 index 000000000..7fd120f26 --- /dev/null +++ b/plan/87_markdown-flavor-github-alerts.md @@ -0,0 +1,155 @@ +--- +id: 87 +title: Flavor validation for GitHub Alerts +status: "🔲" +summary: >- + Extend MDS034 to detect GitHub Alerts syntax + (`> [!NOTE]` blockquote prefix) as a GFM-only + feature with auto-fix that demotes the alert to + a plain blockquote when the target flavor does + not support it. +--- +# Flavor validation for GitHub Alerts + +Extends [plan 86](86_markdown-flavor-validation.md) +(MDS034, flavor validation). Add one feature — +GitHub Alerts — to the MDS034 feature enum. + +Depends on: plan 86 lands first (provides the +dual parser, feature enum, fix pipeline). + +## Goal + +MDS034 flags `> [!NOTE]`-style alert blockquotes +when the target flavor is `commonmark` or +`goldmark`. `gfm` accepts them. Auto-fix demotes +the alert marker so the blockquote still renders +on non-GFM renderers. + +## Context + +GitHub added Alerts to GFM in December 2023 +(see the `github.blog` changelog entry for +`new-syntax-for-alerts-on-github`). Five tokens +are recognized: `[!NOTE]`, `[!TIP]`, +`[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`. +Obsidian callouts use the same prefix and accept +extra tokens, but only these five are standard +GFM. + +On CommonMark / goldmark-default, the marker +renders as literal text inside a blockquote: + +```markdown +> [!NOTE] +> Something to remember. +``` + +becomes a blockquote whose first line is the +literal string `[!NOTE]`. The author intended a +styled callout; the reader sees unstyled text +with the marker token visible inside the +blockquote. The failure is visible, not silent, +but the author's intent is still lost. + +### Why not a generic container rule + +The research spike evaluated four other +container syntaxes (Pandoc `:::` fenced divs, +MyST `:::{note}`, markdown-it-container, MkDocs +`!!! note`). None are mutually compatible and no +linter in the comparison covers them. GitHub +Alerts are the only variant with a standardized +spec, broad renderer support, and a clear +failure mode — so this plan covers them alone. + +## Design + +### Detection + +GitHub Alerts need no new goldmark extension. +The syntax is a plain Blockquote. Its first +paragraph text must match +`^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$` +(case-sensitive per GFM). + +Detection is an AST walk over `ast.Blockquote` +nodes on the dual parser's tree. The same walk +pattern the other 12 features use. + +### Configuration + +No new settings. GitHub Alerts join the existing +feature enum in +`internal/rules/markdownflavor/features.go` as +feature 13 (`GitHubAlerts`). Flavor support: + +| Flavor | GitHub Alerts | +|------------|---------------| +| commonmark | unsupported | +| gfm | supported | +| goldmark | unsupported | + +### Auto-fix + +Remove the `[!TOKEN]` marker line, keeping the +rest of the blockquote intact: + +```markdown +> [!NOTE] > Something to +> Something to → > remember. +> remember. +``` + +If the alert marker is the only line in the +blockquote, remove the whole blockquote. The +marker line has no meaningful content once the +token is gone. + +### Error message + +`github alerts are not supported by {flavor}` + +Severity: `warning`, matching the other MDS034 +features. + +## Tasks + +1. Add `GitHubAlerts` to the feature enum in + `internal/rules/markdownflavor/features.go` +2. Add flavor support table entry: supported in + `gfm`, unsupported in `commonmark` and + `goldmark` +3. Implement an AST detector that walks + `ast.Blockquote` nodes and matches the five + GFM tokens on the first paragraph child +4. Implement the fix: strip the marker line; + drop the blockquote if empty afterward +5. Add unit tests: each of the five tokens, + lower-case tokens (should not match), mixed + content after the marker, marker as the only + line +6. Add good/bad fixtures under + `internal/rules/MDS034-markdown-flavor/alerts/` +7. Update the MDS034 README to list GitHub + Alerts as the 13th feature + +## Acceptance Criteria + +- [ ] `flavor: commonmark` flags all five alert + tokens +- [ ] `flavor: goldmark` flags all five alert + tokens +- [ ] `flavor: gfm` accepts all five tokens +- [ ] `mdsmith fix` removes the marker line, + preserves remaining blockquote content +- [ ] `mdsmith fix` removes the whole blockquote + when the marker was its only line +- [ ] Lower-case or unknown tokens (e.g. + `[!note]`, `[!INFO]`) produce no + diagnostic — they are ordinary blockquote + text +- [ ] Nested blockquotes are checked recursively +- [ ] All tests pass: `go test ./...` +- [ ] `go tool golangci-lint run` reports no + issues diff --git a/plan/88_toc-directive-migration.md b/plan/88_toc-directive-migration.md new file mode 100644 index 000000000..10ec5d067 --- /dev/null +++ b/plan/88_toc-directive-migration.md @@ -0,0 +1,243 @@ +--- +id: 88 +title: TOC directive migration aid +status: "🔲" +summary: >- + New rule MDS035 that flags renderer-specific + table-of-contents directives (`[TOC]`, + `[[_TOC_]]`, `[[toc]]`, `${toc}`) which render + as literal text on CommonMark / goldmark + instead of expanding into a TOC. The + diagnostic points authors at mdsmith's + `` directive for the file-index use + case; heading-level TOCs have no direct + mdsmith equivalent. +--- +# TOC directive migration aid + +## Goal + +Catch renderer-specific TOC directives that do +not expand into a TOC on CommonMark or +goldmark. The diagnostic tells authors which +use case has a mdsmith equivalent and which +does not. + +## Context + +Four TOC directive variants appear in the wild: + +- `[TOC]` — Python-Markdown, MultiMarkdown, + Pandoc (with `--toc`) +- `[[_TOC_]]` — GitLab Flavored Markdown, + Azure DevOps +- `[[toc]]` — markdown-it-toc-done-right, + VitePress +- `${toc}` — some VitePress configurations + +None are standardized TOC directives in +CommonMark, GFM, or goldmark. `[TOC]` is still +valid CommonMark syntax — a shortcut reference +link — so it only renders as literal text when +no matching `[TOC]: ` definition is +present; with a definition, it renders as a +link. The other three tokens are not valid +link-reference shorthand and always render as +literal text. On those renderers no token in +this set expands into a generated TOC. + +Concrete render outputs when no link reference +is present: + +- `[TOC]` renders as the literal string `[TOC]` + (goldmark emits a "no matching link + reference" fallback, which is verbatim text) +- `[[_TOC_]]` renders as `[[_TOC_]]` inside a + paragraph +- `[[toc]]` renders as `[[toc]]` inside a + paragraph +- `${toc}` renders as `${toc}` inside a + paragraph + +The author intended a generated table of +contents; the reader sees the directive token +instead. This is a visible failure, not a +silent one, but it is still a failure worth +catching at lint time. + +### Heading TOC vs file index + +The flagged directives and +[``][catalog] solve different +problems: + +| Directive | Generates | Input | +|----------------|-------------------------------------------|--------------------| +| `[TOC]` et al. | Table of **headings in the current file** | Current doc | +| `` | Table of **other files** matching a glob | Glob + frontmatter | + +[catalog]: ../internal/rules/MDS019-catalog/README.md + +`` is the right replacement only +when a directive is used on an index page to +list sibling or child documents (e.g. a wiki +homepage with `[[_TOC_]]` listing all pages in +the space). For in-document heading TOCs — the +more common case — mdsmith has no built-in +generator; the author must either drop the +directive or maintain a manual list. + +### Why this rule, not MDS034 + +MDS034 ([plan 86](86_markdown-flavor-validation.md)) +validates syntax support against a declared +flavor. TOC directives are not "flavor features" +— they are per-renderer conventions with no +canonical spec and no fix path that applies to +every call site. A dedicated opt-in rule with a +diagnostic tailored to the use-case distinction +above is a better fit than folding them into +MDS034's fix pipeline. + +### Scope + +Flag only the four directives above. Do not try +to auto-generate a `` block — the +right glob and front-matter fields depend on the +project and are not knowable from the TOC call +site. The diagnostic is informational and names +both the file-index case (points to MDS019) and +the heading-TOC case (no equivalent). + +## Design + +### Detection + +Line-level regex on the raw source, scoped to +paragraph nodes (skip code blocks, HTML blocks, +and inline code spans). Four patterns: + +- `^\[TOC\]\s*$` +- `^\[\[_TOC_\]\]\s*$` +- `^\[\[toc\]\]\s*$` +- `^\$\{toc\}\s*$` + +Goldmark parses `[TOC]` as a link reference node +and `[[_TOC_]]` / `[[toc]]` as text inside a +paragraph. AST detection would require +per-variant walkers; raw-line regex is simpler +and avoids false positives by restricting the +match to paragraph-only regions. + +#### Link reference exception for `[TOC]` + +`[TOC]` is syntactically a valid CommonMark +shortcut reference link. If the document +contains a matching link reference definition +(`[TOC]: `), `[TOC]` resolves to a +legitimate link and must not be flagged. + +Before emitting a diagnostic for the `[TOC]` +pattern, consult the goldmark parser context's +link reference map for a definition with the +label `TOC` (case-insensitive, per the +[CommonMark matching rules][cm-refs]). If one +exists, suppress the diagnostic. + +[cm-refs]: https://spec.commonmark.org/0.31.2/#matches + +The other three patterns do not have this +ambiguity: `[[_TOC_]]`, `[[toc]]`, and `${toc}` +do not form valid link references in CommonMark +and always render as literal text in a +paragraph. No exception handling is needed for +them. + +### Configuration + +Rule `toc-directive`, category `meta`, disabled +by default (opt-in) — consistent with MDS034's +opt-in posture. No settings. + +### Error message + +Templated on the matched directive token, so +each variant produces its own message. Given a +matched token `{directive}`: + +```text +unsupported TOC directive `{directive}`; mdsmith has no heading TOC equivalent; use `` for file indexes (MDS019) +``` + +Example messages the rule would emit for each +of the four patterns: + +```text +unsupported TOC directive `[TOC]`; mdsmith has no heading TOC equivalent; use `` for file indexes (MDS019) +unsupported TOC directive `[[_TOC_]]`; mdsmith has no heading TOC equivalent; use `` for file indexes (MDS019) +unsupported TOC directive `[[toc]]`; mdsmith has no heading TOC equivalent; use `` for file indexes (MDS019) +unsupported TOC directive `${toc}`; mdsmith has no heading TOC equivalent; use `` for file indexes (MDS019) +``` + +The leading word is lowercase. No trailing +punctuation, per [CLAUDE.md](../CLAUDE.md). +Both the matched token and the replacement +`` are backticked so they read as +quoted directives, not prose. + +Severity: `warning`. + +### No auto-fix + +The rule is detection-only. Whether the right +replacement is ``, a manually +maintained list, or deletion depends on intent +that is not recoverable from the directive +alone. + +## Tasks + +1. Create `internal/rules/MDS035-toc-directive/` + with `rule.go`, `README.md` +2. Implement paragraph-scoped line scanning for + the four directive patterns +3. For the `[TOC]` pattern, consult the goldmark + parser context's link reference definition + map; suppress the diagnostic when a label + `TOC` (case-insensitive) is defined +4. Implement `rule.Defaultable` with + `EnabledByDefault` returning `false` +5. Register as MDS035 in category `meta` +6. Add good/bad fixtures with front-matter + specifying the expected diagnostics, including + a good fixture that has `[TOC]: https://x` as + a reference definition alongside a `[TOC]` + line +7. Document the rule in the flavor comparison + table in + [docs/background/markdown-linters.md](../docs/background/markdown-linters.md) + +## Acceptance Criteria + +- [ ] `[TOC]` on its own line produces a + diagnostic that names both the heading-TOC + gap and the `` file-index + alternative +- [ ] `[[_TOC_]]` on its own line produces the + same diagnostic +- [ ] `[[toc]]` on its own line produces the + same diagnostic +- [ ] `${toc}` on its own line produces the + same diagnostic +- [ ] `[TOC]` inside a fenced code block + produces no diagnostic +- [ ] `[TOC]` inside an inline code span + produces no diagnostic +- [ ] `[TOC]` used as legitimate link text + (with a matching `[TOC]: url` definition) + produces no diagnostic +- [ ] Rule is disabled by default (opt-in) +- [ ] No auto-fix is applied +- [ ] All tests pass: `go test ./...` +- [ ] `go tool golangci-lint run` reports no + issues