Obsidian Flavored Markdown support for unified, remark, rehype, and React Markdown.
mdian adds Obsidian-style links, embeds, callouts, highlights, comments, and anchor behavior to Markdown pipelines without taking over the rest of your Markdown stack. Use it with plain unified pipelines, or use the React preset when rendering with react-markdown.
- Unified-compatible
remarkOfmandrehypeOfmplugins. - Wikilinks, note embeds, image/file embeds, highlights, comments, and callouts.
- Heading and block anchor output that works with native browser hash navigation.
- Optional YouTube and X/Twitter external embeds from standard Markdown image syntax.
- Stable
data-ofm-*attributes andofm-*class names for app integrations. - Optional
mdian/reactpreset forreact-markdowncomponents, note embeds, image URL rewriting, and X/Twitter enhancement. - Small optional stylesheet at
mdian/styles.css.
Regular Markdown still works as usual. GFM, math, syntax highlighting, and other Markdown extensions are not bundled; add plugins such as remark-gfm, remark-math, or rehype-katex in your own pipeline.
| Feature | Examples |
|---|---|
| Wikilinks | [[Page]], [[Page#Heading]], [[Page#^block-id]], [[Page|Alias]] |
| Embeds | ![[Page]], ![[Page#Heading]], ![[Page#^block-id]], ![[image.png|500]] |
| Highlights | ==important== |
| Comments | %%hidden note%% |
| Callouts | > [!warning] Caution |
| External embeds | ,  |
pnpm add mdianFor a complete unified HTML pipeline, install the surrounding unified packages you use in your app:
pnpm add unified remark-parse remark-rehype rehype-stringifyFor React Markdown usage:
pnpm add mdian react react-markdownmdian requires Node.js 18 or newer.
import rehypeStringify from 'rehype-stringify'
import {rehypeOfm, remarkOfm} from 'mdian'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import {unified} from 'unified'
import 'mdian/styles.css'
const source = [
'# Project Notes',
'',
'See [[Roadmap|the roadmap]].',
'',
'> [!tip] Remember',
'> You can highlight ==important== details.',
'',
'![[Architecture#Overview]]'
].join('\n')
const html = String(
await unified()
.use(remarkParse)
.use(remarkOfm)
.use(remarkRehype)
.use(rehypeOfm, {
resolveHref(href) {
return href.startsWith('#') ? href : `/wiki/${href}`
},
resolvePathCandidates(path) {
return path === 'Roadmap' ? ['docs/Roadmap'] : []
}
})
.use(rehypeStringify)
.process(source)
)resolvePathCandidates(path) receives the raw OFM target path. When it returns one or more candidates, the first candidate becomes the canonical rendered target for data-ofm-path, title, and generated hrefs.
Use createOfmReactPreset from mdian/react when rendering with react-markdown. The preset wires the remark and rehype plugins together and adds components for wiki links, embeds, images, and supported external embeds.
import ReactMarkdown from 'react-markdown'
import {createOfmReactPreset} from 'mdian/react'
import remarkGfm from 'remark-gfm'
import 'mdian/styles.css'
const notes = new Map([
['Project Notes', {markdown: '# Project Notes\n\nHello world.', title: 'Project Notes'}]
])
const ofm = createOfmReactPreset({
markdown: {
remarkPlugins: [remarkGfm]
},
ofm: {
rehype: {
resolveHref(href) {
return href.startsWith('#') ? href : `/wiki/${href}`
},
resolveResourceUrl(url) {
return url.startsWith('assets/') ? `/${url}` : url
},
resolvePathCandidates(path) {
return path === 'Project Notes' ? ['workspace/Project Notes'] : []
}
}
},
noteEmbed: {
resolve({path}) {
return notes.get(path) ?? {
markdown: `# ${path}\n\nMissing note content.`,
title: path
}
}
},
externalEmbeds: {
twitter: {
enhance: true
}
}
})
export function Markdown({markdown}: {markdown: string}) {
return (
<ReactMarkdown
components={ofm.components}
rehypePlugins={ofm.rehypePlugins}
remarkPlugins={ofm.remarkPlugins}
>
{markdown}
</ReactMarkdown>
)
}The React preset keeps application-specific behavior in your app:
markdown.remarkPluginsandmarkdown.rehypePluginsadd shared Markdown extensions once.ofm.rehype.resolveHrefrewrites generated OFM link and embed hrefs.ofm.rehype.resolveResourceUrlrewrites resource URLs for OFM image/file embeds.wikiLink.rendercan replace the default<a>rendering with your router link.noteEmbed.resolveresolves![[note]]embeds to Markdown content.noteEmbed.maxDepthlimits recursive note embed rendering.externalEmbeds.twitter.enhanceenables the built-in X/Twitter widget enhancement path.
| Import | Exports |
|---|---|
mdian |
remarkOfm, rehypeOfm, buildOfmSlugPath, ofmClassNames, ofmPublicKind, ofmPublicPropKeys, ofmPublicProvider, ofmPublicVariant, readOfmPublicProps |
mdian/react |
createOfmReactPreset, loadTwitterWidgets, React preset types |
mdian/styles.css |
Optional default styles for generated OFM markup |
remarkOfm registers the micromark extensions, mdast bridges, and post-parse remark transforms for enabled OFM syntax. All syntax options default to true.
| Option | Syntax |
|---|---|
wikilinks |
[[Page]], [[Page#Heading]], [[Page|Alias]] |
embeds |
![[Page]], ![[image.png|500]] |
highlights |
==important== |
comments |
%%hidden note%% |
callouts |
> [!note] Title |
softBreaks |
Newline-to-<br> inside paragraphs (Obsidian default) |
Extended syntax patterns:
| Pattern | Example | Notes |
|---|---|---|
| Callout image metadata | `> [!grid | masonry]` |
rehypeOfm walks the hast tree once and applies OFM HTML transforms.
| Option | Type | Default | Effect |
|---|---|---|---|
externalEmbeds |
{youtube?: boolean; twitter?: boolean} |
both enabled | Toggle external Markdown image upgrades per provider. |
resolveHref |
(href: string) => string |
undefined |
Resolve the final href for wikilinks and embed links. |
resolveResourceUrl |
(url: string) => string |
undefined |
Resolve the final resource URL for image/file embeds. |
resolvePathCandidates |
(path: string) => readonly string[] |
undefined |
Resolve a raw OFM target path to zero or more canonical candidate paths. |
buildOfmSlugPath(path) returns the canonical slug used for OFM page routes. Use it to align your router with the slugs mdian generates internally.
Generated elements use stable public attributes accessible through ofmPublicPropKeys:
data-ofm-kind="wikilink"data-ofm-kind="embed"withdata-ofm-variant="note" | "image" | "file" | "external"data-ofm-kind="embed"withdata-ofm-provider="youtube" | "twitter"for external embedsdata-ofm-kind="callout"withdata-ofm-callout, optionaldata-ofm-callout-metadata, and the boolean-presence attrsdata-ofm-foldable/data-ofm-collapseddata-ofm-kind="highlight"data-ofm-kind="block-anchor"withdata-ofm-block-idon paragraphs/list items carrying a block anchor
Additional metadata: data-ofm-path, data-ofm-alias, data-ofm-fragment.
Generated elements also use stable class names from ofmClassNames: ofm-wikilink, ofm-embed, ofm-external-embed, ofm-highlight, ofm-callout, ofm-callout-title, ofm-callout-content, ofm-heading-target, ofm-block-target, ofm-block-anchor-label.
Heading and block anchor targets render native HTML id attributes whose values match the canonical OFM fragment slug, so browser hash navigation works without custom JavaScript.
To consume these attributes in TypeScript with type-safe narrowing:
import {readOfmPublicProps} from 'mdian'
const props = readOfmPublicProps(element)
if (!props) return
switch (props.kind) {
case 'wikilink':
console.log('wikilink to', props.path, props.fragment)
break
case 'embed':
if (props.variant === 'image') console.log('image at', props.path)
if (props.variant === 'external') console.log('external embed', props.provider)
break
case 'callout':
console.log('callout', props.calloutType, props.metadata)
break
case 'block-anchor':
console.log('block anchor', props.blockId)
break
// highlight has no extra fields
}pnpm install
pnpm build
pnpm typecheck
pnpm test
pnpm demo:devTests live in test/ and run against built output in dist/. The demo app in demo/ consumes the workspace package directly and is useful for checking parser and rendering behavior interactively.
- Regular feature and fix PRs do not change
package.json.version. - To cut a release, merge the desired version bump to
main, then runpnpm release. pnpm releasetags the currentmaincommit asv<version>and pushes that tag toorigin.- The publish workflow runs from that tag, verifies the package, publishes to npm, and creates the matching GitHub Release.
Apache-2.0. See LICENSE.