Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Markdown Plugin (@lapis-notes/markdown)

Source: packages/plugins/plugin-markdown/src/

Manifest: id markdown, version 0.0.1, min app version 1.7.7.

The markdown package is the primary document-editing plugin. It owns Markdown language support, media viewing, outline/property side views, markdown preview rendering, metadata extraction, and most CodeMirror extensions used by note editing. Metadata-driven markdown surfaces are expected to scope cache reactions to the active note or its direct reference neighborhood rather than treating every metadata event as a full-view refresh trigger. Markdown preview surfaces that receive shared Editor instances are expected to scope change subscriptions to the specific editor instance they mounted so embed/file swaps can tear down safely even after the live prop is cleared.

Non-image file embeds route through the app embed registry so other plugins can supply plugin-owned inline viewers without widening shared preview sanitization rules. The rehype embed component resolves short embed paths against the active source note through MetadataCache, so embeds render from either full vault paths or the same shortest-path link text used elsewhere, and it re-resolves mounted embeds when metadata finishes loading, when the source note changes, and when the directly resolved embed target changes or disappears. Vault create, delete, and rename events remain the fallback path for new shortest-path resolutions that do not have enough metadata context yet. Image media views consume Vault.getResourceUrl() and revoke URLs on teardown, which lets Electron use the scoped native resource protocol while browser vaults keep blob URL fallback behavior. In the rich editor, standalone ![[...]] paragraphs mount through the same markdown block-widget renderer path, so live preview uses the same file-embed surface as reading mode for PDF and other non-image embeds. That registry path now also carries first-party read-only Bases and Canvas file embeds for .base/.bases and .canvas targets. The package also exports a reusable read-only FileEmbed surface so other plugin UIs can reuse the same image, section, custom-embed, and text fallback behavior outside markdown documents.

The package now also exports narrow embed surfaces through @lapis-notes/markdown/embed: read-only MarkdownEmbed, editable EditableMarkdownPreview, and read-only FileEmbed. Official external plugins use that subpath when they need markdown surfaces without importing the full markdown plugin root graph. MarkdownEmbed accepts plain markdown text plus optional source-path and plugin-extension hooks, and renders preview output without requiring an Editor instance, plugin lifecycle, or app-owned directive/postprocessor registries. The first embed slice intentionally excludes app-resolved file embeds, custom directive registration through app.markdownDirectiveRenderers, and markdown postprocessor execution.

Markdown style ownership is split by surface. Shared variables and rendered markdown rules live separately from source-mode, live-preview, reading-mode, and embedded-mode overrides under src/styles/. The stable mode hooks are markdown-source-mode / markdown-source-view, markdown-live-preview-mode / markdown-live-preview-view, markdown-reading-view, and markdown-preview-surface--embedded / markdown-embed-surface, so CSS selectors can target one surface without relying on broad global markdown selectors or :not(.cm-live-preview) fallbacks.

Top-tab-specific markdown CSS hides markdown view icons only for top tabs, while leaving stacked and other tab header variants to display their icons.

The tracked e2e-vault/plugin-markdown/CodeMirror Layout Showcase.md fixture is the focused manual and automated sample for CodeMirror layout behavior. It covers GFM tables, grid tables with inline markdown, blockquotes, quoted lists and checklists, nested list indentation, continuation paragraphs, line wrapping, embeds, internal links, tags, math, fenced code, and Mermaid across source mode, live preview, and reading mode.

Inline Mermaid blocks render as scroll-friendly static SVG previews in reading mode and live preview, while the Mermaid expand dialog owns the interactive svg-pan-zoom controls for large diagrams.

Registered Views

View TypeClassExtensionsPurpose
MarkdownViewTypeMarkdownView.md, .markdownSource, preview, and live-preview editor
MediaViewTypeMediaView.jpg, .jpeg, .png, .svg, .bmp, .gifImage and asset viewer
AllPropertiesViewTypeAllPropertiesView-Property browser across all files
OutlineViewTypeOutlineView-Real-time heading tree
FilePropertiesViewTypeFilePropertiesView-Frontmatter editor for the active file
file:backlinksBacklinksView-Active-note linked and unlinked reverse references
file:outgoing-linksOutgoingLinksView-Active-note resolved outgoing links and unlinked mentions

Public Embeds

  • @lapis-notes/markdown/embed exposes MarkdownEmbed, EditableMarkdownPreview, and FileEmbed for markdown preview surfaces that official external plugins can bundle without importing the full markdown plugin root graph.
  • @lapis-notes/markdown continues to expose MarkdownPreview, EditableMarkdownPreview, FileEmbed, and NoteLink for richer plugin-integrated use cases. MarkdownPreview accepts frontmatterOpen (default true); FileEmbed forwards frontmatterOpen (default false) so note-link hover previews and inline embeds start with the Properties block collapsed while reading mode and live preview remain expanded.

