feat(richtext-lexical): mdx support (#9160)
Supports bi-directional import/export between MDX <=> Lexical. JSX will be mapped to lexical blocks back and forth. This will allow editing our mdx docs in payload while keeping mdx as the source of truth --------- Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
This commit is contained in:
2
test/lexical-mdx/.gitignore
vendored
Normal file
2
test/lexical-mdx/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/media
|
||||
/media-gif
|
||||
33
test/lexical-mdx/collections/Media/index.ts
Normal file
33
test/lexical-mdx/collections/Media/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const mediaSlug = 'media'
|
||||
|
||||
export const MediaCollection: CollectionConfig = {
|
||||
slug: mediaSlug,
|
||||
access: {
|
||||
create: () => true,
|
||||
read: () => true,
|
||||
},
|
||||
fields: [],
|
||||
upload: {
|
||||
crop: true,
|
||||
focalPoint: true,
|
||||
imageSizes: [
|
||||
{
|
||||
name: 'thumbnail',
|
||||
height: 200,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
name: 'medium',
|
||||
height: 800,
|
||||
width: 800,
|
||||
},
|
||||
{
|
||||
name: 'large',
|
||||
height: 1200,
|
||||
width: 1200,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
41
test/lexical-mdx/collections/Posts/CodeFields.tsx
Normal file
41
test/lexical-mdx/collections/Posts/CodeFields.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import type { CodeFieldClientProps } from 'payload'
|
||||
|
||||
import { CodeField, useFormFields } from '@payloadcms/ui'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { languages } from './shared.js'
|
||||
|
||||
const languageKeyToMonacoLanguageMap = {
|
||||
plaintext: 'plaintext',
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
}
|
||||
|
||||
export const Code: React.FC<CodeFieldClientProps> = ({ field }) => {
|
||||
const languageField = useFormFields(([fields]) => fields['language'])
|
||||
|
||||
const language: string =
|
||||
(languageField?.value as string) || (languageField.initialValue as string) || 'typescript'
|
||||
|
||||
const label = languages[language as keyof typeof languages]
|
||||
|
||||
const props: typeof field = useMemo(
|
||||
() => ({
|
||||
...field,
|
||||
admin: {
|
||||
...field.admin,
|
||||
components: field.admin?.components || {},
|
||||
editorOptions: field.admin?.editorOptions || {},
|
||||
label,
|
||||
language: languageKeyToMonacoLanguageMap[language] || language,
|
||||
},
|
||||
}),
|
||||
[field, language, label],
|
||||
)
|
||||
|
||||
const key = `${field.name}-${language}-${label}`
|
||||
|
||||
return <CodeField field={props} key={key} />
|
||||
}
|
||||
107
test/lexical-mdx/collections/Posts/index.ts
Normal file
107
test/lexical-mdx/collections/Posts/index.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
EXPERIMENTAL_TableFeature,
|
||||
FixedToolbarFeature,
|
||||
lexicalEditor,
|
||||
TreeViewFeature,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { loadMDXAfterRead, saveMDXBeforeChange } from '../../mdx/hooks.js'
|
||||
import { BannerBlock } from '../../mdx/jsxBlocks/banner.js'
|
||||
import { CodeBlock } from '../../mdx/jsxBlocks/code/code.js'
|
||||
import { InlineCodeBlock } from '../../mdx/jsxBlocks/inlineCode.js'
|
||||
import { PackageInstallOptions } from '../../mdx/jsxBlocks/packageInstallOptions.js'
|
||||
import { TextContainerBlock } from '../../mdx/jsxBlocks/TextContainer.js'
|
||||
import { TextContainerNoTrimBlock } from '../../mdx/jsxBlocks/TextContainerNoTrim.js'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'docPath',
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [saveMDXBeforeChange],
|
||||
afterRead: [loadMDXAfterRead],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'docPath',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'FrontMatter',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'frontMatter',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'key',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeViewFeature(),
|
||||
EXPERIMENTAL_TableFeature(),
|
||||
FixedToolbarFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
BannerBlock,
|
||||
CodeBlock,
|
||||
PackageInstallOptions,
|
||||
TextContainerNoTrimBlock,
|
||||
TextContainerBlock,
|
||||
],
|
||||
inlineBlocks: [InlineCodeBlock],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'richTextUnconverted',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeViewFeature(),
|
||||
EXPERIMENTAL_TableFeature(),
|
||||
FixedToolbarFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
BannerBlock,
|
||||
CodeBlock,
|
||||
PackageInstallOptions,
|
||||
TextContainerNoTrimBlock,
|
||||
TextContainerBlock,
|
||||
],
|
||||
inlineBlocks: [InlineCodeBlock],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
15
test/lexical-mdx/collections/Posts/shared.ts
Normal file
15
test/lexical-mdx/collections/Posts/shared.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const docsBasePath = '/Users/alessio/Documents/payloadcms-mdx-mock/docs'
|
||||
|
||||
export const languages = {
|
||||
ts: 'TypeScript',
|
||||
plaintext: 'Plain Text',
|
||||
tsx: 'TSX',
|
||||
js: 'JavaScript',
|
||||
jsx: 'JSX',
|
||||
}
|
||||
|
||||
export const bannerTypes = {
|
||||
success: 'Success',
|
||||
info: 'Info',
|
||||
warning: 'Warning',
|
||||
}
|
||||
81
test/lexical-mdx/config.ts
Normal file
81
test/lexical-mdx/config.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import * as fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { MediaCollection } from './collections/Media/index.js'
|
||||
import { PostsCollection } from './collections/Posts/index.js'
|
||||
import { docsBasePath } from './collections/Posts/shared.js'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
// ...extend config here
|
||||
collections: [
|
||||
PostsCollection,
|
||||
{
|
||||
slug: 'simple',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
MediaCollection,
|
||||
],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
editor: lexicalEditor({}),
|
||||
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||
globals: [],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.delete({
|
||||
collection: 'posts',
|
||||
where: {},
|
||||
})
|
||||
|
||||
// Recursively collect all paths to .mdx files RELATIVE to basePath
|
||||
const walkSync = (dir: string, filelist: string[] = []) => {
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
filelist = fs.statSync(path.join(dir, file)).isDirectory()
|
||||
? walkSync(path.join(dir, file), filelist)
|
||||
: filelist.concat(path.join(dir, file))
|
||||
})
|
||||
return filelist
|
||||
}
|
||||
|
||||
const mdxFiles = walkSync(docsBasePath)
|
||||
.filter((file) => file.endsWith('.mdx'))
|
||||
.map((file) => file.replace(docsBasePath, ''))
|
||||
|
||||
for (const file of mdxFiles) {
|
||||
await payload.create({
|
||||
collection: 'posts',
|
||||
depth: 0,
|
||||
context: {
|
||||
seed: true,
|
||||
},
|
||||
data: {
|
||||
docPath: file,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
20
test/lexical-mdx/eslint.config.js
Normal file
20
test/lexical-mdx/eslint.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.FlatConfig} */
|
||||
let FlatConfig
|
||||
|
||||
/** @type {FlatConfig[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
1519
test/lexical-mdx/int.spec.ts
Normal file
1519
test/lexical-mdx/int.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
170
test/lexical-mdx/mdx/hooks.ts
Normal file
170
test/lexical-mdx/mdx/hooks.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
import type { CollectionAfterReadHook, CollectionBeforeChangeHook, RichTextField } from 'payload'
|
||||
|
||||
import { createHeadlessEditor } from '@lexical/headless'
|
||||
import { $convertToMarkdownString } from '@lexical/markdown'
|
||||
import {
|
||||
$convertFromMarkdownString,
|
||||
extractFrontmatter,
|
||||
frontmatterToObject,
|
||||
getEnabledNodes,
|
||||
type LexicalRichTextAdapter,
|
||||
objectToFrontmatter,
|
||||
type SanitizedServerEditorConfig,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import fs from 'node:fs'
|
||||
import path from 'path'
|
||||
import { deepCopyObjectSimple } from 'payload'
|
||||
|
||||
import { docsBasePath } from '../collections/Posts/shared.js'
|
||||
|
||||
export const editorJSONToMDX = ({
|
||||
editorState,
|
||||
editorConfig,
|
||||
frontMatterData,
|
||||
}: {
|
||||
editorConfig: SanitizedServerEditorConfig
|
||||
editorState: any
|
||||
frontMatterData?: any
|
||||
}) => {
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
// Convert lexical state to markdown
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(editorState)) // This should commit the editor state immediately
|
||||
} catch (e) {
|
||||
console.error('Error parsing editor state', e)
|
||||
}
|
||||
|
||||
// Export to markdown
|
||||
let markdown: string
|
||||
headlessEditor.getEditorState().read(() => {
|
||||
markdown = $convertToMarkdownString(editorConfig?.features?.markdownTransformers)
|
||||
})
|
||||
|
||||
if (!frontMatterData) {
|
||||
return markdown
|
||||
}
|
||||
|
||||
const frontMatterOriginalData = deepCopyObjectSimple(frontMatterData)
|
||||
|
||||
//Frontmatter
|
||||
const frontmatterData = {}
|
||||
|
||||
if (frontMatterOriginalData) {
|
||||
for (const frontMatterArrayEntry of frontMatterOriginalData) {
|
||||
frontmatterData[frontMatterArrayEntry.key] = frontMatterArrayEntry.value
|
||||
}
|
||||
|
||||
const frontmatterString = objectToFrontmatter(frontmatterData)
|
||||
|
||||
if (frontmatterString?.length) {
|
||||
markdown = frontmatterString + '\n' + markdown
|
||||
}
|
||||
}
|
||||
|
||||
return markdown
|
||||
}
|
||||
|
||||
export const saveMDXBeforeChange: CollectionBeforeChangeHook = ({ collection, data, context }) => {
|
||||
if (context.seed) {
|
||||
return data
|
||||
}
|
||||
const docFilePath = path.join(docsBasePath, data.docPath)
|
||||
|
||||
const field: RichTextField = collection.fields.find(
|
||||
(field) => 'name' in field && field.name === 'richText',
|
||||
) as RichTextField
|
||||
const value = data[field.name]
|
||||
|
||||
const editorConfig: SanitizedServerEditorConfig = (field.editor as LexicalRichTextAdapter)
|
||||
.editorConfig
|
||||
|
||||
const markdown = editorJSONToMDX({
|
||||
editorState: value,
|
||||
editorConfig,
|
||||
frontMatterData: data.frontMatter,
|
||||
})
|
||||
|
||||
if (markdown?.trim()?.length) {
|
||||
// Write markdown to '../../../../docs/admin/overview.mdx'
|
||||
fs.writeFileSync(docFilePath, markdown, {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
}
|
||||
|
||||
return null // Do not save anything to database
|
||||
}
|
||||
|
||||
export function mdxToEditorJSON({
|
||||
mdxWithFrontmatter,
|
||||
editorConfig,
|
||||
}: {
|
||||
editorConfig: SanitizedServerEditorConfig
|
||||
mdxWithFrontmatter: string
|
||||
}): {
|
||||
editorState: SerializedEditorState
|
||||
frontMatter: { key: string; value: string }[]
|
||||
} {
|
||||
const frontMatter = extractFrontmatter(mdxWithFrontmatter)
|
||||
|
||||
const mdx = frontMatter.content
|
||||
|
||||
const headlessEditor = createHeadlessEditor({
|
||||
nodes: getEnabledNodes({
|
||||
editorConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
$convertFromMarkdownString(mdx, editorConfig.features.markdownTransformers)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
|
||||
const frontMatterArray = frontMatter?.frontmatter?.length
|
||||
? Object.entries(frontmatterToObject(frontMatter.frontmatter)).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}))
|
||||
: []
|
||||
|
||||
return {
|
||||
editorState: headlessEditor.getEditorState().toJSON(),
|
||||
frontMatter: frontMatterArray,
|
||||
}
|
||||
}
|
||||
|
||||
export const loadMDXAfterRead: CollectionAfterReadHook = ({ collection, doc, context }) => {
|
||||
if (context.seed) {
|
||||
return doc
|
||||
}
|
||||
const field: RichTextField = collection.fields.find(
|
||||
(field) => 'name' in field && field.name === 'richText',
|
||||
) as RichTextField
|
||||
|
||||
const docFilePath = path.join(docsBasePath, doc.docPath)
|
||||
|
||||
const mdxWithFrontmatter = fs.readFileSync(docFilePath, {
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
const editorConfig: SanitizedServerEditorConfig = (field.editor as LexicalRichTextAdapter)
|
||||
.editorConfig
|
||||
|
||||
const result = mdxToEditorJSON({
|
||||
mdxWithFrontmatter,
|
||||
editorConfig,
|
||||
})
|
||||
|
||||
return {
|
||||
...doc,
|
||||
richText: result.editorState,
|
||||
frontMatter: result.frontMatter,
|
||||
}
|
||||
}
|
||||
24
test/lexical-mdx/mdx/jsxBlocks/TextContainer.ts
Normal file
24
test/lexical-mdx/mdx/jsxBlocks/TextContainer.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const TextContainerBlock: Block = {
|
||||
slug: 'TextContainer',
|
||||
jsx: {
|
||||
import: ({ children }) => {
|
||||
return {
|
||||
text: children,
|
||||
}
|
||||
},
|
||||
export: ({ fields }) => {
|
||||
return {
|
||||
props: {},
|
||||
children: fields.text,
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
25
test/lexical-mdx/mdx/jsxBlocks/TextContainerNoTrim.ts
Normal file
25
test/lexical-mdx/mdx/jsxBlocks/TextContainerNoTrim.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const TextContainerNoTrimBlock: Block = {
|
||||
slug: 'TextContainerNoTrim',
|
||||
jsx: {
|
||||
import: ({ children }) => {
|
||||
return {
|
||||
text: children,
|
||||
}
|
||||
},
|
||||
export: ({ fields }) => {
|
||||
return {
|
||||
props: {},
|
||||
children: fields.text,
|
||||
}
|
||||
},
|
||||
doNotTrimChildren: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
53
test/lexical-mdx/mdx/jsxBlocks/banner.ts
Normal file
53
test/lexical-mdx/mdx/jsxBlocks/banner.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import { BlocksFeature, lexicalEditor, TreeViewFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { bannerTypes } from '../../collections/Posts/shared.js'
|
||||
import { InlineCodeBlock } from './inlineCode.js'
|
||||
|
||||
export const BannerBlock: Block = {
|
||||
slug: 'Banner',
|
||||
jsx: {
|
||||
import: ({ props, children, markdownToLexical }) => {
|
||||
return {
|
||||
type: props?.type,
|
||||
content: markdownToLexical({ markdown: children }),
|
||||
}
|
||||
},
|
||||
export: ({ fields, lexicalToMarkdown }) => {
|
||||
const props: any = {}
|
||||
if (fields.type) {
|
||||
props.type = fields.type
|
||||
}
|
||||
|
||||
return {
|
||||
props,
|
||||
children: lexicalToMarkdown({ editorState: fields.content }),
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'type',
|
||||
options: Object.entries(bannerTypes).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
})),
|
||||
defaultValue: 'info',
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeViewFeature(),
|
||||
BlocksFeature({
|
||||
inlineBlocks: [InlineCodeBlock],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
32
test/lexical-mdx/mdx/jsxBlocks/code/code.ts
Normal file
32
test/lexical-mdx/mdx/jsxBlocks/code/code.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import { languages } from '../../../collections/Posts/shared.js'
|
||||
import { codeConverter } from './converter.js'
|
||||
|
||||
export const CodeBlock: Block = {
|
||||
slug: 'Code',
|
||||
admin: {
|
||||
jsx: './mdx/jsxBlocks/code/converterClient.js#codeConverterClient',
|
||||
},
|
||||
jsx: codeConverter,
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'language',
|
||||
options: Object.entries(languages).map(([key, value]) => ({
|
||||
label: value,
|
||||
value: key,
|
||||
})),
|
||||
defaultValue: 'ts',
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
components: {
|
||||
Field: './collections/Posts/CodeFields.js#Code',
|
||||
},
|
||||
},
|
||||
name: 'code',
|
||||
type: 'code',
|
||||
},
|
||||
],
|
||||
}
|
||||
36
test/lexical-mdx/mdx/jsxBlocks/code/converter.ts
Normal file
36
test/lexical-mdx/mdx/jsxBlocks/code/converter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { BlockJSX } from 'payload'
|
||||
|
||||
export const codeConverter: BlockJSX = {
|
||||
customStartRegex: /^[ \t]*```(\w+)?/,
|
||||
customEndRegex: {
|
||||
optional: true,
|
||||
regExp: /[ \t]*```$/,
|
||||
},
|
||||
doNotTrimChildren: true,
|
||||
import: ({ openMatch, children, closeMatch }) => {
|
||||
const language = openMatch[1]
|
||||
|
||||
const isSingleLineAndComplete =
|
||||
!!closeMatch && !children.includes('\n') && openMatch.input?.trim() !== '```' + language
|
||||
|
||||
if (isSingleLineAndComplete) {
|
||||
return {
|
||||
language: '',
|
||||
code: language + (children?.length ? children : ''), // No need to add space to children as they are not trimmed
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
language,
|
||||
code: children,
|
||||
}
|
||||
},
|
||||
export: ({ fields }) => {
|
||||
const isSingleLine = !fields.code.includes('\n') && !fields.language?.length
|
||||
if (isSingleLine) {
|
||||
return '```' + fields.code + '```'
|
||||
}
|
||||
|
||||
return '```' + (fields.language || '') + (fields.code ? '\n' + fields.code : '') + '\n' + '```'
|
||||
},
|
||||
}
|
||||
3
test/lexical-mdx/mdx/jsxBlocks/code/converterClient.ts
Normal file
3
test/lexical-mdx/mdx/jsxBlocks/code/converterClient.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export { codeConverter as codeConverterClient } from './converter.js'
|
||||
24
test/lexical-mdx/mdx/jsxBlocks/inlineCode.ts
Normal file
24
test/lexical-mdx/mdx/jsxBlocks/inlineCode.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const InlineCodeBlock: Block = {
|
||||
slug: 'InlineCode',
|
||||
jsx: {
|
||||
import: ({ children }) => {
|
||||
return {
|
||||
code: children,
|
||||
}
|
||||
},
|
||||
export: ({ fields }) => {
|
||||
return {
|
||||
props: {},
|
||||
children: fields.code,
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'code',
|
||||
},
|
||||
],
|
||||
}
|
||||
52
test/lexical-mdx/mdx/jsxBlocks/packageInstallOptions.ts
Normal file
52
test/lexical-mdx/mdx/jsxBlocks/packageInstallOptions.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const PackageInstallOptions: Block = {
|
||||
slug: 'PackageInstallOptions',
|
||||
jsx: {
|
||||
import: ({ props, children, markdownToLexical }) => {
|
||||
return {
|
||||
global: props?.global,
|
||||
packageId: props?.packageId,
|
||||
someNestedObject: props?.someNestedObject,
|
||||
uniqueId: props?.uniqueId,
|
||||
update: props?.update,
|
||||
}
|
||||
},
|
||||
export: ({ fields, lexicalToMarkdown }) => {
|
||||
return {
|
||||
props: {
|
||||
global: fields?.global,
|
||||
packageId: fields?.packageId,
|
||||
someNestedObject: fields?.someNestedObject,
|
||||
uniqueId: fields?.uniqueId,
|
||||
update: fields?.update,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'packageId',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'global',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
name: 'update',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
name: 'uniqueId',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'someNestedObject',
|
||||
type: 'code',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
231
test/lexical-mdx/payload-types.ts
Normal file
231
test/lexical-mdx/payload-types.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
posts: Post;
|
||||
simple: Simple;
|
||||
media: Media;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
docPath: string;
|
||||
frontMatter?:
|
||||
| {
|
||||
key?: string | null;
|
||||
value?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "simple".
|
||||
*/
|
||||
export interface Simple {
|
||||
id: string;
|
||||
text?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
sizes?: {
|
||||
thumbnail?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
medium?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
large?: {
|
||||
url?: string | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
filename?: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'simple';
|
||||
value: string | Simple;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null);
|
||||
editedAt?: string | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
1902
test/lexical-mdx/schema.graphql
Normal file
1902
test/lexical-mdx/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
387
test/lexical-mdx/tableJson.ts
Normal file
387
test/lexical-mdx/tableJson.ts
Normal file
@@ -0,0 +1,387 @@
|
||||
export const tableJson = {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' Option ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 1,
|
||||
rowSpan: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' Default route ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 1,
|
||||
rowSpan: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' Description ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 1,
|
||||
rowSpan: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablerow',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 16,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'account',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 0,
|
||||
rowSpan: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 0,
|
||||
rowSpan: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: " The user's account page. ",
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 0,
|
||||
rowSpan: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablerow',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 16,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'createFirstUser',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 0,
|
||||
rowSpan: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 16,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '/create-first-user',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 0,
|
||||
rowSpan: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' The page to create the first user. ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablecell',
|
||||
version: 1,
|
||||
backgroundColor: null,
|
||||
colSpan: 1,
|
||||
headerState: 0,
|
||||
rowSpan: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'tablerow',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'table',
|
||||
version: 1,
|
||||
}
|
||||
33
test/lexical-mdx/textToRichText.ts
Normal file
33
test/lexical-mdx/textToRichText.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export function textToRichText(text: string) {
|
||||
return {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
13
test/lexical-mdx/tsconfig.eslint.json
Normal file
13
test/lexical-mdx/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/lexical-mdx/tsconfig.json
Normal file
3
test/lexical-mdx/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user