2
0

🚸 Rewrite the markdown deserializer to improve br… (#1198)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Updated markdown handling and serialization libraries for improved
performance and accuracy in text formatting.
- **New Features**
- Enhanced rich text and markdown conversion capabilities, providing
users with more reliable and seamless text formatting options.
- **Documentation**
- Added detailed documentation for markdown to rich text conversion and
vice versa, ensuring easier understanding and implementation for
developers.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2024-01-31 08:03:13 +01:00
committed by GitHub
parent 26872e2bac
commit ff9c4726cc
22 changed files with 1017 additions and 478 deletions

View File

@@ -0,0 +1,253 @@
import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'
import {
ELEMENT_CODE_BLOCK,
ELEMENT_CODE_LINE,
} from '@udecode/plate-code-block'
import {
getPluginType,
TDescendant,
TElement,
TText,
Value,
} from '@udecode/plate-common'
import {
ELEMENT_H1,
ELEMENT_H2,
ELEMENT_H3,
ELEMENT_H4,
ELEMENT_H5,
ELEMENT_H6,
} from '@udecode/plate-heading'
import { ELEMENT_HR } from '@udecode/plate-horizontal-rule'
import { ELEMENT_LINK } from '@udecode/plate-link'
import {
ELEMENT_LI,
ELEMENT_LIC,
ELEMENT_OL,
ELEMENT_UL,
} from '@udecode/plate-list'
import { ELEMENT_IMAGE } from '@udecode/plate-media'
import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'
import { remarkTransformElementChildren } from './remarkTransformElementChildren'
import { MdastNode, RemarkElementRules, RemarkPluginOptions } from './types'
export const remarkDefaultElementRules: RemarkElementRules<Value> = {
heading: {
transform: (node, lastLineNumber, options) => {
const headingType = {
1: ELEMENT_H1,
2: ELEMENT_H2,
3: ELEMENT_H3,
4: ELEMENT_H4,
5: ELEMENT_H5,
6: ELEMENT_H6,
}[node.depth ?? 1]
return [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(options.editor, headingType),
children: remarkTransformElementChildren(
node,
lastLineNumber,
options
),
},
]
},
},
list: {
transform: (node, lastLineNumber, options) => {
if (options.indentList) {
const listStyleType = node.ordered ? 'decimal' : 'disc'
const parseListItems = (
_node: MdastNode,
listItems: TElement[] = [],
indent = 1
) => {
_node.children!.forEach((listItem) => {
const [paragraph, ...subLists] = listItem.children!
listItems.push({
type: getPluginType(options.editor, ELEMENT_PARAGRAPH),
listStyleType,
indent,
children: remarkTransformElementChildren(
paragraph || '',
lastLineNumber,
options
),
})
subLists.forEach((subList) => {
parseListItems(subList, listItems, indent + 1)
})
})
return listItems
}
return [
...parseLineBreakNodes(node, lastLineNumber, options),
parseListItems(node),
]
} else {
return [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(
options.editor,
node.ordered ? ELEMENT_OL : ELEMENT_UL
),
children: remarkTransformElementChildren(
node,
lastLineNumber,
options
),
},
]
}
},
},
listItem: {
transform: (node, lastLineNumber, options) => ({
type: getPluginType(options.editor, ELEMENT_LI),
children: remarkTransformElementChildren(
node,
lastLineNumber,
options
).map(
(child) =>
({
...child,
type:
child.type === getPluginType(options.editor, ELEMENT_PARAGRAPH)
? getPluginType(options.editor, ELEMENT_LIC)
: child.type,
} as TDescendant)
),
}),
},
paragraph: {
transform: (node, lastLineNumber, options) => {
const children = remarkTransformElementChildren(
node,
lastLineNumber,
options
)
const paragraphType = getPluginType(options.editor, ELEMENT_PARAGRAPH)
const splitBlockTypes = new Set([
getPluginType(options.editor, ELEMENT_IMAGE),
])
const elements: TElement[] = []
let inlineNodes: TDescendant[] = []
const flushInlineNodes = () => {
if (inlineNodes.length > 0) {
elements.push({
type: paragraphType,
children: inlineNodes,
})
inlineNodes = []
}
}
children.forEach((child) => {
const { type } = child
if (type && splitBlockTypes.has(type as string)) {
flushInlineNodes()
elements.push(child as TElement)
} else {
inlineNodes.push(child)
}
})
flushInlineNodes()
return [
...parseLineBreakNodes(node, lastLineNumber, options),
...elements,
]
},
},
link: {
transform: (node, lastLineNumber, options) => [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(options.editor, ELEMENT_LINK),
url: node.url,
children: remarkTransformElementChildren(node, lastLineNumber, options),
},
],
},
image: {
transform: (node, lastLineNumber, options) => [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(options.editor, ELEMENT_IMAGE),
children: [{ text: '' } as TText],
url: node.url,
caption: [{ text: node.alt } as TText],
},
],
},
blockquote: {
transform: (node, lastLineNumber, options) => [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(options.editor, ELEMENT_BLOCKQUOTE),
children: node.children!.flatMap((paragraph) =>
remarkTransformElementChildren(paragraph, lastLineNumber, options)
),
},
],
},
code: {
transform: (node, lastLineNumber, options) => [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(options.editor, ELEMENT_CODE_BLOCK),
lang: node.lang ?? undefined,
children: (node.value || '').split('\n').map((line) => ({
type: getPluginType(options.editor, ELEMENT_CODE_LINE),
children: [{ text: line } as TText],
})),
},
],
},
thematicBreak: {
transform: (node, lastLineNumber, options) => [
...parseLineBreakNodes(node, lastLineNumber, options),
{
type: getPluginType(options.editor, ELEMENT_HR),
children: [{ text: '' } as TText],
},
],
},
}
const parseLineBreakNodes = (
node: MdastNode,
lastLineNumber: number,
options: RemarkPluginOptions<Value>
) => {
const lineBreaks = node.position.start.line - lastLineNumber
let lineBreakNodes = []
if (lineBreaks > 1)
lineBreakNodes.push(
...Array(lineBreaks - 1).fill({
type: getPluginType(options.editor, ELEMENT_PARAGRAPH),
children: [{ text: '' } as TText],
})
)
return lineBreakNodes
}

