415 lines
14 KiB
Plaintext
415 lines
14 KiB
Plaintext
---
|
|
title: Lexical Converters
|
|
label: Converters
|
|
order: 20
|
|
desc: Conversion between lexical, markdown and html
|
|
keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export
|
|
---
|
|
|
|
## Lexical => HTML
|
|
|
|
Lexical saves data in JSON, but can also generate its HTML representation via two main methods:
|
|
|
|
1. **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.
|
|
2. **Generating HTML on any server** Convert JSON to HTML on-demand on the server.
|
|
|
|
The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML.
|
|
|
|
### Outputting HTML from the Collection
|
|
|
|
To add HTML generation directly within the collection, follow the example below:
|
|
|
|
```ts
|
|
import type { CollectionConfig } from 'payload'
|
|
|
|
import { HTMLConverterFeature, lexicalEditor, lexicalHTML } from '@payloadcms/richtext-lexical'
|
|
|
|
const Pages: CollectionConfig = {
|
|
slug: 'pages',
|
|
fields: [
|
|
{
|
|
name: 'nameOfYourRichTextField',
|
|
type: 'richText',
|
|
editor: lexicalEditor({
|
|
features: ({ defaultFeatures }) => [
|
|
...defaultFeatures,
|
|
// The HTMLConverter Feature is the feature which manages the HTML serializers.
|
|
// If you do not pass any arguments to it, it will use the default serializers.
|
|
HTMLConverterFeature({}),
|
|
],
|
|
}),
|
|
},
|
|
lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }),
|
|
],
|
|
}
|
|
```
|
|
|
|
The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook.
|
|
|
|
### Generating HTML anywhere on the server
|
|
|
|
If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function:
|
|
|
|
```ts
|
|
import { consolidateHTMLConverters, convertLexicalToHTML } from '@payloadcms/richtext-lexical'
|
|
|
|
|
|
await convertLexicalToHTML({
|
|
converters: consolidateHTMLConverters({ editorConfig }),
|
|
data: editorData,
|
|
payload, // if you have Payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes)
|
|
req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in Payload if req is passed in.
|
|
})
|
|
```
|
|
This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML.
|
|
|
|
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
|
|
|
|
#### Example: Generating HTML within an afterRead hook
|
|
|
|
```ts
|
|
import type { FieldHook } from 'payload'
|
|
|
|
import {
|
|
HTMLConverterFeature,
|
|
consolidateHTMLConverters,
|
|
convertLexicalToHTML,
|
|
defaultEditorConfig,
|
|
defaultEditorFeatures,
|
|
sanitizeServerEditorConfig,
|
|
} from '@payloadcms/richtext-lexical'
|
|
|
|
const hook: FieldHook = async ({ req, siblingData }) => {
|
|
const editorConfig = defaultEditorConfig
|
|
|
|
editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})]
|
|
|
|
const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config)
|
|
|
|
const html = await convertLexicalToHTML({
|
|
converters: consolidateHTMLConverters({ editorConfig: sanitizedEditorConfig }),
|
|
data: siblingData.lexicalSimple,
|
|
req,
|
|
})
|
|
return html
|
|
}
|
|
```
|
|
|
|
### 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;
|
|
}
|
|
```
|
|
|
|
### Creating your own HTML Converter
|
|
|
|
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:
|
|
|
|
```ts
|
|
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
|
|
|
|
const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = {
|
|
converter: async ({ node, req }) => {
|
|
const uploadDocument: {
|
|
value?: any
|
|
} = {}
|
|
if(req) {
|
|
await populate({
|
|
id,
|
|
collectionSlug: node.relationTo,
|
|
currentDepth: 0,
|
|
data: uploadDocument,
|
|
depth: 1,
|
|
draft: false,
|
|
key: 'value',
|
|
overrideAccess: false,
|
|
req,
|
|
showHiddenFields: false,
|
|
})
|
|
}
|
|
|
|
const url = (req?.payload?.config?.serverURL || '') + uploadDocument?.value?.url
|
|
|
|
if (!(uploadDocument?.value?.mimeType as string)?.startsWith('image')) {
|
|
// Only images can be serialized as HTML
|
|
return ``
|
|
}
|
|
|
|
return `<img src="${url}" alt="${uploadDocument?.value?.filename}" width="${uploadDocument?.value?.width}" height="${uploadDocument?.value?.height}"/>`
|
|
},
|
|
nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static.
|
|
}
|
|
```
|
|
|
|
As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML.
|
|
|
|
The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node.
|
|
|
|
### Embedding the HTML Converter in your Feature
|
|
|
|
You can embed your HTML Converter directly within your custom `ServerFeature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example:
|
|
|
|
```ts
|
|
import { createNode } from '@payloadcms/richtext-lexical'
|
|
import type { FeatureProviderProviderServer } from '@payloadcms/richtext-lexical'
|
|
|
|
export const UploadFeature: FeatureProviderProviderServer<
|
|
UploadFeatureProps,
|
|
UploadFeaturePropsClient
|
|
> = (props) => {
|
|
/*...*/
|
|
return {
|
|
feature: () => {
|
|
return {
|
|
nodes: [
|
|
createNode({
|
|
converters: {
|
|
html: yourHTMLConverter, // <= This is where you define your HTML Converter
|
|
},
|
|
node: UploadNode,
|
|
//...
|
|
}),
|
|
],
|
|
ClientComponent: UploadFeatureClientComponent,
|
|
clientFeatureProps: clientProps,
|
|
serverFeatureProps: props,
|
|
/*...*/
|
|
}
|
|
},
|
|
key: 'upload',
|
|
serverFeatureProps: props,
|
|
}
|
|
}
|
|
```
|
|
|
|
## Headless Editor
|
|
|
|
Lexical provides a seamless way to perform conversions between various other formats:
|
|
|
|
- HTML to Lexical (or, importing HTML into the lexical editor)
|
|
- Markdown to Lexical (or, importing Markdown into the lexical editor)
|
|
- 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 '@lexical/headless' // <= make sure this package is installed
|
|
import { getEnabledNodes, sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
|
|
|
const yourEditorConfig // <= your editor config here
|
|
const payloadConfig // <= your Payload Config here
|
|
|
|
const headlessEditor = createHeadlessEditor({
|
|
nodes: getEnabledNodes({
|
|
editorConfig: sanitizeServerEditorConfig(yourEditorConfig, payloadConfig),
|
|
}),
|
|
})
|
|
```
|
|
|
|
### Getting the editor config
|
|
|
|
As you can see, 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, simply import the default editor config and adjust it - just like you did inside of the `editor: lexicalEditor({})` property:
|
|
|
|
```ts
|
|
import { defaultEditorConfig, defaultEditorFeatures } from '@payloadcms/richtext-lexical' // <= make sure this package is installed
|
|
|
|
const yourEditorConfig = defaultEditorConfig
|
|
|
|
// If you made changes to the features of the field's editor config, you should also make those changes here:
|
|
yourEditorConfig.features = [
|
|
...defaultEditorFeatures,
|
|
// Add your custom features here
|
|
]
|
|
```
|
|
|
|
### Getting the editor config from an existing field
|
|
|
|
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:
|
|
|
|
```ts
|
|
import type { CollectionConfig, RichTextField } from 'payload'
|
|
import { createHeadlessEditor } from '@lexical/headless'
|
|
import type { LexicalRichTextAdapter, SanitizedServerEditorConfig } from '@payloadcms/richtext-lexical'
|
|
import {
|
|
getEnabledNodes,
|
|
lexicalEditor
|
|
} from '@payloadcms/richtext-lexical'
|
|
|
|
export const MyCollection: CollectionConfig = {
|
|
slug: 'slug',
|
|
fields: [
|
|
{
|
|
name: 'text',
|
|
type: 'text',
|
|
hooks: {
|
|
afterRead: [
|
|
({ value, collection }) => {
|
|
const otherRichTextField: RichTextField = collection.fields.find(
|
|
(field) => 'name' in field && field.name === 'richText',
|
|
) as RichTextField
|
|
|
|
const lexicalAdapter: LexicalRichTextAdapter =
|
|
otherRichTextField.editor as LexicalRichTextAdapter
|
|
|
|
const sanitizedServerEditorConfig: SanitizedServerEditorConfig =
|
|
lexicalAdapter.editorConfig
|
|
|
|
const headlessEditor = createHeadlessEditor({
|
|
nodes: getEnabledNodes({
|
|
editorConfig: sanitizedServerEditorConfig,
|
|
}),
|
|
})
|
|
|
|
// Do whatever you want with the headless editor
|
|
|
|
return value
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: 'richText',
|
|
type: 'richText',
|
|
editor: lexicalEditor({
|
|
features,
|
|
}),
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## HTML => Lexical
|
|
|
|
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
|
|
|
|
```ts
|
|
import { $generateNodesFromDOM } from '@lexical/html'
|
|
import { $getRoot, $getSelection } from 'lexical'
|
|
import { JSDOM } from 'jsdom'
|
|
|
|
headlessEditor.update(
|
|
() => {
|
|
// In a headless environment you can use a package such as JSDom to parse the HTML string.
|
|
const dom = new JSDOM(htmlString)
|
|
|
|
// Once you have the DOM instance it's easy to generate LexicalNodes.
|
|
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
|
|
|
|
// Select the root
|
|
$getRoot().select()
|
|
|
|
// Insert them at a selection.
|
|
const selection = $getSelection()
|
|
selection.insertNodes(nodes)
|
|
},
|
|
{ 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">
|
|
<strong>Note:</strong>
|
|
<br />
|
|
Using the <code>discrete: true</code> flag ensures instant updates to the editor state. If
|
|
immediate reading of the updated state isn't necessary, you can omit the flag.
|
|
</Banner>
|
|
|
|
## Markdown => Lexical
|
|
|
|
Convert markdown content to the Lexical editor format with the following:
|
|
|
|
```ts
|
|
import { $convertFromMarkdownString } from '@lexical/markdown'
|
|
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
|
|
|
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
|
|
const markdown = `# Hello World`
|
|
|
|
headlessEditor.update(
|
|
() => {
|
|
$convertFromMarkdownString(markdown, yourSanitizedEditorConfig.features.markdownTransformers)
|
|
},
|
|
{ discrete: true },
|
|
)
|
|
|
|
// Do this if you then want to get the editor JSON
|
|
const editorJSON = headlessEditor.getEditorState().toJSON()
|
|
```
|
|
|
|
## 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 { $convertToMarkdownString } from '@lexical/markdown'
|
|
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
|
|
import type { SerializedEditorState } from 'lexical'
|
|
|
|
const yourSanitizedEditorConfig = sanitizeServerEditorConfig(yourEditorConfig, payloadConfig) // <= your editor config & Payload Config here
|
|
const yourEditorState: SerializedEditorState // <= your current editor state here
|
|
|
|
// Import editor state into your headless editor
|
|
try {
|
|
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // 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(yourSanitizedEditorConfig?.features?.markdownTransformers)
|
|
})
|
|
```
|
|
|
|
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
|
|
|
|
## 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 'lexical'
|
|
import { $getRoot } from 'lexical'
|
|
|
|
const yourEditorState: SerializedEditorState // <= your current editor state here
|
|
|
|
// Import editor state into your headless editor
|
|
try {
|
|
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // 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()
|
|
}) || ''
|
|
```
|