Local-only VS Code extension that customises the built-in markdown preview, globally across every workspace. Never published; sideloaded via symlink.
Two things it does:
- Injects custom CSS into every preview (
markdown.previewStyles). - Prepends an Obsidian-style Properties table above any markdown file with YAML frontmatter, plus a source-line gutter and other tweaks (
markdown.markdownItPlugins).
The extension is symlinked into VS Code's extensions directory. The folder name must match <publisher>.<name>-<version> from package.json:
~/.vscode/extensions/local.markdown-preview-styles-0.1.0 → ~/Dev/vscode-extensions/markdown-preview-styles
If the symlink is missing (new machine, accidental deletion, or after a version bump):
ln -s ~/Dev/vscode-extensions/markdown-preview-styles \
~/.vscode/extensions/local.markdown-preview-styles-0.1.0Remove any older versioned symlinks (e.g. local.markdown-preview-styles-0.0.1) so VS Code doesn't load both, then fully quit and relaunch VS Code (Cmd+Q).
The extension's defaults assume a few preview settings. Only breaks differs from VS Code's stock defaults; the others are noted for awareness:
"markdown.preview.breaks": true- renders consecutive source lines as separate lines (Obsidian-style). Without this, a single newline collapses into a space within the same paragraph."markdown.preview.lineHeight": 1.6(default) -.mps-blank-lineis calibrated to1.06lhagainst this; changing it shifts the gutter rhythm."markdown.preview.fontSize": 15- this repo is calibrated at 15 (VS Code's stock default is 14). Theem-based spacing instyle.cssscales with whatever you set here."markdown.preview.linkify": true(default) - auto-links bare URLs in body text; complements our Properties-value URL linking."markdown.preview.markEditorSelection": true(default) - shows the editor-caret line in the preview. The hover-indicator variant is suppressed (line numbers occupy that pseudo-element); the active-line is instead highlighted by brightening its gutter line number, same convention as the editor."markdown.preview.doubleClickToSwitchToEditor": false- VS Code defaults this totrue; disable if you'd rather double-click select text in the preview than jump back to the editor. The visible line-number gutter still lets you eyeball the source row."markdown.preview.typographer": false(default) - enable if you want smart quotes ("x"→"x") and en/em dash auto-conversion.
example.md at the project root exercises every Properties-table feature in one file. Open the preview with Cmd+K V after install to verify everything renders.
- Caps preview width at 880px and left-aligns content (no centring).
- Removes the default
border-bottomunderh1andh2(Obsidian-style flat headings). - Replaces VS Code's source-line hover indicator with permanent 1-indexed line numbers in a 4em left gutter; numbers also appear next to blank source lines.
- Default block margins on body content are zeroed so vertical spacing comes from blank-line placeholders - one source line ≈ one visual row, matching the editor's gutter rhythm.
- Inline code (backtick-quoted spans) shrunk to
0.9em. Fenced code blocks inside<pre>are untouched. - Renders YAML frontmatter as a Properties table with type-aware icons (text / list / tags / date / datetime / checkbox) and pill chips for
tagsand string arrays. Non-editable (v1). - Resolves
[[wiki-links]]against a workspace-wide index of every.mdfile under the open folders (plus any extra roots configured viamarkdownPreviewStyles.wikilinks.extraIndexRoots). Case-insensitive basename match; shortest-path tiebreak on collision. Supports the full Obsidian/Foam syntax matrix - see Wikilink syntax below. - Renders Obsidian-style image embeds (
![[image.png]], with optional![[image.png|N]]for a px width). Bare filenames are retried underattachments/on first error. Failed loads show a dashed placeholder with the original path. - Renders
![[note]](non-image) embeds inline as transclusions - the referenced note's body (frontmatter stripped) renders inside anmps-embed-notecontainer. Optional#headingor^blockfragment narrows the embed to that section. Recursive embeds are capped at depth 2 to prevent cycles. - Add
mps-hide: trueto a file's frontmatter to suppress the Properties table for that file.
| Form | Renders as |
|---|---|
[[name]] |
Link to name.md (resolved workspace-wide by basename). |
[[name|alias]] |
Link to name.md, displaying alias. Pipe-after-name (Obsidian/Foam convention - not GitHub Wiki's pipe-before-name). |
[[name#heading]] |
Link to name.md and scroll to #heading. |
[[name^block]] |
Link to name.md and scroll to the paragraph or list-item carrying a trailing ^block marker. |
[[name#heading|alias]] |
Combined - canonical fragment-before-pipe order. |
![[image.png]] |
Inline image (with optional |N for width). |
![[name]] |
Inline transclusion of the referenced note's body. |
![[name#heading]] / ![[name^block]] |
Inline transclusion narrowed to that section/block. |
Reverse-order combined forms ([[name|alias#heading]]) are not supported - the trailing #heading becomes part of the alias display text and there is no anchor jump. The canonical fragment-before-pipe order matches Obsidian and the markdown-it-wikilinks parser.
Resolution is case-insensitive on basename. Multiple matches resolve by shortest path (fewest separators), then alphabetical within the same depth. When indexing multiple roots (workspace folders + extraIndexRoots), entries are ordered alphabetically by root path first - so shortest-path only carries meaning within a single root.
To pin a block as a scroll target, append ^my-id at the end of a paragraph or list item. The extension strips the marker from the rendered text and adds id="mps-block-my-id" to the wrapping element so links can scroll to it.
All settings live under markdownPreviewStyles.wikilinks.*:
| Key | Default | Purpose |
|---|---|---|
enabled |
true |
Master switch. Off = document-relative resolution (no workspace index). |
extraIndexRoots |
[] |
Extra folders to index alongside the open workspace. Useful when you keep notes outside the workspace (e.g. ~/Documents/notes). Tilde-prefixed paths are expanded. Missing paths are skipped with a warning. |
embedNotes |
true |
Whether ![[name]] for non-image targets renders inline (true) or as a link with the mps-embed-fallback style (false). |
embedMaxBytes |
262144 (256KB) |
File size cap for inline transclusion. Larger targets degrade to a link. |
The four keys appear in VS Code's Settings UI under "Markdown Preview Styles → Wikilinks".
Top-level scalars (string, boolean, null, ISO date YYYY-MM-DD, ISO datetime YYYY-MM-DDTHH:MM[...]), block-style arrays (tags: followed by - foo), and inline arrays (tags: [foo, bar]). Numeric values stay as strings to preserve IDs like task-id: 20260101. Nested objects, multiline strings, anchors, and flow maps are not supported.
Date-only values are formatted without timezone shift so the day always matches what's in the YAML.
- Workspace index is built asynchronously on activation. Open previews are refreshed automatically once the index finishes building, but there's a brief window where wikilinks render with document-relative hrefs before the refresh fires.
- Watcher behaviour on iCloud-synced roots is noisy. If you point
extraIndexRootsat an iCloud folder, the file watcher fires on sync events as well as edits. Index correctness is unaffected; CPU may briefly spike on heavy sync activity. - Wiki-link
<a>clicks go through VS Code's webview link handler. Non-existent targets (no index match and no document-relative file) surface a "file not found" toast rather than navigating anywhere - no in-preview broken-link styling. - Cross-root collision ordering. When two indexed roots both contain a file with the same basename, the resolver orders entries alphabetically by root path then by relative path. "Shortest path" only applies within a single root.
See CLAUDE.md for the reload-by-change-type matrix, architecture gotchas, tests, and project conventions.
