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,436 @@
|
||||
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'
|
||||
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\\%$ '
|
||||
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\\%$  $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'
|
||||
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 = '$$'
|
||||
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$  $b$  $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$  $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 = ''
|
||||
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 = ''
|
||||
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 = ''
|
||||
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\\)  \\(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 = ' '
|
||||
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$  $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 = ''
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user