Presets
// markdown.ts
import { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
export const appMarkdown = defineMarkdownPreset({
extensions: [gfm()],
components: { a: AppLink, img: AppImage },
})<Markdown preset={appMarkdown}>{content}</Markdown>A preset defines what Markdown means in your application.
You define it once, in one file, and reuse it everywhere Markdown gets read or written.
Why not pass props each time#
Props are fine for a single call site. A preset covers every call site, including the ones in other packages.
The same preset object goes to the renderer, the editor and the variables plugin.
<Markdown preset={appMarkdown}>{content}</Markdown>
<MarkdownEditor preset={appMarkdown} value={value} onChange={setValue} />
const doc = compileMarkdown(source, { preset: appMarkdown })The editor's preview mode hands off to the renderer with the same preset, so what an author sees while editing matches what a reader sees.
What a preset holds#
defineMarkdownPreset({
extends: [basePreset],
profile: 'docs',
extensions: [gfm()],
components: { a: DocsLink },
classNames: { code: 'rounded bg-slate-100 px-1' },
policy: { skipHtml: true },
remarkPlugins: [],
rehypePlugins: [],
remarkRehypeOptions: {},
})profile names the dialect. It defaults to gfm when a gfm extension is present and commonmark otherwise. It gets recorded on every compiled document, so a cache key or a diagnostic can tell which dialect produced a tree.
policy groups the security and content settings into one object so they travel with the dialect. It holds allowedElements, disallowedElements, allowElement, skipHtml, unwrapDisallowed and urlTransform.
A preset doesn't hold React elements or closures, apart from any handlers you put in it. isMarkdownPreset(value) is the runtime guard.
Read the [handbook](https://example.com). > Presets keep this styling in one place.
Read the handbook.
Presets keep this styling in one place.
Composing with extends#
You can compose presets instead of copying them.
export const docsMarkdown = defineMarkdownPreset({
extends: [appMarkdown],
extensions: [callouts()],
components: { a: DocsLink },
})extends takes a list. Presets apply left to right, and this preset's own fields apply last.
A preset is a plain object with a structural contract (there's no shared runtime class), so the renderer, the editor and the variables plugin will all accept a preset built anywhere in your application.
Merge rules#
Merging is deterministic and follows five rules, taken from section 6.3 of the specification.
1. The preset is the baseline. Everything else gets layered on top of it.
2. Local extensions append after preset extensions. An extension you pass locally runs after the preset's, unless rule 5 applies to it.
<Markdown preset={appMarkdown} extensions={[callouts()]}>{content}</Markdown>3. Local components shallowly override matching preset components. It goes one key at a time, with no deep merging.
// Keeps the preset's img and code, replaces only a.
<Markdown preset={appMarkdown} components={{ a: SpecialLink }}>{content}</Markdown>4. A policy prop overrides the preset only when you pass it explicitly. If you leave a prop out, it's inherited. Passing undefined counts as not passing it, so a spread can't quietly wipe out a preset's policy.
// skipHtml comes from the preset; urlTransform is replaced here.
<Markdown preset={appMarkdown} urlTransform={myUrlTransform}>{content}</Markdown>5. Duplicate extensions are resolved by name. A later extension with the same name replaces the earlier one in place and keeps its position. A new name gets appended.
Keeping the position matters because ordering is part of how an extension behaves, and overriding one entry of a shared preset shouldn't shuffle the others around.
// gfm() runs where the base preset put it, with the new options.
const strictMarkdown = defineMarkdownPreset({
extends: [appMarkdown],
extensions: [gfm({ singleTilde: false })],
})mergeExtensions(base, next) is exported if you need to apply rule 5 yourself.
Arbitrary plugin options don't get deep-merged. If you want different options, pass a different extension instance.
A worked example#
// markdown/base.ts
import { defineMarkdownPreset, gfm } from '@react-markdown-kit/renderer'
import { AppLink, AppImage } from '../components'
export const appMarkdown = defineMarkdownPreset({
profile: 'app',
extensions: [gfm()],
components: { a: AppLink, img: AppImage },
policy: { skipHtml: true },
})// markdown/comments.ts
import { defineMarkdownPreset } from '@react-markdown-kit/renderer'
import { appMarkdown } from './base'
// User comments: same dialect, no images, plainer links.
export const commentMarkdown = defineMarkdownPreset({
extends: [appMarkdown],
profile: 'comment',
components: { img: () => null },
policy: { skipHtml: true, disallowedElements: ['img'] },
})That gives you two dialects, each defined once, and every document has a profile saying which one produced it.
Related#
Extensions covers writing your own extension.
Compiled documents keep the profile they were built with. See Compiling.
The same preset also configures the editor. See Editor basics.
Read next#
The whole syntax on one page you can edit: Renderer demo.
Last updated on