| id | 168 |
|---|---|
| title | Obsidian Flavored Markdown support |
| status | ✅ |
| summary | Add an `obsidian` convention that validates wikilinks (`[[Page]]`) via extended MDS027 settings, checks callout types via new rule MDS067, and tolerates Dataview inline fields without surfacing false-positive diagnostics. |
| model | sonnet |
Teams that write docs in Obsidian vaults can run mdsmith on
their .md files. This plan adds three things: broken
wikilink detection, callout type validation, and no false
positives from OFM syntax that CommonMark parsers already
treat as plain text.
Obsidian Flavored Markdown (OFM) adds four constructs on top of CommonMark/GFM that mdsmith currently ignores:
| Construct | OFM syntax | mdsmith today |
|---|---|---|
| Wikilink | [[Page]], [[Page|alias]], [[Page#anchor]] |
silent (parsed as text) |
| Embed | ![[file.png]], ![[note]] |
silent (parsed as text) |
| Callout | > [!note] at blockquote start |
silent (treated as prose) |
| Dataview inline | key:: value |
silent (read as paragraph text) |
The research sketches a wikilinks setting on
MDS027 as the right surface for wikilink validation. The
backlinks plan explicitly defers wikilink coverage
to this plan. Dataview inline fields already pass all
existing rules without any diagnostic. The only gap is that
require:/schema: directives don't read them as front
matter — out of scope here.
Obsidian resolves [[Page]] by searching the entire vault
for any file whose stem matches Page (case-insensitive).
It prefers the shortest relative path when multiple files
match. [[Page#Heading]] also validates the heading anchor
in the resolved file. [[Page|alias]] uses Page as the
target; alias is display text only.
Embeds (![[file.png]]) follow the same resolution rules
but target any file type, not just .md.
Obsidian defines 13 base callout types, each with aliases:
noteabstract(aliases:summary,tldr)info(alias:todo)tip(aliases:hint,important)success(aliases:check,done)question(aliases:help,faq)warning(aliases:caution,attention)failure(aliases:fail,missing)danger(alias:error)bugexamplequote(alias:cite)
Type matching is case-insensitive. Custom types are valid in
Obsidian but opt-in here via the allow setting.
Add wikilinks.go to the linkgraph package with a
source-level scanner. It must skip code spans, fenced code
blocks, and <?...?> PI blocks (same guards as MDS054's
bracket scanner). It returns a slice of WikiLink:
type WikiLink struct {
Target string // "Page", "Page#anchor", "Page.png"
Anchor string // heading fragment, if present
Alias string // display alias, if present
Embed bool // true for ![[...]]
Line int // 1-based source line
Col int // 1-based byte offset within line
}Resolution helper ResolveWikiLink(root fs.FS, from, target string) (path string, ok bool) implements the
shortest-path Obsidian algorithm:
- If
targetends in.mdor has no extension, search for files whose stem matchestarget(case-insensitive). - Otherwise (embed with extension), search by exact name.
- Return the match with the fewest path components. Ties break alphabetically.
- Search is sandboxed to the workspace root (no
..).
Add two new settings to
internal/rules/crossfilereferenceintegrity/:
rules:
cross-file-reference-integrity:
wikilinks: false # default; set true to enable
wikilink-style: obsidian # only supported style initiallyWhen wikilinks: true, Check() calls
linkgraph.ExtractWikiLinks(f) and runs each through
ResolveWikiLink. Unresolved targets emit:
wikilink target "Missing Page" not found in workspace
Resolved targets with a missing heading anchor emit:
wikilink "[[Notes#Old Heading]]": anchor "Old Heading"
not found in docs/notes.md
The existing placeholders setting applies to wikilink
targets the same way it applies to standard link
destinations.
New package internal/rules/callouttype/.
rules:
callout-type:
allow: [] # empty = use built-in Obsidian set
allow-unknown: falseallow: [] (default) permits only the 13 base types plus
aliases. allow: [custom] adds custom to the valid set.
allow-unknown: true disables all type validation.
Detection scans blockquote nodes. For each, check whether
the first non-whitespace line matches
\[!([A-Za-z0-9_-]+)\]. If the capture group is not in
the effective allow set, emit:
unknown callout type "REVIEW"; valid types: note, abstract,
info, tip, success, question, warning, failure, danger, bug,
example, quote (or configure allow-unknown: true)
Category: structure. Disabled by default (opt-in). No
auto-fix — renaming a callout type changes meaning.
Extend mdsmith list backlinks to call
linkgraph.ExtractWikiLinks unconditionally. Extraction is
cheap and read-only. The command already has a --json
flag; wikilink sources appear there with
"kind": "wikilink".
Add to internal/convention/convention.go:
"obsidian": {
Name: "obsidian",
Flavor: FlavorGFM,
Rules: map[string]RulePreset{
"markdown-flavor": {
Enabled: true,
Settings: map[string]any{"flavor": "gfm"},
},
"cross-file-reference-integrity": {
Enabled: true,
Settings: map[string]any{
"wikilinks": true,
"wikilink-style": "obsidian",
},
},
"callout-type": {Enabled: true},
},
},convention: obsidian activates both rules with one config
line. Standard Markdown link settings stay at their
defaults.
Add obsidian to the built-in convention table in
docs/reference/conventions.md.
Update docs/background/markdown-linters.md. The Obsidian
comparison table rows for wikilinks and callouts change from
"no validation" to cite the new rules.
- Add
internal/linkgraph/wikilinks.gowithWikiLink,ExtractWikiLinks(f *lint.File), andResolveWikiLink(root fs.FS, from, target string). Add unit tests covering: bare page, page with anchor, page with alias, embed, code-span skipping, fenced-code skipping, PI-block skipping, case-insensitive match, shortest-path tie-break, not-found, root-escape rejected. - Extend
internal/rules/crossfilereferenceintegrity/withwikilinks boolandwikilink-style stringfields. UpdateApplySettings,DefaultSettings, andCheck()to callExtractWikiLinkswhen enabled. Add tests for resolved, unresolved, anchor-broken, and placeholder-suppressed wikilink diagnostics. Add wikilink fixtures ininternal/rules/MDS027-cross-file-reference-integrity/. - Scaffold
internal/rules/callouttype/withrule.goandrule_test.go. ImplementCheck(),ApplySettings,DefaultSettings, andEnabledByDefault. Register as MDS067 in categorystructure. Add fixtures ininternal/rules/MDS067-callout-type/:good/covers standard types andallow-unknown: true;bad/covers unknown type flagging. - Add
obsidianconvention entry ininternal/convention/convention.go. - Extend
mdsmith list backlinksto callExtractWikiLinks; add"kind": "wikilink"to JSON. - Update
docs/reference/conventions.mdanddocs/background/markdown-linters.md. - Run
go run ./cmd/mdsmith fix .and confirmgo run ./cmd/mdsmith check .passes.
-
[[Missing]]withwikilinks: trueemits one diagnostic naming the unresolved target. -
[[Present]]wherepresent.mdexists emits no diagnostic. -
[[Notes#Old Heading]]with a missing anchor emits one anchor-not-found diagnostic. -
[[Page|Alias]]resolvesPage, notAlias. -
![[image.png]]resolves as an embed (any file type). - Wikilinks inside fenced code, code spans, and PI blocks are never flagged.
- A wikilink target in
placeholdersis never flagged. -
wikilinks: false(default) emits no wikilink diagnostics. -
> [!note](and all 12 base types + aliases listed in this plan) emits no diagnostic from MDS067 with default settings. -
> [!REVIEW](unknown type) emits one diagnostic naming the type and listing valid options. -
> [!custom]withallow: [custom]emits no diagnostic. -
> [!anything]withallow-unknown: trueemits no diagnostic. - MDS067 is disabled by default.
-
convention: obsidianactivates both rules with no other config. -
mdsmith list backlinks docs/page.mdlists files with[[page]]wikilinks pointing atpage.md. - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues -
mdsmith check .passes on the repo.
- Dataview inline field recognition in
require:/schema:— those lines already pass without errors. - Auto-fix to rewrite wikilinks as standard Markdown links — that breaks Obsidian for users who want wikilinks.
- Non-Obsidian wikilink styles (Foam, Logseq) — the
wikilink-stylekey is extensible but onlyobsidianships here. - LSP hover or completion for wikilinks — follow-up work.