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:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,46 @@
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
import { describe, expect, it } from 'vitest'
import { links, textIncludes } from '../utils/midstate-utils'
const md = getMarkdown('midstates')
describe('autolink handling', () => {
it('bare http URL should be parsed as a link node (autolink)', () => {
const nodes = parseMarkdownToStructure('http://a', md)
const ls = links(nodes)
expect(ls.length).toBeGreaterThanOrEqual(1)
const l = ls[0] as any
expect(textIncludes(l, 'http://a') || l.href === 'http://a' || /http:\/\/.+/.test(String(l.href))).toBe(true)
// autolinks should not be considered loading
expect(!!l.loading).toBe(false)
})
it('angle-bracket autolink <http://a> should be parsed as link', () => {
const nodes = parseMarkdownToStructure('<http://a>', md)
const ls = links(nodes)
if (ls.length > 0) {
const l = ls[0] as any
expect(textIncludes(l, 'http://a') || String(l.href).includes('http://a')).toBe(true)
}
else {
// tokenizer may choose to leave raw text; accept that fallback
expect(textIncludes(nodes, '<http://a>') || textIncludes(nodes, 'http://a')).toBe(true)
}
})
it('bracketed link with title containing emoji (mid-state) should NOT be misclassified as autolink', () => {
const nodes = parseMarkdownToStructure('[x](http://a "t😁"', md)
const ls = links(nodes)
if (ls.length > 0) {
// If parser produced a link, it should be loading (mid-state) rather than finalized autolink
expect(!!ls[0].loading).toBe(true)
// title or children should include emoji or stub
const ok = textIncludes(ls[0], '😁') || textIncludes(ls[0], 't') || textIncludes(nodes, 't') || textIncludes(nodes, '😁')
expect(ok).toBe(true)
}
else {
// Accept text fallback containing the raw mid-state
expect(textIncludes(nodes, '[x](http://a "t😁"') || textIncludes(nodes, 't😁')).toBe(true)
}
})
})
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { parseCheckboxInputToken, parseCheckboxToken } from '../../packages/markdown-parser/src/parser/inline-parsers/checkbox-parser'
describe('checkbox inline parser', () => {
it('parses checkbox token from meta.checked true', () => {
const token: any = { meta: { checked: true } }
const node = parseCheckboxToken(token)
expect(node.type).toBe('checkbox')
expect(node.checked).toBe(true)
expect(node.raw).toBe('[x]')
})
it('parses checkbox input token from attrGet', () => {
const token: any = {
attrGet: (name: string) => {
if (name === 'checked')
return 'true'
return null
},
}
const node = parseCheckboxInputToken(token)
expect(node.type).toBe('checkbox_input')
expect(node.checked).toBe(true)
expect(node.raw).toBe('[x]')
})
})
@@ -0,0 +1,55 @@
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)
}
nodes.forEach(walk)
return out
}
describe('fix-link-inline edge cases', () => {
it('handles "[*x](xx" tolerantly (link with emphasis or text fallback)', () => {
const nodes = parseMarkdownToStructure('[*x](xx', md)
const links = collectLinks(nodes as any[])
if (links.length > 0) {
const l = links[0]
const textOk = l.text === 'x' || (l.children && l.children.some((c: any) => c.type === 'emphasis'))
expect(textOk).toBe(true)
expect(!!l.loading).toBe(true)
}
else {
// fallback: ensure original raw text remains
const raw = JSON.stringify(nodes)
expect(raw.includes('[*x](xx') || raw.includes('emphasis')).toBe(true)
}
})
it('handles emoji at end of link text with unclosed URL conservatively', () => {
const nodes = parseMarkdownToStructure('[x:smile:](u', md)
const links = collectLinks(nodes as any[])
if (links.length > 0) {
const l = links[0]
expect(!!l.loading).toBe(true)
// link text should contain the emoji shortcode or contain an emoji node
const hasEmojiText = typeof l.text === 'string' && l.text.includes(':smile:')
const hasEmojiNode = Array.isArray(l.children) && l.children.some((c: any) => c.type === 'emoji' || (c.children && c.children.some((cc: any) => cc.type === 'emoji')))
expect(hasEmojiText || hasEmojiNode).toBe(true)
}
else {
const raw = JSON.stringify(nodes)
expect(raw.includes('[x:smile:](u') || raw.includes('emoji')).toBe(true)
}
})
})
@@ -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 = '[![OpenAI logo](https://openai.com/content/images/2022/05/openai-avatar.png)](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 &amp; 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 '&amp;'
expect(String(text)).toMatch(/&amp;|&/)
})
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 = '**[![OpenAI logo](https://openai.com/content/images/2022/05/openai-avatar.png)](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 = '![Picture](https://imzbf.github.io/md-editor-rt/imgs/mark_emoji.gif)'
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('\\![Picture](https://imzbf.github.io/md-editor-rt/imgs/mark_emoji.gif', 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).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 = '[![Picture](https://img'
const hrefEarlyNodes = parseMarkdownToStructure(hrefEarlyMarkdown, md, { final: false })
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 = '[![Picture](https://img)'
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 = '[![Picture](https://img)]('
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 [![Picture](https://img)'
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 [
'\\[![P',
'\\[![Picture](https://img',
'\\[![Picture](https://img)',
'\\[![Picture](https://img)](',
'\\[![Picture](https://img)](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 \\[![Picture](https://img'
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).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 = '\\\\[![Picture](https://img'
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).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 = '[![Picture](https://img)](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)
})
})
@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest'
import { parseImageToken } from '../../packages/markdown-parser/src/parser/inline-parsers/image-parser'
// Minimal token-like shapes used by parser (only include fields used by parser)
function makeChildImageToken() {
return {
type: 'image',
tag: 'img',
attrs: [
['src', 'https://example.com/vue.png'],
['alt', ''],
['title', 'Vue Logo'],
],
content: 'Vue Logo',
children: [
{
type: 'text',
content: 'Vue Logo',
},
],
} as unknown as any
}
function makeOuterInlineToken() {
return {
type: 'inline',
tag: '',
attrs: null,
content: '![Vue Logo](https://example.com/vue.png "Vue Logo")',
children: [makeChildImageToken()],
} as unknown as any
}
describe('image-parser', () => {
it('falls back to child attrs or children content when attrs.alt is empty', () => {
const outer = makeOuterInlineToken()
const node = parseImageToken(outer as any)
expect(node.src).toBe('https://example.com/vue.png')
expect(node.title).toBe('Vue Logo')
// alt should be taken from token.content/child when attrs.alt is empty
expect(node.alt).toBe('Vue Logo')
})
it('uses child text when attrs.alt and parent content are empty', () => {
const child = makeChildImageToken()
// make the child have empty alt attr and parent content empty
child.attrs = [
['src', 'https://example.com/vue.png'],
['alt', ''],
['title', 'Vue Logo'],
]
child.content = ''
child.children = [
{
type: 'text',
content: 'Child Alt Text',
},
]
const outer = {
type: 'inline',
tag: '',
attrs: null,
content: '',
children: [child],
} as unknown as any
const node = parseImageToken(outer as any)
expect(node.src).toBe('https://example.com/vue.png')
expect(node.title).toBe('Vue Logo')
// alt should be taken from the inner child's text content
expect(node.alt).toBe('Child Alt Text')
})
})
@@ -0,0 +1,37 @@
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
import { describe, expect, it } from 'vitest'
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
}
describe('parseMarkdownToStructure - multiple links same line', () => {
it('parses 3 adjacent links as 3 link nodes', () => {
const md = getMarkdown('multiple-links-same-line')
const markdown = '[citation](http://url1) [citation](http://url1) [citation](http://url1)'
const nodes = parseMarkdownToStructure(markdown, md)
const links = collectLinks(nodes)
expect(links).toHaveLength(3)
expect(links.map(l => l.href)).toEqual(['http://url1', 'http://url1', 'http://url1'])
})
})