mirror of
https://github.com/langgenius/dify.git
synced 2026-03-26 05:29:50 +08:00
fix: prevent hydration warning from div nesting inside p for inline markdown images (#32419)
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
a808389122
commit
a5832df586
@ -8,13 +8,14 @@ vi.mock('@/app/components/base/image-gallery', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
type MockChildNode = {
|
||||
tagName?: string
|
||||
properties?: { src?: string }
|
||||
children?: MockChildNode[]
|
||||
}
|
||||
|
||||
type MockNode = {
|
||||
children?: Array<{
|
||||
tagName?: string
|
||||
properties?: {
|
||||
src?: string
|
||||
}
|
||||
}>
|
||||
children?: MockChildNode[]
|
||||
}
|
||||
|
||||
type ParagraphProps = {
|
||||
@ -93,4 +94,38 @@ describe('Paragraph', () => {
|
||||
|
||||
expect(screen.getByText('Fallback').tagName).toBe('P')
|
||||
})
|
||||
|
||||
it('should render div instead of p when image is not the first child', () => {
|
||||
renderParagraph({
|
||||
node: {
|
||||
children: [
|
||||
{ tagName: 'span' },
|
||||
{ tagName: 'img', properties: { src: 'test.png' } },
|
||||
],
|
||||
},
|
||||
children: [<span key="0">Text before</span>, <img key="1" src="test.png" alt="" />],
|
||||
})
|
||||
|
||||
const wrapper = screen.getByText('Text before').closest('.markdown-p')
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper!.tagName).toBe('DIV')
|
||||
})
|
||||
|
||||
it('should render div when image is nested inside a link', () => {
|
||||
renderParagraph({
|
||||
node: {
|
||||
children: [
|
||||
{
|
||||
tagName: 'a',
|
||||
children: [{ tagName: 'img', properties: { src: 'nested.png' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
children: <a href="#"><img src="nested.png" alt="" /></a>,
|
||||
})
|
||||
|
||||
const wrapper = screen.getByRole('link').closest('.markdown-p')
|
||||
expect(wrapper).toBeInTheDocument()
|
||||
expect(wrapper!.tagName).toBe('DIV')
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||
import { PluginParagraph } from '../plugin-paragraph'
|
||||
import { getMarkdownImageURL } from '../utils'
|
||||
import { getMarkdownImageURL, hasImageChild } from '../utils'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
@ -13,6 +13,7 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
|
||||
vi.mock('../utils', () => ({
|
||||
getMarkdownImageURL: vi.fn(),
|
||||
hasImageChild: vi.fn((): boolean => false),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/image-uploader/image-preview', () => ({
|
||||
@ -178,4 +179,24 @@ describe('PluginParagraph', () => {
|
||||
await user.click(closeBtn)
|
||||
expect(screen.queryByTestId('image-preview-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render div instead of p when image is not the first child', () => {
|
||||
vi.mocked(hasImageChild).mockReturnValue(true)
|
||||
|
||||
const node: MockNode = {
|
||||
children: [
|
||||
{ tagName: 'span' },
|
||||
{ tagName: 'img', properties: { src: 'test.png' } },
|
||||
],
|
||||
}
|
||||
|
||||
render(
|
||||
<PluginParagraph node={node}>
|
||||
<span>Text</span>
|
||||
</PluginParagraph>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('image-fallback-paragraph')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('image-fallback-paragraph').tagName).toBe('DIV')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,26 +1,25 @@
|
||||
/**
|
||||
* @fileoverview Paragraph component for rendering <p> tags in Markdown.
|
||||
* Extracted from the main markdown renderer for modularity.
|
||||
* Handles special rendering for paragraphs that directly contain an image.
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
import { hasImageChild } from './utils'
|
||||
|
||||
const Paragraph = (paragraph: any) => {
|
||||
const { node }: any = paragraph
|
||||
const children_node = node.children
|
||||
if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') {
|
||||
return (
|
||||
<div className="markdown-img-wrapper">
|
||||
<ImageGallery srcs={[children_node[0].properties.src]} />
|
||||
{
|
||||
Array.isArray(paragraph.children) && paragraph.children.length > 1 && (
|
||||
<div className="mt-2">{paragraph.children.slice(1)}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
const hasImage = hasImageChild(children_node)
|
||||
|
||||
if (hasImage) {
|
||||
if (children_node[0]?.tagName === 'img') {
|
||||
return (
|
||||
<div className="markdown-img-wrapper">
|
||||
<ImageGallery srcs={[children_node[0].properties.src]} />
|
||||
{Array.isArray(paragraph.children) && paragraph.children.length > 1
|
||||
? <div className="mt-2">{paragraph.children.slice(1)}</div>
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="markdown-p">{paragraph.children}</div>
|
||||
}
|
||||
|
||||
return <p>{paragraph.children}</p>
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
import type { SimplePluginInfo } from '../markdown/react-markdown-wrapper'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
/**
|
||||
* @fileoverview Paragraph component for rendering <p> tags in Markdown.
|
||||
* Extracted from the main markdown renderer for modularity.
|
||||
* Handles special rendering for paragraphs that directly contain an image.
|
||||
*/
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
import { usePluginReadmeAsset } from '@/service/use-plugins'
|
||||
import { getMarkdownImageURL } from './utils'
|
||||
import { getMarkdownImageURL, hasImageChild } from './utils'
|
||||
|
||||
type PluginParagraphProps = {
|
||||
pluginInfo?: SimplePluginInfo
|
||||
@ -66,5 +61,8 @@ export const PluginParagraph: React.FC<PluginParagraphProps> = ({ pluginInfo, no
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (hasImageChild(childrenNode))
|
||||
return <div className="markdown-p" data-testid="image-fallback-paragraph">{children}</div>
|
||||
|
||||
return <p data-testid="standard-paragraph">{children}</p>
|
||||
}
|
||||
|
||||
@ -1,5 +1,19 @@
|
||||
import { ALLOW_UNSAFE_DATA_SCHEME, MARKETPLACE_API_PREFIX } from '@/config'
|
||||
|
||||
type MdastNode = {
|
||||
tagName?: string
|
||||
children?: MdastNode[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export const hasImageChild = (children: MdastNode[] | undefined): boolean => {
|
||||
return children?.some((child) => {
|
||||
if (child.tagName === 'img')
|
||||
return true
|
||||
return child.children ? hasImageChild(child.children) : false
|
||||
}) ?? false
|
||||
}
|
||||
|
||||
export const isValidUrl = (url: string): boolean => {
|
||||
const validPrefixes = ['http:', 'https:', '//', 'mailto:']
|
||||
if (ALLOW_UNSAFE_DATA_SCHEME)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user