Skip to content

Commit

Permalink
feat: support markdown directives and custom editors
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed Jul 22, 2023
1 parent 02a1f5f commit 24bb110
Show file tree
Hide file tree
Showing 27 changed files with 892 additions and 45 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Contributing to MDX Editor
# Contributing to MDXEditor

First things first, if you're doing something in the space, [contact me over the email in my profile](https://github.com/petyosi/).
The project is still in its infancy, and I would love to get additional perspective.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# MDX Editor
# MDXEditor

![npm](https://img.shields.io/npm/v/@mdxeditor/editor)
![npm bundle size (scoped)](https://img.shields.io/bundlephobia/minzip/@mdxeditor/editor)

> Because markdown editing can be even more delightful.
MDX Editor is an open-source React component that allows users to author markdown documents naturally. Just like in Google docs or Notion. [See the live demo](https://mdxeditor.dev/editor/demo) that has the default features turned on. It supports most (if not all) of the markdown syntax, including tables, images, code blocks, etc. It also allows users to edit JSX components with a dedicated property editor.
MDXEditor is an open-source React component that allows users to author markdown documents naturally. Just like in Google docs or Notion. [See the live demo](https://mdxeditor.dev/editor/demo) that has the default features turned on. It supports the core markdown syntax and certain extensions, including tables, images, code blocks, etc. It also allows users to edit JSX components with a dedicated property editor.

```jsx
import {MDXEditor} from '@mdxeditor/editor';
Expand Down
2 changes: 1 addition & 1 deletion scripts/build_docs_api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

set -e

./node_modules/.bin/api-extractor run --local --verbose
api-extractor run --local --verbose
api-documenter markdown --input-folder ./temp --output-folder ./docs/api
1 change: 0 additions & 1 deletion src/content/theme.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { EditorThemeClasses } from 'lexical'
import styles from './theme.module.css'
import more from '../ui/styles.module.css'

export const theme: EditorThemeClasses = {
text: {
Expand Down
14 changes: 12 additions & 2 deletions src/export/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ import {
TextNode
} from 'lexical'
import * as Mdast from 'mdast'
import { ContainerDirective } from 'mdast-util-directive'
import { ContainerDirective, LeafDirective } from 'mdast-util-directive'
import type { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
import {
$isAdmonitionNode,
$isCodeBlockNode,
$isFrontmatterNode,
$isImageNode,
$isJsxNode,
$isLeafDirectiveNode,
$isSandpackNode,
$isTableNode,
AdmonitionNode,
CodeBlockNode,
FrontmatterNode,
ImageNode,
JsxNode,
LeafDirectiveNode,
SandpackNode,
TableNode
} from '../nodes'
Expand Down Expand Up @@ -351,6 +353,13 @@ const LexicalTableVisitor: LexicalExportVisitor<TableNode, Mdast.Table> = {
}
}

const LexicalLeafDirectiveVisitor: LexicalExportVisitor<LeafDirectiveNode, LeafDirective> = {
testLexicalNode: $isLeafDirectiveNode,
visitLexicalNode({ actions, mdastParent, lexicalNode }) {
actions.appendToParent(mdastParent, lexicalNode.getMdastNode())
}
}

const JsxVisitor: LexicalExportVisitor<JsxNode, MdxJsxFlowElement | MdxJsxTextElement> = {
testLexicalNode: $isJsxNode,
visitLexicalNode({ mdastParent, lexicalNode, actions }) {
Expand Down Expand Up @@ -390,5 +399,6 @@ export const defaultLexicalVisitors = {
AdmonitionVisitor,
LexicalImageVisitor,
JsxVisitor,
LexicalTableVisitor
LexicalTableVisitor,
LexicalLeafDirectiveVisitor
}
26 changes: 17 additions & 9 deletions src/import/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { $createHorizontalRuleNode, HorizontalRuleNode } from '@lexical/react/Le
import { $createHeadingNode, $createQuoteNode, $isQuoteNode, HeadingNode, QuoteNode } from '@lexical/rich-text'
import { $createParagraphNode, $createTextNode, ElementNode, Klass, LexicalNode, RootNode as LexicalRootNode, ParagraphNode } from 'lexical'
import * as Mdast from 'mdast'
import { ContainerDirective, directiveFromMarkdown } from 'mdast-util-directive'
import { ContainerDirective, LeafDirective, directiveFromMarkdown } from 'mdast-util-directive'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { FromMarkdownOptions, ParseOptions } from 'mdast-util-from-markdown/lib'
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'
import { gfmTableFromMarkdown } from 'mdast-util-gfm-table'
import { MdxJsxTextElement, MdxjsEsm, mdxFromMarkdown } from 'mdast-util-mdx'
Expand All @@ -21,6 +22,7 @@ import {
$createFrontmatterNode,
$createImageNode,
$createJsxNode,
$createLeafDirectiveNode,
$createSandpackNode,
$createTableNode,
$isAdmonitionNode,
Expand All @@ -30,10 +32,10 @@ import {
FrontmatterNode,
ImageNode,
JsxNode,
LeafDirectiveNode,
SandpackNode,
TableNode
} from '../nodes'
import { FromMarkdownOptions, ParseOptions } from 'mdast-util-from-markdown/lib'

/**
* A set of actions that can be used to modify the lexical tree while visiting the mdast tree.
Expand Down Expand Up @@ -128,6 +130,13 @@ export const MdastAdmonitionVisitor: MdastImportVisitor<ContainerDirective> = {
}
}

export const MdastLeafDirectiveVisitor: MdastImportVisitor<LeafDirective> = {
testNode: 'leafDirective',
visitNode({ lexicalParent, mdastNode }) {
;(lexicalParent as ElementNode).append($createLeafDirectiveNode(mdastNode))
}
}

export const MdastHeadingVisitor: MdastImportVisitor<Mdast.Heading> = {
testNode: 'heading',
visitNode: function ({ mdastNode, actions }): void {
Expand Down Expand Up @@ -294,7 +303,8 @@ export const defaultMdastVisitors: Record<string, MdastImportVisitor<Mdast.Conte
MdastAdmonitionVisitor,
MdastMdxJsEsmVisitor,
MdastMdxJsxElementVisitor,
MdastTableVisitor
MdastTableVisitor,
MdastLeafDirectiveVisitor
}

function isParent(node: unknown): node is Mdast.Parent {
Expand All @@ -310,10 +320,6 @@ export interface MdastTreeImportOptions {
mdastRoot: Mdast.Root
}

/**
* Options that control how the the markdown input string is parsed into a tree.
* @see {@link https://github.com/syntax-tree/mdast-util-from-markdown | fromMarkdown}
*/
export interface MarkdownParseOptions extends Omit<MdastTreeImportOptions, 'mdastRoot'> {
markdown: string
syntaxExtensions: NonNullable<ParseOptions['extensions']>
Expand Down Expand Up @@ -375,7 +381,8 @@ export function importMdastTreeToLexical({ root, mdastRoot, visitors }: MdastTre
return visitor.testNode(mdastNode)
})
if (!visitor) {
throw new Error(`no unist visitor found for ${mdastNode.type}`, {
debugger
throw new Error(`no MdastImportVisitor found for ${mdastNode.type}`, {
cause: mdastNode
})
}
Expand Down Expand Up @@ -423,5 +430,6 @@ export const defaultLexicalNodes: Record<string, Klass<LexicalNode>> = {
AdmonitionNode,
JsxNode,
CodeNode, // this one should not be used, but markdown shortcuts complain about it
TableNode
TableNode,
LeafDirectiveNode
}
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
*/
export type { SandpackConfig, SandpackPreset } from './system/Sandpack'
export type * from './types/JsxComponentDescriptors'
export type { MdastTreeImportOptions, MarkdownParseOptions, MdastVisitActions, MdastVisitParams, MdastImportVisitor } from './import'
export type { MdastTreeImportOptions, MdastVisitActions, MdastVisitParams, MdastImportVisitor } from './import'
export type { ToMarkdownOptions, LexicalVisitActions, LexicalNodeVisitParams, LexicalExportVisitor, LexicalConvertOptions } from './export'
export type { NestedEditorProps } from './ui/NodeDecorators/NestedEditor'
export type { CustomLeafDirectiveEditor, LeafDirectiveEditorProps } from './types/NodeDecoratorsProps'

export * from './nodes'
export * from './ui/MDXEditor'
export { NestedEditor, useMdastNodeUpdater } from './ui/NodeDecorators/NestedEditor'
export { ToolbarComponents } from './ui/ToolbarPlugin/toolbarComponents'
99 changes: 99 additions & 0 deletions src/nodes/LeafDirectiveNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react'
import type { LexicalEditor, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from 'lexical'

import { DecoratorNode } from 'lexical'
import { ExtendedEditorConfig } from '../types/ExtendedEditorConfig'
import { LeafDirective } from 'mdast-util-directive'

/**
* A serialized representation of an {@link LeafDirectiveNode}.
*/
export type SerializedLeafDirectiveNode = Spread<
{
mdastNode: LeafDirective
type: 'leafDirective'
version: 1
},
SerializedLexicalNode
>

/**
* A lexical node that represents an image. Use {@link "$createLeafDirectiveNode"} to construct one.
*/
export class LeafDirectiveNode extends DecoratorNode<JSX.Element> {
__mdastNode: LeafDirective

static getType(): string {
return 'leafDirective'
}

static clone(node: LeafDirectiveNode): LeafDirectiveNode {
return new LeafDirectiveNode(structuredClone(node.__mdastNode))
}

static importJSON(serializedNode: SerializedLeafDirectiveNode): LeafDirectiveNode {
return $createLeafDirectiveNode(serializedNode.mdastNode)
}

constructor(mdastNode: LeafDirective, key?: NodeKey) {
super(key)
this.__mdastNode = mdastNode
}

getMdastNode(): LeafDirective {
return this.__mdastNode
}

exportJSON(): SerializedLeafDirectiveNode {
return {
mdastNode: this.getMdastNode(),
type: 'leafDirective',
version: 1
}
}

createDOM(): HTMLElement {
return document.createElement('div')
}

updateDOM(): false {
return false
}

setMdastNode(mdastNode: LeafDirective): void {
this.getWritable().__mdastNode = mdastNode
}

decorate(
parentEditor: LexicalEditor,
{
theme: {
nodeDecoratorComponents: { LeafDirectiveEditor }
}
}: ExtendedEditorConfig
): JSX.Element {
return <LeafDirectiveEditor leafDirective={this} mdastNode={this.getMdastNode()} parentEditor={parentEditor} />
}

isInline(): boolean {
return false
}

isKeyboardSelectable(): boolean {
return true
}
}

/**
* Creates an {@link LeafDirectiveNode}.
*/
export function $createLeafDirectiveNode(mdastNode: LeafDirective): LeafDirectiveNode {
return new LeafDirectiveNode(mdastNode)
}

/**
* Retruns true if the node is an {@link LeafDirectiveNode}.
*/
export function $isLeafDirectiveNode(node: LexicalNode | null | undefined): node is LeafDirectiveNode {
return node instanceof LeafDirectiveNode
}
1 change: 1 addition & 0 deletions src/nodes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './ImageNode'
export * from './JsxNode'
export * from './SandpackNode'
export * from './TableNode'
export * from './LeafDirectiveNode'
94 changes: 94 additions & 0 deletions src/stories/button-youtube.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $createParagraphNode, $insertNodes } from 'lexical'
import { LeafDirective } from 'mdast-util-directive'
import React from 'react'
import { $createLeafDirectiveNode, MDXEditor, ToolbarComponents } from '../'

const {
BoldItalicUnderlineButtons,
ToolbarSeparator,
CodeFormattingButton,
ListButtons,
BlockTypeSelect,
LinkButton,
ImageButton,
TableButton,
HorizontalRuleButton,
FrontmatterButton,
CodeBlockButton,
SandpackButton,
DialogButton
} = ToolbarComponents

const YouTubeButton = () => {
const [editor] = useLexicalComposerContext()
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) {
editor.update(() => {
const youtubeDirectiveMdastNode: LeafDirective = {
type: 'leafDirective',
name: 'youtube',
attributes: { id: videoId },
children: []
}
const lexicalNode = $createLeafDirectiveNode(youtubeDirectiveMdastNode)
$insertNodes([lexicalNode])

if (lexicalNode.getParent()?.getLastChild() == lexicalNode) {
lexicalNode.getParent()?.append($createParagraphNode())
}
})
} else {
alert('Invalid YouTube URL')
}
}}
/>
)
}

const toolbarComponents = [
BoldItalicUnderlineButtons,
ToolbarSeparator,

CodeFormattingButton,
ToolbarSeparator,

ListButtons,
ToolbarSeparator,
BlockTypeSelect,
ToolbarSeparator,
LinkButton,
ImageButton,
TableButton,
HorizontalRuleButton,
FrontmatterButton,

ToolbarSeparator,

CodeBlockButton,
SandpackButton,
ToolbarSeparator,
YouTubeButton
]

export function Hello() {
return (
<MDXEditor
toolbarComponents={toolbarComponents}
markdown={`
This should be an youtube video:
::youtube{#A5lXAKrttBU}
::callout[there is some *markdown* in here]
`}
/>
)
}
Loading

0 comments on commit 24bb110

Please sign in to comment.