| id | 108 |
|---|---|
| title | Horizontal rule style rule |
| status | ✅ |
| summary | New rule MDS044 that pins one of `---`, `***`, or `___` for thematic breaks and requires blank lines on both sides so the rule cannot be confused with a setext heading underline. Closes one of the ambiguity cases in "Exhibit C" of the bgslabs.org rant. |
| model | sonnet |
Let users pin a single delimiter for thematic breaks.
CommonMark accepts ---, ***, and ___ (with any
length ≥ 3 and any internal spaces). One of the three
also collides with the setext heading underline:
--- under text becomes an <h2> instead of a
horizontal rule. This rule pins one delimiter and
requires surrounding blank lines so the collision
cannot occur.
Thematic breaks are *ast.ThematicBreak. The node
carries no delimiter information; the rule must read
the source line that produced the node.
Setext headings (Title\n===== or Title\n-----)
are *ast.Heading with IsSetext true. MDS002
already lets users pin ATX-only, which removes the
collision risk from one direction. MDS044 closes the
other direction: even with setext allowed, a
horizontal rule must not look like a setext underline.
MDS002 is about headings. MDS044 is about thematic breaks. They share an underlying syntax collision but operate on disjoint AST node types and have independent toggles.
rules:
horizontal-rule-style:
style: dash # dash | asterisk | underscore
length: 3 # required exact length
require-blank-lines: trueCategory: whitespace. Disabled by default (opt-in).
Plan 112 ships profiles that auto-enable this rule:
profile: portableactivates withstyle: dash,length: 3, andrequire-blank-lines: true.profile: githubdoes not activate this rule.profile: plainactivates withstyle: dash,length: 3, andrequire-blank-lines: true.
User overrides on top of the profile still win via deep-merge.
Walk *ast.ThematicBreak. For each node:
- Read the source line. Strip leading/trailing whitespace.
- If any character is not the configured delimiter,
emit
wrong delimiter. - If internal spaces exist, emit
delimiter has internal spaces. - If the visible character count differs from
length, emitwrong length. - If
require-blank-linesis true and the previous or next line is non-blank, emitmissing blank line.
Replace the line with the canonical delimiter
repeated length times. Insert a blank line above
and below if missing.
horizontal rule uses {actual}; configured style is {expected}
horizontal rule has internal spaces
horizontal rule has length {actual}; configured length is {expected}
horizontal rule needs a blank line {above|below}
- Scaffold
internal/rules/horizontalrulestyle/. - Implement
Check()walking*ast.ThematicBreak. - Implement
rule.Configurableforstyle,length, andrequire-blank-lines. - Implement
Fix()rewriting the line and adding surrounding blank lines. - Register as MDS044 in category
whitespace. - Add fixture tests covering each delimiter choice, wrong length, internal spaces, missing blank lines, and a thematic break adjacent to a setext heading underline.
- Add rule README.
-
---withstyle: dashandlength: 3emits no diagnostic. -
***withstyle: dashemits one diagnostic and fixes to---. -
- - -emits an internal-spaces diagnostic. -
-----withlength: 3emits a length diagnostic and fixes to---. - A thematic break with no blank line above
emits one diagnostic when
require-blank-lines: true. - A setext heading underline (
====after text) emits no diagnostic from this rule. - Rule is disabled by default.
- All tests pass:
go test ./... -
go tool golangci-lint runreports no issues