diff --git a/docs/rich-text/converters.mdx b/docs/rich-text/converters.mdx
index 4437b4e3cb..406ab8f0a4 100644
--- a/docs/rich-text/converters.mdx
+++ b/docs/rich-text/converters.mdx
@@ -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
-}
-```
+### 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.
-
-
- 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.
-
-
-### 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 = ({
- defaultConverters,
-}) => ({
- ...defaultConverters,
- ...LinkJSXConverter({ internalDocToHref }),
-})
-
-export const MyComponent: React.FC<{
- lexicalData: SerializedEditorState
-}> = ({ lexicalData }) => {
- return
-}
-```
-
-### 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
- | SerializedInlineBlockNode
-
-const jsxConverters: JSXConvertersFunction = ({
- defaultConverters,
-}) => ({
- ...defaultConverters,
- blocks: {
- // Each key should match your block's slug
- myNumberBlock: ({ node }) => {node.fields.number}
,
- myTextBlock: ({ node }) => (
- {node.fields.text}
- ),
- },
- inlineBlocks: {
- // Each key should match your inline block's slug
- myInlineBlock: ({ node }) => {node.fields.text},
- },
-})
-
-export const MyComponent: React.FC<{
- lexicalData: SerializedEditorState
-}> = ({ lexicalData }) => {
- return
-}
-```
-
-### 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
- }
-
- return null
-}
-
-const jsxConverters: JSXConvertersFunction = ({
- defaultConverters,
-}) => ({
- ...defaultConverters,
- // Override the default upload converter
- upload: ({ node }) => {
- return
- },
-})
-
-export const MyComponent: React.FC<{
- lexicalData: SerializedEditorState
-}> = ({ lexicalData }) => {
- return
-}
-```
-
-## 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
-}
-```
-
-### 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)
- useEffect(() => {
- async function convert() {
- const html = await convertLexicalToHTMLAsync({
- data,
- populate: getRestPopulateFn({
- apiURL: `http://localhost:3000/api`,
- }),
- })
- setHTML(html)
- }
-
- void convert()
- }, [data])
-
- return 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 &&
-}
-```
-
-### 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
- | SerializedInlineBlockNode
-
-const htmlConverters: HTMLConvertersFunction = ({
- defaultConverters,
-}) => ({
- ...defaultConverters,
- blocks: {
- // Each key should match your block's slug
- myTextBlock: ({ node, providedCSSString }) =>
- `${node.fields.text}
`,
- },
- inlineBlocks: {
- // Each key should match your inline block's slug
- myInlineBlock: ({ node, providedStyleTag }) =>
- `${node.fields.text}`,
- },
-})
-
-export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
- const html = convertLexicalToHTML({
- converters: htmlConverters,
- data,
- })
-
- return
-}
-```
-
-### 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 }) =>
- `${node.fields.text}
`,
- },
- })) as HTMLConvertersFunction<
- DefaultNodeTypes | SerializedBlockNode
- >,
- }),
- ],
-}
-```
-
-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: 'text
',
- 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).
-
-
- **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.
-
-
-
-## 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()
- }) || ''
-```
diff --git a/docs/rich-text/converting-html.mdx b/docs/rich-text/converting-html.mdx
new file mode 100644
index 0000000000..024cbc57d6
--- /dev/null
+++ b/docs/rich-text/converting-html.mdx
@@ -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
+}
+```
+
+### 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
+ | SerializedInlineBlockNode
+
+const htmlConverters: HTMLConvertersFunction = ({
+ defaultConverters,
+}) => ({
+ ...defaultConverters,
+ blocks: {
+ // Each key should match your block's slug
+ myTextBlock: ({ node, providedCSSString }) =>
+ `${node.fields.text}
`,
+ },
+ inlineBlocks: {
+ // Each key should match your inline block's slug
+ myInlineBlock: ({ node, providedStyleTag }) =>
+ `${node.fields.text}`,
+ },
+})
+
+export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
+ const html = convertLexicalToHTML({
+ converters: htmlConverters,
+ data,
+ })
+
+ return
+}
+```
+
+### 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 }) =>
+ `${node.fields.text}
`,
+ },
+ })) as HTMLConvertersFunction<
+ DefaultNodeTypes | SerializedBlockNode
+ >,
+ }),
+ ],
+}
+```
+
+### 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)
+ useEffect(() => {
+ async function convert() {
+ const html = await convertLexicalToHTMLAsync({
+ data,
+ populate: getRestPopulateFn({
+ apiURL: `http://localhost:3000/api`,
+ }),
+ })
+ setHTML(html)
+ }
+
+ void convert()
+ }, [data])
+
+ return 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 &&
+}
+```
+
+## 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: 'text
',
+ JSDOM, // Pass in the JSDOM import; it's not bundled to keep package size small
+})
+```
diff --git a/docs/rich-text/converting-jsx.mdx b/docs/rich-text/converting-jsx.mdx
new file mode 100644
index 0000000000..48462aac5d
--- /dev/null
+++ b/docs/rich-text/converting-jsx.mdx
@@ -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
+}
+```
+
+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.
+
+
+ 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.
+
+
+### 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 = ({
+ defaultConverters,
+}) => ({
+ ...defaultConverters,
+ ...LinkJSXConverter({ internalDocToHref }),
+})
+
+export const MyComponent: React.FC<{
+ lexicalData: SerializedEditorState
+}> = ({ lexicalData }) => {
+ return
+}
+```
+
+### 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
+ | SerializedInlineBlockNode
+
+const jsxConverters: JSXConvertersFunction = ({
+ defaultConverters,
+}) => ({
+ ...defaultConverters,
+ blocks: {
+ // Each key should match your block's slug
+ myNumberBlock: ({ node }) => {node.fields.number}
,
+ myTextBlock: ({ node }) => (
+ {node.fields.text}
+ ),
+ },
+ inlineBlocks: {
+ // Each key should match your inline block's slug
+ myInlineBlock: ({ node }) => {node.fields.text},
+ },
+})
+
+export const MyComponent: React.FC<{
+ lexicalData: SerializedEditorState
+}> = ({ lexicalData }) => {
+ return
+}
+```
+
+### 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
+ }
+
+ return null
+}
+
+const jsxConverters: JSXConvertersFunction = ({
+ defaultConverters,
+}) => ({
+ ...defaultConverters,
+ // Override the default upload converter
+ upload: ({ node }) => {
+ return
+ },
+})
+
+export const MyComponent: React.FC<{
+ lexicalData: SerializedEditorState
+}> = ({ lexicalData }) => {
+ return
+}
+```
diff --git a/docs/rich-text/converting-markdown.mdx b/docs/rich-text/converting-markdown.mdx
new file mode 100644
index 0000000000..ff0b2b8ff5
--- /dev/null
+++ b/docs/rich-text/converting-markdown.mdx
@@ -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.
+
+
+
+```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:
+ * child content
+ */
+ 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.
diff --git a/docs/rich-text/converting-plaintext.mdx b/docs/rich-text/converting-plaintext.mdx
new file mode 100644
index 0000000000..b3ea50697b
--- /dev/null
+++ b/docs/rich-text/converting-plaintext.mdx
@@ -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
+> = {
+ 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.
diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts
index 8f71aa8e4e..1c94c41b31 100644
--- a/packages/payload/src/fields/config/types.ts
+++ b/packages/payload/src/fields/config/types.ts
@@ -1349,7 +1349,7 @@ export type BlockJSX = {
*/
export: (props: {
fields: BlockFields
- lexicalToMarkdown?: (props: { editorState: Record }) => string
+ lexicalToMarkdown: (props: { editorState: Record }) => 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
+ markdownToLexical: (props: { markdown: string }) => Record
openMatch?: RegExpMatchArray
props: Record
}) => BlockFields | false
diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json
index b80a25747a..71d2804344 100644
--- a/packages/richtext-lexical/package.json
+++ b/packages/richtext-lexical/package.json
@@ -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",
diff --git a/packages/richtext-lexical/src/exports/html-async/index.ts b/packages/richtext-lexical/src/exports/html-async/index.ts
index aaf296b68e..12ce4254ea 100644
--- a/packages/richtext-lexical/src/exports/html-async/index.ts
+++ b/packages/richtext-lexical/src/exports/html-async/index.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'
diff --git a/packages/richtext-lexical/src/exports/html/index.ts b/packages/richtext-lexical/src/exports/html/index.ts
index 614dd33aa1..7aed35cfa4 100644
--- a/packages/richtext-lexical/src/exports/html/index.ts
+++ b/packages/richtext-lexical/src/exports/html/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/exports/plaintext/index.ts b/packages/richtext-lexical/src/exports/plaintext/index.ts
new file mode 100644
index 0000000000..091c97fdff
--- /dev/null
+++ b/packages/richtext-lexical/src/exports/plaintext/index.ts
@@ -0,0 +1,6 @@
+export { convertLexicalToPlaintext } from '../../features/converters/lexicalToPlaintext/sync/index.js'
+
+export type {
+ PlaintextConverter,
+ PlaintextConverters,
+} from '../../features/converters/lexicalToPlaintext/sync/types.js'
diff --git a/packages/richtext-lexical/src/exports/react/index.ts b/packages/richtext-lexical/src/exports/react/index.ts
index 904fdf92cf..279d0ecaf0 100644
--- a/packages/richtext-lexical/src/exports/react/index.ts
+++ b/packages/richtext-lexical/src/exports/react/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/blockquote/server/index.ts b/packages/richtext-lexical/src/features/blockquote/server/index.ts
index be87f77af7..4d5bb15e66 100644
--- a/packages/richtext-lexical/src/features/blockquote/server/index.ts
+++ b/packages/richtext-lexical/src/features/blockquote/server/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/blocks/server/linesFromMatchToContentAndPropsString.ts b/packages/richtext-lexical/src/features/blocks/server/linesFromMatchToContentAndPropsString.ts
index eb56719f14..99f083ac09 100644
--- a/packages/richtext-lexical/src/features/blocks/server/linesFromMatchToContentAndPropsString.ts
+++ b/packages/richtext-lexical/src/features/blocks/server/linesFromMatchToContentAndPropsString.ts
@@ -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),
+ )
}
}
diff --git a/packages/richtext-lexical/src/features/converters/htmlToLexical/index.ts b/packages/richtext-lexical/src/features/converters/htmlToLexical/index.ts
index 1db2425df5..cbafa7d41f 100644
--- a/packages/richtext-lexical/src/features/converters/htmlToLexical/index.ts
+++ b/packages/richtext-lexical/src/features/converters/htmlToLexical/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/blockquote.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/blockquote.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/blockquote.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/blockquote.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/heading.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/heading.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/heading.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/heading.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/horizontalRule.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/horizontalRule.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/horizontalRule.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/horizontalRule.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/linebreak.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/linebreak.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/linebreak.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/linebreak.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/link.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/link.ts
similarity index 87%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/link.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/link.ts
index 0799726015..b5e2fd7c3d 100644
--- a/packages/richtext-lexical/src/features/converters/html/async/converters/link.ts
+++ b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/link.ts
@@ -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 `(
-
+ return `
${children}
-
- )`
+ `
},
link: async ({ node, nodesToHTML, populate, providedStyleTag }) => {
const children = (
@@ -47,10 +45,8 @@ export const LinkHTMLConverterAsync: (args: {
}
}
- return `(
-
+ return `
${children}
-
- )`
+ `
},
})
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/list.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/list.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/list.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/list.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/paragraph.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/paragraph.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/paragraph.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/paragraph.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/tab.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/tab.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/tab.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/tab.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/table.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/table.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/table.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/table.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/text.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/text.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/text.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/text.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/converters/upload.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/upload.ts
similarity index 98%
rename from packages/richtext-lexical/src/features/converters/html/async/converters/upload.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/upload.ts
index 7ff32453fd..55fb67862d 100644
--- a/packages/richtext-lexical/src/features/converters/html/async/converters/upload.ts
+++ b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/converters/upload.ts
@@ -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 = {
diff --git a/packages/richtext-lexical/src/features/converters/html/async/defaultConverters.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/defaultConverters.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/defaultConverters.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/defaultConverters.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/field/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/field/index.ts
similarity index 90%
rename from packages/richtext-lexical/src/features/converters/html/async/field/index.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/field/index.ts
index 28f7d87f44..7b83ed87cd 100644
--- a/packages/richtext-lexical/src/features/converters/html/async/field/index.ts
+++ b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/field/index.ts
@@ -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
}
diff --git a/packages/richtext-lexical/src/features/converters/html/async/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/index.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/index.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/index.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/async/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/async/types.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/async/types.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/async/types.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/shared/findConverterForNode.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/shared/findConverterForNode.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/shared/findConverterForNode.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/shared/findConverterForNode.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/shared/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/shared/types.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/shared/types.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/shared/types.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/blockquote.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/blockquote.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/blockquote.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/blockquote.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/heading.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/heading.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/heading.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/heading.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/horizontalRule.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/horizontalRule.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/horizontalRule.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/horizontalRule.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/linebreak.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/linebreak.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/linebreak.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/linebreak.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/link.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/link.ts
similarity index 86%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/link.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/link.ts
index e69a461d9a..591ec22038 100644
--- a/packages/richtext-lexical/src/features/converters/html/sync/converters/link.ts
+++ b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/link.ts
@@ -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 `(
-
+ return `
${children}
-
- )`
+ `
},
link: ({ node, nodesToHTML, providedStyleTag }) => {
const children = nodesToHTML({
@@ -38,10 +36,8 @@ export const LinkHTMLConverter: (args: {
}
}
- return `(
-
+ return `
${children}
-
- )`
+ `
},
})
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/list.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/list.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/list.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/list.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/paragraph.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/paragraph.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/paragraph.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/paragraph.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/tab.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/tab.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/tab.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/tab.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/table.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/table.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/table.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/table.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/text.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/text.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/text.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/text.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/converters/upload.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/upload.ts
similarity index 98%
rename from packages/richtext-lexical/src/features/converters/html/sync/converters/upload.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/upload.ts
index fbd15863eb..ae4e5f7944 100644
--- a/packages/richtext-lexical/src/features/converters/html/sync/converters/upload.ts
+++ b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/converters/upload.ts
@@ -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 = {
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/defaultConverters.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/defaultConverters.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/defaultConverters.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/defaultConverters.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/index.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/index.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/index.ts
diff --git a/packages/richtext-lexical/src/features/converters/html/sync/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/types.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html/sync/types.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml/sync/types.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/linebreak.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/linebreak.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/linebreak.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/linebreak.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/paragraph.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/paragraph.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/paragraph.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/paragraph.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/tab.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/tab.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/tab.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/tab.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/text.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/text.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/converters/text.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/converters/text.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/defaultConverters.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/defaultConverters.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/defaultConverters.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/defaultConverters.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/index.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/index.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/index.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/converter/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/types.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/converter/types.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/converter/types.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/field/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/field/index.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/field/index.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/field/index.ts
diff --git a/packages/richtext-lexical/src/features/converters/html_deprecated/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/index.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/html_deprecated/index.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToHtml_deprecated/index.ts
diff --git a/packages/richtext-lexical/src/features/converters/jsx/Component/index.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/Component/index.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/Component/index.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/Component/index.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/blockquote.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/blockquote.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/blockquote.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/blockquote.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/heading.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/heading.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/heading.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/heading.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/horizontalRule.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/horizontalRule.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/horizontalRule.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/horizontalRule.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/linebreak.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/linebreak.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/linebreak.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/linebreak.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/link.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/link.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/link.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/link.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/list.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/list.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/list.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/list.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/paragraph.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/paragraph.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/paragraph.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/paragraph.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/tab.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/tab.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/tab.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/tab.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/table.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/table.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/table.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/table.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/text.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/text.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/text.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/text.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/converters/upload.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/upload.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/converters/upload.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/converters/upload.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/defaultConverters.ts b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/defaultConverters.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/defaultConverters.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/defaultConverters.ts
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/index.tsx b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/index.tsx
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/index.tsx
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/index.tsx
diff --git a/packages/richtext-lexical/src/features/converters/jsx/converter/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/types.ts
similarity index 100%
rename from packages/richtext-lexical/src/features/converters/jsx/converter/types.ts
rename to packages/richtext-lexical/src/features/converters/lexicalToJSX/converter/types.ts
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToMarkdown/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToMarkdown/index.ts
new file mode 100644
index 0000000000..acd2e533b3
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/lexicalToMarkdown/index.ts
@@ -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
+}
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/convertLexicalToPlaintext.spec.ts b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/convertLexicalToPlaintext.spec.ts
new file mode 100644
index 0000000000..eaf53b96f9
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/convertLexicalToPlaintext.spec.ts
@@ -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')
+ })
+})
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/shared/findConverterForNode.ts b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/shared/findConverterForNode.ts
new file mode 100644
index 0000000000..1db49a65bb
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/shared/findConverterForNode.ts
@@ -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,
+>({
+ 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
+}
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/shared/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/shared/types.ts
new file mode 100644
index 0000000000..be7264a8f1
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/shared/types.ts
@@ -0,0 +1,4 @@
+import type { SerializedLexicalNode } from 'lexical'
+export type SerializedLexicalNodeWithParent = {
+ parent?: SerializedLexicalNode
+} & SerializedLexicalNode
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/sync/index.ts b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/sync/index.ts
new file mode 100644
index 0000000000..c29049d6b2
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/sync/index.ts
@@ -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)
+}
diff --git a/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/sync/types.ts b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/sync/types.ts
new file mode 100644
index 0000000000..5f80af25ec
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/lexicalToPlaintext/sync/types.ts
@@ -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
+ }
+ | PlaintextConverter
+ | undefined
+} & {
+ [nodeType in Exclude, 'block' | 'inlineBlock'>]?: PlaintextConverter<
+ Extract
+ >
+} & {
+ blocks?: {
+ [K in Extract<
+ Extract extends SerializedBlockNode
+ ? B extends { blockType: string }
+ ? B['blockType']
+ : never
+ : never,
+ string
+ >]?: PlaintextConverter<
+ Extract extends SerializedBlockNode
+ ? SerializedBlockNode>
+ : SerializedBlockNode
+ >
+ }
+ inlineBlocks?: {
+ [K in Extract<
+ Extract extends SerializedInlineBlockNode
+ ? B extends { blockType: string }
+ ? B['blockType']
+ : never
+ : never,
+ string
+ >]?: PlaintextConverter<
+ Extract extends SerializedInlineBlockNode
+ ? SerializedInlineBlockNode>
+ : SerializedInlineBlockNode
+ >
+ }
+}
diff --git a/packages/richtext-lexical/src/features/converters/markdownToLexical/index.ts b/packages/richtext-lexical/src/features/converters/markdownToLexical/index.ts
new file mode 100644
index 0000000000..11803803b4
--- /dev/null
+++ b/packages/richtext-lexical/src/features/converters/markdownToLexical/index.ts
@@ -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 => {
+ const headlessEditor = createHeadlessEditor({
+ nodes: getEnabledNodes({
+ editorConfig,
+ }),
+ })
+
+ headlessEditor.update(
+ () => {
+ $convertFromMarkdownString(markdown, editorConfig.features.markdownTransformers)
+ },
+ { discrete: true },
+ )
+
+ const editorJSON = headlessEditor.getEditorState().toJSON()
+
+ return editorJSON as TypedEditorState
+}
diff --git a/packages/richtext-lexical/src/features/converters/utilities/payloadPopulateFn.ts b/packages/richtext-lexical/src/features/converters/utilities/payloadPopulateFn.ts
index f567b1d371..98ca79d8fd 100644
--- a/packages/richtext-lexical/src/features/converters/utilities/payloadPopulateFn.ts
+++ b/packages/richtext-lexical/src/features/converters/utilities/payloadPopulateFn.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/converters/utilities/restPopulateFn.ts b/packages/richtext-lexical/src/features/converters/utilities/restPopulateFn.ts
index efe5f83baf..885623c137 100644
--- a/packages/richtext-lexical/src/features/converters/utilities/restPopulateFn.ts
+++ b/packages/richtext-lexical/src/features/converters/utilities/restPopulateFn.ts
@@ -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: {
/**
diff --git a/packages/richtext-lexical/src/features/experimental_table/server/index.ts b/packages/richtext-lexical/src/features/experimental_table/server/index.ts
index 8e257b77b8..ec1943c88f 100644
--- a/packages/richtext-lexical/src/features/experimental_table/server/index.ts
+++ b/packages/richtext-lexical/src/features/experimental_table/server/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/heading/server/index.ts b/packages/richtext-lexical/src/features/heading/server/index.ts
index 82093a91ab..7858250371 100644
--- a/packages/richtext-lexical/src/features/heading/server/index.ts
+++ b/packages/richtext-lexical/src/features/heading/server/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/link/server/index.ts b/packages/richtext-lexical/src/features/link/server/index.ts
index 96062fd9b0..cd6b399c6d 100644
--- a/packages/richtext-lexical/src/features/link/server/index.ts
+++ b/packages/richtext-lexical/src/features/link/server/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/features/lists/htmlConverter.ts b/packages/richtext-lexical/src/features/lists/htmlConverter.ts
index 43e610b093..b6e5c7631c 100644
--- a/packages/richtext-lexical/src/features/lists/htmlConverter.ts
+++ b/packages/richtext-lexical/src/features/lists/htmlConverter.ts
@@ -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 = {
converter: async ({
diff --git a/packages/richtext-lexical/src/features/typesServer.ts b/packages/richtext-lexical/src/features/typesServer.ts
index 10c576d062..9746dc7d9c 100644
--- a/packages/richtext-lexical/src/features/typesServer.ts
+++ b/packages/richtext-lexical/src/features/typesServer.ts
@@ -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 = (args: {
diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts
index bb4330285d..62e2d67318 100644
--- a/packages/richtext-lexical/src/index.ts
+++ b/packages/richtext-lexical/src/index.ts
@@ -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'
diff --git a/packages/richtext-lexical/src/nodeTypes.ts b/packages/richtext-lexical/src/nodeTypes.ts
index e475db7162..277bbc34c5 100644
--- a/packages/richtext-lexical/src/nodeTypes.ts
+++ b/packages/richtext-lexical/src/nodeTypes.ts
@@ -93,6 +93,7 @@ export type DefaultNodeTypes =
| SerializedParagraphNode
| SerializedQuoteNode
| SerializedRelationshipNode
+ | SerializedTabNode
| SerializedTextNode
| SerializedUploadNode