🚸 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:
@@ -2879,6 +2879,21 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "timeFilter",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"today",
|
||||||
|
"last7Days",
|
||||||
|
"last30Days",
|
||||||
|
"yearToDate",
|
||||||
|
"allTime"
|
||||||
|
],
|
||||||
|
"default": "today"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -3186,6 +3201,21 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "timeFilter",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"today",
|
||||||
|
"last7Days",
|
||||||
|
"last30Days",
|
||||||
|
"yearToDate",
|
||||||
|
"allTime"
|
||||||
|
],
|
||||||
|
"default": "today"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"@typebot.io/tsconfig": "workspace:*",
|
"@typebot.io/tsconfig": "workspace:*",
|
||||||
"@typebot.io/variables": "workspace:*",
|
"@typebot.io/variables": "workspace:*",
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-common": "21.1.5",
|
||||||
"@udecode/plate-serializer-md": "24.4.0",
|
|
||||||
"ai": "2.2.31",
|
"ai": "2.2.31",
|
||||||
"chrono-node": "2.7.0",
|
"chrono-node": "2.7.0",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import {
|
|||||||
getVariablesToParseInfoInText,
|
getVariablesToParseInfoInText,
|
||||||
parseVariables,
|
parseVariables,
|
||||||
} from '@typebot.io/variables/parseVariables'
|
} from '@typebot.io/variables/parseVariables'
|
||||||
import { TDescendant, createPlateEditor } from '@udecode/plate-common'
|
import { TDescendant } from '@udecode/plate-common'
|
||||||
import {
|
|
||||||
createDeserializeMdPlugin,
|
|
||||||
deserializeMd,
|
|
||||||
} from '@udecode/plate-serializer-md'
|
|
||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||||
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
import { defaultVideoBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||||
|
import { convertMarkdownToRichText } from '@typebot.io/lib/markdown/convertMarkdownToRichText'
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
version: 1 | 2
|
version: 1 | 2
|
||||||
@@ -207,28 +204,3 @@ const applyElementStyleToDescendants = (
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const convertMarkdownToRichText = (text: string): TDescendant[] => {
|
|
||||||
const spacesBefore = text.match(/^[\s]+/)
|
|
||||||
const spacesAfter = text.match(/[\s]+$/)
|
|
||||||
const plugins = [createDeserializeMdPlugin()]
|
|
||||||
return [
|
|
||||||
...(spacesBefore?.at(0)
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: 'p',
|
|
||||||
text: spacesBefore.at(0),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...deserializeMd(createPlateEditor({ plugins }) as unknown as any, text),
|
|
||||||
...(spacesAfter?.at(0)
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
type: 'p',
|
|
||||||
text: spacesAfter.at(0),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/con
|
|||||||
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
|
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
|
||||||
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
|
||||||
import { convertRichTextToMarkdown } from '@typebot.io/lib/serializer/convertRichTextToMarkdown'
|
import { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
|
||||||
|
|
||||||
export const convertInputToWhatsAppMessages = (
|
export const convertInputToWhatsAppMessages = (
|
||||||
input: NonNullable<ContinueChatResponse['input']>,
|
input: NonNullable<ContinueChatResponse['input']>,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { WhatsAppSendingMessage } from '@typebot.io/schemas/features/whatsapp'
|
|||||||
import { isSvgSrc } from '@typebot.io/lib/utils'
|
import { isSvgSrc } from '@typebot.io/lib/utils'
|
||||||
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
|
||||||
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
|
||||||
import { convertRichTextToMarkdown } from '@typebot.io/lib/serializer/convertRichTextToMarkdown'
|
import { convertRichTextToMarkdown } from '@typebot.io/lib/markdown/convertRichTextToMarkdown'
|
||||||
|
|
||||||
const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/
|
const mp4HttpsUrlRegex = /^https:\/\/.*\.mp4$/
|
||||||
|
|
||||||
|
|||||||
69
packages/lib/markdown/convertMarkdownToRichText.ts
Normal file
69
packages/lib/markdown/convertMarkdownToRichText.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
createPlateEditor,
|
||||||
|
createPluginFactory,
|
||||||
|
getPluginOptions,
|
||||||
|
isUrl,
|
||||||
|
Value,
|
||||||
|
} from '@udecode/plate-common'
|
||||||
|
import markdown from 'remark-parse'
|
||||||
|
import { unified } from 'unified'
|
||||||
|
|
||||||
|
import { DeserializeMdPlugin } from './deserializer/types'
|
||||||
|
import { remarkPlugin } from './remark-slate/remarkPlugin'
|
||||||
|
import { RemarkPluginOptions } from './remark-slate/types'
|
||||||
|
import { remarkDefaultElementRules } from './remark-slate/remarkDefaultElementRules'
|
||||||
|
import { remarkDefaultTextRules } from './remark-slate/remarkDefaultTextRules'
|
||||||
|
import { deserialize } from './deserializer/deserialize'
|
||||||
|
|
||||||
|
export const convertMarkdownToRichText = <V extends Value>(data: string) => {
|
||||||
|
const plugins = [createDeserializeMdPlugin()]
|
||||||
|
const editor = createPlateEditor({ plugins }) as unknown as any
|
||||||
|
const { elementRules, textRules, indentList } = getPluginOptions<
|
||||||
|
DeserializeMdPlugin,
|
||||||
|
V
|
||||||
|
>(editor, KEY_DESERIALIZE_MD)
|
||||||
|
|
||||||
|
const tree: any = unified()
|
||||||
|
.use(markdown)
|
||||||
|
.use(remarkPlugin, {
|
||||||
|
editor,
|
||||||
|
elementRules,
|
||||||
|
textRules,
|
||||||
|
indentList,
|
||||||
|
} as unknown as RemarkPluginOptions<V>)
|
||||||
|
.processSync(data)
|
||||||
|
|
||||||
|
return tree.result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KEY_DESERIALIZE_MD = 'deserializeMd'
|
||||||
|
|
||||||
|
const createDeserializeMdPlugin = createPluginFactory<DeserializeMdPlugin>({
|
||||||
|
key: KEY_DESERIALIZE_MD,
|
||||||
|
then: (editor) => ({
|
||||||
|
editor: {
|
||||||
|
insertData: {
|
||||||
|
format: 'text/plain',
|
||||||
|
query: ({ data, dataTransfer }) => {
|
||||||
|
const htmlData = dataTransfer.getData('text/html')
|
||||||
|
if (htmlData) return false
|
||||||
|
|
||||||
|
const { files } = dataTransfer
|
||||||
|
if (
|
||||||
|
!files?.length && // if content is simply a URL pass through to not break LinkPlugin
|
||||||
|
isUrl(data)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
getFragment: ({ data }) => deserialize<Value>(editor, data),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
options: {
|
||||||
|
elementRules: remarkDefaultElementRules,
|
||||||
|
textRules: remarkDefaultTextRules,
|
||||||
|
indentList: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,41 +1,36 @@
|
|||||||
import { TElement } from '@udecode/plate-common'
|
import { TElement } from '@udecode/plate-common'
|
||||||
import serialize from './serialize'
|
import serialize from './serializer/serialize'
|
||||||
import { defaultNodeTypes } from './ast-types'
|
import { defaultNodeTypes } from './serializer/ast-types'
|
||||||
|
|
||||||
export const convertRichTextToMarkdown = (
|
export const convertRichTextToMarkdown = (
|
||||||
richText: TElement[],
|
richText: TElement[],
|
||||||
options?: { flavour?: 'common' | 'whatsapp' }
|
options?: { flavour?: 'common' | 'whatsapp' }
|
||||||
) => {
|
) => {
|
||||||
let extraNewLinesCount = 0
|
|
||||||
const test = richText
|
const test = richText
|
||||||
.reduce<string[]>((acc, node) => {
|
.reduce<string[]>((acc, node) => {
|
||||||
if (node.type === 'variable') {
|
if (node.type === 'variable') {
|
||||||
return [
|
return [
|
||||||
...acc,
|
...acc,
|
||||||
...node.children.map(
|
...node.children.reduce<string[]>((acc, node) => {
|
||||||
(child) =>
|
const serializedElement = serialize(node, {
|
||||||
serialize(child, {
|
nodeTypes: defaultNodeTypes,
|
||||||
nodeTypes: defaultNodeTypes,
|
flavour: options?.flavour,
|
||||||
flavour: options?.flavour,
|
}) as string
|
||||||
}) as string
|
if (!serializedElement || serializedElement === '<br>\n\n')
|
||||||
),
|
return [...acc, '\n']
|
||||||
|
return [...acc, serializedElement]
|
||||||
|
}, []),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const serializedElement = serialize(node, {
|
const serializedElement = serialize(node, {
|
||||||
nodeTypes: defaultNodeTypes,
|
nodeTypes: defaultNodeTypes,
|
||||||
flavour: options?.flavour,
|
flavour: options?.flavour,
|
||||||
})
|
})
|
||||||
if (!serializedElement || serializedElement === '<br>\n\n') {
|
if (!serializedElement || serializedElement === '<br>\n\n')
|
||||||
if (extraNewLinesCount > 0) {
|
return [...acc, '\n']
|
||||||
return [...acc, '']
|
|
||||||
}
|
|
||||||
extraNewLinesCount++
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
extraNewLinesCount = 0
|
|
||||||
return [...acc, serializedElement]
|
return [...acc, serializedElement]
|
||||||
}, [])
|
}, [])
|
||||||
.join('\n')
|
.join('')
|
||||||
|
|
||||||
return test.endsWith('\n') ? test.slice(0, -1) : test
|
return test.endsWith('\n') ? test.slice(0, -1) : test
|
||||||
}
|
}
|
||||||
30
packages/lib/markdown/deserializer/deserialize.ts
Normal file
30
packages/lib/markdown/deserializer/deserialize.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { getPluginOptions, PlateEditor, Value } from '@udecode/plate-common'
|
||||||
|
import markdown from 'remark-parse'
|
||||||
|
import { unified } from 'unified'
|
||||||
|
|
||||||
|
import { DeserializeMdPlugin } from './types'
|
||||||
|
import { remarkPlugin } from '../remark-slate/remarkPlugin'
|
||||||
|
import { RemarkPluginOptions } from '../remark-slate/types'
|
||||||
|
import { KEY_DESERIALIZE_MD } from '../convertMarkdownToRichText'
|
||||||
|
|
||||||
|
export const deserialize = <V extends Value>(
|
||||||
|
editor: PlateEditor<V>,
|
||||||
|
data: string
|
||||||
|
) => {
|
||||||
|
const { elementRules, textRules, indentList } = getPluginOptions<
|
||||||
|
DeserializeMdPlugin,
|
||||||
|
V
|
||||||
|
>(editor, KEY_DESERIALIZE_MD)
|
||||||
|
|
||||||
|
const tree: any = unified()
|
||||||
|
.use(markdown)
|
||||||
|
.use(remarkPlugin, {
|
||||||
|
editor,
|
||||||
|
elementRules,
|
||||||
|
textRules,
|
||||||
|
indentList,
|
||||||
|
} as unknown as RemarkPluginOptions<V>)
|
||||||
|
.processSync(data)
|
||||||
|
|
||||||
|
return tree.result
|
||||||
|
}
|
||||||
8
packages/lib/markdown/deserializer/types.ts
Normal file
8
packages/lib/markdown/deserializer/types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Value } from '@udecode/plate-common'
|
||||||
|
import { RemarkElementRules, RemarkTextRules } from '../remark-slate/types'
|
||||||
|
|
||||||
|
export interface DeserializeMdPlugin<V extends Value = Value> {
|
||||||
|
elementRules?: RemarkElementRules<V>
|
||||||
|
textRules?: RemarkTextRules<V>
|
||||||
|
indentList?: boolean
|
||||||
|
}
|
||||||
253
packages/lib/markdown/remark-slate/remarkDefaultElementRules.ts
Normal file
253
packages/lib/markdown/remark-slate/remarkDefaultElementRules.ts
Normal 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
|
||||||
|
}
|
||||||
12
packages/lib/markdown/remark-slate/remarkDefaultTextRules.ts
Normal file
12
packages/lib/markdown/remark-slate/remarkDefaultTextRules.ts
Normal 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') },
|
||||||
|
}
|
||||||
18
packages/lib/markdown/remark-slate/remarkPlugin.ts
Normal file
18
packages/lib/markdown/remark-slate/remarkPlugin.ts
Normal 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
|
||||||
|
}
|
||||||
10
packages/lib/markdown/remark-slate/remarkTextTypes.ts
Normal file
10
packages/lib/markdown/remark-slate/remarkTextTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { MdastNodeType } from './types'
|
||||||
|
|
||||||
|
export const remarkTextTypes: MdastNodeType[] = [
|
||||||
|
'emphasis',
|
||||||
|
'strong',
|
||||||
|
'delete',
|
||||||
|
'inlineCode',
|
||||||
|
'html',
|
||||||
|
'text',
|
||||||
|
]
|
||||||
17
packages/lib/markdown/remark-slate/remarkTransformElement.ts
Normal file
17
packages/lib/markdown/remark-slate/remarkTransformElement.ts
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
20
packages/lib/markdown/remark-slate/remarkTransformNode.ts
Normal file
20
packages/lib/markdown/remark-slate/remarkTransformNode.ts
Normal 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)
|
||||||
|
}
|
||||||
36
packages/lib/markdown/remark-slate/remarkTransformText.ts
Normal file
36
packages/lib/markdown/remark-slate/remarkTransformText.ts
Normal 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]
|
||||||
|
}
|
||||||
67
packages/lib/markdown/remark-slate/types.ts
Normal file
67
packages/lib/markdown/remark-slate/types.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -26,12 +26,23 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/nextjs": "7.77.0",
|
"@sentry/nextjs": "7.77.0",
|
||||||
"@trpc/server": "10.40.0",
|
"@trpc/server": "10.40.0",
|
||||||
|
"@udecode/plate-basic-marks": "21.1.5",
|
||||||
|
"@udecode/plate-block-quote": "30.1.2",
|
||||||
|
"@udecode/plate-code-block": "30.1.2",
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-common": "21.1.5",
|
||||||
|
"@udecode/plate-heading": "30.1.2",
|
||||||
|
"@udecode/plate-horizontal-rule": "30.1.2",
|
||||||
|
"@udecode/plate-link": "21.2.0",
|
||||||
|
"@udecode/plate-list": "30.1.2",
|
||||||
|
"@udecode/plate-media": "30.1.2",
|
||||||
|
"@udecode/plate-paragraph": "30.1.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"google-auth-library": "8.9.0",
|
"google-auth-library": "8.9.0",
|
||||||
"got": "12.6.0",
|
"got": "12.6.0",
|
||||||
"minio": "7.1.3",
|
"minio": "7.1.3",
|
||||||
|
"remark-parse": "11.0.0",
|
||||||
"stripe": "12.13.0",
|
"stripe": "12.13.0",
|
||||||
|
"unified": "11.0.4",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
827
pnpm-lock.yaml
generated
827
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user