Skip to content

Commit cd29978

Browse files
authored
feat(richtext-lexical): add htmlToLexical helper (#11479)
This adds a new `convertHTMLToLexical` helper that makes converting HTML to Lexical easy
1 parent e1b3084 commit cd29978

File tree

3 files changed

+88
-51
lines changed

3 files changed

+88
-51
lines changed

docs/rich-text/converters.mdx

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,8 @@ export const UploadFeature: FeatureProviderProviderServer<
338338

339339
Lexical provides a seamless way to perform conversions between various other formats:
340340

341-
- HTML to Lexical (or, importing HTML into the lexical editor)
342-
- Markdown to Lexical (or, importing Markdown into the lexical editor)
341+
- HTML to Lexical
342+
- Markdown to Lexical
343343
- Lexical to Markdown
344344

345345
A headless editor can perform such conversions outside of the main editor instance. Follow this method to initiate a headless editor:
@@ -455,46 +455,22 @@ export const MyCollection: CollectionConfig = {
455455

456456
## HTML => Lexical
457457

458-
Once you have your headless editor instance, you can use it to convert HTML to Lexical:
458+
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:
459459

460460
```ts
461-
import { $generateNodesFromDOM } from '@payloadcms/richtext-lexical/lexical/html'
462-
import { $getRoot, $getSelection } from '@payloadcms/richtext-lexical/lexical'
461+
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
462+
// Make sure you have jsdom and @types/jsdom installed
463463
import { JSDOM } from 'jsdom'
464464

465-
headlessEditor.update(
466-
() => {
467-
// In a headless environment you can use a package such as JSDom to parse the HTML string.
468-
const dom = new JSDOM(htmlString)
469-
470-
// Once you have the DOM instance it's easy to generate LexicalNodes.
471-
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
472-
473-
// Select the root
474-
$getRoot().select()
475-
476-
// Insert them at a selection.
477-
const selection = $getSelection()
478-
selection.insertNodes(nodes)
479-
},
480-
{ discrete: true },
481-
)
482-
483-
// Do this if you then want to get the editor JSON
484-
const editorJSON = headlessEditor.getEditorState().toJSON()
465+
const html = convertHTMLToLexical({
466+
editorConfig: await editorConfigFactory.default({
467+
config, // <= make sure you have access to your Payload Config
468+
}),
469+
html: '<p>text</p>',
470+
JSDOM, // pass the JSDOM import. As it's a relatively large package, richtext-lexical does not include it by default.
471+
})
485472
```
486473

487-
Functions prefixed with a `$` can only be run inside an `editor.update()` or `editorState.read()` callback.
488-
489-
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
490-
491-
<Banner type="success">
492-
**Note:**
493-
494-
Using the `discrete: true` flag ensures instant updates to the editor state. If
495-
immediate reading of the updated state isn't necessary, you can omit the flag.
496-
</Banner>
497-
498474
## Markdown => Lexical
499475

500476
Convert markdown content to the Lexical editor format with the following:
@@ -516,6 +492,17 @@ headlessEditor.update(
516492
const editorJSON = headlessEditor.getEditorState().toJSON()
517493
```
518494

495+
Functions prefixed with a `$` can only be run inside an `editor.update()` or `editorState.read()` callback.
496+
497+
This has been taken from the [lexical serialization & deserialization docs](https://lexical.dev/docs/concepts/serialization#html---lexical).
498+
499+
<Banner type="success">
500+
**Note:**
501+
502+
Using the `discrete: true` flag ensures instant updates to the editor state. If
503+
immediate reading of the updated state isn't necessary, you can omit the flag.
504+
</Banner>
505+
519506
## Lexical => Markdown
520507

521508
Export content from the Lexical editor into Markdown format using these steps:
@@ -564,7 +551,6 @@ Here's the code for it:
564551

565552
```ts
566553
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
567-
568554
import { $getRoot } from '@payloadcms/richtext-lexical/lexical'
569555

570556
const yourEditorState: SerializedEditorState // <= your current editor state here
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createHeadlessEditor } from '@lexical/headless'
2+
import { $getRoot, $getSelection, type SerializedLexicalNode } from 'lexical'
3+
4+
import type { SanitizedServerEditorConfig } from '../../../lexical/config/types.js'
5+
import type { DefaultNodeTypes, TypedEditorState } from '../../../nodeTypes.js'
6+
7+
import {} from '../../../lexical/config/server/sanitize.js'
8+
import { getEnabledNodes } from '../../../lexical/nodes/index.js'
9+
import { $generateNodesFromDOM } from '../../../lexical-proxy/@lexical-html.js'
10+
11+
export const convertHTMLToLexical = <TNodeTypes extends SerializedLexicalNode = DefaultNodeTypes>({
12+
editorConfig,
13+
html,
14+
JSDOM,
15+
}: {
16+
editorConfig: SanitizedServerEditorConfig
17+
html: string
18+
JSDOM: new (html: string) => {
19+
window: {
20+
document: Document
21+
}
22+
}
23+
}): TypedEditorState<TNodeTypes> => {
24+
const headlessEditor = createHeadlessEditor({
25+
nodes: getEnabledNodes({
26+
editorConfig,
27+
}),
28+
})
29+
30+
headlessEditor.update(
31+
() => {
32+
const dom = new JSDOM(html)
33+
34+
const nodes = $generateNodesFromDOM(headlessEditor, dom.window.document)
35+
36+
$getRoot().select()
37+
38+
const selection = $getSelection()
39+
if (selection === null) {
40+
throw new Error('Selection is null')
41+
}
42+
selection.insertNodes(nodes)
43+
},
44+
{ discrete: true },
45+
)
46+
47+
const editorJSON = headlessEditor.getEditorState().toJSON()
48+
49+
return editorJSON as TypedEditorState<TNodeTypes>
50+
}

packages/richtext-lexical/src/index.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -886,48 +886,49 @@ export {
886886
HTMLConverterFeature,
887887
type HTMLConverterFeatureProps,
888888
} from './features/converters/html/index.js'
889+
export { convertHTMLToLexical } from './features/converters/htmlToLexical/index.js'
889890
export { TestRecorderFeature } from './features/debug/testRecorder/server/index.js'
890891
export { TreeViewFeature } from './features/debug/treeView/server/index.js'
891892
export { EXPERIMENTAL_TableFeature } from './features/experimental_table/server/index.js'
892893
export { BoldFeature } from './features/format/bold/feature.server.js'
893894
export { InlineCodeFeature } from './features/format/inlineCode/feature.server.js'
894-
export { ItalicFeature } from './features/format/italic/feature.server.js'
895895

896+
export { ItalicFeature } from './features/format/italic/feature.server.js'
896897
export { StrikethroughFeature } from './features/format/strikethrough/feature.server.js'
897898
export { SubscriptFeature } from './features/format/subscript/feature.server.js'
898899
export { SuperscriptFeature } from './features/format/superscript/feature.server.js'
899900
export { UnderlineFeature } from './features/format/underline/feature.server.js'
900901
export { HeadingFeature, type HeadingFeatureProps } from './features/heading/server/index.js'
901902
export { HorizontalRuleFeature } from './features/horizontalRule/server/index.js'
903+
902904
export { IndentFeature } from './features/indent/server/index.js'
903905

904906
export { AutoLinkNode } from './features/link/nodes/AutoLinkNode.js'
905-
906907
export { LinkNode } from './features/link/nodes/LinkNode.js'
907908
export type { LinkFields } from './features/link/nodes/types.js'
908909
export { LinkFeature, type LinkFeatureServerProps } from './features/link/server/index.js'
909910
export { ChecklistFeature } from './features/lists/checklist/server/index.js'
910911
export { OrderedListFeature } from './features/lists/orderedList/server/index.js'
912+
911913
export { UnorderedListFeature } from './features/lists/unorderedList/server/index.js'
912914

913915
export type {
914916
SlateNode,
915917
SlateNodeConverter,
916918
} from './features/migrations/slateToLexical/converter/types.js'
917-
918919
export { ParagraphFeature } from './features/paragraph/server/index.js'
919920
export {
920921
RelationshipFeature,
921922
type RelationshipFeatureProps,
922923
} from './features/relationship/server/index.js'
924+
923925
export {
924926
type RelationshipData,
925927
RelationshipServerNode,
926928
} from './features/relationship/server/nodes/RelationshipNode.js'
927-
928929
export { FixedToolbarFeature } from './features/toolbars/fixed/server/index.js'
929-
export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js'
930930

931+
export { InlineToolbarFeature } from './features/toolbars/inline/server/index.js'
931932
export type { ToolbarGroup, ToolbarGroupItem } from './features/toolbars/types.js'
932933
export type {
933934
BaseClientFeatureProps,
@@ -942,6 +943,7 @@ export type {
942943
SanitizedClientFeatures,
943944
SanitizedPlugin,
944945
} from './features/typesClient.js'
946+
945947
export type {
946948
AfterChangeNodeHook,
947949
AfterChangeNodeHookArgs,
@@ -967,37 +969,36 @@ export type {
967969
export { createNode } from './features/typeUtilities.js' // Only useful in feature.server.ts
968970

969971
export { UploadFeature } from './features/upload/server/feature.server.js'
970-
971972
export type { UploadFeatureProps } from './features/upload/server/feature.server.js'
972-
export { type UploadData, UploadServerNode } from './features/upload/server/nodes/UploadNode.js'
973973

974+
export { type UploadData, UploadServerNode } from './features/upload/server/nodes/UploadNode.js'
974975
export type { EditorConfigContextType } from './lexical/config/client/EditorConfigProvider.js'
976+
975977
export {
976978
defaultEditorConfig,
977979
defaultEditorFeatures,
978980
defaultEditorLexicalConfig,
979981
} from './lexical/config/server/default.js'
980-
981982
export { loadFeatures, sortFeaturesForOptimalLoading } from './lexical/config/server/loader.js'
983+
982984
export {
983985
sanitizeServerEditorConfig,
984986
sanitizeServerFeatures,
985987
} from './lexical/config/server/sanitize.js'
986-
987988
export type {
988989
ClientEditorConfig,
989990
SanitizedClientEditorConfig,
990991
SanitizedServerEditorConfig,
991992
ServerEditorConfig,
992993
} from './lexical/config/types.js'
993-
export { getEnabledNodes, getEnabledNodesFromServerNodes } from './lexical/nodes/index.js'
994994
export type { AdapterProps }
995995

996+
export { getEnabledNodes, getEnabledNodesFromServerNodes } from './lexical/nodes/index.js'
997+
996998
export type {
997999
SlashMenuGroup,
9981000
SlashMenuItem,
9991001
} from './lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.js'
1000-
10011002
export {
10021003
DETAIL_TYPE_TO_DETAIL,
10031004
DOUBLE_LINE_BREAK,
@@ -1012,26 +1013,26 @@ export {
10121013
TEXT_TYPE_TO_FORMAT,
10131014
TEXT_TYPE_TO_MODE,
10141015
} from './lexical/utils/nodeFormat.js'
1016+
10151017
export { sanitizeUrl, validateUrl } from './lexical/utils/url.js'
10161018

10171019
export type * from './nodeTypes.js'
10181020

10191021
export { $convertFromMarkdownString } from './packages/@lexical/markdown/index.js'
1020-
10211022
export { defaultRichTextValue } from './populateGraphQL/defaultValue.js'
10221023
export { populate } from './populateGraphQL/populate.js'
1024+
10231025
export type { LexicalEditorProps, LexicalFieldAdminProps, LexicalRichTextAdapter } from './types.js'
10241026

10251027
export { createServerFeature } from './utilities/createServerFeature.js'
1026-
10271028
export { editorConfigFactory } from './utilities/editorConfigFactory.js'
10281029
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
10291030
export { extractPropsFromJSXPropsString } from './utilities/jsx/extractPropsFromJSXPropsString.js'
1031+
10301032
export {
10311033
extractFrontmatter,
10321034
frontmatterToObject,
10331035
objectToFrontmatter,
10341036
propsToJSXString,
10351037
} from './utilities/jsx/jsx.js'
1036-
10371038
export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js'

0 commit comments

Comments
 (0)