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:
@@ -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()
|
||||
}) || ''
|
||||
```
|
||||
|
||||
243
docs/rich-text/converting-html.mdx
Normal file
243
docs/rich-text/converting-html.mdx
Normal 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
|
||||
})
|
||||
```
|
||||
190
docs/rich-text/converting-jsx.mdx
Normal file
190
docs/rich-text/converting-jsx.mdx
Normal 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} />
|
||||
}
|
||||
```
|
||||
265
docs/rich-text/converting-markdown.mdx
Normal file
265
docs/rich-text/converting-markdown.mdx
Normal 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.
|
||||
70
docs/rich-text/converting-plaintext.mdx
Normal file
70
docs/rich-text/converting-plaintext.mdx
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
6
packages/richtext-lexical/src/exports/plaintext/index.ts
Normal file
6
packages/richtext-lexical/src/exports/plaintext/index.ts
Normal 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'
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>`
|
||||
},
|
||||
})
|
||||
@@ -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> = {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>`
|
||||
},
|
||||
})
|
||||
@@ -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> = {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
export type SerializedLexicalNodeWithParent = {
|
||||
parent?: SerializedLexicalNode
|
||||
} & SerializedLexicalNode
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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: {
|
||||
/**
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -93,6 +93,7 @@ export type DefaultNodeTypes =
|
||||
| SerializedParagraphNode
|
||||
| SerializedQuoteNode
|
||||
| SerializedRelationshipNode
|
||||
| SerializedTabNode
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
|
||||
|
||||
Reference in New Issue
Block a user