Skip to content

Commit d198473

Browse files
committed
add markdown block
1 parent 7100c53 commit d198473

File tree

11 files changed

+754
-14
lines changed

11 files changed

+754
-14
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@
6161
"react": "19.2.1",
6262
"react-dom": "19.2.1",
6363
"react-hook-form": "7.45.4",
64+
"react-markdown": "^10.1.0",
6465
"recharts": "2.15.4",
66+
"rehype-katex": "^7.0.1",
67+
"remark-gfm": "^4.0.1",
68+
"remark-math": "^6.0.0",
6569
"sharp": "0.34.2",
6670
"tailwind-merge": "^2.3.0",
6771
"tailwindcss-animate": "^1.0.7"

pnpm-lock.yaml

Lines changed: 616 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(payload)/admin/importMap.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997e
88
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
99
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
1010
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
11+
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
1112
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
1213
import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
1314
import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
@@ -16,7 +17,6 @@ import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea
1617
import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client'
1718
import { SlugField as SlugField_3817bf644402e67bfe6577f60ef982de } from '@payloadcms/ui'
1819
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
19-
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
2020
import { FolderTableCell as FolderTableCell_ab83ff7e88da8d3530831f296ec4756a } from '@payloadcms/ui/rsc'
2121
import { FolderField as FolderField_ab83ff7e88da8d3530831f296ec4756a } from '@payloadcms/ui/rsc'
2222
import { default as default_7b12fdf6bcfe60cc126dc2cbee538cd0 } from '@/components/Divider/index.tsx'
@@ -42,6 +42,7 @@ export const importMap = {
4242
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
4343
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
4444
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
45+
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
4546
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
4647
"@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
4748
"@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860,
@@ -50,7 +51,6 @@ export const importMap = {
5051
"@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860,
5152
"@payloadcms/ui#SlugField": SlugField_3817bf644402e67bfe6577f60ef982de,
5253
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
53-
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
5454
"@payloadcms/ui/rsc#FolderTableCell": FolderTableCell_ab83ff7e88da8d3530831f296ec4756a,
5555
"@payloadcms/ui/rsc#FolderField": FolderField_ab83ff7e88da8d3530831f296ec4756a,
5656
"@/components/Divider/index.tsx#default": default_7b12fdf6bcfe60cc126dc2cbee538cd0,
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import React from 'react'
22
import { Chart, ChartProps } from './charts'
33

4-
type Props = ChartProps & {
4+
import type { ChartBlock as ChartBlockProps } from 'src/payload-types'
5+
6+
type Props = {
57
className?: string
6-
}
8+
} & ChartBlockProps
79

8-
export const ChartBlock: React.FC<Props> = ({ className, ...restProps }) => {
10+
export const ChartBlock: React.FC<Props> = ({ className, ...rest }) => {
911
return (
1012
<div className={[className, 'not-prose'].filter(Boolean).join(' ')}>
11-
<Chart {...restProps} />
13+
<Chart {...(rest as ChartProps)} />
1214
</div>
1315
)
1416
}

src/blocks/ChartBlock/charts/index.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ export type ChartProps =
77
| ({ type: 'composed' } & ComposedChartProps)
88
| { type: 'line' | 'bar' | 'area' | 'pie' }
99

10-
export const Chart: React.FC<ChartProps> = (props) => {
11-
switch (props.type) {
10+
export const Chart: React.FC<ChartProps> = ({ type, ...rest }) => {
11+
switch (type) {
1212
case 'composed':
13-
return <ComposedChart {...props} />
13+
return <ComposedChart {...(rest as ComposedChartProps)} />
14+
default:
15+
return null
1416
}
15-
return null
1617
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { MarkdownBlock as MarkdownBlockProps } from 'src/payload-types'
2+
3+
import { cn } from '@/utilities/ui'
4+
import React from 'react'
5+
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext'
6+
7+
import Markdown from 'react-markdown'
8+
9+
// autolink literals, footnotes, strikethrough, tables, tasklists
10+
import remarkGfm from 'remark-gfm'
11+
12+
// math
13+
import rehypeKatex from 'rehype-katex'
14+
import remarkMath from 'remark-math'
15+
import 'katex/dist/katex.min.css'
16+
17+
import { Code as CodeHighlighter } from '../Code/Component.client'
18+
19+
type Props = {
20+
className?: string
21+
} & MarkdownBlockProps
22+
23+
export const MarkdownBlock: React.FC<Props> = ({ className, content }) => {
24+
const withClassName = cn(className)
25+
const markdownContent = (
26+
<Markdown
27+
remarkPlugins={[remarkGfm, remarkMath]}
28+
rehypePlugins={[rehypeKatex]}
29+
components={{
30+
// 1. 拦截 pre 标签
31+
pre(props) {
32+
const { children, ...rest } = props
33+
return <>{children}</>
34+
},
35+
// 2. 代码高亮逻辑
36+
code(props) {
37+
const { children, className, ...rest } = props
38+
const match = /language-(\w+)/.exec(className || '')
39+
if (match) {
40+
const content = String(children).replace(/\n$/, '')
41+
return <CodeHighlighter code={content} language={match[1]} />
42+
}
43+
return (
44+
<code {...rest} className={className}>
45+
{children}
46+
</code>
47+
)
48+
},
49+
}}
50+
>
51+
{convertLexicalToPlaintext({ data: content })}
52+
</Markdown>
53+
)
54+
55+
if (withClassName) {
56+
return <div className={withClassName}>{markdownContent}</div>
57+
} else {
58+
return markdownContent
59+
}
60+
}

src/blocks/MarkdownBlock/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Block } from 'payload'
2+
3+
import { lexicalEditor } from '@payloadcms/richtext-lexical'
4+
5+
export const MarkdownBlock: Block = {
6+
slug: 'markdown',
7+
interfaceName: 'MarkdownBlock',
8+
fields: [
9+
{
10+
name: 'content',
11+
type: 'richText',
12+
editor: lexicalEditor({
13+
features: [],
14+
}),
15+
label: false,
16+
required: true,
17+
},
18+
],
19+
}

src/collections/Posts/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Banner } from '../../blocks/Banner/config'
1515
import { Code } from '../../blocks/Code/config'
1616
import { MediaBlock } from '../../blocks/MediaBlock/config'
1717
import { ChartBlock } from '../../blocks/ChartBlock/config'
18+
import { MarkdownBlock } from '../../blocks/MarkdownBlock/config'
1819
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
1920
import { populateAuthors } from './hooks/populateAuthors'
2021
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
@@ -93,7 +94,7 @@ export const Posts: CollectionConfig<'posts'> = {
9394
...rootFeatures,
9495
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
9596
BlocksFeature({
96-
blocks: [Banner, Code, MediaBlock, ChartBlock()],
97+
blocks: [MediaBlock, Code, MarkdownBlock, Banner, ChartBlock()],
9798
}),
9899
FixedToolbarFeature(),
99100
InlineToolbarFeature(),

src/components/RichText/index.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,29 @@ import {
1212
} from '@payloadcms/richtext-lexical/react'
1313

1414
import { CodeBlock, CodeBlockProps } from '@/blocks/Code/Component'
15-
import { ChartBlock, ChartBlockProps } from '@/blocks/ChartBlock/Component'
1615

1716
import type {
1817
BannerBlock as BannerBlockProps,
1918
CallToActionBlock as CTABlockProps,
2019
MediaBlock as MediaBlockProps,
20+
ChartBlock as ChartBlockProps,
21+
MarkdownBlock as MarkdownBlockProps,
2122
} from '@/payload-types'
2223
import { BannerBlock } from '@/blocks/Banner/Component'
2324
import { CallToActionBlock } from '@/blocks/CallToAction/Component'
25+
import { ChartBlock } from '@/blocks/ChartBlock/Component'
26+
import { MarkdownBlock } from '@/blocks/MarkdownBlock/Component'
2427
import { cn } from '@/utilities/ui'
2528

2629
type NodeTypes =
2730
| DefaultNodeTypes
2831
| SerializedBlockNode<
29-
CTABlockProps | MediaBlockProps | BannerBlockProps | CodeBlockProps | ChartBlockProps
32+
| CTABlockProps
33+
| MediaBlockProps
34+
| BannerBlockProps
35+
| CodeBlockProps
36+
| ChartBlockProps
37+
| MarkdownBlockProps
3038
>
3139

3240
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
@@ -56,6 +64,7 @@ const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters })
5664
code: ({ node }) => <CodeBlock className="col-start-2" {...node.fields} />,
5765
cta: ({ node }) => <CallToActionBlock {...node.fields} />,
5866
chart: ({ node }) => <ChartBlock {...node.fields} />,
67+
markdown: ({ node }) => <MarkdownBlock {...node.fields} />,
5968
},
6069
})
6170

src/fields/defaultLexical.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ import {
66
ParagraphFeature,
77
lexicalEditor,
88
UnderlineFeature,
9+
BlocksFeature,
910
type LinkFields,
1011
} from '@payloadcms/richtext-lexical'
12+
import { MarkdownBlock } from '@/blocks/MarkdownBlock/config'
1113

1214
export const defaultLexical = lexicalEditor({
1315
features: [
1416
ParagraphFeature(),
1517
UnderlineFeature(),
1618
BoldFeature(),
1719
ItalicFeature(),
20+
BlocksFeature({
21+
blocks: [MarkdownBlock],
22+
}),
1823
LinkFeature({
1924
enabledCollections: ['pages', 'posts'],
2025
fields: ({ defaultFields }) => {

0 commit comments

Comments
 (0)