Commands

  • show-all-properties
  • show-outline
  • show-file-properties
  • show-backlinks
  • show-outgoing-links
  • show-links-sidebar

Markdown owns Obsidian-inspired Backlinks and Outgoing Links side views. Both register as sidebar views on the right side under the grouped sidebar tab named Links with Backlinks above Outgoing links. The plugin seeds that group only when the right sidebar has no saved children; existing non-empty user layouts are preserved until a command explicitly opens a view.

The Backlinks view follows the active markdown note. Its Linked mentions section groups cached links and embeds from other markdown notes that resolve to the active file. Its Unlinked mentions section searches indexed markdown document content for exact active-note basename and alias matches, excludes matches already inside link/embed ranges, skips frontmatter, and falls back to vault reads for files that are not yet present in the generated search index.

The Outgoing Links view follows the same active markdown note. Its Links section lists resolved internal links and embeds from the active note grouped by target file. Its Unlinked mentions section scans the active note for exact basename and alias matches for markdown files that are not already linked by that note.

Both sidebars use the same compact sidebar result language as All Properties and Search: fixed icon toolbar, optional search panel, collapsible section titles, collapsible result groups, search filtering, context expansion, sort menu, and scrollable rows. Result rows open files in the main workspace area; backlink rows jump to the source mention. Hovering a result uses the shared Hover Card plus FileEmbed preview surface, previewing the backlink source note or outgoing target note.

Editor Ownership

The package extends CodeMirror with markdown table editing, heading/list decorations, grid tables, wiki links, tags, embeds, directives, LaTeX, paste handlers, completion, rich-editor behavior, local path-link parsing for standard markdown destinations that contain raw spaces, and a markdown-owned frontmatter line-decoration pass that marks every YAML frontmatter source line with cm-hmd-frontmatter, including both delimiter lines, so the editor can style the full block consistently. Standard live-preview markdown tables now mount @dnd-kit/svelte row drag handles and drop targets inside the rich-editor widget surface, so row reordering no longer depends on the older native HTML5 drag path. Shared autocomplete defaults and tooltip alignment come from the API lapisCodeMirrorAutocomplete() helper and @lapis-notes/ui/codemirror-autocomplete.css; markdown supplies override completion sources (wiki links, suggestions) and markdown-only addToOptions renderers. Vault file scanning for link completions uses API helpers (vaultFilesMatchingPathSubstring, embed filters). Completion-driven note, embed, heading, and display-text link insertion delegates to the shared API internal-link formatter so link text resolves relative to the active note and uses the same shortest-path rules as metadata and file-management helpers. Its custom language extends the Markdown Lezer parser with GridTable, Table, WikiLink, path-link, Tag, directive nodes, YAML frontmatter, and fold support, while keeping wiki-link and embed parsing line-local so malformed cross-line link text does not create invalid rich-editor replacement ranges and recovering embed tokens when a literal exclamation mark directly precedes ![[...]]. Both the standard editor link-decoration pass and the rich-editor pass apply the same internal-link, target, and display-text classes to embed nodes that they already apply to wikilinks.

The plugin mounts the shared CodeMirror diagnostics/lint-gutter adapters for MarkdownViewType, while the external official Markdown Lint plugin owns markdown diagnostics provider registration when installed and enabled. Markdown diagnostics remain single-document only and do not depend on metadata cache, vault-wide link resolution, or markdown preview rendering.

Markdown-owned list, plain-indent, and blockquote editor decorations emit first-paint hanging-indent fallback variables, and the measured-indent pass later writes measured pixel variables over the same CSS contract. Source and live-preview wrapping therefore keep continuation rows aligned during opening, scrolling, and caret movement into raw prefixes instead of waiting for a post-layout style replacement. Unordered-list marker spacing is expressed through shared marker-slot custom properties, so list fallbacks, anchored continuation padding, and indentation-guide offsets account for the same visual bullet width across source mode and live preview.

Markdown fenced code blocks continue to dispatch through the shared per-language code-block processor registry. The bundled Bases plugin now uses that path for fenced base and bases blocks, rendering the fence body as a read-only Bases document in reading mode and live preview without adding new markdown syntax.

List Callouts

The markdown plugin owns inline list callout markers (for example - & Highlighted item) in source, live preview, and reading mode.

SurfaceMechanism
Source / live previewCodeMirror line decorations and marker widgets (lc-list-callout, lc-list-marker, lc-list-bg)
Reading / previewMarkdown post-processor on rendered list items (sort order 10000)

Default callout characters: & (yellow), ? (orange), ! (red), ~ (purple), @ (cyan, book-open icon), $ (green), % (gray). Users can add custom callouts from the Markdown plugin settings tab, whose catalog editor uses the shared icon picker for icon-backed callouts and keeps preview rows with representative sample text.

