feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,761 @@
|
||||
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const md = getMarkdown('fix-link-inline')
|
||||
|
||||
function collectLinks(nodes: any[]) {
|
||||
const out: any[] = []
|
||||
const walk = (n: any) => {
|
||||
if (!n)
|
||||
return
|
||||
if (n.type === 'link')
|
||||
out.push(n)
|
||||
if (Array.isArray(n.children))
|
||||
n.children.forEach(walk)
|
||||
if (Array.isArray(n.items))
|
||||
n.items.forEach(walk)
|
||||
if (n.type === 'table') {
|
||||
n.rows.forEach(walk)
|
||||
n.header.cells.forEach(walk)
|
||||
}
|
||||
else if (n.type === 'table_row') {
|
||||
n.cells.forEach(walk)
|
||||
}
|
||||
}
|
||||
nodes.forEach(walk)
|
||||
return out
|
||||
}
|
||||
|
||||
function collectTarget(nodes: any[], targetType: string) {
|
||||
const out: any[] = []
|
||||
const walk = (n: any) => {
|
||||
if (!n)
|
||||
return
|
||||
if (n.type === targetType)
|
||||
out.push(n)
|
||||
if (Array.isArray(n.children))
|
||||
n.children.forEach(walk)
|
||||
if (Array.isArray(n.items))
|
||||
n.items.forEach(walk)
|
||||
if (n.type === 'table') {
|
||||
n.rows.forEach(walk)
|
||||
n.header.cells.forEach(walk)
|
||||
}
|
||||
else if (n.type === 'table_row') {
|
||||
n.cells.forEach(walk)
|
||||
}
|
||||
}
|
||||
nodes.forEach(walk)
|
||||
return out
|
||||
}
|
||||
|
||||
describe('inline parser fixes (link mid-states)', () => {
|
||||
it('handles loading link [x](http://a as loading link node', () => {
|
||||
const nodes = parseMarkdownToStructure('[x](http://a', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.text).toBe('x')
|
||||
// loading should be true for unclosed parenthesis
|
||||
expect(Boolean(l.loading)).toBe(true)
|
||||
// href may be present or empty depending on tokenizer, but if present contains the partial href
|
||||
if (l.href)
|
||||
expect(String(l.href)).toContain('http')
|
||||
})
|
||||
|
||||
it('does not treat [*x as a finalized link', () => {
|
||||
const nodes = parseMarkdownToStructure('[*x', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
// Should not create a link from just '[*x'
|
||||
expect(links.length).toBe(0)
|
||||
})
|
||||
|
||||
it('parses finalized link [x](http://a) as non-loading link', () => {
|
||||
const nodes = parseMarkdownToStructure('[x](http://a)', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.loading).toBeFalsy()
|
||||
expect(String(l.href)).toContain('http://a')
|
||||
})
|
||||
|
||||
it('simple inline link', () => {
|
||||
const nodes = parseMarkdownToStructure('[OpenAI](https://openai.com)', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.loading).toBeFalsy()
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
expect(l.text).toBe('OpenAI')
|
||||
})
|
||||
|
||||
it('inline link with a *title* (tooltip)', () => {
|
||||
const nodes = parseMarkdownToStructure('[OpenAI](https://openai.com "OpenAI – AI research")', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.loading).toBeFalsy()
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
expect(l.text).toBe('OpenAI')
|
||||
// title may be stored on `title` or inside attributes depending on tokenizer
|
||||
if ('title' in l)
|
||||
expect(String((l as any).title)).toContain('OpenAI – AI research')
|
||||
else if (l.attrs && l.attrs.title)
|
||||
expect(String(l.attrs.title)).toContain('OpenAI – AI research')
|
||||
})
|
||||
|
||||
it('**Bold** link', () => {
|
||||
const nodes = parseMarkdownToStructure('**[OpenAI](https://openai.com)**', md)
|
||||
const strong = (nodes[0] as any).children[0]
|
||||
const link = strong.children[0]
|
||||
expect(strong.type).toBe('strong')
|
||||
expect(strong.raw).toBe('**OpenAI**')
|
||||
expect(strong.children[0]).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
expect(link.text).toBe('OpenAI')
|
||||
expect(link.loading).toBeFalsy()
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
})
|
||||
|
||||
it('*Italic* link', () => {
|
||||
const nodes = parseMarkdownToStructure('*[OpenAI](https://openai.com)*', md)
|
||||
const emphasis = (nodes[0] as any).children[0]
|
||||
const link = emphasis.children[0]
|
||||
expect(emphasis.type).toBe('emphasis')
|
||||
expect(emphasis.raw).toBe('*OpenAI*')
|
||||
expect(emphasis.children[0]).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
expect(link.text).toBe('OpenAI')
|
||||
expect(link.loading).toBeFalsy()
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
})
|
||||
|
||||
it('***Bold + Italic*** link', () => {
|
||||
const nodes = parseMarkdownToStructure('***[OpenAI](https://openai.com)***', md)
|
||||
const wrapper = (nodes[0] as any).children[0]
|
||||
// markdown parsers may represent triple-asterisk as strong->emphasis or emphasis->strong
|
||||
expect(wrapper).toBeDefined()
|
||||
// If wrapper is strong, its first child should be emphasis which contains the link
|
||||
if (wrapper.type === 'strong') {
|
||||
const emphasis = wrapper.children[0]
|
||||
expect(emphasis).toBeDefined()
|
||||
expect(emphasis.type).toBe('emphasis')
|
||||
const link = emphasis.children[0]
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
expect(link.text).toBe('OpenAI')
|
||||
expect(link.loading).toBeFalsy()
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
}
|
||||
else if (wrapper.type === 'emphasis') {
|
||||
// If wrapper is emphasis, its first child may be strong which contains the link
|
||||
const strong = wrapper.children[0]
|
||||
expect(strong).toBeDefined()
|
||||
expect(strong.type).toBe('strong')
|
||||
const link = strong.children[0]
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
expect(link.text).toBe('OpenAI')
|
||||
expect(link.loading).toBeFalsy()
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
}
|
||||
else {
|
||||
// Fallback: try to find link anywhere under wrapper
|
||||
const walk = (n: any): any => {
|
||||
if (!n)
|
||||
return null
|
||||
if (n.type === 'link')
|
||||
return n
|
||||
if (Array.isArray(n.children)) {
|
||||
for (const c of n.children) {
|
||||
const found = walk(c)
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const link = walk(wrapper)
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
expect(link.text).toBe('OpenAI')
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
}
|
||||
})
|
||||
|
||||
it('link inside a heading', () => {
|
||||
const nodes = parseMarkdownToStructure('## Learn more at [OpenAI](https://openai.com)', md)
|
||||
const heading = nodes[0] as any
|
||||
expect(heading).toBeDefined()
|
||||
expect(heading.type).toBe('heading')
|
||||
// heading children should contain the link somewhere
|
||||
const link = collectLinks(heading.children || [])[0] as any
|
||||
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
expect(link.text || link.children?.[0]?.content).toBeDefined()
|
||||
// text content should include OpenAI
|
||||
const text = link.text || link.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
})
|
||||
|
||||
it('link inside a blockquote', () => {
|
||||
const nodes = parseMarkdownToStructure('> Check out the docs: [OpenAI API](https://platform.openai.com/docs)', md)
|
||||
const blockquote = nodes[0] as any
|
||||
expect(blockquote).toBeDefined()
|
||||
expect(blockquote.type).toBe('blockquote')
|
||||
const link = collectLinks(blockquote.children || [])[0] as any
|
||||
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
const text = link.text || link.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI API')
|
||||
expect(String(link.href)).toContain('https://platform.openai.com/docs')
|
||||
})
|
||||
|
||||
it('link inside a list', () => {
|
||||
const nodes = parseMarkdownToStructure('- Official site: [OpenAI](https://openai.com)', md)
|
||||
const listNode = nodes[0] as any
|
||||
expect(listNode).toBeDefined()
|
||||
// lists may be represented as 'list' with items or 'list_item' children; search recursively
|
||||
|
||||
const link = collectLinks(listNode.children || listNode.items || [])[0] as any
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
const text = link.text || link.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
})
|
||||
|
||||
it('numbered list with links', () => {
|
||||
const nodes = parseMarkdownToStructure('1. First: [OpenAI](https://openai.com)', md)
|
||||
const listNode = nodes[0] as any
|
||||
expect(listNode).toBeDefined()
|
||||
|
||||
const link = collectLinks(listNode.children || listNode.items || [])[0] as any
|
||||
expect(link).toBeDefined()
|
||||
expect(link.type).toBe('link')
|
||||
const text = link.text || link.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
})
|
||||
|
||||
it('link inside a table', () => {
|
||||
const tableMd = `| Service | URL |
|
||||
|---------|-----------------------------|
|
||||
| OpenAI | [openai.com](https://openai.com) |
|
||||
| ChatGPT | [chat.openai.com](https://chat.openai.com) |`
|
||||
|
||||
const nodes = parseMarkdownToStructure(tableMd, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
// expect at least two links from the two table cells
|
||||
expect(links.length).toBeGreaterThanOrEqual(2)
|
||||
const hrefs = links.map((l: any) => String(l.href))
|
||||
expect(hrefs.some((h: string) => h.includes('https://openai.com'))).toBe(true)
|
||||
expect(hrefs.some((h: string) => h.includes('https://chat.openai.com'))).toBe(true)
|
||||
})
|
||||
|
||||
it('**Bold link inside a table**', () => {
|
||||
const tableMd = `| Service | URL |
|
||||
|---------|-------------------------------------|
|
||||
| OpenAI | **[openai.com](https://openai.com)** |
|
||||
| ChatGPT | **[chat.openai.com](https://chat.openai.com)** |`
|
||||
|
||||
const nodes = parseMarkdownToStructure(tableMd, md)
|
||||
const strongs = collectTarget(nodes as any[], 'strong')
|
||||
expect(strongs.length).toBeGreaterThanOrEqual(2)
|
||||
const links = collectLinks(strongs as any[])
|
||||
expect(links.length).toBeGreaterThanOrEqual(2)
|
||||
const hrefs = links.map((l: any) => String(l.href))
|
||||
expect(hrefs.some((h: string) => h.includes('https://openai.com'))).toBe(true)
|
||||
expect(hrefs.some((h: string) => h.includes('https://chat.openai.com'))).toBe(true)
|
||||
})
|
||||
|
||||
it('link with **escaped parentheses** in the URL', () => {
|
||||
const mdText = '[Google Maps](https://www.google.com/maps/place/Statue+of+Liberty+(New+York)/)'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.text).toBe('Google Maps')
|
||||
const hrefStr = String(l.href)
|
||||
expect(hrefStr).toContain('https://www.google.com/maps')
|
||||
// parentheses may be preserved or percent-encoded; ensure key parts exist
|
||||
expect(hrefStr).toMatch(/Statue/i)
|
||||
expect(hrefStr).toMatch(/New\s*York|New\+York|New%20York/i)
|
||||
})
|
||||
|
||||
it('autolink (bare URL)', () => {
|
||||
const nodes = parseMarkdownToStructure('<https://openai.com>', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
// href should contain the URL
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
// text may be on `text` or children content
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toBe('https://openai.com')
|
||||
})
|
||||
|
||||
it('reference‑style link', () => {
|
||||
const mdText = `Visit the [OpenAI website][openai].
|
||||
|
||||
[openai]: https://openai.com "OpenAI – AI research"`
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
// href should point to OpenAI
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
// text should include the label used
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toBe('OpenAI website')
|
||||
expect(String(l.title)).toBe('OpenAI – AI research')
|
||||
expect(l.loading).toBeFalsy()
|
||||
})
|
||||
|
||||
it('reference‑style link **inside bold**', () => {
|
||||
const mdText = `**See the [OpenAI docs][docs] for details.**
|
||||
|
||||
[docs]: https://platform.openai.com/docs`
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const strong = (nodes[0] as any).children?.find((c: any) => c.type === 'strong')
|
||||
expect(strong).toBeDefined()
|
||||
const link = (strong.children || []).find((c: any) => c.type === 'link')
|
||||
expect(link).toBeDefined()
|
||||
expect(String(link.href)).toBe('https://platform.openai.com/docs')
|
||||
const text = link.text || link.children?.[0]?.content || ''
|
||||
expect(String(text)).toBe('OpenAI docs')
|
||||
})
|
||||
|
||||
it('image that is also a link', () => {
|
||||
const mdText = '[](https://openai.com)'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const hasImageChild = (Array.isArray(l.children) && l.children.some((c: any) => c.type === 'image'))
|
||||
|| (Array.isArray((l as any).items) && (l as any).items.some((c: any) => c.type === 'image'))
|
||||
expect(hasImageChild).toBe(true)
|
||||
})
|
||||
|
||||
it('link inside **inline code** (displayed as code, not clickable)', () => {
|
||||
const mdText = '`[OpenAI](https://openai.com)`'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
// ensure no links are parsed
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBe(0)
|
||||
// ensure an inline_code node exists and contains the raw code
|
||||
const inline = (nodes[0] as any).children?.find((c: any) => c.type === 'inline_code')
|
||||
expect(inline).toBeDefined()
|
||||
expect(inline.code || inline.raw).toContain('[OpenAI](https://openai.com)')
|
||||
})
|
||||
|
||||
it('link inside a **code block** (fenced) – not rendered as a link', () => {
|
||||
const mdText = '```\n[OpenAI](https://openai.com)\n```'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
// No link nodes should be produced from code block content
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBe(0)
|
||||
// The raw code should still appear somewhere in the node tree
|
||||
const serialized = JSON.stringify(nodes)
|
||||
expect(serialized).toContain('[OpenAI](https://openai.com)')
|
||||
})
|
||||
|
||||
it('link inside **HTML** (raw HTML allowed in many Markdown flavours)', () => {
|
||||
const mdText = '<p>Visit <a href="https://openai.com">OpenAI</a> for more info.'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
// Some tokenizers may emit an HTML node, others may parse the anchor into link nodes.
|
||||
const links = collectLinks(nodes as any[])
|
||||
if (links.length > 0) {
|
||||
const l = links[0]
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
}
|
||||
else {
|
||||
// fallback: ensure raw HTML is present in serialized output
|
||||
const serialized = JSON.stringify(nodes)
|
||||
expect(serialized).toContain('<a href=\\"https://openai.com\\">OpenAI</a>')
|
||||
}
|
||||
})
|
||||
|
||||
it('link with **emoji** and bold', () => {
|
||||
const mdText = '**🚀 [Launch OpenAI](https://openai.com)**'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
// expect a strong node wrapping the emoji+link
|
||||
const strong = (nodes[0] as any).children?.find((c: any) => c.type === 'strong')
|
||||
expect(strong).toBeDefined()
|
||||
expect(strong)
|
||||
// link may be a direct child of strong
|
||||
const link = (strong.children || []).find((c: any) => c.type === 'link')
|
||||
expect(link).toBeDefined()
|
||||
expect(String(link.href)).toContain('https://openai.com')
|
||||
const text = link.text || link.children?.[0]?.content || ''
|
||||
// ensure link text contains expected word
|
||||
expect(String(text)).toContain('Launch')
|
||||
})
|
||||
|
||||
it('link inside a **definition list** (GitHub‑flavoured Markdown)', () => {
|
||||
const mdText = 'Term \n: Definition with a link to the [OpenAI site](https://openai.com).'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
})
|
||||
|
||||
it('link with **multiple titles** (only the first title is used)', () => {
|
||||
const mdText = `[OpenAI](https://openai.com "First title" "Second title")`
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toBe('OpenAI')
|
||||
const titleVal = ('title' in l) ? (l as any).title : (l.attrs && l.attrs.title)
|
||||
// Only assert title behavior if a non-empty title is present
|
||||
if (titleVal && String(titleVal).length > 0) {
|
||||
expect(String(titleVal)).toBe('First title')
|
||||
expect(String(titleVal)).not.toContain('Second title')
|
||||
}
|
||||
})
|
||||
|
||||
it('link inside a **nested list**', () => {
|
||||
const mdText = `- Main item
|
||||
1. Sub‑item with a link: [OpenAI](https://openai.com)
|
||||
2. Another sub‑item`
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links.find((x: any) => String(x.href) === 'https://openai.com')
|
||||
expect(l).toBeDefined()
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
})
|
||||
|
||||
it('link with **non‑ASCII characters**', () => {
|
||||
const mdText = `[Åbent bibliotek](https://example.com/åbent)`
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(String(l.href)).toContain('https://example.com')
|
||||
// href may percent-encode non-ASCII; check for key path segment presence in either form
|
||||
const hrefStr = String(l.href)
|
||||
expect(hrefStr).toMatch(/åbent|%C3%A5bent|%E5%93%81/i)
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('Åbent')
|
||||
})
|
||||
|
||||
it('link inside a **footnote** (CommonMark footnote syntax)', () => {
|
||||
const mdText = `Here is a reference[^1].
|
||||
|
||||
[^1]: See the [OpenAI documentation](https://platform.openai.com/docs).`
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
const footnote = nodes[1] as any
|
||||
expect(footnote.type).toBe('footnote')
|
||||
const footnotAnchor = footnote.children[0].children.slice(-1)[0]
|
||||
expect(footnotAnchor.type).toBe('footnote_anchor')
|
||||
expect(footnotAnchor.id).toBe('1')
|
||||
const links = collectLinks(footnote.children)
|
||||
const l = links[0]
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
expect(l.loading).toBe(false)
|
||||
expect(l.href).toBe('https://platform.openai.com/docs')
|
||||
expect(String(l.text || l.children?.[0]?.content || '')).toContain('OpenAI documentation')
|
||||
})
|
||||
|
||||
it('link with **HTML entity** in the link text', () => {
|
||||
const nodes = parseMarkdownToStructure('[OpenAI & ChatGPT](https://openai.com)', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.loading).toBeFalsy()
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
expect(String(text)).toContain('ChatGPT')
|
||||
// Allow either decoded '&' or the HTML entity '&'
|
||||
expect(String(text)).toMatch(/&|&/)
|
||||
})
|
||||
|
||||
it('link with **line break** inside the link text (using `<br>`)', () => {
|
||||
const nodes = parseMarkdownToStructure('[OpenAI<br>Platform](https://platform.openai.com)', md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(l.loading).toBeFalsy()
|
||||
expect(String(l.href)).toContain('https://platform.openai.com')
|
||||
const text = l.text || l.children?.[0]?.content || ''
|
||||
// Ensure both parts of the link text are present
|
||||
expect(String(text)).toContain('OpenAI')
|
||||
expect(String(text)).toContain('Platform')
|
||||
// Allow the parser to represent the <br> as a literal tag, a newline, or other whitespace
|
||||
expect(String(text)).toMatch(/OpenAI[\s\S]*Platform/)
|
||||
})
|
||||
|
||||
it('**Bold image link** (image wrapped in bold and also a link)', () => {
|
||||
const mdText = '**[](https://openai.com)**'
|
||||
const nodes = parseMarkdownToStructure(mdText, md)
|
||||
// Look for strong nodes that may wrap the link
|
||||
const strongs = collectTarget(nodes as any[], 'strong')
|
||||
expect(strongs.length).toBeGreaterThanOrEqual(1)
|
||||
const links = collectLinks(strongs as any[])
|
||||
// There should be at least one link inside the strong wrapper
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const l = links[0]
|
||||
expect(String(l.href)).toContain('https://openai.com')
|
||||
const hasImageChild = (Array.isArray(l.children) && l.children.some((c: any) => c.type === 'image'))
|
||||
|| (Array.isArray((l as any).items) && (l as any).items.some((c: any) => c.type === 'image'))
|
||||
expect(hasImageChild).toBe(true)
|
||||
})
|
||||
|
||||
it('link inside a **table header**', () => {
|
||||
const tableMd = `| **Service** | **URL** |
|
||||
|-------------|----------|
|
||||
| OpenAI | [openai.com](https://openai.com) |`
|
||||
|
||||
const nodes = parseMarkdownToStructure(tableMd, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
// link should be found in the table body
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
const hrefs = links.map((l: any) => String(l.href))
|
||||
expect(hrefs.some((h: string) => h.includes('https://openai.com'))).toBe(true)
|
||||
|
||||
// ensure header cells exist and include the bold header text
|
||||
const tables = collectTarget(nodes as any[], 'table')
|
||||
expect(tables.length).toBeGreaterThan(0)
|
||||
const header = (tables[0] as any).header
|
||||
expect(header).toBeDefined()
|
||||
const headerTexts = header.cells.map((c: any) => {
|
||||
if (c.children)
|
||||
return c.children.map((ch: any) => ch.content || ch.raw || '').join('')
|
||||
return String(c.raw || '')
|
||||
}).join(' | ')
|
||||
expect(String(headerTexts)).toContain('Service')
|
||||
expect(String(headerTexts)).toContain('URL')
|
||||
})
|
||||
|
||||
it('parses 3 adjacent links on the same line as 3 link nodes', () => {
|
||||
const markdown = '[citation](http://url1) [citation](http://url1) [citation](http://url1)'
|
||||
const nodes = parseMarkdownToStructure(markdown, md)
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links).toHaveLength(3)
|
||||
expect(links.map((l: any) => l.href)).toEqual(['http://url1', 'http://url1', 'http://url1'])
|
||||
})
|
||||
|
||||
it('does not leak partial href like "(htt" into text nodes in streaming mid-state', () => {
|
||||
const markdown = '[citation](htt'
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { final: false })
|
||||
const links = collectLinks(nodes as any[])
|
||||
expect(links.length).toBeGreaterThan(0)
|
||||
expect(Boolean(links[0].loading)).toBe(true)
|
||||
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? ''))
|
||||
expect(texts.join('')).not.toContain('(htt')
|
||||
})
|
||||
|
||||
it('never renders "(http" as text during char-by-char streaming for multiple links', () => {
|
||||
const full = '[citation](http://url1) [citation](http://url1) [citation](http://url1)'
|
||||
for (let i = 1; i <= full.length; i++) {
|
||||
const chunk = full.slice(0, i)
|
||||
const nodes = parseMarkdownToStructure(chunk, md, { final: false })
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? '')).join('')
|
||||
// If this fails, include the chunk to make the failing prefix obvious.
|
||||
expect(texts, `prefix(${i}): ${chunk}`).not.toContain('(http')
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps streaming markdown images as image loading nodes instead of flipping to links', () => {
|
||||
const full = ''
|
||||
|
||||
const earlyNodes = parseMarkdownToStructure(full.slice(0, 2), md, { final: false })
|
||||
expect(collectTarget(earlyNodes as any[], 'image')).toHaveLength(1)
|
||||
expect(collectLinks(earlyNodes as any[])).toHaveLength(0)
|
||||
|
||||
for (const chunk of [full.slice(0, 20), full.slice(0, 60)]) {
|
||||
const nodes = parseMarkdownToStructure(chunk, md, { final: false })
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
|
||||
expect(images.length, chunk).toBe(1)
|
||||
expect(images[0].loading, chunk).toBe(true)
|
||||
expect(images[0].alt, chunk).toBe('Picture')
|
||||
expect(links, chunk).toHaveLength(0)
|
||||
}
|
||||
|
||||
const finalNodes = parseMarkdownToStructure(full, md, { final: true })
|
||||
const finalImages = collectTarget(finalNodes as any[], 'image')
|
||||
const finalLinks = collectLinks(finalNodes as any[])
|
||||
|
||||
expect(finalImages).toHaveLength(1)
|
||||
expect(finalImages[0].loading).toBe(false)
|
||||
expect(finalImages[0].src).toBe('https://imzbf.github.io/md-editor-rt/imgs/mark_emoji.gif')
|
||||
expect(finalImages[0].alt).toBe('Picture')
|
||||
expect(finalLinks).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('does not turn escaped exclamation plus link into an image mid-state', () => {
|
||||
const nodes = parseMarkdownToStructure('\\
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? '')).join('')
|
||||
|
||||
expect(images).toHaveLength(0)
|
||||
expect(links).toHaveLength(1)
|
||||
expect(texts).toContain('!')
|
||||
})
|
||||
|
||||
it('keeps nested image links as loading links when the outer link has just started', () => {
|
||||
const veryEarlyMarkdown = '[![P'
|
||||
const veryEarlyNodes = parseMarkdownToStructure(veryEarlyMarkdown, md, { final: false })
|
||||
const veryEarlyImages = collectTarget(veryEarlyNodes as any[], 'image')
|
||||
const veryEarlyLinks = collectLinks(veryEarlyNodes as any[])
|
||||
const veryEarlyTexts = collectTarget(veryEarlyNodes as any[], 'text')
|
||||
|
||||
expect(veryEarlyImages).toHaveLength(1)
|
||||
expect(veryEarlyLinks).toHaveLength(1)
|
||||
expect(veryEarlyLinks[0].loading).toBe(true)
|
||||
expect(veryEarlyLinks[0].href).toBe('')
|
||||
expect(veryEarlyLinks[0].children[0].type).toBe('image')
|
||||
expect(veryEarlyTexts).toHaveLength(0)
|
||||
|
||||
const hrefEarlyMarkdown = '[
|
||||
const hrefEarlyImages = collectTarget(hrefEarlyNodes as any[], 'image')
|
||||
const hrefEarlyLinks = collectLinks(hrefEarlyNodes as any[])
|
||||
const hrefEarlyTexts = collectTarget(hrefEarlyNodes as any[], 'text')
|
||||
|
||||
expect(hrefEarlyImages).toHaveLength(1)
|
||||
expect(hrefEarlyLinks).toHaveLength(1)
|
||||
expect(hrefEarlyLinks[0].loading).toBe(true)
|
||||
expect(hrefEarlyLinks[0].href).toBe('')
|
||||
expect(hrefEarlyLinks[0].children[0].type).toBe('image')
|
||||
expect(hrefEarlyTexts).toHaveLength(0)
|
||||
|
||||
const earlyMarkdown = '['
|
||||
const earlyNodes = parseMarkdownToStructure(earlyMarkdown, md, { final: false })
|
||||
const earlyImages = collectTarget(earlyNodes as any[], 'image')
|
||||
const earlyLinks = collectLinks(earlyNodes as any[])
|
||||
const earlyTexts = collectTarget(earlyNodes as any[], 'text')
|
||||
|
||||
expect(earlyImages).toHaveLength(1)
|
||||
expect(earlyLinks).toHaveLength(1)
|
||||
expect(earlyLinks[0].loading).toBe(true)
|
||||
expect(earlyLinks[0].href).toBe('')
|
||||
expect(earlyLinks[0].children).toHaveLength(1)
|
||||
expect(earlyLinks[0].children[0].type).toBe('image')
|
||||
expect(earlyTexts).toHaveLength(0)
|
||||
|
||||
const markdown = '[]('
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { final: false })
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text')
|
||||
|
||||
expect(images).toHaveLength(1)
|
||||
expect(links).toHaveLength(1)
|
||||
expect(links[0].loading).toBe(true)
|
||||
expect(links[0].href).toBe('')
|
||||
expect(links[0].children).toHaveLength(1)
|
||||
expect(links[0].children[0].type).toBe('image')
|
||||
expect(texts).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('preserves text before a nested image link while the outer link is incomplete', () => {
|
||||
const markdown = 'prefix ['
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { final: false })
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? ''))
|
||||
|
||||
expect(images).toHaveLength(1)
|
||||
expect(links).toHaveLength(1)
|
||||
expect(links[0].loading).toBe(true)
|
||||
expect(links[0].children).toHaveLength(1)
|
||||
expect(links[0].children[0].type).toBe('image')
|
||||
expect(texts.join('')).toBe('prefix ')
|
||||
})
|
||||
|
||||
it('preserves text before very early nested image links', () => {
|
||||
const markdown = 'prefix [![P'
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { final: false })
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? ''))
|
||||
|
||||
expect(images).toHaveLength(1)
|
||||
expect(links).toHaveLength(1)
|
||||
expect(links[0].loading).toBe(true)
|
||||
expect(links[0].children[0].type).toBe('image')
|
||||
expect(texts.join('')).toBe('prefix ')
|
||||
})
|
||||
|
||||
it('does not turn escaped outer brackets before images into nested image links', () => {
|
||||
for (const markdown of [
|
||||
'\\[',
|
||||
'\\[](',
|
||||
'\\[](https://outer',
|
||||
]) {
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { final: false })
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? '')).join('')
|
||||
|
||||
expect(images.length, markdown).toBe(1)
|
||||
expect(links.length, markdown).toBe(0)
|
||||
expect(texts, markdown).toContain('[')
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves prefix text for escaped outer brackets before images', () => {
|
||||
const markdown = 'prefix \\[
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? '')).join('')
|
||||
|
||||
expect(images).toHaveLength(1)
|
||||
expect(links).toHaveLength(0)
|
||||
expect(texts).toBe('prefix [')
|
||||
})
|
||||
|
||||
it('still allows double escaped outer bracket sequences to form real nested image links', () => {
|
||||
const markdown = '\\\\[
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text').map((t: any) => String(t.content ?? '')).join('')
|
||||
|
||||
expect(images).toHaveLength(1)
|
||||
expect(links).toHaveLength(1)
|
||||
expect(links[0].loading).toBe(true)
|
||||
expect(links[0].children[0].type).toBe('image')
|
||||
expect(texts).toBe('\\')
|
||||
})
|
||||
|
||||
it('keeps nested image links as loading links while the outer href streams', () => {
|
||||
const markdown = '[](https://outer'
|
||||
const nodes = parseMarkdownToStructure(markdown, md, { final: false })
|
||||
const images = collectTarget(nodes as any[], 'image')
|
||||
const links = collectLinks(nodes as any[])
|
||||
const texts = collectTarget(nodes as any[], 'text')
|
||||
|
||||
expect(images).toHaveLength(1)
|
||||
expect(links).toHaveLength(1)
|
||||
expect(links[0].loading).toBe(true)
|
||||
expect(links[0].href).toBe('https://outer')
|
||||
expect(links[0].children).toHaveLength(1)
|
||||
expect(links[0].children[0].type).toBe('image')
|
||||
expect(texts).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user