| id | 106 |
|---|---|
| title | Emphasis style rule |
| status | ✅ |
| summary | New rule MDS042 that pins one delimiter per role: one of asterisk or underscore for bold, one for italic. Removes the bold/italic ambiguity called out as "Exhibit A" in the bgslabs.org markdown rant. |
| model | sonnet |
Let users pin a single delimiter for bold and a single
delimiter for italic. CommonMark accepts **bold**,
__bold__, *italic*, and _italic_ interchangeably.
Multiple delimiters in one corpus produce inconsistent
diffs and stress every parser implementation. This rule
makes the choice explicit and enforces it.
Bold and italic both render to *ast.Emphasis nodes.
The node carries a Level field: 1 for italic, 2
for bold. The chosen delimiter character does not
appear on the AST node — it must be read back from
the source via the node's first child segment, looking
at the byte immediately before the segment start.
The rant flags cross-delimiter nests like _*bold*_
and *_bold_* as ambiguous. CommonMark resolves
them deterministically, but humans cannot. A
forbid-mixed-nesting setting flags any Emphasis
whose delimiter differs from its parent Emphasis.
MDS018 (no-emphasis-as-heading) flags emphasis used as a heading. MDS042 flags emphasis with the wrong delimiter. The two rules can coexist on the same file without overlap.
rules:
emphasis-style:
bold: asterisk # asterisk | underscore
italic: underscore # asterisk | underscore
forbid-mixed-nesting: trueCategory: whitespace is wrong; use a new category
or reuse meta. Chosen: meta, matching MDS034
and the new MDS041.
Disabled by default (opt-in) — existing corpora vary.
Plan 112 ships profiles that auto-enable this rule:
profile: portableactivates withbold: asterisk,italic: underscore, andforbid-mixed-nesting: true.profile: githubactivates with the same defaults.profile: plainactivates with the same defaults. (A futureno-emphasisrule would forbid*and_runs entirely underplain.)
User overrides on top of the profile still win via deep-merge.
Walk the AST. For every *ast.Emphasis:
- Find the byte immediately before the first child segment to read the actual opening delimiter.
- If
Level == 2and the byte does not match theboldsetting, emit one diagnostic. - If
Level == 1and the byte does not match theitalicsetting, emit one diagnostic. - If
forbid-mixed-nestingis true and the parent chain contains an Emphasis with a different delimiter, emit one diagnostic.
Replace the opening and closing delimiter bytes in the source. Bold uses two delimiter characters per side, italic uses one. The fix is byte-for-byte substitution; no AST rewrite is required.
Edge case: when bold and italic share the same
delimiter character (e.g. both asterisk), the
boundary between ***bolditalic*** and ___both___
becomes ambiguous to fix mechanically. Skip auto-fix
for triple-delimiter runs and emit the diagnostic
only.
bold uses {actual}; configured style is {expected}
italic uses {actual}; configured style is {expected}
mixed emphasis delimiters: {outer} wraps {inner}
- Scaffold
internal/rules/emphasisstyle/withrule.go,rule_test.go, andinit()rule.Register. - Implement
Check()walking*ast.Emphasisand reading the source byte before each emphasis segment. - Implement
rule.Configurableforbold,italic, andforbid-mixed-nesting. - Implement
rule.Defaultablereturningfalse. - Implement
Fix()for bold and italic delimiter replacement; skip triple-delimiter runs. - Register as MDS042 in category
meta. - Add fixture tests in
internal/rules/MDS042-emphasis-style/covering each delimiter combination, mixed nesting, triple delimiters, and emphasis inside code spans (must not flag). - Add rule README.
-
**bold**withbold: asteriskemits no diagnostic. -
__bold__withbold: asteriskemits one diagnostic and fixes to**bold**. -
*italic*withitalic: underscoreemits one diagnostic and fixes to_italic_. -
_*x*_withforbid-mixed-nesting: trueemits one diagnostic for the mixed nest. -
***x***triple-delimiter run emits a diagnostic but is not auto-fixed. - Emphasis inside
`code`and fenced code blocks emits no diagnostic. - Rule is disabled by default.
- All tests pass:
go test ./... -
go tool golangci-lint runreports no issues