Files
boos e0a5f57793 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.
2026-06-22 11:32:57 +08:00

437 lines
19 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ParsedNode } from 'stream-markdown-parser'
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
import { describe, expect, it } from 'vitest'
describe('math followed by image bug', () => {
it('should parse math and image correctly - not math, image, and link', () => {
const md = getMarkdown()
const content = '$5\\%$ \n![image](https://img.shetu66.com/2023/04/27/1682576769453692.png)'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result:', JSON.stringify(result, null, 2))
// Should have 1 paragraph node with math_inline, text, and image children
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
expect(children.length).toBe(3)
// First child should be math_inline
expect(children[0].type).toBe('math_inline')
expect((children[0] as { content: string }).content).toBe('5\\%')
// Second child should be text (space and newline)
expect(children[1].type).toBe('text')
expect((children[1] as { content: string }).content).toBe(' \n')
// Third child should be image
expect(children[2].type).toBe('image')
expect((children[2] as { src: string }).src).toBe('https://img.shetu66.com/2023/04/27/1682576769453692.png')
expect((children[2] as { alt: string }).alt).toBe('image')
// Should NOT have a link node in children
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should parse math and image without newline', () => {
const md = getMarkdown()
const content = '$5\\%$ ![image](https://img.shetu66.com/2023/04/27/1682576769453692.png)'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result (no newline):', JSON.stringify(result, null, 2))
// Should have 1 paragraph node with math_inline, text, and image children
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
expect(children.length).toBe(3)
// First child should be math_inline
expect(children[0].type).toBe('math_inline')
expect((children[0] as { content: string }).content).toBe('5\\%')
// Second child should be text (space)
expect(children[1].type).toBe('text')
expect((children[1] as { content: string }).content).toBe(' ')
// Third child should be image
expect(children[2].type).toBe('image')
expect((children[2] as { src: string }).src).toBe('https://img.shetu66.com/2023/04/27/1682576769453692.png')
expect((children[2] as { alt: string }).alt).toBe('image')
// Should NOT have a link node in children
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should parse math, image, and math correctly', () => {
const md = getMarkdown()
const content = '$5\\%$ ![image](https://img.shetu66.com/2023/04/27/1682576769453692.png) $5\\%$'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result (math-image-math):', JSON.stringify(result, null, 2))
// Should have 1 paragraph node with math_inline, text, image, and math_inline children
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// Now we have: math, text, image, text, math (the space after image is now correctly preserved)
expect(children.length).toBe(5)
// First child should be math_inline
expect(children[0].type).toBe('math_inline')
expect((children[0] as { content: string }).content).toBe('5\\%')
// Second child should be text (space before image)
expect(children[1].type).toBe('text')
expect((children[1] as { content: string }).content).toBe(' ')
// Third child should be image
expect(children[2].type).toBe('image')
expect((children[2] as { src: string }).src).toBe('https://img.shetu66.com/2023/04/27/1682576769453692.png')
expect((children[2] as { alt: string }).alt).toBe('image')
// Fourth child should be text (space after image)
expect(children[3].type).toBe('text')
expect((children[3] as { content: string }).content).toBe(' ')
// Fifth child should be math_inline
expect(children[4].type).toBe('math_inline')
expect((children[4] as { content: string }).content).toBe('5\\%')
// Should NOT have a link node in children
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should handle incomplete math followed by image', () => {
const md = getMarkdown()
const content = '$ \n![image](https://img.shetu66.com/2023/04/27/1682576769453692.png)'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result (incomplete math):', JSON.stringify(result, null, 2))
// Should have 1 paragraph node
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// Incomplete math $ might be treated as text, followed by image
expect(children.length).toBeGreaterThanOrEqual(1)
// Last child should be image
const lastChild = children[children.length - 1]
expect(lastChild.type).toBe('image')
expect((lastChild as { src: string }).src).toBe('https://img.shetu66.com/2023/04/27/1682576769453692.png')
expect((lastChild as { alt: string }).alt).toBe('image')
// Should NOT have a link node
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should handle empty math followed by image', () => {
const md = getMarkdown()
const content = '$$![image](https://img.shetu66.com/2023/04/27/1682576769453692.png)'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result (empty math):', JSON.stringify(result, null, 2))
// Should have 1 paragraph node
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
expect(children.length).toBeGreaterThanOrEqual(1)
// Should have an image
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://img.shetu66.com/2023/04/27/1682576769453692.png')
// Should NOT have a link node
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should handle multiple math and image combinations', () => {
const md = getMarkdown()
const content = '$a$ ![img1](url1) $b$ ![img2](url2) $c$'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result (multiple):', JSON.stringify(result, null, 2))
// Should have 1 paragraph node
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// Now we have: math, text, image, text, math, text, image, text, math
// (spaces after images are now correctly preserved)
expect(children.length).toBe(9)
// Check math nodes
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(3)
expect((mathNodes[0] as { content: string }).content).toBe('a')
expect((mathNodes[1] as { content: string }).content).toBe('b')
expect((mathNodes[2] as { content: string }).content).toBe('c')
// Check image nodes
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(2)
expect((imageNodes[0] as { src: string }).src).toBe('url1')
expect((imageNodes[1] as { src: string }).src).toBe('url2')
// Should NOT have a link node
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should handle image with title attribute', () => {
const md = getMarkdown()
const content = '$x$ ![img](url "title") $y$'
const result = parseMarkdownToStructure(content, md)
console.log('Parse result (with title):', JSON.stringify(result, null, 2))
// Should have 1 paragraph node
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// Now we have: math, text, image, text, math (the space after image is now correctly preserved)
expect(children.length).toBe(5)
// Check image with title
const imageNode = children[2]
expect(imageNode.type).toBe('image')
expect((imageNode as { src: string }).src).toBe('url')
expect((imageNode as { alt: string }).alt).toBe('img')
expect((imageNode as { title: string | null }).title).toBe('title')
})
it('should parse image whose alt text contains inline math formulas', () => {
const md = getMarkdown()
const content = '![图6:Brain-Score大脑对齐性能(RSA)。我们测试了TopoLM和非拓扑控制在Brain-Score语言上的表现,使用表示相似性分析(RSA)来估计对齐。TopoLM在Pereira2018上优于控制,但在Blank2014和Fedorenko2016上表现较差(对每个基准分别进行的\\(t\\)-检验:\\(p<0.05\\))。对于每个基准,我们从10个交叉验证循环中抽样,以计算自助法95%置信区间(黑条)。当结果在3个基准上取平均时(最后一面板"Brain-Score"),我们发现TopoLM与其控制之间存在显著差异(\\(t\\)-检验:\\(p>0.05\\))。然而,这一结果需要谨慎解读,因为它主要受Fedorenko2016的结果影响,该结果仅显示非常大的绝对值,因为用于归一化的基础数据非常嘈杂,即噪声上限非常低。](https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png)'
const result = parseMarkdownToStructure(content, md)
// Should have 1 paragraph node
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// Should contain a single image node (not broken into multiple tokens)
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png')
// Should NOT have math_inline nodes (math is inside the image alt text, not standalone)
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(0)
// Should NOT have link nodes
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should parse image with dollar-sign math in alt text as a single image', () => {
const md = getMarkdown()
const content = '![graph shows $E=mc^2$ result](https://example.com/img.png)'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://example.com/img.png')
// Dollar-sign math inside alt text should NOT produce standalone math_inline tokens
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(0)
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should parse image with double-dollar math in alt text as a single image', () => {
const md = getMarkdown()
const content = '![formula $$x^2 + y^2$$ here](https://example.com/img.png)'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://example.com/img.png')
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(0)
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should still parse standalone \\(math\\) outside images correctly', () => {
const md = getMarkdown()
const content = 'The result is \\(x^2 + y^2 = z^2\\) as expected.'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(1)
expect((mathNodes[0] as { content: string }).content).toBe('x^2 + y^2 = z^2')
})
it('should parse math before and after an image with math in alt text', () => {
const md = getMarkdown()
const content = '\\(a\\) ![alt with \\(b\\) inside](https://example.com/img.png) \\(c\\)'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// The standalone \(a\) and \(c\) should still be math_inline
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(2)
expect((mathNodes[0] as { content: string }).content).toBe('a')
expect((mathNodes[1] as { content: string }).content).toBe('c')
// The image should be intact
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://example.com/img.png')
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should parse multiple images each with math in alt text on the same line', () => {
const md = getMarkdown()
const content = '![fig1 \\(x\\)](url1) ![fig2 \\(y\\)](url2)'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// Both images should be intact
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(2)
expect((imageNodes[0] as { src: string }).src).toBe('url1')
expect((imageNodes[1] as { src: string }).src).toBe('url2')
// No math_inline tokens should leak out
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(0)
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should handle $-math mixed with image containing $-math in alt text', () => {
const md = getMarkdown()
const content = '$a$ ![alt with $b$ inside](https://example.com/img.png) $c$'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// The standalone $a$ and $c$ should still be math_inline
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(2)
expect((mathNodes[0] as { content: string }).content).toBe('a')
expect((mathNodes[1] as { content: string }).content).toBe('c')
// The image should be intact
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://example.com/img.png')
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
it('should handle incomplete image syntax followed by math-like content', () => {
const md = getMarkdown()
// Incomplete image: no ](url) closing
const content = '![alt text \\(math\\) here'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// The key verification: no link nodes should be produced
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
// The parser may produce a loading image node from partial syntax - that's existing behavior
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
if (imageNodes.length > 0) {
// If image is produced, it should be loading (incomplete)
expect((imageNodes[0] as { loading: boolean }).loading).toBe(true)
}
})
it('should still parse math when ! is not followed by [ (not image syntax)', () => {
const md = getMarkdown()
const content = 'Important! The result is \\(x\\) here.'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// ! without [ is not image syntax, so math should still be parsed
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(1)
expect((mathNodes[0] as { content: string }).content).toBe('x')
// No image nodes
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(0)
})
it('should handle image with multiple different math delimiter types in alt', () => {
const md = getMarkdown()
const content = '![混合: \\(a\\) and $b$ and $$c$$](https://example.com/img.png)'
const result = parseMarkdownToStructure(content, md)
expect(result.length).toBe(1)
expect(result[0].type).toBe('paragraph')
const children = (result[0] as ParsedNode & { children?: ParsedNode[] }).children || []
// The image should be intact regardless of which math delimiters are used
const imageNodes = children.filter((n: ParsedNode) => n.type === 'image')
expect(imageNodes.length).toBe(1)
expect((imageNodes[0] as { src: string }).src).toBe('https://example.com/img.png')
const mathNodes = children.filter((n: ParsedNode) => n.type === 'math_inline')
expect(mathNodes.length).toBe(0)
const linkNodes = children.filter((n: ParsedNode) => n.type === 'link')
expect(linkNodes.length).toBe(0)
})
})