first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
# `markdown-fixer/rules`
## Purpose
`rules.ts` houses the individual remark plugins that implement each markdown-lint rule
the fixer enforces, plus the shared heuristic table (`PROPER_NAMES`) and the
`inferCodeLanguage` heuristic. It is the rule-by-rule contract of the fixer; the
`markdown-fixer.ts` entry point composes these plugins into a single `unified()`
pipeline so the orchestration code stays short and the rule logic stays in one place,
testable and reviewable per rule.
## Features
- One exported plugin factory per rule — `fixMultipleH1s`, `fixEmphasisAsHeading`,
`fixBlankLines`, `fixProperNames`, `fixOrderedListNumbering`, `fixHorizontalRules`,
`fixCodeBlockLanguages`. Each takes a `RuleSet` sink the caller mutates as the
plugin walks the AST so the fixer can report exactly which rules altered a file.
- `HEADING_HINT_BASIC` and `HEADING_HINT_EXTENDED` regexes — the basic matcher
recognises `Step N:`, `A)`, and all-caps `LABEL:` prefixes; the extended matcher
adds `NEVER`, `ALWAYS`, `CRITICAL`, `IMPORTANT`, `MANDATORY` keywords used in
rules / safety docs.
- `inferCodeLanguage(value)` — heuristic mapper from code-block content to a
language identifier (`bash`, `javascript`, `typescript`, `python`, `markdown`,
`html`, `json`, `text`).
- `PROPER_NAMES` — closed allow-list of `(lower, proper)` pairs for MD044.
## Usage: Basic
```ts
import {
fixMultipleH1s,
fixCodeBlockLanguages,
} from "@suaveplan/linter/markdown-fixer";
const rules = new Set<string>();
const sink = { add: (r: string) => rules.add(r) };
// then plug each factory into a `unified().use(...)` pipeline
unified().use(fixMultipleH1s, sink).use(fixCodeBlockLanguages, sink);
```
## Usage: Advanced
Compose the same plugin set in a custom pipeline that strips Markdown to plain text:
```ts
import remarkParse from "remark-parse";
import remarkStringify from "remark-stringify";
import { unified } from "unified";
import {
fixCodeBlockLanguages,
fixMultipleH1s,
HEADING_HINT_EXTENDED,
fixEmphasisAsHeading,
} from "@suaveplan/linter/markdown-fixer";
const rules = new Set<string>();
const sink = { add: (r: string) => rules.add(r) };
const processor = unified()
.use(remarkParse)
.use(fixMultipleH1s, sink)
.use(fixEmphasisAsHeading, sink, HEADING_HINT_EXTENDED)
.use(fixCodeBlockLanguages, sink)
.use(remarkStringify);
const file = await processor.process("# A\n# B\n");
const applied = [...rules];
```
## API Reference
### `interface RuleSet { add(rule: string): void }`
The mutable sink the plugins write to. The fixer passes a `Set<string>` wrapper.
### `HEADING_HINT_BASIC: RegExp`
Matches `^(Step \d+:|[A-Z]\)|[A-Z][A-Z\s]+:)` — used by the first emphasis-as-heading
pass.
### `HEADING_HINT_EXTENDED: RegExp`
Extended matcher used by the second pass; adds keyword prefixes.
### `PROPER_NAMES: ReadonlyArray<readonly [string, string]>`
Closed pair list — every lowercase form on the left rewrites to the canonical form on
the right with a case-insensitive word-boundary match.
### `inferCodeLanguage(value: string): string`
Heuristic returning one of `"bash" | "javascript" | "typescript" | "python" | "markdown" | "html" | "json" | "text"`.
### Plugin factories
Each factory accepts the `RuleSet` (and a matcher where applicable) and returns a
`unified` transformer of type `(tree: Root) => void`. Exported names:
`fixMultipleH1s`, `fixEmphasisAsHeading`, `fixBlankLines`, `fixProperNames`,
`fixOrderedListNumbering`, `fixHorizontalRules`, `fixCodeBlockLanguages`.
## Implementation Notes
- The blank-line pass walks the tree depth-first, fixing each `Parent`'s children
bottom-up so a heading nested inside a `blockquote` gets the same surround-with-blanks
treatment as one at the document root. The pass also collapses adjacent empty
paragraphs as MD012.
- The emphasis-as-heading pass runs TWICE in the pipeline — once with the basic
matcher, once with the extended matcher. The basic matcher handles the common
`Step 1: …` form; the extended pass picks up safety-doc prefixes
(`NEVER`, `MANDATORY`) that the basic pattern is intentionally too narrow to catch.
- `fixOrderedListNumbering` resets `list.start = 1` only when it isn't already 1.
This avoids treating an already-canonical list as "changed" — necessary so
`fixMarkdown` can report `changed: false` correctly.
- `inferCodeLanguage` is intentionally conservative — any block that doesn't match a
known heuristic falls back to `"text"` rather than guessing. The remark fixer is a
formatter, not a syntax analyser.