Directives
Markdown supports custom constructs called directives, which can describe arbitrary content (a popular example of that being YouTube videos).
This is the syntax for a custom YouTube directive.
::youtube[Video of a cat in a box]{#01ab2cd3efg}
The directive plugin allows you to create custom editors for the various directives in your markdown source. To get started, you can use the bundled GenericDirectiveEditor
:
// markdown with a custom container directive
const markdown = `
:::callout
you better watch out!
:::
`
const CalloutDirectiveDescriptor: DirectiveDescriptor = {
name: 'callout',
testNode(node) {
return node.name === 'callout'
},
// set some attribute names to have the editor display a property editor popup.
attributes: [],
// used by the generic editor to determine whether or not to render a nested editor.
hasChildren: true,
Editor: GenericDirectiveEditor
}
export const CalloutEditor: React.FC = () => {
return (
<MDXEditor
onChange={console.log}
markdown={markdown}
plugins={[directivesPlugin({ directiveDescriptors: [CalloutDirectiveDescriptor] })]}
/>
)
}
If you need something more flexible, implement a custom directive editor. The example below creates a simple wrapper around the NestedLexicalEditor
component:
const CalloutCustomDirectiveDescriptor: DirectiveDescriptor = {
name: 'callout',
testNode(node) {
return node.name === 'callout'
},
attributes: [],
hasChildren: true,
Editor: (props) => {
return (
<div style={{ border: '1px solid red', padding: 8, margin: 8 }}>
<NestedLexicalEditor<ContainerDirective>
block
getContent={(node) => node.children}
getUpdatedMdastNode={(mdastNode, children: any) => {
return { ...mdastNode, children }
}}
/>
</div>
)
}
}
Adding custom directive buttons to the toolbar
You can tap into the directivesPlugin
state management exports to build an UI that inserts a custom directive node in the editor.
Below you can find an example toolbar dialog button that will insert an YouTube directive based on user input.
const YouTubeButton = () => {
// grab the insertDirective action (a.k.a. publisher) from the
// state management system of the directivesPlugin
const insertDirective = usePublisher(insertDirective$)
return (
<DialogButton
tooltipTitle="Insert Youtube video"
submitButtonTitle="Insert video"
dialogInputPlaceholder="Paste the youtube video URL"
buttonContent="YT"
onSubmit={(url) => {
const videoId = new URL(url).searchParams.get('v')
if (videoId) {
insertDirective({
name: 'youtube',
type: 'leafDirective',
attributes: { id: videoId },
children: []
} as LeafDirective)
} else {
alert('Invalid YouTube URL')
}
}}
/>
)
}
export const Youtube: React.FC = () => {
return (
<MDXEditor
markdown={youtubeMarkdown}
plugins={[
directivesPlugin({ directiveDescriptors: [YoutubeDirectiveDescriptor] }),
toolbarPlugin({
toolbarContents: () => {
return <YouTubeButton />
}
})
]}
/>
)
}
Update the directive attributes
The useMdastNodeUpdater
hook returns a function that allows you to update the directive node attributes.
You don't need to maintain a local state; the component gets re-rendered with the new mdast node property.
Rendering custom directives in production
To replicate the custom directives behavior in "read" mode, you can use the remark-directive package to render directives up to your requirements.