Skip to content

Commit

Permalink
feat(richtext-lexical): link markdown transformers (#6543)
Browse files Browse the repository at this point in the history
Closes #6507

---------

Co-authored-by: ShawnVogt <[email protected]>
  • Loading branch information
AlessioGr and shawnvogt authored May 29, 2024
1 parent e0b201c commit 33d5312
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { LinkIcon } from '../../lexical/ui/icons/Link/index.js'
import { getSelectedNode } from '../../lexical/utils/getSelectedNode.js'
import { createClientComponent } from '../createClientComponent.js'
import { toolbarFeatureButtonsGroupWithItems } from '../shared/toolbar/featureButtonsGroup.js'
import { LinkMarkdownTransformer } from './markdownTransformer.js'
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from './nodes/LinkNode.js'
import { AutoLinkPlugin } from './plugins/autoLink/index.js'
Expand Down Expand Up @@ -84,6 +85,7 @@ const LinkFeatureClient: FeatureProviderProviderClient<ClientProps> = (props) =>
clientFeatureProps: props,
feature: () => ({
clientFeatureProps: props,
markdownTransformers: [LinkMarkdownTransformer],
nodes: [LinkNode, AutoLinkNode],
plugins: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js
import { createNode } from '../typeUtilities.js'
import { LinkFeatureClientComponent } from './feature.client.js'
import { i18n } from './i18n.js'
import { LinkMarkdownTransformer } from './markdownTransformer.js'
import { AutoLinkNode } from './nodes/AutoLinkNode.js'
import { LinkNode } from './nodes/LinkNode.js'
import { transformExtraFields } from './plugins/floatingLinkEditor/utilities.js'
Expand Down Expand Up @@ -110,6 +111,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
return schemaMap
},
i18n,
markdownTransformers: [LinkMarkdownTransformer],
nodes: [
createNode({
converters: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Code taken from https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/MarkdownTransformers.ts#L357
*/

// Order of text transformers matters:
//
// - code should go first as it prevents any transformations inside

import type { TextMatchTransformer } from '@lexical/markdown'

import { $createTextNode, $isTextNode } from 'lexical'

import { $createLinkNode, $isLinkNode, LinkNode } from './nodes/LinkNode.js'

// - then longer tags match (e.g. ** or __ should go before * or _)
export const LinkMarkdownTransformer: TextMatchTransformer = {
type: 'text-match',
dependencies: [LinkNode],
export: (_node, exportChildren, exportFormat) => {
if (!$isLinkNode(_node)) {
return null
}
const node: LinkNode = _node
const { url } = node.getFields()
const linkContent = `[${node.getTextContent()}](${url})`
const firstChild = node.getFirstChild()
// Add text styles only if link has single text node inside. If it's more
// then one we ignore it as markdown does not support nested styles for links
if (node.getChildrenSize() === 1 && $isTextNode(firstChild)) {
return exportFormat(firstChild, linkContent)
} else {
return linkContent
}
},
importRegExp: /\[([^[]+)\]\(([^()\s]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)/,
regExp: /\[([^[]+)\]\(([^()\s]+)(?:\s"((?:[^"]*\\")*[^"]*)"\s*)?\)$/,
replace: (textNode, match) => {
const [, linkText, linkUrl] = match
const linkNode = $createLinkNode({
fields: {
doc: null,
linkType: 'custom',
newTab: false,
url: linkUrl,
},
})
const linkTextNode = $createTextNode(linkText)
linkTextNode.setFormat(textNode.getFormat())
linkNode.append(linkTextNode)
textNode.replace(linkNode)
},
trigger: ')',
}
19 changes: 6 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 96 additions & 49 deletions test/fields/collections/Lexical/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { ServerEditorConfig } from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from 'lexical'
import type { CollectionConfig } from 'payload/types'

import { createHeadlessEditor } from '@lexical/headless'
import { $convertToMarkdownString } from '@lexical/markdown'
import { getEnabledNodes } from '@payloadcms/richtext-lexical'
import { sanitizeServerEditorConfig } from '@payloadcms/richtext-lexical'
import {
BlocksFeature,
FixedToolbarFeature,
HeadingFeature,
LinkFeature,
TreeViewFeature,
UploadFeature,
defaultEditorFeatures,
lexicalEditor,
} from '@payloadcms/richtext-lexical'

Expand All @@ -23,6 +30,58 @@ import {
UploadAndRichTextBlock,
} from './blocks.js'

const editorConfig: ServerEditorConfig = {
features: [
...defaultEditorFeatures,
//TestRecorderFeature(),
TreeViewFeature(),
//HTMLConverterFeature(),
FixedToolbarFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
BlocksFeature({
blocks: [
RichTextBlock,
TextBlock,
UploadAndRichTextBlock,
SelectFieldBlock,
RelationshipBlock,
RelationshipHasManyBlock,
SubBlockBlock,
RadioButtonsBlock,
ConditionalLayoutBlock,
],
}),
],
}

export const LexicalFields: CollectionConfig = {
slug: lexicalFieldsSlug,
admin: {
Expand Down Expand Up @@ -70,56 +129,44 @@ export const LexicalFields: CollectionConfig = {
admin: {
hideGutter: false,
},
features: ({ defaultFeatures }) => [
...defaultFeatures,
//TestRecorderFeature(),
TreeViewFeature(),
//HTMLConverterFeature(),
FixedToolbarFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
uploads: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
BlocksFeature({
blocks: [
RichTextBlock,
TextBlock,
UploadAndRichTextBlock,
SelectFieldBlock,
RelationshipBlock,
RelationshipHasManyBlock,
SubBlockBlock,
RadioButtonsBlock,
ConditionalLayoutBlock,
],
}),
],
features: editorConfig.features,
}),
},
{
name: 'lexicalWithBlocks_markdown',
type: 'textarea',
hooks: {
afterRead: [
async ({ data, req, siblingData }) => {
const yourSanitizedEditorConfig = await sanitizeServerEditorConfig(
editorConfig,
req.payload.config,
)

const headlessEditor = createHeadlessEditor({
nodes: getEnabledNodes({
editorConfig: yourSanitizedEditorConfig,
}),
})

const yourEditorState: SerializedEditorState = siblingData.lexicalWithBlocks
try {
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
} catch (e) {
/* empty */
}

// Export to markdown
let markdown: string
headlessEditor.getEditorState().read(() => {
markdown = $convertToMarkdownString(
yourSanitizedEditorConfig?.features?.markdownTransformers,
)
})
return markdown
},
],
},
},
],
}
Loading

0 comments on commit 33d5312

Please sign in to comment.