View File

@@ -0,0 +1,12 @@
import { MARK_BOLD, MARK_CODE, MARK_ITALIC } from '@udecode/plate-basic-marks'
import { getPluginType, Value } from '@udecode/plate-common'
import { RemarkTextRules } from './types'
export const remarkDefaultTextRules: RemarkTextRules<Value> = {
text: {},
emphasis: { mark: ({ editor }) => getPluginType(editor, MARK_ITALIC) },
strong: { mark: ({ editor }) => getPluginType(editor, MARK_BOLD) },
inlineCode: { mark: ({ editor }) => getPluginType(editor, MARK_CODE) },
html: { transform: (text: string) => text.replaceAll('<br>', '\n') },
}

View File

@@ -0,0 +1,18 @@
import { Value } from '@udecode/plate-common'
import { remarkTransformNode } from './remarkTransformNode'
import { MdastNode, RemarkPluginOptions } from './types'
export function remarkPlugin<V extends Value>(options: RemarkPluginOptions<V>) {
let lastLineNumber = 1
const compiler = (node: { children: Array<MdastNode> }) => {
return node.children.flatMap((child) => {
const parsedChild = remarkTransformNode(child, lastLineNumber, options)
lastLineNumber = child.position?.end.line || lastLineNumber
return parsedChild
})
}
// @ts-ignore
this.Compiler = compiler
}

View File

@@ -0,0 +1,10 @@
import { MdastNodeType } from './types'
export const remarkTextTypes: MdastNodeType[] = [
'emphasis',
'strong',
'delete',
'inlineCode',
'html',
'text',
]

View File

@@ -0,0 +1,17 @@
import { TElement, Value } from '@udecode/plate-common'
import { MdastNode, RemarkPluginOptions } from './types'
export const remarkTransformElement = <V extends Value>(
node: MdastNode,
lastLineNumber: number,
options: RemarkPluginOptions<V>
): TElement | TElement[] => {
const { elementRules } = options
const { type } = node
const elementRule = (elementRules as any)[type!]
if (!elementRule) return []
return elementRule.transform(node, lastLineNumber, options)
}

