# `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(); 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(); 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` 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` 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.