import { describe, expect, it } from 'vitest'
import { getMarkdown, parseMarkdownToStructure } from '../src'
function expectLinkChild(node: any, expected: {
href: string
text: string
loading: boolean
childType: string
childText: string
}) {
expect(node.type).toBe('link')
expect(node.href).toBe(expected.href)
expect(node.text).toBe(expected.text)
expect(node.loading).toBe(expected.loading)
expect(node.attrs).toEqual(expect.arrayContaining([['href', expected.href]]))
expect(node.children).toHaveLength(1)
expect(node.children[0].type).toBe(expected.childType)
expect(node.children[0].raw).toBe(expected.text)
expect(node.children[0].children).toHaveLength(1)
expect(node.children[0].children[0].type).toBe('text')
expect(node.children[0].children[0].content).toBe(expected.childText)
}
describe('strong link followed by inline content', () => {
const md = getMarkdown('strong-link-followed-by-inline-content')
it('keeps trailing text and the next italic link outside the preceding strong link', () => {
const markdown = `[**加粗的链接**](https://example.com)会导致后面的斜体链接解析不正确
[*斜体的链接*](https://test.com)`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(3)
expectLinkChild(paragraph.children[0], {
href: 'https://example.com',
text: '**加粗的链接**',
loading: false,
childType: 'strong',
childText: '加粗的链接',
})
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: '会导致后面的斜体链接解析不正确\n',
raw: '会导致后面的斜体链接解析不正确\n',
}))
expectLinkChild(paragraph.children[2], {
href: 'https://test.com',
text: '*斜体的链接*',
loading: false,
childType: 'emphasis',
childText: '斜体的链接',
})
})
it('keeps two inline links separated by the plain text between them', () => {
const markdown = `[**加粗的链接**](https://example.com) 或者 [*斜体的链接*](https://test.com)`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(3)
expectLinkChild(paragraph.children[0], {
href: 'https://example.com',
text: '**加粗的链接**',
loading: false,
childType: 'strong',
childText: '加粗的链接',
})
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: ' 或者 ',
raw: ' 或者 ',
}))
expectLinkChild(paragraph.children[2], {
href: 'https://test.com',
text: '*斜体的链接*',
loading: false,
childType: 'emphasis',
childText: '斜体的链接',
})
})
it('keeps trailing punctuation and explanatory text outside the strong link', () => {
const markdown = `[**加粗的链接**](https://example.com),这里不是链接部分,但是有链接效果`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
expectLinkChild(paragraph.children[0], {
href: 'https://example.com',
text: '**加粗的链接**',
loading: false,
childType: 'strong',
childText: '加粗的链接',
})
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: ',这里不是链接部分,但是有链接效果',
raw: ',这里不是链接部分,但是有链接效果',
}))
})
it('keeps immediate trailing text outside a strong link even without whitespace', () => {
const markdown = `[**加粗的链接**](https://example.com)tail`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
expectLinkChild(paragraph.children[0], {
href: 'https://example.com',
text: '**加粗的链接**',
loading: false,
childType: 'strong',
childText: '加粗的链接',
})
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: 'tail',
raw: 'tail',
}))
})
it('keeps completed strong and italic links stable in non-final parsing mode', () => {
const markdown = `[**加粗的链接**](https://example.com) 或者 [*斜体的链接*](https://test.com)`
const nodes = parseMarkdownToStructure(markdown, md, { final: false }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(3)
expectLinkChild(paragraph.children[0], {
href: 'https://example.com',
text: '**加粗的链接**',
loading: false,
childType: 'strong',
childText: '加粗的链接',
})
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: ' 或者 ',
raw: ' 或者 ',
}))
expectLinkChild(paragraph.children[2], {
href: 'https://test.com',
text: '*斜体的链接*',
loading: false,
childType: 'emphasis',
childText: '斜体的链接',
})
})
it('keeps strong links with inline code labels separated from trailing text', () => {
const markdown = `[**用 \`pnpm\` 安装**](https://example.com)tail`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
const link = paragraph.children[0]
expect(link.type).toBe('link')
expect(link.href).toBe('https://example.com')
expect(link.text).toBe('**用 pnpm 安装**')
expect(link.loading).toBe(false)
expect(link.children).toHaveLength(1)
expect(link.children[0].type).toBe('strong')
expect(link.children[0].children).toHaveLength(3)
expect(link.children[0].children[0]).toEqual(expect.objectContaining({
type: 'text',
content: '用 ',
}))
expect(link.children[0].children[1]).toEqual(expect.objectContaining({
type: 'inline_code',
code: 'pnpm',
}))
expect(link.children[0].children[2]).toEqual(expect.objectContaining({
type: 'text',
content: ' 安装',
}))
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: 'tail',
raw: 'tail',
}))
})
it('preserves title attrs on strong links while keeping trailing text outside the link', () => {
const markdown = `[**x**](https://a.com "title")tail`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
const link = paragraph.children[0]
expect(link.type).toBe('link')
expect(link.href).toBe('https://a.com')
expect(link.title).toBe('title')
expect(link.text).toBe('**x**')
expect(link.loading).toBe(false)
expect(link.attrs).toEqual(expect.arrayContaining([
['href', 'https://a.com'],
['title', 'title'],
]))
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: 'tail',
raw: 'tail',
}))
})
it('keeps adjacent strong-label and code-label links independent', () => {
const markdown = `[**x**](https://a.com)[\`y\`](https://b.com)`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
expectLinkChild(paragraph.children[0], {
href: 'https://a.com',
text: '**x**',
loading: false,
childType: 'strong',
childText: 'x',
})
const codeLink = paragraph.children[1]
expect(codeLink.type).toBe('link')
expect(codeLink.href).toBe('https://b.com')
expect(codeLink.text).toBe('y')
expect(codeLink.loading).toBe(false)
expect(codeLink.children).toHaveLength(1)
expect(codeLink.children[0]).toEqual(expect.objectContaining({
type: 'inline_code',
code: 'y',
raw: 'y',
}))
})
it('marks an unfinished strong link URL as loading in non-final mode', () => {
const markdown = `[**x**](https://a.com`
const nodes = parseMarkdownToStructure(markdown, md, { final: false }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(1)
const strong = paragraph.children[0]
expect(strong.type).toBe('strong')
expect(strong.children).toHaveLength(1)
const link = strong.children[0]
expect(link.type).toBe('link')
expect(link.loading).toBe(true)
expect(link.text).toBe('x')
expect(link.children).toHaveLength(1)
expect(link.children[0]).toEqual(expect.objectContaining({
type: 'text',
content: 'x',
raw: 'x',
}))
})
it('keeps triple-emphasis labels separated from trailing text', () => {
const markdown = `[***x***](https://a.com)tail`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
const link = paragraph.children[0]
expect(link.type).toBe('link')
expect(link.href).toBe('https://a.com')
expect(link.text).toBe('*x*')
expect(link.loading).toBe(false)
expect(link.children).toHaveLength(1)
expect(link.children[0].type).toBe('emphasis')
expect(link.children[0].children).toHaveLength(1)
expect(link.children[0].children[0].type).toBe('strong')
expect(link.children[0].children[0].children[0]).toEqual(expect.objectContaining({
type: 'text',
content: 'x',
}))
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: 'tail',
raw: 'tail',
}))
})
it('keeps html inline labels separated from trailing text', () => {
const markdown = `[x](https://a.com)tail`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
const link = paragraph.children[0]
expect(link.type).toBe('link')
expect(link.href).toBe('https://a.com')
expect(link.text).toBe('x')
expect(link.loading).toBe(false)
expect(link.children).toHaveLength(1)
expect(link.children[0]).toEqual(expect.objectContaining({
type: 'html_inline',
tag: 'span',
content: 'x',
loading: false,
}))
expect(link.children[0].children).toHaveLength(1)
expect(link.children[0].children[0]).toEqual(expect.objectContaining({
type: 'text',
content: 'x',
}))
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: 'tail',
raw: 'tail',
}))
})
it('keeps punctuation after a strong link outside the link node', () => {
const markdown = `[**x**](https://a.com)!tail`
const nodes = parseMarkdownToStructure(markdown, md, { final: true }) as any[]
expect(nodes).toHaveLength(1)
const paragraph = nodes[0]
expect(paragraph.type).toBe('paragraph')
expect(paragraph.children).toHaveLength(2)
expectLinkChild(paragraph.children[0], {
href: 'https://a.com',
text: '**x**',
loading: false,
childType: 'strong',
childText: 'x',
})
expect(paragraph.children[1]).toEqual(expect.objectContaining({
type: 'text',
content: '!tail',
raw: '!tail',
}))
})
})