Skip to content

Commit c85030c

Browse files
committed
Handle markdown image links
1 parent 0e5baec commit c85030c

File tree

2 files changed

+83
-8
lines changed

2 files changed

+83
-8
lines changed

packages/components/src/components/Markdown.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ export default function Markdown({ text, className }: MarkdownProps) {
1414
function renderTextSegments(text: string): ReactNode[] {
1515
let result: ReactNode[] = []
1616

17-
// Parse images: ![alt](url)
17+
// Process in order: image links, images, regular links, and then formatting
18+
const imageInsideLinkRegex = /\[!\[([^\]]*)\]\(([^)]+)\)\]\(([^)]+)\)/g
19+
// Handle mixed content within links: [text ![img](img_url) more text](link_url)
20+
const mixedContentLinkRegex = /\[([^\]]*?!\[[^\]]*?\]\([^)]+?\)[^\]]*?)\]\(([^)]+)\)/g
1821
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
1922
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
2023
const codeRegex = /`([^`]+)`/g
@@ -33,6 +36,7 @@ export default function Markdown({ text, className }: MarkdownProps) {
3336
const str = segment
3437
let lastIndex = 0
3538
let match: RegExpExecArray | null
39+
regex.lastIndex = 0 // Reset regex for safety
3640
while ((match = regex.exec(str)) !== null) {
3741
// Add text before match
3842
if (match.index > lastIndex) {
@@ -56,13 +60,24 @@ export default function Markdown({ text, className }: MarkdownProps) {
5660
// Start with entire text as a single segment
5761
result = [text]
5862

59-
// Apply in a certain order:
60-
result = applyRegex(result, imageRegex, (m) => <img alt={m[1]} src={m[2]} />)
61-
result = applyRegex(result, linkRegex, (m) => <a href={m[2]}>{m[1]}</a>)
62-
result = applyRegex(result, codeRegex, (m) => <code>{m[1]}</code>)
63-
result = applyRegex(result, boldRegex, (m) => <strong>{m[1]}</strong>)
64-
result = applyRegex(result, italicRegex, (m) => <em>{m[1]}</em>)
65-
result = applyRegex(result, underlineRegex, (m) => <u>{m[1]}</u>)
63+
// Apply in a specific order to handle nested elements:
64+
// First handle image-inside-link pattern
65+
result = applyRegex(result, imageInsideLinkRegex, (m) => <a href={m[3]} key={`imglink-${m[3]}`}>
66+
<img alt={m[1]} src={m[2]} key={`img-in-link-${m[2]}`} />
67+
</a>)
68+
69+
// Then handle mixed content links (with images and text)
70+
result = applyRegex(result, mixedContentLinkRegex, (m) => <a href={m[2]} key={`mixed-${m[2]}`}>{parseInline(m[1])}</a>)
71+
72+
// Then handle regular images and links
73+
result = applyRegex(result, imageRegex, (m) => <img key={`img-${m[2]}`} alt={m[1]} src={m[2]} />)
74+
result = applyRegex(result, linkRegex, (m) => <a href={m[2]} key={`link-${m[2]}`}>{parseInline(m[1])}</a>)
75+
76+
// Finally handle text formatting
77+
result = applyRegex(result, codeRegex, (m) => <code key={`code-${m.index}`}>{m[1]}</code>)
78+
result = applyRegex(result, boldRegex, (m) => <strong key={`bold-${m.index}`}>{m[1]}</strong>)
79+
result = applyRegex(result, italicRegex, (m) => <em key={`italic-${m.index}`}>{m[1]}</em>)
80+
result = applyRegex(result, underlineRegex, (m) => <u key={`underline-${m.index}`}>{m[1]}</u>)
6681

6782
return result
6883
}

packages/components/test/components/Markdown.test.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,63 @@ describe('Markdown', () => {
9999
expect(code?.textContent).toBe('console.log(\'Hello, world!\')')
100100
})
101101
})
102+
103+
describe('Markdown with nested elements', () => {
104+
it('renders an image inside a link', () => {
105+
const text = '[![mit license](https://img.shields.io/badge/License-MIT-orange.svg)](https://opensource.org/licenses/MIT)'
106+
const { container } = render(<Markdown text={text} />)
107+
108+
// Check that we have an image
109+
const img = container.querySelector('img')
110+
expect(img).toBeDefined()
111+
expect(img?.getAttribute('alt')).toBe('mit license')
112+
expect(img?.getAttribute('src')).toBe('https://img.shields.io/badge/License-MIT-orange.svg')
113+
114+
// Check that the image is inside a link
115+
const link = container.querySelector('a')
116+
expect(link).toBeDefined()
117+
expect(link?.getAttribute('href')).toBe('https://opensource.org/licenses/MIT')
118+
expect(link?.contains(img)).toBe(true)
119+
})
120+
121+
it('handles multiple images inside links in one paragraph', () => {
122+
const text = 'Check [![license](https://img.shields.io/badge/License-MIT-orange.svg)](https://opensource.org/licenses/MIT) and [![npm](https://img.shields.io/npm/v/package.svg)](https://www.npmjs.com/package)'
123+
const { container } = render(<Markdown text={text} />)
124+
125+
const links = container.querySelectorAll('a')
126+
expect(links.length).toBe(2)
127+
128+
const images = container.querySelectorAll('img')
129+
expect(images.length).toBe(2)
130+
131+
// First link contains first image
132+
expect(links[0].getAttribute('href')).toBe('https://opensource.org/licenses/MIT')
133+
expect(links[0].contains(images[0])).toBe(true)
134+
expect(images[0].getAttribute('alt')).toBe('license')
135+
136+
// Second link contains second image
137+
expect(links[1].getAttribute('href')).toBe('https://www.npmjs.com/package')
138+
expect(links[1].contains(images[1])).toBe(true)
139+
expect(images[1].getAttribute('alt')).toBe('npm')
140+
})
141+
142+
it('handles images and text inside links', () => {
143+
const text = '[Click here ![icon](https://example.com/icon.png) for more info](https://example.com)'
144+
const { container } = render(<Markdown text={text} />)
145+
146+
const link = container.querySelector('a')
147+
expect(link).toBeDefined()
148+
expect(link?.getAttribute('href')).toBe('https://example.com')
149+
150+
// Check that the link contains both text fragments
151+
const linkText = link?.textContent ?? ''
152+
expect(linkText.includes('Click here')).toBe(true)
153+
expect(linkText.includes('for more info')).toBe(true)
154+
155+
// Image should be inside the link
156+
const img = container.querySelector('img')
157+
expect(img).toBeDefined()
158+
expect(img?.getAttribute('src')).toBe('https://example.com/icon.png')
159+
expect(link?.contains(img)).toBe(true)
160+
})
161+
})

0 commit comments

Comments
 (0)