Compare commits

...

1 Commits

Author SHA1 Message Date
Joel
908820acb4 feat: support render custom directive in markdown 2026-03-04 17:11:43 +08:00
3 changed files with 170 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
'use client'
import type { ReactNode } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkDirective from 'remark-directive'
import { visit } from 'unist-util-visit'
const markdown = `
Were speaking with technical teams to better understand:
- How you discovered Dify
- What resonated — and what didnt
- How we can improve the experience
:::::withiconlist{.mt-4}
::::withiconitem{icon="amazon"}
$100 Amazon gift card
:::withiconitem{icon="abc"}
inner
:::
::::
::::withiconitem{icon="dify"}
Exclusive **Dify** swag
::::
:::::
`
type WithIconListProps = {
children?: ReactNode
mt?: string | number
className?: string
class?: string
}
type WithIconItemProps = {
icon?: string
children?: ReactNode
}
type DirectiveNode = {
name?: string
attributes?: Record<string, string>
data?: {
hName?: string
hProperties?: Record<string, string>
}
}
function WithIconList({ children, mt, className }: WithIconListProps) {
const classValue = className || ''
const classMarginTop = classValue.includes('mt-4') ? 16 : 0
return (
<div style={{ marginTop: Number(mt || classMarginTop) }}>
<div style={{ padding: 16 }}>
{children}
</div>
</div>
)
}
function WithIconItem({ icon, children }: WithIconItemProps) {
return (
<div style={{ display: 'flex', border: '1px solid #ddd', gap: 8 }}>
<span>🔹</span>
<span>{children}</span>
<small style={{ color: '#999' }}>
{`(${icon})`}
</small>
</div>
)
}
function directivePlugin() {
return (tree: Parameters<typeof visit>[0]) => {
visit(
tree,
['textDirective', 'leafDirective', 'containerDirective'],
(node) => {
const directiveNode = node as DirectiveNode
const attributes = directiveNode.attributes || {}
const hProperties: Record<string, string> = { ...attributes }
if (hProperties.class) {
hProperties.className = hProperties.class
delete hProperties.class
}
const data = directiveNode.data || (directiveNode.data = {})
data.hName = directiveNode.name?.toLowerCase()
data.hProperties = hProperties
},
)
}
}
const directiveComponents = {
withiconlist: WithIconList,
withiconitem: WithIconItem,
} as unknown as import('react-markdown').Components
export default function RemarkDirectiveTestPage() {
return (
<main style={{ padding: 24 }}>
<h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 16 }}>
remark-directive test page
</h1>
<div className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkDirective, directivePlugin]}
components={directiveComponents}
>
{markdown}
</ReactMarkdown>
</div>
</main>
)
}

View File

@@ -152,6 +152,7 @@
"rehype-katex": "7.0.1",
"rehype-raw": "7.0.0",
"remark-breaks": "4.0.0",
"remark-directive": "4.0.0",
"remark-gfm": "4.0.1",
"remark-math": "6.0.0",
"scheduler": "0.27.0",
@@ -162,6 +163,7 @@
"tailwind-merge": "2.6.1",
"tldts": "7.0.17",
"ufo": "1.6.3",
"unist-util-visit": "5.1.0",
"use-context-selector": "2.0.0",
"uuid": "10.0.0",
"zod": "4.3.6",

48
web/pnpm-lock.yaml generated
View File

@@ -327,6 +327,9 @@ importers:
remark-breaks:
specifier: 4.0.0
version: 4.0.0
remark-directive:
specifier: 4.0.0
version: 4.0.0
remark-gfm:
specifier: 4.0.1
version: 4.0.1
@@ -357,6 +360,9 @@ importers:
ufo:
specifier: 1.6.3
version: 1.6.3
unist-util-visit:
specifier: 5.1.0
version: 5.1.0
use-context-selector:
specifier: 2.0.0
version: 2.0.0(react@19.2.4)(scheduler@0.27.0)
@@ -5989,6 +5995,9 @@ packages:
engines: {node: '>= 18'}
hasBin: true
mdast-util-directive@3.1.0:
resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==}
mdast-util-find-and-replace@3.0.2:
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
@@ -6074,6 +6083,9 @@ packages:
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
micromark-extension-directive@4.0.0:
resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==}
micromark-extension-frontmatter@2.0.0:
resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==}
@@ -6971,6 +6983,9 @@ packages:
remark-breaks@4.0.0:
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
remark-directive@4.0.0:
resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -14048,6 +14063,20 @@ snapshots:
marked@15.0.12: {}
mdast-util-directive@3.1.0:
dependencies:
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
ccount: 2.0.1
devlop: 1.1.0
mdast-util-from-markdown: 2.0.2
mdast-util-to-markdown: 2.1.2
parse-entities: 4.0.2
stringify-entities: 4.0.4
unist-util-visit-parents: 6.0.2
transitivePeerDependencies:
- supports-color
mdast-util-find-and-replace@3.0.2:
dependencies:
'@types/mdast': 4.0.4
@@ -14295,6 +14324,16 @@ snapshots:
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
micromark-extension-directive@4.0.0:
dependencies:
devlop: 1.1.0
micromark-factory-space: 2.0.1
micromark-factory-whitespace: 2.0.1
micromark-util-character: 2.1.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.2
parse-entities: 4.0.2
micromark-extension-frontmatter@2.0.0:
dependencies:
fault: 2.0.1
@@ -15427,6 +15466,15 @@ snapshots:
mdast-util-newline-to-break: 2.0.0
unified: 11.0.5
remark-directive@4.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-directive: 3.1.0
micromark-extension-directive: 4.0.0
unified: 11.0.5
transitivePeerDependencies:
- supports-color
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4