mirror of
https://github.com/langgenius/dify.git
synced 2026-04-26 21:55:58 +08:00
feat: support check valid
This commit is contained in:
@ -0,0 +1,37 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { WithIconItemDirectiveProps, WithIconListDirectiveProps } from './directive-props-schema'
|
||||
|
||||
type WithIconListProps = WithIconListDirectiveProps & {
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
type WithIconItemProps = WithIconItemDirectiveProps & {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
|
||||
export function WithIconItem({ icon, b, children }: WithIconItemProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', border: '1px solid #ddd', gap: 8 }}>
|
||||
<span>🔹</span>
|
||||
{b && <span>{b}</span>}
|
||||
<span>{children}</span>
|
||||
<small style={{ color: '#999' }}>
|
||||
{`(${icon})`}
|
||||
</small>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
import type { Components } from 'react-markdown'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkDirective from 'remark-directive'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { WithIconItem, WithIconList } from './directive-components'
|
||||
import { directivePropsSchemas } from './directive-props-schema'
|
||||
|
||||
type DirectiveNode = {
|
||||
type?: string
|
||||
name?: string
|
||||
attributes?: Record<string, unknown>
|
||||
data?: {
|
||||
hName?: string
|
||||
hProperties?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
type DirectiveName = keyof typeof directivePropsSchemas
|
||||
|
||||
function isDirectiveName(name: string): name is DirectiveName {
|
||||
return Object.hasOwn(directivePropsSchemas, name)
|
||||
}
|
||||
|
||||
function isValidDirectiveProps(name: string, attributes: Record<string, string>): boolean {
|
||||
if (!isDirectiveName(name))
|
||||
return false
|
||||
|
||||
return directivePropsSchemas[name].safeParse(attributes).success
|
||||
}
|
||||
|
||||
type MdastRoot = {
|
||||
type: 'root'
|
||||
children: Array<{
|
||||
type: string
|
||||
children?: Array<{ type: string, value?: string }>
|
||||
value?: string
|
||||
}>
|
||||
}
|
||||
|
||||
function isMdastRoot(node: Parameters<typeof visit>[0]): node is MdastRoot {
|
||||
if (typeof node !== 'object' || node === null)
|
||||
return false
|
||||
|
||||
const candidate = node as { type?: unknown, children?: unknown }
|
||||
return candidate.type === 'root' && Array.isArray(candidate.children)
|
||||
}
|
||||
|
||||
function normalizeDirectiveAttributeBlocks(markdown: string): string {
|
||||
const lines = markdown.split('\n')
|
||||
|
||||
return lines.map((line) => {
|
||||
const match = line.match(/^(\s*:+[a-z][\w-]*(?:\[[^\]\n]*\])?)\s+((?:\{[^}\n]*\}\s*)+)$/i)
|
||||
if (!match)
|
||||
return line
|
||||
|
||||
const directivePrefix = match[1]
|
||||
const attributeBlocks = match[2]
|
||||
const attrMatches = [...attributeBlocks.matchAll(/\{([^}\n]*)\}/g)]
|
||||
if (attrMatches.length === 0)
|
||||
return line
|
||||
|
||||
const mergedAttributes = attrMatches
|
||||
.map(result => result[1].trim())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return mergedAttributes
|
||||
? `${directivePrefix}{${mergedAttributes}}`
|
||||
: directivePrefix
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
function normalizeDirectiveAttributes(attributes?: Record<string, unknown>): Record<string, string> {
|
||||
const normalized: Record<string, string> = {}
|
||||
|
||||
if (!attributes)
|
||||
return normalized
|
||||
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
if (typeof value === 'string')
|
||||
normalized[key] = value
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function isValidDirectiveAst(tree: Parameters<typeof visit>[0]): boolean {
|
||||
let isValid = true
|
||||
|
||||
visit(
|
||||
tree,
|
||||
['textDirective', 'leafDirective', 'containerDirective'],
|
||||
(node) => {
|
||||
if (!isValid)
|
||||
return
|
||||
|
||||
const directiveNode = node as DirectiveNode
|
||||
const directiveName = directiveNode.name?.toLowerCase()
|
||||
if (!directiveName) {
|
||||
isValid = false
|
||||
return
|
||||
}
|
||||
|
||||
const attributes = normalizeDirectiveAttributes(directiveNode.attributes)
|
||||
if (!isValidDirectiveProps(directiveName, attributes))
|
||||
isValid = false
|
||||
},
|
||||
)
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
function hasUnparsedDirectiveLikeText(tree: Parameters<typeof visit>[0]): boolean {
|
||||
let hasInvalidText = false
|
||||
|
||||
visit(tree, 'text', (node) => {
|
||||
if (hasInvalidText)
|
||||
return
|
||||
|
||||
const textNode = node as { value?: string }
|
||||
const value = textNode.value || ''
|
||||
if (/^\s*:{2,}[a-z][\w-]*/im.test(value))
|
||||
hasInvalidText = true
|
||||
})
|
||||
|
||||
return hasInvalidText
|
||||
}
|
||||
|
||||
function replaceWithInvalidContent(tree: Parameters<typeof visit>[0]) {
|
||||
if (!isMdastRoot(tree))
|
||||
return
|
||||
|
||||
const root = tree
|
||||
root.children = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'invalid content',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function directivePlugin() {
|
||||
return (tree: Parameters<typeof visit>[0]) => {
|
||||
if (!isValidDirectiveAst(tree) || hasUnparsedDirectiveLikeText(tree)) {
|
||||
replaceWithInvalidContent(tree)
|
||||
return
|
||||
}
|
||||
|
||||
visit(
|
||||
tree,
|
||||
['textDirective', 'leafDirective', 'containerDirective'],
|
||||
(node) => {
|
||||
const directiveNode = node as DirectiveNode
|
||||
const attributes = normalizeDirectiveAttributes(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 Components
|
||||
|
||||
type DirectiveMarkdownRendererProps = {
|
||||
markdown: string
|
||||
}
|
||||
|
||||
export function DirectiveMarkdownRenderer({ markdown }: DirectiveMarkdownRendererProps) {
|
||||
const normalizedMarkdown = normalizeDirectiveAttributeBlocks(markdown)
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkDirective, directivePlugin]}
|
||||
components={directiveComponents}
|
||||
>
|
||||
{normalizedMarkdown}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const withIconListDirectivePropsSchema = z.object({
|
||||
class: z.string().trim().min(1).optional(),
|
||||
mt: z.string().trim().min(1).optional(),
|
||||
}).strict()
|
||||
|
||||
export const withIconItemDirectivePropsSchema = z.object({
|
||||
icon: z.string().trim().min(1),
|
||||
b: z.string().trim().min(1).optional(),
|
||||
}).strict()
|
||||
|
||||
export const directivePropsSchemas = {
|
||||
withiconlist: withIconListDirectivePropsSchema,
|
||||
withiconitem: withIconItemDirectivePropsSchema,
|
||||
} as const
|
||||
|
||||
export type WithIconListDirectiveProps = z.infer<typeof withIconListDirectivePropsSchema>
|
||||
export type WithIconItemDirectiveProps = z.infer<typeof withIconItemDirectivePropsSchema>
|
||||
@ -1,8 +1,5 @@
|
||||
'use client'
|
||||
import type { ReactNode } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkDirective from 'remark-directive'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { DirectiveMarkdownRenderer } from './directive-markdown-renderer'
|
||||
|
||||
const markdown = `
|
||||
We’re speaking with technical teams to better understand:
|
||||
@ -11,109 +8,29 @@ We’re speaking with technical teams to better understand:
|
||||
- What resonated — and what didn’t
|
||||
- How we can improve the experience
|
||||
|
||||
:::::withiconlist{.mt-4}
|
||||
::::withiconlist{.mt-4}
|
||||
|
||||
::::withiconitem{icon="amazon"}
|
||||
:::withiconitem {icon="amazon"} {b="3"}
|
||||
$100 Amazon gift card
|
||||
:::withiconitem{icon="abc"}
|
||||
inner
|
||||
:::
|
||||
::::
|
||||
|
||||
::::withiconitem{icon="dify"}
|
||||
Exclusive **Dify** swag
|
||||
::::
|
||||
:::withiconitem {icon="amazon2"}
|
||||
$100 Amazon gift card2
|
||||
:::
|
||||
|
||||
:::::
|
||||
::withiconitem[Exclusive Dify swag]{icon="dify"}
|
||||
|
||||
::::
|
||||
`
|
||||
|
||||
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>
|
||||
<DirectiveMarkdownRenderer markdown={markdown} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user