Settings:

  • Callout catalog persists in /.obsidian/markdown.json under listCallouts.
  • Appearance (background opacity and padding) persists as the canonical object setting markdown.listCallouts.style in app.json. Runtime style access still reads child values through markdown.listCallouts.style.*, and those values map to --lc-* CSS variables on markdown surfaces.
  • Legacy flat app.json keys such as markdown.listCallouts.style.paddingLeft are normalized into markdown.listCallouts.style when no object value exists and are pruned after normalization. If both forms exist, the object value wins.
  • Legacy obsidian-list-callouts community plugin data is imported once into markdown.json when no catalog is present yet.

Blockquote Callouts

The markdown plugin also owns Obsidian-style blockquote callouts (for example > [!warning] Title) in reading mode and live-preview block widgets.

SurfaceMechanism
Reading / previewremarkCallout mdast transform, markdown-owned admonition rehype handler, and the callout renderer component
Live preview widgetsThe same markdown parser and rehype handler path, mounted through block widgets

The blockquote callout transform runs after markdown parsing and must recognize both raw [!type] text prefixes and the bracket-only internal-link tokens produced by the markdown path-link extension. Rendered callouts use the markdown plugin’s callout component plus shared callout theme variables, so reading mode and live-preview widgets share the same callout DOM shape and styling.

Metadata Ownership

Markdown metadata extraction is worker-backed through PromiseWorker. The plugin registers the metadata processor for .md files and provides the frontmatter/property widgets used by the workspace and related plugins. Package build emits dist/lib/metadata/worker.js after svelte-package so downstream plugin builds can load the worker through import.meta.url instead of unresolved Vite ?worker&inline imports.

The shared frontmatter editor now uses a Svelte-owned property-name input with anchored suggestions in both live preview and the File Properties side view instead of the older imperative menu-attached blur flow. Property rows are keyed by exact nested frontmatter paths, so rename and type-menu actions now target the concrete property path that exists in a note rather than only the top-level key. Normal row rendering depends on the active note frontmatter, persisted type declarations, registered widgets, and local inference rather than vault-wide observed property counts, while property-name suggestions load the vault-wide property list lazily when an input is focused. Note-local property edits in live preview and the File Properties side view now share the same helper around FileManager.processFrontMatter(), which lets first-property adds create a new YAML block, last-property removals drop the whole block, and widget edits preserve concurrent note changes. Built-in widgets now commit text values on input/blur/Enter and checkbox/date/number changes on stable change/blur events so remounts and focus handoffs do not lose pending edits. Embed-hosted previews (FileEmbed, including note-link hovers and inline ![[embeds]]) collapse the Properties block by default; reading mode and live preview keep it expanded unless a host overrides frontmatterOpen.

The All Properties side view is a top-level frontmatter key browser. It builds rows from exact keys present in cached YAML objects, preserving literal dotted keys such as note.status while hiding generated nested paths such as tags[0] or prop[0].name from the root list. Object and complex-array rows can expand into an indented nested-property tree. Object rows show concrete generated child paths with counts and the same rename, type-change, and delete actions using nested path semantics. Complex arrays aggregate object-child paths across indexes, so items[0].name and items[1].name render as a single name/items[].name style child instead of separate numeric index rows. Clicking any property row opens the Search sidebar with a quoted property query; aggregate array child rows normalize items[].name to the searchable nested filter ["items.name"]. Each top-level row also exposes bulk Rename, Change Property Type, and Delete Property actions; those actions operate across affected files through exact top-level YAML keys, report progress through the shared notifications status item, and summarize partial failures.

The frontmatter widgets include generic removable pill rendering for tags, aliases, and primitive multi-text arrays. Editable list pills do not force markdown tag prefixes in the Properties editor, autocomplete new values from existing metadata values for the same property, and render whole wikilink values through NoteLink with hover preview and click-to-open behavior resolved from the active note path.

Rendered markdown preview and reading-mode surfaces apply the same markdown heading tokens used by the editor so heading color, size, weight, and font variables stay consistent across modes. Markdown-specific callouts and table-selection styling inherit shared theme-owned compatibility tokens rather than maintaining plugin-local light/dark palette blocks. The shared editor wrapper and Outline side view also cancel deferred focus and heading-observer work on teardown so preview/editor transitions and leaf changes do not leave stale async UI work running. The Outline side view initializes nested heading groups expanded whenever the active note changes, then preserves user collapse choices for that note.

Rendered note-link hover previews and unresolved-link tooltips inherit their popup ownership from the shared Hover Card and Tooltip primitives. Popup-hosted reading views therefore keep note-link overlays inside the popup document without markdown-specific portal props at the call site.