refactor(richtext-lexical): new plaintext and markdown converters, restructure converter docs (#11675)

- Introduces a new lexical => plaintext converter
- Introduces a new lexical <=> markdown converter
- Restructures converter docs. Each conversion type gets its own docs
pag
This commit is contained in:
Alessio Gravili
2025-03-17 14:36:10 -06:00
committed by GitHub
parent 013b515d3c
commit 82840aa09b
85 changed files with 1341 additions and 695 deletions

View File

@@ -6,476 +6,68 @@ desc: Conversion between lexical, markdown, jsx and html
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export, jsx
---
Lexical saves data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats like JSX, HTML or Markdown.
Richtext fields save data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats:
## Lexical => JSX
- [Converting JSX](/docs/rich-text/converting-jsx)
- [Converting HTML](/docs/rich-text/converting-html)
- [Converting Plaintext](/docs/rich-text/converting-plaintext)
- [Converting Markdown and MDX](/docs/rich-text/converting-markdown)
For React-based frontends, converting Lexical content to JSX is the recommended rendering approach. Import the RichText component from @payloadcms/richtext-lexical/react and pass the Lexical content to it:
## Retrieving the Editor Config
```tsx
import React from 'react'
import { RichText } from '@payloadcms/richtext-lexical/react'
import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
Some converters require access to the Lexical editor config, which defines available features and behaviors. Payload provides multiple ways to obtain the editor config through the `editorConfigFactory` from `@payloadcms/richtext-lexical`.
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
return <RichText data={data} />
}
```
### Importing the Factory
The `RichText` component includes built-in serializers for common Lexical nodes but allows customization through the `converters` prop. In our [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) you have an example of how to use `converters` to render custom blocks, custom nodes and override existing converters.
<Banner type="default">
When fetching data, ensure your `depth` setting is high enough to fully
populate Lexical nodes such as uploads. The JSX converter requires fully
populated data to work correctly.
</Banner>
### Converting Internal Links
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
```tsx
import type {
DefaultNodeTypes,
SerializedLinkNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
type JSXConvertersFunction,
LinkJSXConverter,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
const { relationTo, value } = linkNode.fields.doc!
if (typeof value !== 'object') {
throw new Error('Expected value to be an object')
}
const slug = value.slug
return relationTo === 'posts' ? `/posts/${slug}` : `/${slug}`
}
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
...LinkJSXConverter({ internalDocToHref }),
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Converting Lexical Blocks
To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your block to the `RichText` component. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
```tsx
'use client'
import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
type JSXConvertersFunction,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
// Extend the default node types with your custom blocks for full type safety
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyNumberBlock | MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myNumberBlock: ({ node }) => <div>{node.fields.number}</div>,
myTextBlock: ({ node }) => (
<div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>
),
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Overriding Default JSX Converters
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
Example - overriding the upload node converter to use next/image:
```tsx
'use client'
import type {
DefaultNodeTypes,
SerializedUploadNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
type JSXConvertersFunction,
RichText,
} from '@payloadcms/richtext-lexical/react'
import Image from 'next/image'
import React from 'react'
type NodeTypes = DefaultNodeTypes
// Custom upload converter component that uses next/image
const CustomUploadComponent: React.FC<{
node: SerializedUploadNode
}> = ({ node }) => {
if (node.relationTo === 'uploads') {
const uploadDoc = node.value
if (typeof uploadDoc !== 'object') {
return null
}
const { alt, height, url, width } = uploadDoc
return <Image alt={alt} height={height} src={url} width={width} />
}
return null
}
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
// Override the default upload converter
upload: ({ node }) => {
return <CustomUploadComponent node={node} />
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
## Lexical => HTML
If you don't have a React-based frontend, or if you need to send the content to a third-party service, you can convert lexical to HTML. There are two ways to do this:
1. **Generating HTML in your frontend** Convert JSON to HTML on-demand wherever you need it (Recommended).
2. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend. This is not recommended, as this approach adds additional overhead to the Payload API and may not work with live preview.
### Generating HTML in your frontend
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function exported from `@payloadcms/richtext-lexical/html`:
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
import React from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({ data })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Generating HTML in your frontend with dynamic population
The default `convertLexicalToHTML` function does not populate data for nodes like uploads or links - it expects you to pass in the fully populated data. If you want the converter to dynamically populate those nodes as they are encountered, you have to use the async version of the converter, imported from `@payloadcms/richtext-lexical/html-async`, and pass in the `populate` function:
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
Do note that using the REST populate function will result in each node sending a separate request to the REST API, which may be slow for a large amount of nodes. On the server, you can use the payload populate function, which will be more efficient:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import React from 'react'
import config from '../../config.js'
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
const html = await convertLexicalToHTMLAsync({
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Converting Lexical Blocks
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Outputting HTML from the Collection
To add HTML generation directly within the collection, follow the example below:
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
type DefaultNodeTypes,
lexicalEditor,
lexicalHTMLField,
type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
lexicalHTMLField({
htmlFieldName: 'nameOfYourRichTextField_html',
lexicalFieldName: 'nameOfYourRichTextField',
}),
{
name: 'customRichText',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
interfaceName: 'MyTextBlock',
slug: 'myTextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}),
],
}),
},
lexicalHTMLField({
htmlFieldName: 'customRichText_html',
lexicalFieldName: 'customRichText',
// can pass in additional converters or override default ones
converters: (({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
})) as HTMLConvertersFunction<
DefaultNodeTypes | SerializedBlockNode<MyTextBlock>
>,
}),
],
}
```
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
### CSS
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
Here is some "base" CSS you can use to ensure that nested lists render correctly:
```css
/* Base CSS for Lexical HTML */
.nestedListItem,
.list-check {
list-style-type: none;
}
```
## Headless Editor
Lexical provides a seamless way to perform conversions between various other formats:
- HTML to Lexical
- Markdown to Lexical
- Lexical to Markdown
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
```ts
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
import {
getEnabledNodes,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
const payloadConfig // <= your Payload Config here
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: await editorConfigFactory.default({
config: payloadConfig,
}),
}),
})
```
### Getting the editor config
You need to provide an editor config in order to create a headless editor. This is because the editor config is used to determine which nodes & features are enabled, and which converters are used.
To get the editor config, import the `editorConfigFactory` factory - this factory provides a variety of ways to get the editor config, depending on your use case.
First, import the necessary utilities:
```ts
import type { SanitizedConfig } from 'payload'
import { editorConfigFactory } from '@payloadcms/richtext-lexical'
import {
editorConfigFactory,
FixedToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
// Your config needs to be available in order to retrieve the default editor config
// Your Payload Config needs to be available in order to retrieve the default editor config
const config: SanitizedConfig = {} as SanitizedConfig
```
// Version 1 - use the default editor config
const yourEditorConfig = await editorConfigFactory.default({ config })
### Option 1: Default Editor Config
// Version 2 - if you have access to a lexical fields, you can extract the editor config from it
const yourEditorConfig2 = editorConfigFactory.fromField({
field: collectionConfig.fields[1],
If you require the default editor config:
```ts
const defaultEditorConfig = await editorConfigFactory.default({ config })
```
### Option 2: Extract from a Lexical Field
When a lexical field config is available, you can extract the editor config directly:
```ts
const fieldEditorConfig = editorConfigFactory.fromField({
field: config.collections[0].fields[1],
})
```
// Version 3 - create a new editor config - behaves just like instantiating a new `lexicalEditor`
const yourEditorConfig3 = await editorConfigFactory.fromFeatures({
### Option 3: Create a Custom Editor Config
You can create a custom editor configuration by specifying additional features:
```ts
import { FixedToolbarFeature } from '@payloadcms/richtext-lexical'
const customEditorConfig = await editorConfigFactory.fromFeatures({
config,
features: ({ defaultFeatures }) => [
...defaultFeatures,
FixedToolbarFeature(),
],
})
```
// Version 4 - if you have instantiated a lexical editor and are accessing it outside a field (=> this is the unsanitized editor),
// you can extract the editor config from it.
// This is common if you define the editor in a re-usable module scope variable and pass it to the richText field.
// This is the least efficient way to get the editor config, and not recommended. It is recommended to extract the `features` arg
// into a separate variable and use `fromFeatures` instead.
### Option 4: Extract from an Instantiated Editor
If you've created a global or reusable Lexical editor instance, you can access its configuration. This method is typically less efficient and not recommended:
```ts
const editor = lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
@@ -483,15 +75,17 @@ const editor = lexicalEditor({
],
})
const yourEditorConfig4 = await editorConfigFactory.fromEditor({
const instantiatedEditorConfig = await editorConfigFactory.fromEditor({
config,
editor,
})
```
### Example - Getting the editor config from an existing field
For better efficiency, consider extracting the `features` into a separate variable and using `fromFeatures` instead of this method.
If you have access to the sanitized collection config, you can get access to the lexical sanitized editor config & features, as every lexical richText field returns it. Here is an example how you can get it from another field's afterRead hook:
### Example - Retrieving the editor config from an existing field
If you have access to the sanitized collection config, you can access the lexical sanitized editor config, as every lexical richText field returns it. Here is an example how you can retrieve it from another field's afterRead hook:
```ts
import type { CollectionConfig, RichTextField } from 'payload'
@@ -501,7 +95,6 @@ import {
getEnabledNodes,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import { createHeadlessEditor } from '@payloadcms/richtext-lexical/lexical/headless'
export const MyCollection: CollectionConfig = {
slug: 'slug',
@@ -520,13 +113,7 @@ export const MyCollection: CollectionConfig = {
field,
})
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig,
}),
})
// Do whatever you want with the headless editor
// Now you can use the editor config
return value
},
@@ -541,140 +128,3 @@ export const MyCollection: CollectionConfig = {
],
}
```
## HTML => Lexical
If you have access to the Payload Config and the lexical editor config, you can convert HTML to the lexical editor state with the following:
```ts
import {
convertHTMLToLexical,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
// Make sure you have jsdom and @types/jsdom installed
import { JSDOM } from 'jsdom'
const html = convertHTMLToLexical({
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),
html: '<p>text</p>',
JSDOM, // pass the JSDOM import. As it's a relatively large package, richtext-lexical does not include it by default.
})
```
## Markdown => Lexical
Convert markdown content to the Lexical editor format with the following:
```ts
import {
$convertFromMarkdownString,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
const yourEditorConfig = await editorConfigFactory.default({ config })
const markdown = `# Hello World`
headlessEditor.update(
() => {
$convertFromMarkdownString(
markdown,
yourEditorConfig.features.markdownTransformers,
)
},
{ discrete: true },
)
// Do this if you then want to get the editor JSON
const editorJSON = headlessEditor.getEditorState().toJSON()
```
Functions prefixed with a `$` can only be run inside an `editor.update()` or `editorState.read()` callback.
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
<Banner type="success">
**Note:**
Using the `discrete: true` flag ensures instant updates to the editor state. If
immediate reading of the updated state isn't necessary, you can omit the flag.
</Banner>
## Lexical => Markdown
Export content from the Lexical editor into Markdown format using these steps:
1. Import your current editor state into the headless editor.
2. Convert and fetch the resulting markdown string.
Here's the code for it:
```ts
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { editorConfigFactory } from '@payloadcms/richtext-lexical'
import { $convertToMarkdownString } from '@payloadcms/richtext-lexical/lexical/markdown'
const yourEditorConfig = await editorConfigFactory.default({ config })
const yourEditorState: SerializedEditorState // <= your current editor state here
// Import editor state into your headless editor
try {
headlessEditor.update(
() => {
headlessEditor.setEditorState(
headlessEditor.parseEditorState(yourEditorState),
)
},
{ discrete: true }, // This should commit the editor state immediately
)
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
}
// Export to markdown
let markdown: string
headlessEditor.getEditorState().read(() => {
markdown = $convertToMarkdownString(
yourEditorConfig?.features?.markdownTransformers,
)
})
```
## Lexical => Plain Text
Export content from the Lexical editor into plain text using these steps:
1. Import your current editor state into the headless editor.
2. Convert and fetch the resulting plain text string.
Here's the code for it:
```ts
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { $getRoot } from '@payloadcms/richtext-lexical/lexical'
const yourEditorState: SerializedEditorState // <= your current editor state here
// Import editor state into your headless editor
try {
headlessEditor.update(
() => {
headlessEditor.setEditorState(
headlessEditor.parseEditorState(yourEditorState),
)
},
{ discrete: true }, // This should commit the editor state immediately
)
} catch (e) {
logger.error({ err: e }, 'ERROR parsing editor state')
}
// Export to plain text
const plainTextContent =
headlessEditor.getEditorState().read(() => {
return $getRoot().getTextContent()
}) || ''
```

View File

@@ -0,0 +1,243 @@
---
title: Converting HTML
label: Converting HTML
order: 22
desc: Converting between lexical richtext and HTML
keywords: lexical, richtext, html
---
## Converting Rich Text to HTML
There are two main approaches to convert your Lexical-based rich text to HTML:
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
### Generating HTML on-demand (Recommended)
To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend:
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
import React from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({ data })
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Converting Lexical Blocks
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### Outputting HTML from the Collection
To automatically generate HTML from the saved richText field in your Collection, use the `lexicalHTMLField()` helper. This approach converts the JSON to HTML using an `afterRead` hook. For instance:
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
import type { MyTextBlock } from '@/payload-types.js'
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
type DefaultNodeTypes,
lexicalEditor,
lexicalHTMLField,
type SerializedBlockNode,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
lexicalHTMLField({
htmlFieldName: 'nameOfYourRichTextField_html',
lexicalFieldName: 'nameOfYourRichTextField',
}),
{
name: 'customRichText',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [
{
interfaceName: 'MyTextBlock',
slug: 'myTextBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
}),
],
}),
},
lexicalHTMLField({
htmlFieldName: 'customRichText_html',
lexicalFieldName: 'customRichText',
// can pass in additional converters or override default ones
converters: (({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
})) as HTMLConvertersFunction<
DefaultNodeTypes | SerializedBlockNode<MyTextBlock>
>,
}),
],
}
```
### Generating HTML in Your Frontend with Dynamic Population (Advanced)
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
```tsx
'use client'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import React from 'react'
import config from '../../config.js'
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
const html = await convertLexicalToHTMLAsync({
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Converting HTML to Richtext
If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/rich-text/converters#retrieving-the-editor-config):
```ts
import {
convertHTMLToLexical,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
// Make sure you have jsdom and @types/jsdom installed
import { JSDOM } from 'jsdom'
const html = convertHTMLToLexical({
editorConfig: await editorConfigFactory.default({
config, // Your Payload Config
}),
html: '<p>text</p>',
JSDOM, // Pass in the JSDOM import; it's not bundled to keep package size small
})
```

View File

@@ -0,0 +1,190 @@
---
title: Converting JSX
label: Converting JSX
order: 21
desc: Converting between lexical richtext and JSX
keywords: lexical, richtext, jsx
---
## Converting Richtext to JSX
To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it:
```tsx
import React from 'react'
import { RichText } from '@payloadcms/richtext-lexical/react'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
return <RichText data={data} />
}
```
The `RichText` component includes built-in converters for common Lexical nodes. You can add or override converters via the `converters` prop for custom blocks, custom nodes, or any modifications you need. See the [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) for a working example.
<Banner type="default">
When fetching data, ensure your `depth` setting is high enough to fully
populate Lexical nodes such as uploads. The JSX converter requires fully
populated data to work correctly.
</Banner>
### Converting Internal Links
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document.
```tsx
import type {
DefaultNodeTypes,
SerializedLinkNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
type JSXConvertersFunction,
LinkJSXConverter,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
const { relationTo, value } = linkNode.fields.doc!
if (typeof value !== 'object') {
throw new Error('Expected value to be an object')
}
const slug = value.slug
switch (relationTo) {
case 'posts':
return `/posts/${slug}`
case 'categories':
return `/category/${slug}`
case 'pages':
return `/${slug}`
default:
return `/${relationTo}/${slug}`
}
}
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
...LinkJSXConverter({ internalDocToHref }),
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Converting Lexical Blocks
If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
For example:
```tsx
'use client'
import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
type JSXConvertersFunction,
RichText,
} from '@payloadcms/richtext-lexical/react'
import React from 'react'
// Extend the default node types with your custom blocks for full type safety
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyNumberBlock | MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myNumberBlock: ({ node }) => <div>{node.fields.number}</div>,
myTextBlock: ({ node }) => (
<div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>
),
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```
### Overriding Default JSX Converters
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
Example - overriding the upload node converter to use next/image:
```tsx
'use client'
import type {
DefaultNodeTypes,
SerializedUploadNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
type JSXConvertersFunction,
RichText,
} from '@payloadcms/richtext-lexical/react'
import Image from 'next/image'
import React from 'react'
type NodeTypes = DefaultNodeTypes
// Custom upload converter component that uses next/image
const CustomUploadComponent: React.FC<{
node: SerializedUploadNode
}> = ({ node }) => {
if (node.relationTo === 'uploads') {
const uploadDoc = node.value
if (typeof uploadDoc !== 'object') {
return null
}
const { alt, height, url, width } = uploadDoc
return <Image alt={alt} height={height} src={url} width={width} />
}
return null
}
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
// Override the default upload converter
upload: ({ node }) => {
return <CustomUploadComponent node={node} />
},
})
export const MyComponent: React.FC<{
lexicalData: SerializedEditorState
}> = ({ lexicalData }) => {
return <RichText converters={jsxConverters} data={lexicalData} />
}
```

View File

@@ -0,0 +1,265 @@
---
title: Converting Markdown
label: Converting Markdown
order: 23
desc: Converting between lexical richtext and Markdown / MDX
keywords: lexical, richtext, markdown, md, mdx
---
## Converting Richtext to Markdown
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following:
```ts
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToMarkdown,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
// Your richtext data here
const data: SerializedEditorState = {}
const html = convertLexicalToMarkdown({
data,
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),
})
```
### Example - outputting Markdown from the Collection
```ts
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import type { CollectionConfig, RichTextField } from 'payload'
import {
convertLexicalToMarkdown,
editorConfigFactory,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
{
name: 'markdown',
type: 'textarea',
admin: {
hidden: true,
},
hooks: {
afterRead: [
({ siblingData, siblingFields }) => {
const data: SerializedEditorState =
siblingData['nameOfYourRichTextField']
if (!data) {
return ''
}
const markdown = convertLexicalToMarkdown({
data,
editorConfig: editorConfigFactory.fromField({
field: siblingFields.find(
(field) =>
'name' in field && field.name === 'nameOfYourRichTextField',
) as RichTextField,
}),
})
return markdown
},
],
beforeChange: [
({ siblingData }) => {
// Ensure that the markdown field is not saved in the database
delete siblingData['markdown']
return null
},
],
},
},
],
}
```
## Converting Markdown to Richtext
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following:
```ts
import {
convertMarkdownToLexical,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
const html = convertMarkdownToLexical({
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),
markdown: '# Hello world\n\nThis is a **test**.',
})
```
## Converting MDX
Payload supports serializing and deserializing MDX content. While Markdown converters are stored on the features, MDX converters are stored on the blocks that you pass to the `BlocksFeature`.
### Defining a Custom Block
Here is an example of a `Banner` block.
This block:
- Renders in the admin UI as a normal Lexical block with specific fields (e.g. type, content).
- Converts to an MDX `Banner` component.
- Can parse that MDX `Banner` back into a Lexical state.
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/mdx-example-light.png"
srcDark="https://payloadcms.com/images/docs/mdx-example-dark.png"
alt="Shows the Banner field in a lexical editor and the MDX output"
caption="Banner field in a lexical editor and the MDX output"
/>
```ts
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import type { Block, CollectionConfig, RichTextField } from 'payload'
import {
BlocksFeature,
convertLexicalToMarkdown,
editorConfigFactory,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
const BannerBlock: Block = {
slug: 'Banner',
fields: [
{
name: 'type',
type: 'select',
defaultValue: 'info',
options: [
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
{ label: 'Error', value: 'error' },
],
},
{
name: 'content',
type: 'richText',
editor: lexicalEditor(),
},
],
jsx: {
/**
* Convert from Lexical -> MDX:
* <Banner type="..." >child content</Banner>
*/
export: ({ fields, lexicalToMarkdown }) => {
const props: any = {}
if (fields.type) {
props.type = fields.type
}
return {
children: lexicalToMarkdown({ editorState: fields.content }),
props,
}
},
/**
* Convert from MDX -> Lexical:
*/
import: ({ children, markdownToLexical, props }) => {
return {
type: props?.type,
content: markdownToLexical({ markdown: children }),
}
},
},
}
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [BannerBlock],
}),
],
}),
},
{
name: 'markdown',
type: 'textarea',
hooks: {
afterRead: [
({ siblingData, siblingFields }) => {
const data: SerializedEditorState =
siblingData['nameOfYourRichTextField']
if (!data) {
return ''
}
const markdown = convertLexicalToMarkdown({
data,
editorConfig: editorConfigFactory.fromField({
field: siblingFields.find(
(field) =>
'name' in field && field.name === 'nameOfYourRichTextField',
) as RichTextField,
}),
})
return markdown
},
],
beforeChange: [
({ siblingData }) => {
// Ensure that the markdown field is not saved in the database
delete siblingData['markdown']
return null
},
],
},
},
],
}
```
The conversion is done using the `jsx` property of the block. The `export` function is called when converting from lexical to MDX, and the `import` function is called when converting from MDX to lexical.
### Export
The `export` function takes the block field data and the `lexicalToMarkdown` function as arguments. It returns the following object:
| Property | Type | Description |
| ---------- | ------ | ------------------------------------------------------------------ |
| `children` | string | This will be in between the opening and closing tags of the block. |
| `props` | object | This will be in the opening tag of the block. |
### Import
The `import` function provides data extracted from the MDX. It takes the following arguments:
| Argument | Type | Description |
| ---------- | ------ | ------------------------------------------------------------------------------------ |
| `children` | string | This will be the text between the opening and closing tags of the block. |
| `props` | object | These are the props passed to the block, parsed from the opening tag into an object. |
The returning object is equal to the block field data.

View File

@@ -0,0 +1,70 @@
---
title: Converting Plaintext
label: Converting Plaintext
order: 24
desc: Converting between lexical richtext and plaintext
keywords: lexical, richtext, plaintext, text
---
## Converting Richtext to Plaintext
Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`.
```ts
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext'
// Your richtext data here
const data: SerializedEditorState = {}
const plaintext = convertLexicalToPlaintext({ data })
```
### Custom Converters
The `convertLexicalToPlaintext` functions accepts a `converters` object that allows you to customize how specific nodes are converted to plaintext.
```ts
import type {
DefaultNodeTypes,
SerializedBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import type { MyTextBlock } from '@/payload-types'
import {
convertLexicalToPlaintext,
type PlaintextConverters,
} from '@payloadcms/richtext-lexical/plaintext'
// Your richtext data here
const data: SerializedEditorState = {}
const converters: PlaintextConverters<
DefaultNodeTypes | SerializedBlockNode<MyTextBlock>
> = {
blocks: {
textBlock: ({ node }) => {
return node.fields.text ?? ''
},
},
link: ({ node }) => {
return node.fields.url ?? ''
},
}
const plaintext = convertLexicalToPlaintext({
converters,
data,
})
```
Unlike other converters, there are no default converters for plaintext.
If a node does not have a converter defined, the following heuristics are used to convert it to plaintext:
- If the node has a `text` field, it will be used as the plaintext.
- If the node has a `children` field, the children will be recursively converted to plaintext.
- If the node has neither, it will be ignored.
- Paragraph, text and tab nodes insert newline / tab characters.

View File

@@ -1349,7 +1349,7 @@ export type BlockJSX = {
*/
export: (props: {
fields: BlockFields
lexicalToMarkdown?: (props: { editorState: Record<string, any> }) => string
lexicalToMarkdown: (props: { editorState: Record<string, any> }) => string
}) =>
| {
children?: string
@@ -1367,7 +1367,7 @@ export type BlockJSX = {
children: string
closeMatch: null | RegExpMatchArray // Only available when customEndRegex is set
htmlToLexical?: ((props: { html: string }) => any) | null
markdownToLexical?: (props: { markdown: string }) => Record<string, any>
markdownToLexical: (props: { markdown: string }) => Record<string, any>
openMatch?: RegExpMatchArray
props: Record<string, any>
}) => BlockFields | false

View File

@@ -45,6 +45,11 @@
"types": "./src/exports/html-async/index.ts",
"default": "./src/exports/html-async/index.ts"
},
"./plaintext": {
"import": "./src/exports/plaintext/index.ts",
"types": "./src/exports/plaintext/index.ts",
"default": "./src/exports/plaintext/index.ts"
},
"./rsc": {
"import": "./src/exports/server/rsc.ts",
"types": "./src/exports/server/rsc.ts",
@@ -437,6 +442,11 @@
"types": "./dist/exports/html-async/index.d.ts",
"default": "./dist/exports/html-async/index.js"
},
"./plaintext": {
"import": "./dist/exports/plaintext/index.js",
"types": "./dist/exports/plaintext/index.d.ts",
"default": "./dist/exports/plaintext/index.js"
},
"./rsc": {
"import": "./dist/exports/server/rsc.js",
"types": "./dist/exports/server/rsc.d.ts",

View File

@@ -1,25 +1,25 @@
export { BlockquoteHTMLConverterAsync } from '../../features/converters/html/async/converters/blockquote.js'
export { HeadingHTMLConverterAsync } from '../../features/converters/html/async/converters/heading.js'
export { HorizontalRuleHTMLConverterAsync } from '../../features/converters/html/async/converters/horizontalRule.js'
export { LinebreakHTMLConverterAsync } from '../../features/converters/html/async/converters/linebreak.js'
export { LinkHTMLConverterAsync } from '../../features/converters/html/async/converters/link.js'
export { ListHTMLConverterAsync } from '../../features/converters/html/async/converters/list.js'
export { ParagraphHTMLConverterAsync } from '../../features/converters/html/async/converters/paragraph.js'
export { TabHTMLConverterAsync } from '../../features/converters/html/async/converters/tab.js'
export { TableHTMLConverterAsync } from '../../features/converters/html/async/converters/table.js'
export { TextHTMLConverterAsync } from '../../features/converters/html/async/converters/text.js'
export { BlockquoteHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/blockquote.js'
export { HeadingHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/heading.js'
export { HorizontalRuleHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/horizontalRule.js'
export { LinebreakHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/linebreak.js'
export { LinkHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/link.js'
export { ListHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/list.js'
export { ParagraphHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/paragraph.js'
export { TabHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/tab.js'
export { TableHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/table.js'
export { TextHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/text.js'
export { UploadHTMLConverterAsync } from '../../features/converters/html/async/converters/upload.js'
export { UploadHTMLConverterAsync } from '../../features/converters/lexicalToHtml/async/converters/upload.js'
export { defaultHTMLConvertersAsync } from '../../features/converters/html/async/defaultConverters.js'
export { convertLexicalToHTMLAsync } from '../../features/converters/html/async/index.js'
export { defaultHTMLConvertersAsync } from '../../features/converters/lexicalToHtml/async/defaultConverters.js'
export { convertLexicalToHTMLAsync } from '../../features/converters/lexicalToHtml/async/index.js'
export type {
HTMLConverterAsync,
HTMLConvertersAsync,
HTMLConvertersFunctionAsync,
} from '../../features/converters/html/async/types.js'
} from '../../features/converters/lexicalToHtml/async/types.js'
export type {
ProvidedCSS,
SerializedLexicalNodeWithParent,
} from '../../features/converters/html/shared/types.js'
} from '../../features/converters/lexicalToHtml/shared/types.js'

View File

@@ -1,25 +1,25 @@
export type {
ProvidedCSS,
SerializedLexicalNodeWithParent,
} from '../../features/converters/html/shared/types.js'
export { BlockquoteHTMLConverter } from '../../features/converters/html/sync/converters/blockquote.js'
export { HeadingHTMLConverter } from '../../features/converters/html/sync/converters/heading.js'
export { HorizontalRuleHTMLConverter } from '../../features/converters/html/sync/converters/horizontalRule.js'
export { LinebreakHTMLConverter } from '../../features/converters/html/sync/converters/linebreak.js'
export { LinkHTMLConverter } from '../../features/converters/html/sync/converters/link.js'
export { ListHTMLConverter } from '../../features/converters/html/sync/converters/list.js'
export { ParagraphHTMLConverter } from '../../features/converters/html/sync/converters/paragraph.js'
export { TabHTMLConverter } from '../../features/converters/html/sync/converters/tab.js'
export { TableHTMLConverter } from '../../features/converters/html/sync/converters/table.js'
} from '../../features/converters/lexicalToHtml/shared/types.js'
export { BlockquoteHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/blockquote.js'
export { HeadingHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/heading.js'
export { HorizontalRuleHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/horizontalRule.js'
export { LinebreakHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/linebreak.js'
export { LinkHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/link.js'
export { ListHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/list.js'
export { ParagraphHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/paragraph.js'
export { TabHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/tab.js'
export { TableHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/table.js'
export { TextHTMLConverter } from '../../features/converters/html/sync/converters/text.js'
export { TextHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/text.js'
export { UploadHTMLConverter } from '../../features/converters/html/sync/converters/upload.js'
export { defaultHTMLConverters } from '../../features/converters/html/sync/defaultConverters.js'
export { convertLexicalToHTML } from '../../features/converters/html/sync/index.js'
export { UploadHTMLConverter } from '../../features/converters/lexicalToHtml/sync/converters/upload.js'
export { defaultHTMLConverters } from '../../features/converters/lexicalToHtml/sync/defaultConverters.js'
export { convertLexicalToHTML } from '../../features/converters/lexicalToHtml/sync/index.js'
export type {
HTMLConverter,
HTMLConverters,
HTMLConvertersFunction,
} from '../../features/converters/html/sync/types.js'
} from '../../features/converters/lexicalToHtml/sync/types.js'

View File

@@ -0,0 +1,6 @@
export { convertLexicalToPlaintext } from '../../features/converters/lexicalToPlaintext/sync/index.js'
export type {
PlaintextConverter,
PlaintextConverters,
} from '../../features/converters/lexicalToPlaintext/sync/types.js'

View File

@@ -1,24 +1,24 @@
export {
type JSXConvertersFunction,
RichText,
} from '../../features/converters/jsx/Component/index.js'
export { BlockquoteJSXConverter } from '../../features/converters/jsx/converter/converters/blockquote.js'
export { HeadingJSXConverter } from '../../features/converters/jsx/converter/converters/heading.js'
export { HorizontalRuleJSXConverter } from '../../features/converters/jsx/converter/converters/horizontalRule.js'
export { LinebreakJSXConverter } from '../../features/converters/jsx/converter/converters/linebreak.js'
export { LinkJSXConverter } from '../../features/converters/jsx/converter/converters/link.js'
export { ListJSXConverter } from '../../features/converters/jsx/converter/converters/list.js'
export { ParagraphJSXConverter } from '../../features/converters/jsx/converter/converters/paragraph.js'
export { TabJSXConverter } from '../../features/converters/jsx/converter/converters/tab.js'
export { TableJSXConverter } from '../../features/converters/jsx/converter/converters/table.js'
} from '../../features/converters/lexicalToJSX/Component/index.js'
export { BlockquoteJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/blockquote.js'
export { HeadingJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/heading.js'
export { HorizontalRuleJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/horizontalRule.js'
export { LinebreakJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/linebreak.js'
export { LinkJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/link.js'
export { ListJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/list.js'
export { ParagraphJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/paragraph.js'
export { TabJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/tab.js'
export { TableJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/table.js'
export { TextJSXConverter } from '../../features/converters/jsx/converter/converters/text.js'
export { TextJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/text.js'
export { UploadJSXConverter } from '../../features/converters/jsx/converter/converters/upload.js'
export { defaultJSXConverters } from '../../features/converters/jsx/converter/defaultConverters.js'
export { convertLexicalNodesToJSX } from '../../features/converters/jsx/converter/index.js'
export { UploadJSXConverter } from '../../features/converters/lexicalToJSX/converter/converters/upload.js'
export { defaultJSXConverters } from '../../features/converters/lexicalToJSX/converter/defaultConverters.js'
export { convertLexicalNodesToJSX } from '../../features/converters/lexicalToJSX/converter/index.js'
export type {
JSXConverter,
JSXConverters,
SerializedLexicalNodeWithParent,
} from '../../features/converters/jsx/converter/types.js'
} from '../../features/converters/lexicalToJSX/converter/types.js'

View File

@@ -4,7 +4,7 @@ import type { Spread } from 'lexical'
import { QuoteNode } from '@lexical/rich-text'
import { createServerFeature } from '../../../utilities/createServerFeature.js'
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
import { convertLexicalNodesToHTML } from '../../converters/lexicalToHtml_deprecated/converter/index.js'
import { createNode } from '../../typeUtilities.js'
import { MarkdownTransformer } from '../markdownTransformer.js'
import { i18n } from './i18n.js'

View File

@@ -155,7 +155,12 @@ export function linesFromStartToContentAndPropsString({
}
if (lineIndex === linesCopy.length - 1 && !isEndOptional && !isSelfClosing) {
throw new Error('End match not found for lines ' + lines.join('\n'))
throw new Error(
'End match not found for lines ' +
lines.join('\n') +
'\n\n. Start match: ' +
JSON.stringify(startMatch),
)
}
}

View File

@@ -4,7 +4,6 @@ import { $getRoot, $getSelection, type SerializedLexicalNode } from 'lexical'
import type { SanitizedServerEditorConfig } from '../../../lexical/config/types.js'
import type { DefaultNodeTypes, TypedEditorState } from '../../../nodeTypes.js'
import {} from '../../../lexical/config/server/sanitize.js'
import { getEnabledNodes } from '../../../lexical/nodes/index.js'
import { $generateNodesFromDOM } from '../../../lexical-proxy/@lexical-html.js'

View File

@@ -19,11 +19,9 @@ export const LinkHTMLConverterAsync: (args: {
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
return `(
<a${providedStyleTag} href="${node.fields.url}" rel=${rel} target=${target}>
return `<a${providedStyleTag} href="${node.fields.url}" rel=${rel} target=${target}>
${children}
</a>
)`
</a>`
},
link: async ({ node, nodesToHTML, populate, providedStyleTag }) => {
const children = (
@@ -47,10 +45,8 @@ export const LinkHTMLConverterAsync: (args: {
}
}
return `(
<a${providedStyleTag} href="${href}" rel=${rel} target=${target}>
return `<a${providedStyleTag} href="${href}" rel=${rel} target=${target}>
${children}
</a>
)`
</a>`
},
})

View File

@@ -1,7 +1,7 @@
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.jsx'
import type { HTMLConvertersAsync } from '../types.js'
export const UploadHTMLConverterAsync: HTMLConvertersAsync<SerializedUploadNode> = {

View File

@@ -37,7 +37,7 @@ type Args = {
*/
export const lexicalHTMLField: (args: Args) => Field = (args) => {
const { converters, hidden = true, htmlFieldName, lexicalFieldName, storeInDB = false } = args
return {
const field: Field = {
name: htmlFieldName,
type: 'code',
admin: {
@@ -79,15 +79,18 @@ export const lexicalHTMLField: (args: Args) => Field = (args) => {
})
},
],
beforeChange: [
({ siblingData, value }) => {
if (storeInDB) {
return value
}
delete siblingData[htmlFieldName]
return null
},
],
},
}
if (!storeInDB) {
field.hooks = field.hooks ?? {}
field.hooks.beforeChange = [
({ siblingData }) => {
delete siblingData[htmlFieldName]
return null
},
]
}
return field
}

View File

@@ -12,11 +12,9 @@ export const LinkHTMLConverter: (args: {
const rel: string | undefined = node.fields.newTab ? 'noopener noreferrer' : undefined
const target: string | undefined = node.fields.newTab ? '_blank' : undefined
return `(
<a${providedStyleTag} href="${node.fields.url}" rel=${rel} target=${target}>
return `<a${providedStyleTag} href="${node.fields.url}" rel=${rel} target=${target}>
${children}
</a>
)`
</a>`
},
link: ({ node, nodesToHTML, providedStyleTag }) => {
const children = nodesToHTML({
@@ -38,10 +36,8 @@ export const LinkHTMLConverter: (args: {
}
}
return `(
<a${providedStyleTag} href="${href}" rel=${rel} target=${target}>
return `<a${providedStyleTag} href="${href}" rel=${rel} target=${target}>
${children}
</a>
)`
</a>`
},
})

View File

@@ -1,7 +1,7 @@
import type { FileData, FileSizeImproved, TypeWithID } from 'payload'
import type { SerializedUploadNode } from '../../../../../nodeTypes.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.js'
import type { UploadDataImproved } from '../../../../upload/server/nodes/UploadNode.jsx'
import type { HTMLConverters } from '../types.js'
export const UploadHTMLConverter: HTMLConverters<SerializedUploadNode> = {

View File

@@ -0,0 +1,36 @@
import type { SerializedEditorState } from 'lexical'
import { createHeadlessEditor } from '@lexical/headless'
import type { SanitizedServerEditorConfig } from '../../../lexical/config/types.js'
import { getEnabledNodes } from '../../../lexical/nodes/index.js'
import { $convertToMarkdownString } from '../../../packages/@lexical/markdown/index.js'
export const convertLexicalToMarkdown = ({
data,
editorConfig,
}: {
data: SerializedEditorState
editorConfig: SanitizedServerEditorConfig
}): string => {
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig,
}),
})
headlessEditor.update(
() => {
headlessEditor.setEditorState(headlessEditor.parseEditorState(data))
},
{ discrete: true },
)
let markdown: string = ''
headlessEditor.getEditorState().read(() => {
markdown = $convertToMarkdownString(editorConfig?.features?.markdownTransformers)
})
return markdown
}

View File

@@ -0,0 +1,114 @@
import type {
DefaultNodeTypes,
DefaultTypedEditorState,
SerializedTabNode,
SerializedParagraphNode,
SerializedTextNode,
SerializedLineBreakNode,
} from '../../../nodeTypes.js'
import { convertLexicalToPlaintext } from './sync/index.js'
function textNode(text: string, bold?: boolean): SerializedTextNode {
return {
type: 'text',
detail: 0,
format: bold ? 1 : 0,
mode: 'normal',
style: '',
text,
version: 1,
}
}
function linebreakNode(): SerializedLineBreakNode {
return {
type: 'linebreak',
version: 1,
}
}
function tabNode(): SerializedTabNode {
return {
type: 'tab',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: '',
version: 1,
}
}
function paragraphNode(children: DefaultNodeTypes[]): SerializedParagraphNode {
return {
type: 'paragraph',
children,
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
version: 1,
}
}
function rootNode(nodes: DefaultNodeTypes[]): DefaultTypedEditorState {
return {
root: {
type: 'root',
children: nodes,
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
}
}
describe('convertLexicalToPlaintext', () => {
it('ensure paragraph with text is correctly converted', () => {
const data: DefaultTypedEditorState = rootNode([paragraphNode([textNode('Basic Text')])])
const plaintext = convertLexicalToPlaintext({
data,
})
console.log('plaintext', plaintext)
expect(plaintext).toBe('Basic Text')
})
it('ensure paragraph with multiple text nodes is correctly converted', () => {
const data: DefaultTypedEditorState = rootNode([
paragraphNode([textNode('Basic Text'), textNode(' Bold', true), textNode(' Text')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic Text Bold Text')
})
it('ensure linebreaks are converted correctly', () => {
const data: DefaultTypedEditorState = rootNode([
paragraphNode([textNode('Basic Text'), linebreakNode(), textNode('Next Line')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic Text\nNext Line')
})
it('ensure tabs are converted correctly', () => {
const data: DefaultTypedEditorState = rootNode([
paragraphNode([textNode('Basic Text'), tabNode(), textNode('Next Line')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic Text\tNext Line')
})
})

View File

@@ -0,0 +1,31 @@
import type { SerializedLexicalNode } from 'lexical'
import type { SerializedBlockNode, SerializedInlineBlockNode } from '../../../../nodeTypes.js'
import type { PlaintextConverter, PlaintextConverters } from '../sync/types.js'
export function findConverterForNode<
TConverters extends PlaintextConverters,
TConverter extends PlaintextConverter<any>,
>({
converters,
node,
}: {
converters: TConverters
node: SerializedLexicalNode
}): TConverter | undefined {
let converterForNode: TConverter | undefined
if (node.type === 'block') {
converterForNode = converters?.blocks?.[
(node as SerializedBlockNode)?.fields?.blockType
] as TConverter
} else if (node.type === 'inlineBlock') {
converterForNode = converters?.inlineBlocks?.[
(node as SerializedInlineBlockNode)?.fields?.blockType
] as TConverter
} else {
converterForNode = converters[node.type] as TConverter
}
return converterForNode
}

View File

@@ -0,0 +1,4 @@
import type { SerializedLexicalNode } from 'lexical'
export type SerializedLexicalNodeWithParent = {
parent?: SerializedLexicalNode
} & SerializedLexicalNode

View File

@@ -0,0 +1,115 @@
/* eslint-disable no-console */
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical'
import type { SerializedLexicalNodeWithParent } from '../shared/types.js'
import type { PlaintextConverters } from './types.js'
import { hasText } from '../../../../validate/hasText.js'
import { findConverterForNode } from '../shared/findConverterForNode.js'
export type ConvertLexicalToPlaintextArgs = {
/**
* A map of node types to their corresponding plaintext converter functions.
* This is optional - if not provided, the following heuristic will be used:
*
* - If the node has a `text` property, it will be used as the plaintext.
* - If the node has a `children` property, the children will be recursively converted to plaintext.
* - If the node has neither, it will be ignored.
**/
converters?: PlaintextConverters
data: SerializedEditorState
}
export function convertLexicalToPlaintext({
converters,
data,
}: ConvertLexicalToPlaintextArgs): string {
if (hasText(data)) {
const plaintext = convertLexicalNodesToPlaintext({
converters: converters ?? {},
nodes: data?.root?.children,
parent: data?.root,
}).join('')
return plaintext
}
return ''
}
export function convertLexicalNodesToPlaintext({
converters,
nodes,
parent,
}: {
converters: PlaintextConverters
nodes: SerializedLexicalNode[]
parent: SerializedLexicalNodeWithParent
}): string[] {
const plainTextArray: string[] = []
let i = -1
for (const node of nodes) {
i++
const converter = findConverterForNode({
converters,
node,
})
if (converter) {
try {
const converted =
typeof converter === 'function'
? converter({
childIndex: i,
converters,
node,
nodesToPlaintext: (args) => {
return convertLexicalNodesToPlaintext({
converters: args.converters ?? converters,
nodes: args.nodes,
parent: args.parent ?? {
...node,
parent,
},
})
},
parent,
})
: converter
if (converted && typeof converted === 'string') {
plainTextArray.push(converted)
}
} catch (error) {
console.error('Error converting lexical node to plaintext:', error, 'node:', node)
}
} else {
// Default plaintext converter heuristic
if (node.type === 'paragraph') {
if (plainTextArray?.length) {
// Only add a new line if there is already text in the array
plainTextArray.push('\n\n')
}
} else if (node.type === 'linebreak') {
plainTextArray.push('\n')
} else if (node.type === 'tab') {
plainTextArray.push('\t')
} else if ('text' in node && node.text) {
plainTextArray.push(node.text as string)
}
if ('children' in node && node.children) {
plainTextArray.push(
...convertLexicalNodesToPlaintext({
converters,
nodes: node.children as SerializedLexicalNode[],
parent: node,
}),
)
}
}
}
return plainTextArray.filter(Boolean)
}

View File

@@ -0,0 +1,73 @@
import type { SerializedLexicalNode } from 'lexical'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '../../../../nodeTypes.js'
import type { SerializedLexicalNodeWithParent } from '../shared/types.js'
export type PlaintextConverter<
T extends { [key: string]: any; type?: string } = SerializedLexicalNode,
> =
| ((args: {
childIndex: number
converters: PlaintextConverters
node: T
nodesToPlaintext: (args: {
converters?: PlaintextConverters
nodes: SerializedLexicalNode[]
parent?: SerializedLexicalNodeWithParent
}) => string[]
parent: SerializedLexicalNodeWithParent
}) => string)
| string
export type DefaultPlaintextNodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<{ blockName?: null | string; blockType: string }> // need these to ensure types for blocks and inlineBlocks work if no generics are provided
| SerializedInlineBlockNode<{ blockName?: null | string; blockType: string }>
export type PlaintextConverters<
T extends { [key: string]: any; type?: string } = DefaultPlaintextNodeTypes,
> = {
[key: string]:
| {
[blockSlug: string]: PlaintextConverter<any>
}
| PlaintextConverter<any>
| undefined
} & {
[nodeType in Exclude<NonNullable<T['type']>, 'block' | 'inlineBlock'>]?: PlaintextConverter<
Extract<T, { type: nodeType }>
>
} & {
blocks?: {
[K in Extract<
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
? B extends { blockType: string }
? B['blockType']
: never
: never,
string
>]?: PlaintextConverter<
Extract<T, { type: 'block' }> extends SerializedBlockNode<infer B>
? SerializedBlockNode<Extract<B, { blockType: K }>>
: SerializedBlockNode
>
}
inlineBlocks?: {
[K in Extract<
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
? B extends { blockType: string }
? B['blockType']
: never
: never,
string
>]?: PlaintextConverter<
Extract<T, { type: 'inlineBlock' }> extends SerializedInlineBlockNode<infer B>
? SerializedInlineBlockNode<Extract<B, { blockType: K }>>
: SerializedInlineBlockNode
>
}
}

View File

@@ -0,0 +1,36 @@
import type { SerializedLexicalNode } from 'lexical'
import { createHeadlessEditor } from '@lexical/headless'
import type { SanitizedServerEditorConfig } from '../../../lexical/config/types.js'
import type { DefaultNodeTypes, TypedEditorState } from '../../../nodeTypes.js'
import { getEnabledNodes } from '../../../lexical/nodes/index.js'
import { $convertFromMarkdownString } from '../../../packages/@lexical/markdown/index.js'
export const convertMarkdownToLexical = <
TNodeTypes extends SerializedLexicalNode = DefaultNodeTypes,
>({
editorConfig,
markdown,
}: {
editorConfig: SanitizedServerEditorConfig
markdown: string
}): TypedEditorState<TNodeTypes> => {
const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig,
}),
})
headlessEditor.update(
() => {
$convertFromMarkdownString(markdown, editorConfig.features.markdownTransformers)
},
{ discrete: true },
)
const editorJSON = headlessEditor.getEditorState().toJSON()
return editorJSON as TypedEditorState<TNodeTypes>
}

View File

@@ -1,6 +1,6 @@
import { createLocalReq, type Payload, type PayloadRequest, type TypedLocale } from 'payload'
import type { HTMLPopulateFn } from '../html/async/types.js'
import type { HTMLPopulateFn } from '../lexicalToHtml/async/types.js'
import { populate } from '../../../populateGraphQL/populate.js'

View File

@@ -1,6 +1,6 @@
import { stringify } from 'qs-esm'
import type { HTMLPopulateFn } from '../html/async/types.js'
import type { HTMLPopulateFn } from '../lexicalToHtml/async/types.js'
export const getRestPopulateFn: (args: {
/**

View File

@@ -10,7 +10,7 @@ import { TableCellNode, TableNode, TableRowNode } from '@lexical/table'
import { sanitizeFields } from 'payload'
import { createServerFeature } from '../../../utilities/createServerFeature.js'
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
import { convertLexicalNodesToHTML } from '../../converters/lexicalToHtml_deprecated/converter/index.js'
import { createNode } from '../../typeUtilities.js'
import { TableMarkdownTransformer } from '../markdownTransformer.js'

View File

@@ -7,7 +7,7 @@ import type { Spread } from 'lexical'
import { HeadingNode } from '@lexical/rich-text'
import { createServerFeature } from '../../../utilities/createServerFeature.js'
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
import { convertLexicalNodesToHTML } from '../../converters/lexicalToHtml_deprecated/converter/index.js'
import { createNode } from '../../typeUtilities.js'
import { MarkdownTransformer } from '../markdownTransformer.js'
import { i18n } from './i18n.js'

View File

@@ -14,7 +14,7 @@ import type { NodeWithHooks } from '../../typesServer.js'
import type { ClientProps } from '../client/index.js'
import { createServerFeature } from '../../../utilities/createServerFeature.js'
import { convertLexicalNodesToHTML } from '../../converters/html_deprecated/converter/index.js'
import { convertLexicalNodesToHTML } from '../../converters/lexicalToHtml_deprecated/converter/index.js'
import { createNode } from '../../typeUtilities.js'
import { LinkMarkdownTransformer } from '../markdownTransformer.js'
import { AutoLinkNode } from '../nodes/AutoLinkNode.js'

View File

@@ -1,10 +1,10 @@
import { ListItemNode, ListNode } from '@lexical/list'
import { v4 as uuidv4 } from 'uuid'
import type { HTMLConverter } from '../converters/html_deprecated/converter/types.js'
import type { HTMLConverter } from '../converters/lexicalToHtml_deprecated/converter/types.js'
import type { SerializedListItemNode, SerializedListNode } from './plugin/index.js'
import { convertLexicalNodesToHTML } from '../converters/html_deprecated/converter/index.js'
import { convertLexicalNodesToHTML } from '../converters/lexicalToHtml_deprecated/converter/index.js'
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
converter: async ({

View File

@@ -27,7 +27,7 @@ import type {
import type { ServerEditorConfig } from '../lexical/config/types.js'
import type { Transformer } from '../packages/@lexical/markdown/index.js'
import type { AdapterProps } from '../types.js'
import type { HTMLConverter } from './converters/html_deprecated/converter/types.js'
import type { HTMLConverter } from './converters/lexicalToHtml_deprecated/converter/types.js'
import type { BaseClientFeatureProps } from './typesClient.js'
export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexicalNode> = (args: {

View File

@@ -870,30 +870,33 @@ export {
ServerBlockNode,
} from './features/blocks/server/nodes/BlocksNode.js'
export { lexicalHTMLField } from './features/converters/html/async/field/index.js'
export { convertHTMLToLexical } from './features/converters/htmlToLexical/index.js'
export { LinebreakHTMLConverter } from './features/converters/html_deprecated/converter/converters/linebreak.js'
export { ParagraphHTMLConverter } from './features/converters/html_deprecated/converter/converters/paragraph.js'
export { lexicalHTMLField } from './features/converters/lexicalToHtml/async/field/index.js'
export { LinebreakHTMLConverter } from './features/converters/lexicalToHtml_deprecated/converter/converters/linebreak.js'
export { TabHTMLConverter } from './features/converters/html_deprecated/converter/converters/tab.js'
export { ParagraphHTMLConverter } from './features/converters/lexicalToHtml_deprecated/converter/converters/paragraph.js'
export { TabHTMLConverter } from './features/converters/lexicalToHtml_deprecated/converter/converters/tab.js'
export { TextHTMLConverter } from './features/converters/lexicalToHtml_deprecated/converter/converters/text.js'
export { defaultHTMLConverters } from './features/converters/lexicalToHtml_deprecated/converter/defaultConverters.js'
export { TextHTMLConverter } from './features/converters/html_deprecated/converter/converters/text.js'
export { defaultHTMLConverters } from './features/converters/html_deprecated/converter/defaultConverters.js'
export {
convertLexicalNodesToHTML,
convertLexicalToHTML,
} from './features/converters/html_deprecated/converter/index.js'
export type { HTMLConverter } from './features/converters/html_deprecated/converter/types.js'
} from './features/converters/lexicalToHtml_deprecated/converter/index.js'
export type { HTMLConverter } from './features/converters/lexicalToHtml_deprecated/converter/types.js'
export {
consolidateHTMLConverters,
lexicalHTML,
} from './features/converters/html_deprecated/field/index.js'
} from './features/converters/lexicalToHtml_deprecated/field/index.js'
export {
HTMLConverterFeature,
type HTMLConverterFeatureProps,
} from './features/converters/html_deprecated/index.js'
export { convertHTMLToLexical } from './features/converters/htmlToLexical/index.js'
} from './features/converters/lexicalToHtml_deprecated/index.js'
export { convertLexicalToMarkdown } from './features/converters/lexicalToMarkdown/index.js'
export { convertMarkdownToLexical } from './features/converters/markdownToLexical/index.js'
export { getPayloadPopulateFn } from './features/converters/utilities/payloadPopulateFn.js'
export { getRestPopulateFn } from './features/converters/utilities/restPopulateFn.js'
export { TestRecorderFeature } from './features/debug/testRecorder/server/index.js'

View File

@@ -93,6 +93,7 @@ export type DefaultNodeTypes =
| SerializedParagraphNode
| SerializedQuoteNode
| SerializedRelationshipNode
| SerializedTabNode
| SerializedTextNode
| SerializedUploadNode