View File

@@ -0,0 +1,17 @@
import { TDescendant, Value } from '@udecode/plate-common'
import { remarkTransformNode } from './remarkTransformNode'
import { MdastNode, RemarkPluginOptions } from './types'
export const remarkTransformElementChildren = <V extends Value>(
node: MdastNode,
lastLineNumber: number,
options: RemarkPluginOptions<V>
): TDescendant[] => {
const { children } = node
if (!children) return []
return children.flatMap((child) =>
remarkTransformNode(child, lastLineNumber, options)
)
}

View File

@@ -0,0 +1,20 @@
import { TDescendant, Value } from '@udecode/plate-common'
import { remarkTextTypes } from './remarkTextTypes'
import { remarkTransformElement } from './remarkTransformElement'
import { remarkTransformText } from './remarkTransformText'
import { MdastNode, RemarkPluginOptions } from './types'
export const remarkTransformNode = <V extends Value>(
node: MdastNode,
lastLineNumber: number,
options: RemarkPluginOptions<V>
): TDescendant | TDescendant[] => {
const { type } = node
if (remarkTextTypes.includes(type!)) {
return remarkTransformText(node, options)
}
return remarkTransformElement(node, lastLineNumber, options)
}

View File

@@ -0,0 +1,36 @@
import { TText, Value } from '@udecode/plate-common'
import { remarkDefaultTextRules } from './remarkDefaultTextRules'
import { MdastNode, RemarkPluginOptions } from './types'
export const remarkTransformText = <V extends Value>(
node: MdastNode,
options: RemarkPluginOptions<V>,
inheritedMarkProps: { [key: string]: boolean } = {}
): TText | TText[] => {
const { editor, textRules } = options
const { type, value, children } = node
const textRule = (textRules as any)[type!] || remarkDefaultTextRules.text
const { mark, transform = (text: string) => text } = textRule
const markProps = mark
? {
...inheritedMarkProps,
[mark({ editor })]: true,
}
: inheritedMarkProps
const childTextNodes =
children?.flatMap((child) =>
remarkTransformText(child, options, markProps)
) || []
const currentTextNodes =
value || childTextNodes.length === 0
? [{ text: transform(value || ''), ...markProps } as TText]
: []
return [...currentTextNodes, ...childTextNodes]
}

View File

@@ -0,0 +1,67 @@
import { PlateEditor, TElement, Value } from '@udecode/plate-common'
export type MdastElementType =
| 'paragraph'
| 'heading'
| 'list'
| 'listItem'
| 'link'
| 'image'
| 'blockquote'
| 'code'
| 'thematicBreak'
export type MdastTextType =
| 'emphasis'
| 'strong'
| 'delete'
| 'inlineCode'
| 'html'
| 'text'
export type MdastNodeType = MdastElementType | MdastTextType
export interface MdastNode {
type?: MdastNodeType
ordered?: boolean
value?: string
text?: string
children?: Array<MdastNode>
depth?: 1 | 2 | 3 | 4 | 5 | 6
url?: string
alt?: string
lang?: string
// mdast metadata
position?: any
spread?: any
checked?: any
indent?: any
}
export type RemarkElementRule<V extends Value> = {
transform: (
node: MdastNode,
lastLineNumber: number,
options: RemarkPluginOptions<V>
) => TElement | TElement[]
}
export type RemarkElementRules<V extends Value> = {
[key in MdastElementType]?: RemarkElementRule<V>
}
export type RemarkTextRule<V extends Value> = {
mark?: (options: RemarkPluginOptions<V>) => string
transform?: (text: string) => string
}
export type RemarkTextRules<V extends Value> = {
[key in MdastTextType]?: RemarkTextRule<V>
}
export type RemarkPluginOptions<V extends Value> = {
editor: PlateEditor<V>
elementRules: RemarkElementRules<V>
textRules: RemarkTextRules<V>
indentList?: boolean
}