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

412 lines
16 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 { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
import { describe, expect, it } from 'vitest'
import { links, textIncludes } from './utils/midstate-utils'
const md = getMarkdown('t')
const MARKDOWN = `**Markdown链接**:
1. [GitHub官网](https://github.com)
2. [知乎 - 有问题就会有答案](https://www.zhihu.com)
3. **加粗链接**:[Google](https://www.google.com)
4. 嵌套格式的链接:[*斜体链接*](https://example.com)
**普通链接**:
1. https://www.wikipedia.org
2. http://example.com/path?query=test
3. 纯文本URL:https://markdown-guide.readthedocs.io
`
describe('link parsing', () => {
it('parses inline and bare links into link nodes', () => {
const nodes = parseMarkdownToStructure(MARKDOWN, md)
// Flatten paragraph children for assertions
const allLinkNodes: any[] = []
const collect = (n: any) => {
if (!n)
return
if (n.type === 'link')
allLinkNodes.push(n)
if (Array.isArray(n.children)) {
for (const c of n.children) collect(c)
}
if (Array.isArray(n.items)) {
for (const it of n.items) collect(it)
}
}
for (const n of nodes) collect(n)
// Some links may initially be loading (href empty) but should include texts
const texts = allLinkNodes.map(l => l.text?.toString() || '')
expect(texts).toContain('GitHub官网')
expect(texts).toContain('知乎 - 有问题就会有答案')
expect(texts).toContain('Google')
expect(texts).toContain('https://www.wikipedia.org')
expect(texts).toContain('http://example.com/path?query=test')
expect(texts).toContain('https://markdown-guide.readthedocs.io')
// The parser may keep emphasis markup in link.text (e.g. "*斜体链接*"),
// so accept either the raw form or verify the nested emphasis child contains the plain text.
// The parser may keep emphasis markup in link.text or emit nested emphasis nodes.
// Accept any of these by inspecting the link nodes with the tolerant helper.
const foundItalic = allLinkNodes.some(l =>
textIncludes(l, '斜体链接') || textIncludes(l, '*斜体链接*') || textIncludes(l, '_斜体链接_'),
)
expect(foundItalic).toBe(true)
// Check for bare URLs rendered as text nodes inside paragraphs
const containsBare = nodes.some((n: any) => {
return textIncludes(n, 'https://www.wikipedia.org') || textIncludes(n, 'http://example.com/path?query=test') || textIncludes(n, 'https://markdown-guide.readthedocs.io')
})
expect(containsBare).toBe(true)
})
it('test the link tag', () => {
const nodes = parseMarkdownToStructure(`<a href="https://example.com">Example</a>`, md) as any
const p = nodes[0]
const children = p.children
expect(children.length).toBe(1)
const link = p.children[0]
expect(link.type).toBe('link')
expect(link.href).toBe('https://example.com')
expect(link.text).toBe('Example')
expect(link.children[0].type).toBe('text')
expect(link.children[0].content).toBe('Example')
})
it('test the intermediate state of the link tag', () => {
const nodes = parseMarkdownToStructure(`<a href="https://example.com">`, md) as any
expect(nodes.length).toBe(1)
})
it('parses link with parentheses and CJK brackets as a single link', () => {
const special = '[【名称】(test).mp4](https://github.com/Simon-He95/markstream-vueer)'
const nodes = parseMarkdownToStructure(special, md)
// Flatten and collect link nodes
const links: any[] = []
const walk = (n: any) => {
if (!n)
return
if (n.type === 'link')
links.push(n)
if (Array.isArray(n.children))
n.children.forEach(walk)
if (Array.isArray(n.items))
n.items.forEach(walk)
}
nodes.forEach(walk)
// Expect only a single link node in the paragraph without stray text nodes
expect(links.length).toBe(1)
expect(links[0].href).toBe('https://github.com/Simon-He95/markstream-vueer')
// Ensure the full visible text is preserved inside the link
// Accept either direct text aggregation or nested children text
const text = links[0].text || ''
const childText = (links[0].children || [])
.map((c: any) => (c.content ?? c.text ?? ''))
.join('')
expect(text || childText).toBe('【名称】(test).mp4')
// Also assert the paragraph has exactly one child which is the link
const para = nodes.find((n: any) => n.type === 'paragraph') as any
expect(para).toBeTruthy()
expect(Array.isArray(para.children)).toBe(true)
expect(para.children.length).toBe(1)
expect(para.children[0].type).toBe('link')
})
it('parses link with parentheses and emphasis and ListNode', () => {
const special = '- **[Link (Test 1)](https://simonhe.me/)**'
const nodes = parseMarkdownToStructure(special, md)
expect(nodes[0]).toMatchInlineSnapshot(`
{
"items": [
{
"children": [
{
"children": [
{
"children": [
{
"attrs": [
[
"href",
"https://simonhe.me/",
],
],
"children": [
{
"center": false,
"content": "Link (Test 1)",
"raw": "Link (Test 1)",
"type": "text",
},
],
"href": "https://simonhe.me/",
"loading": false,
"raw": "[Link (Test 1)](https://simonhe.me/)",
"text": "Link (Test 1)",
"title": null,
"type": "link",
},
],
"raw": "**Link (Test 1)**",
"type": "strong",
},
],
"raw": "**[Link (Test 1)](https://simonhe.me/)**",
"type": "paragraph",
},
],
"raw": "**[Link (Test 1)](https://simonhe.me/)**",
"type": "list_item",
},
],
"ordered": false,
"raw": "**[Link (Test 1)](https://simonhe.me/)**",
"start": undefined,
"type": "list",
}
`)
})
it('parses link with parentheses and emphasis', () => {
const special = '**[Link (Test 2)](https://simonhe.me/)**'
const nodes = parseMarkdownToStructure(special, md)
expect(nodes[0]).toMatchInlineSnapshot(`
{
"children": [
{
"children": [
{
"attrs": [
[
"href",
"https://simonhe.me/",
],
],
"children": [
{
"center": false,
"content": "Link (Test 2)",
"raw": "Link (Test 2)",
"type": "text",
},
],
"href": "https://simonhe.me/",
"loading": false,
"raw": "[Link (Test 2)](https://simonhe.me/)",
"text": "Link (Test 2)",
"title": null,
"type": "link",
},
],
"raw": "**Link (Test 2)**",
"type": "strong",
},
],
"raw": "**[Link (Test 2)](https://simonhe.me/)**",
"type": "paragraph",
}
`)
})
it('parses emphasized link with asterisk inside list item', () => {
const special = '- [*DR (Danmarks Radio)*](https://www.dr.dk/nyheder)'
const nodes = parseMarkdownToStructure(special, md)
// Collect link nodes
const links: any[] = []
const walk = (n: any) => {
if (!n)
return
if (n.type === 'link')
links.push(n)
if (Array.isArray(n.children))
n.children.forEach(walk)
if (Array.isArray(n.items))
n.items.forEach(walk)
}
nodes.forEach(walk)
expect(links.length).toBe(1)
expect(links[0].href).toBe('https://www.dr.dk/nyheder')
// The parser may keep emphasis markup in link.text or emit nested emphasis nodes.
// Accept either form by using the tolerant helper.
const found
= textIncludes(links[0], 'DR (Danmarks Radio)')
|| textIncludes(links[0], '*DR (Danmarks Radio)*')
|| textIncludes(links[0], '_DR (Danmarks Radio)_')
expect(found).toBe(true)
})
it('parses URL with inline trailing text (no newline)', () => {
const mdText = `第一个气泡(无换行):
http://127.0.0.1:8001/upload/20251118/4737bbe0-c42e-11f0-8471-37360564882d.docx 请在文档末尾增加一段示例内容`
const nodes = parseMarkdownToStructure(mdText, md)
expect(
textIncludes(
nodes,
'http://127.0.0.1:8001/upload/20251118/4737bbe0-c42e-11f0-8471-37360564882d.docx',
),
).toBe(true)
expect(textIncludes(nodes, '请在文档末尾增加一段示例内容')).toBe(true)
})
it('parses URL followed by newline and trailing text', () => {
const mdText = `第二个气泡(有换行):
http://127.0.0.1:8001/upload/20251118/4737bbe0-c42e-11f0-8471-37360564882d.docx
请在文档末尾增加一段示例内容`
const nodes = parseMarkdownToStructure(mdText, md)
expect(
textIncludes(
nodes,
'http://127.0.0.1:8001/upload/20251118/4737bbe0-c42e-11f0-8471-37360564882d.docx',
),
).toBe(true)
expect(textIncludes(nodes, '请在文档末尾增加一段示例内容')).toBe(true)
// Ensure a hard line break node is present for the newline between URL and trailing text
let foundHardBreak = false
const walk = (n: any) => {
if (!n)
return
if (n.type === 'hardbreak')
foundHardBreak = true
if (Array.isArray(n.children))
n.children.forEach(walk)
if (Array.isArray(n.items))
n.items.forEach(walk)
}
nodes.forEach(walk)
expect(foundHardBreak).toBe(true)
})
it('parses Danish list item with parenthetical URL', () => {
const special = '- **Kilde:**\u202FGrammatip – “Tekstretter” (https://www.grammatip.com/tekstretter) – her beskrives, hvordan værktøjet automatisk markerer fejl, giver fejltyper, leverer farvekodet feedback, giver lærerne et samlet overblik og muligheden for at tilpasse, hvilke fejl eleven skal se.'
const nodes = parseMarkdownToStructure(special, md)
// Find the paragraph node inside the list item
let para: any = null
const findPara = (n: any) => {
if (!n)
return
if (n.type === 'paragraph') {
para = n
return
}
if (Array.isArray(n.children))
n.children.forEach(findPara)
if (Array.isArray(n.items))
n.items.forEach(findPara)
}
nodes.forEach(findPara)
expect(para).toBeTruthy()
// Flatten paragraph descendants into ordered tokens, marking links specially
const tokens: string[] = []
const flatten = (n: any) => {
if (!n)
return
if (n.type === 'link') {
tokens.push(`<LINK:${n.href || n.text || ''}>`)
return
}
if (n.type === 'text') {
tokens.push(n.content || n.text || '')
return
}
if (Array.isArray(n.children)) {
for (const c of n.children) flatten(c)
}
}
flatten(para)
// There should be at least one link token with the expected href
const linkIndex = tokens.findIndex(t => t.startsWith('<LINK:'))
expect(linkIndex).toBeGreaterThanOrEqual(0)
const linkToken = tokens[linkIndex]
expect(linkToken).toContain('https://www.grammatip.com/tekstretter')
// Check text immediately before the link contains the title/context
const before = tokens[linkIndex - 1] || ''
const hasBefore = before.includes('Tekstretter') || before.includes('Grammatip') || before.includes('Kilde')
expect(hasBefore).toBe(true)
// Check text immediately after the link contains the trailing explanation
const afterTokens = tokens.slice(linkIndex + 1)
const afterFull = afterTokens.join('').trim()
const expectedAfter = ') – her beskrives, hvordan værktøjet automatisk markerer fejl, giver fejltyper, leverer farvekodet feedback, giver lærerne et samlet overblik og muligheden for at tilpasse, hvilke fejl eleven skal se.'
expect(afterFull).toBe(expectedAfter)
})
it('does not include ! suffix in linkify/autolink bare URLs', () => {
const cjkBang = '图表URL: https://www.baidu.com/img/flexible/logo/pc/result.png!文字文字'
const nodes1 = parseMarkdownToStructure(cjkBang, md)
const l1 = links(nodes1)
expect(l1.length).toBe(1)
expect(l1[0].href).toBe('https://www.baidu.com/img/flexible/logo/pc/result.png')
expect(l1[0].text).toBe('https://www.baidu.com/img/flexible/logo/pc/result.png')
expect(textIncludes(nodes1, '!文字文字')).toBe(true)
expect(textIncludes(l1[0], '!')).toBe(false)
const angle = '<https://www.baidu.com/img/flexible/logo/pc/result.png!文字文字>'
const nodes3 = parseMarkdownToStructure(angle, md)
const l3 = links(nodes3)
expect(l3.length).toBe(1)
expect(l3[0].href).toBe('https://www.baidu.com/img/flexible/logo/pc/result.png')
expect(l3[0].text).toBe('https://www.baidu.com/img/flexible/logo/pc/result.png')
expect(textIncludes(nodes3, '!文字文字')).toBe(true)
expect(textIncludes(l3[0], '!')).toBe(false)
})
it('keeps ASCII ! when it is part of the URL', () => {
const mdText = [
'https://example.com/a!b',
'https://example.com/?q=!',
'https://example.com/#!',
'<https://example.com/a!b>',
].join('\n')
const nodes = parseMarkdownToStructure(mdText, md)
const ls = links(nodes)
const hrefs = ls.map((l: any) => String(l.href || ''))
expect(hrefs).toContain('https://example.com/a!b')
expect(hrefs).toContain('https://example.com/?q=!')
expect(hrefs).toContain('https://example.com/#!')
})
it('does not merge punctuation ! into explicit markdown links', () => {
const nodes = parseMarkdownToStructure('[x](https://example.com/#)!', md)
const ls = links(nodes)
expect(ls.length).toBe(1)
expect(ls[0].href).toBe('https://example.com/#')
expect(ls[0].text).toBe('x')
expect(textIncludes(nodes, '!')).toBe(true)
})
it('respects customMarkdownIt validateLink: rejects javascript: and renders as plain text', () => {
const mdSafe = getMarkdown('validate-link-test')
mdSafe.set?.({ validateLink: (url: string) => !/^\s*javascript:/i.test(url.trim()) })
const nodes = parseMarkdownToStructure('[click me](javascript:alert(1))', mdSafe, { final: true })
const ls = links(nodes)
expect(ls.length).toBe(0)
expect(textIncludes(nodes, 'click me')).toBe(true)
// No link node may have javascript: href (paragraph.raw may still hold original source)
const anyBadHref = ls.some((l: any) => String(l.href || '').toLowerCase().includes('javascript:'))
expect(anyBadHref).toBe(false)
})
it('respects customMarkdownIt validateLink: allows https when validateLink returns true', () => {
const mdSafe = getMarkdown('validate-link-allow')
mdSafe.set?.({ validateLink: (url: string) => url.startsWith('https://') })
const nodes = parseMarkdownToStructure('[safe](https://example.com) [unsafe](http://example.com)', mdSafe, { final: true })
const ls = links(nodes)
expect(ls.length).toBe(1)
expect(ls[0].href).toBe('https://example.com')
expect(ls[0].text).toBe('safe')
expect(textIncludes(nodes, 'unsafe')).toBe(true)
})
})