Markdown conventions
Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.
A convention is an opinionated bundle of rule
settings that pairs a Markdown flavor with a set of
style choices. Setting convention: at the top of
your .mdsmith.yml selects one of the built-in
bundles; the rule presets in that bundle are applied
as a base layer beneath your own rule config. It
answers “what kind of Markdown does this project
write?” with one config knob instead of eight.
A convention is distinct from a flavor. Flavor is a property of the renderer (CommonMark, GFM, goldmark — what the parser interprets). Convention is a property of the project (the team’s writing choices among forms the renderer treats equally). See the concepts doc for the full picture and where the concepts overlap.
# Selecting a convention
convention: portableThat single line pins a flavor and a curated set of
style-rule settings. convention: is a top-level
config key, sibling to rules:, kinds:, and
overrides:. An unknown name is a config error.
Built-in values: portable, github, obsidian,
plain, no-llm-tells, slidev, and the four
<linter>-parity conventions below. The key is
optional; omit it for no convention.
You may also set flavor: inside markdown-flavor
alongside convention:. If both are set, they must
agree — a convention that pins a flavor (e.g.
portable requires commonmark) rejects a
conflicting flavor: at config load. Conventions
that pin no flavor (slidev, no-llm-tells) do not
enforce this check.
# Built-in conventions
#
portable
Markdown that renders the same in every CommonMark
parser. Selects flavor: commonmark and turns on
the strict-style rules with their recommended
defaults.
| Rule | Setting |
|---|---|
markdown-flavor | flavor: commonmark |
no-inline-html | enabled |
no-reference-style | allow-footnotes: false |
emphasis-style | bold: asterisk, italic: underscore |
horizontal-rule-style | style: dash, length: 3, require-blank-lines: true |
list-marker-style | style: dash |
ordered-list-numbering | style: sequential, start: 1 |
ambiguous-emphasis | max-run: 2 |
#
github
Markdown that renders well on github.com. Selects
flavor: gfm and keeps the style rules light: the
inline-HTML allowlist permits <details> and
<summary>; emphasis and list-marker style are
pinned for consistency; the rest of the strict
rules stay off.
| Rule | Setting |
|---|---|
markdown-flavor | flavor: gfm |
no-inline-html | allow: [details, summary] |
emphasis-style | bold: asterisk, italic: underscore |
list-marker-style | style: dash |
#
obsidian
Markdown written in an Obsidian vault. Selects
flavor: gfm and turns on the Obsidian-specific
validations — MDS027 resolves [[Page]] wikilink
targets workspace-wide, and MDS067 checks every
[!type] callout against the Obsidian type set.
| Rule | Setting |
|---|---|
markdown-flavor | flavor: gfm |
cross-file-reference-integrity | wikilinks: true, wikilink-style: obsidian |
callout-type | enabled (12 base Obsidian types and aliases) |
Standard style rules stay at their defaults so an Obsidian vault behaves like a GFM project unless the team layers more rules on top.
To run these checks inside the editor, install the Obsidian plugin . It hosts the same engine as a WebAssembly runtime.
#
plain
Markdown that survives cat. The rendered output
should look about the same as the source viewed in
a plaintext reader. Same activations as portable,
plus allow-comments: false on no-inline-html so
HTML comments do not leak through as literal
<!-- ... --> text.
Three additional rules (forbid */_ runs,
require indented code blocks, prefer bare URLs) do
not exist yet. When they ship, plain gains them
and diverges from portable.
#
<linter>-parity
Four conventions match a peer linter’s default rule
set, for benchmarks or a fast peer-equivalent gate.
Each picks flavor: gfm and leaves MDS034 opt-in.
| Convention | Peer | Rules |
|---|---|---|
gomarklint-parity | gomarklint | 20 |
mado-parity | mado | 27 |
rumdl-parity | rumdl | 41 |
markdownlint-parity | markdownlint | 41 |
Each enables the opt-in rules its peer runs and
disables the defaults it skips. Only full covers
count: a partial peer mapping does not run the
heavier mdsmith rule. The CI-checked sets come from
the coverage matrix
. All disable MDS027
(the peers’ link-fragments cover same-file anchors
only), so gomarklint-parity and mado-parity are
parse-skip-safe. Rule tables are generated.
#
no-llm-tells
Flags mechanical LLM-prose tells in CI. MDS056
blocks a curated vocabulary of LLM tell words and
phrases; MDS055 blocks the banned sentence openers;
MDS023 and MDS024 tighten readability budgets. The
lists are sourced from slop-patterns.md
; a
drift-checker test keeps them in sync.
Pins no flavor; it does not enable markdown-flavor
(MDS034). The curated words ship as inline
contains:/starts: presets, which append, so a
project’s terms join rather than replace them.
#
slidev
Disables eight default-on rules that produce false
positives on Slidev
files, and
enables slide-structure (MDS073) to validate
per-slide layouts, slots, fields, and keys. Slidev
uses --- as a slide separator (parsed as a setext
underline); headings restart per slide. Pins no flavor.
| Rule | Why disabled |
|---|---|
heading-style (MDS002) | --- separator parsed as setext underline |
heading-increment (MDS003) | Each slide restarts at H1 |
first-line-heading (MDS004) | Front matter before first heading |
no-duplicate-headings (MDS005) | Same title on multiple slides |
blank-line-around-headings (MDS013) | Layout blocks interfere |
no-trailing-punctuation-in-heading (MDS017) | Stylistic slide titles |
no-emphasis-as-heading (MDS018) | Bold used for slide emphasis |
empty-section-body (MDS030) | Layout-only slides have no body |
# How presets layer with user config
Convention presets sit between built-in defaults and your explicit top-level rules. The merge order, oldest → newest, is:
default— built-in defaults: rules incfg.Rulesthat you did not setconvention.<name>— the preset tableuser— your top-level rules block (rules you explicitly set in.mdsmith.yml)kinds.<name>— each kind in the file’s effective listoverrides[i]— each matching override entry
Each layer deep-merges: scalar leaves replace, maps
recurse key by key, lists replace by default. The
convention provides the floor; your rules: block
overrides on top. mdsmith splits default and
user around the convention so a convention can
enable an opt-in rule without being overridden by
the default’s Enabled: false.
For example, the github convention sets
no-inline-html.allow: [details, summary]. To
extend the allowlist with <sub> and <sup>,
write:
convention: github
rules:
no-inline-html:
allow: [sub, sup]Lists default to replace, so the effective allowlist becomes [sub, sup]. To keep the preset’s entries, explicitly list all of them:
allow: [details, summary, sub, sup]. The lists: key is separate
and always appends — see word-list files
.
# Disabling MDS034
Convention presets apply at config load. A bool-only
markdown-flavor: false entry in rules: toggles
enabled without erasing preset settings — the rule
is gated off but the other convention rules are
untouched.
# User-defined conventions
The built-in conventions cover common cases. Teams
that need something custom define it inline in
.mdsmith.yml. The top-level conventions: key holds
the map:
conventions:
our-team:
flavor: gfm
rules:
no-inline-html:
allow: [details, summary, kbd]
list-marker-style:
style: dash
no-reference-style:
allow-footnotes: true
convention: our-teamEach entry is a { flavor, rules } pair. The rules
block uses the same schema as the top-level rules:
block. To lift one out of .mdsmith.yml into its own
file under .mdsmith/conventions/<name>.yaml, see the
convention files reference
.
# Validation
User-defined conventions are validated at config load:
flavormust be a recognised flavor string such ascommonmark,gfm, orgoldmark.- Each key under
rules:must name a registered rule. - Each rule’s settings must pass the rule’s own schema check.
Validation errors name the convention and the rule:
convention "our-team" rule "no-inline-html": no-inline-html: unknown setting "allowed"# Reserved names
The built-in names portable, github,
obsidian, plain, no-llm-tells, slidev, and
the four <linter>-parity conventions are reserved.
Defining a conventions.portable entry is a config
error. This keeps the built-in names stable across
docs and tutorials.
# Resolution order and layering
The lookup checks user-defined conventions first, then the built-in table. A collision with a reserved name is a config error — shadowing is impossible. When neither matches, the error lists both sets:
unknown convention "bogus" (valid: github, gomarklint-parity, mado-parity, markdownlint-parity, no-llm-tells, obsidian, our-team, plain, portable, rumdl-parity, slidev)User-defined conventions apply as a base layer, like
the built-ins. A top-level rules: entry overrides
the convention preset; mdsmith kinds resolve <file>
labels user-convention layers with a (user) suffix.
# Inspecting an effective convention
mdsmith kinds resolve <file> shows the merge
chain for every rule, including the
convention.<name> layer. Use it to confirm which
value won and where it came from.