e0a5f57793
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.
113 lines
4.6 KiB
TypeScript
113 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { getMarkdown, parseMarkdownToStructure } from '../src'
|
|
|
|
function collectByType(nodes: any[], type: string) {
|
|
const out: any[] = []
|
|
const walk = (node: any) => {
|
|
if (!node)
|
|
return
|
|
if (Array.isArray(node)) {
|
|
node.forEach(walk)
|
|
return
|
|
}
|
|
if (node.type === type)
|
|
out.push(node)
|
|
if (Array.isArray(node.children))
|
|
node.children.forEach(walk)
|
|
if (Array.isArray(node.items))
|
|
node.items.forEach(walk)
|
|
}
|
|
walk(nodes)
|
|
return out
|
|
}
|
|
|
|
function collectText(nodes: any[]) {
|
|
return collectByType(nodes, 'text').map((node: any) => String(node.content ?? '')).join('')
|
|
}
|
|
|
|
describe('numeric reference streaming stability', () => {
|
|
it('keeps completed numeric references stable during char-by-char chained references', () => {
|
|
const md = getMarkdown('numeric-reference-streaming-chain')
|
|
const full = '随便输入一些文字[1][2][3]'
|
|
const firstReady = full.indexOf('[1]') + '[1]'.length
|
|
const secondOpens = full.indexOf('[2]')
|
|
const secondReady = secondOpens + '[2]'.length
|
|
const thirdReady = full.indexOf('[3]') + '[3]'.length
|
|
|
|
for (let i = 1; i <= full.length; i++) {
|
|
const chunk = full.slice(0, i)
|
|
const nodes = parseMarkdownToStructure(chunk, md, { final: false })
|
|
const ids = collectByType(nodes, 'reference').map((node: any) => String(node.id ?? ''))
|
|
|
|
if (i >= firstReady)
|
|
expect(ids[0], `prefix(${i}): ${chunk}`).toBe('1')
|
|
if (i > secondOpens)
|
|
expect(ids[0], `prefix(${i}): ${chunk}`).toBe('1')
|
|
if (i >= secondReady)
|
|
expect(ids.slice(0, 2), `prefix(${i}): ${chunk}`).toEqual(['1', '2'])
|
|
if (i >= thirdReady)
|
|
expect(ids.slice(0, 3), `prefix(${i}): ${chunk}`).toEqual(['1', '2', '3'])
|
|
}
|
|
})
|
|
|
|
it('keeps the numeric reference once an adjacent inline link becomes distinguishable', () => {
|
|
const md = getMarkdown('numeric-reference-streaming-link')
|
|
const full = '随便输入一些文字 [1][百度](www.baidu.com)'
|
|
const distinguishableAt = full.indexOf('[百度](') + '[百度]('.length
|
|
|
|
for (let i = 1; i <= full.length; i++) {
|
|
const chunk = full.slice(0, i)
|
|
const nodes = parseMarkdownToStructure(chunk, md, { final: false })
|
|
const ids = collectByType(nodes, 'reference').map((node: any) => String(node.id ?? ''))
|
|
const links = collectByType(nodes, 'link')
|
|
|
|
if (i >= distinguishableAt) {
|
|
expect(ids[0], `prefix(${i}): ${chunk}`).toBe('1')
|
|
expect(collectText(nodes), `prefix(${i}): ${chunk}`).not.toContain('[1]')
|
|
expect(links, `prefix(${i}): ${chunk}`).toHaveLength(1)
|
|
expect(String(links[0]?.text ?? ''), `prefix(${i}): ${chunk}`).toContain('百度')
|
|
expect(Boolean(links[0]?.loading), `prefix(${i}): ${chunk}`).toBe(i < full.length)
|
|
}
|
|
}
|
|
|
|
const finalNodes = parseMarkdownToStructure(full, md, { final: true })
|
|
const finalIds = collectByType(finalNodes, 'reference').map((node: any) => String(node.id ?? ''))
|
|
const links = collectByType(finalNodes, 'link')
|
|
|
|
expect(finalIds).toEqual(['1'])
|
|
expect(links).toHaveLength(1)
|
|
expect(String(links[0]?.href ?? '')).toBe('www.baidu.com')
|
|
})
|
|
|
|
it('keeps the completed numeric reference while trailing text streams in', () => {
|
|
const md = getMarkdown('numeric-reference-streaming-suffix')
|
|
const full = '随便输入一些文字[1]后面还有文字'
|
|
const readyAt = full.indexOf('[1]') + '[1]'.length
|
|
|
|
for (let i = readyAt; i <= full.length; i++) {
|
|
const chunk = full.slice(0, i)
|
|
const nodes = parseMarkdownToStructure(chunk, md, { final: false })
|
|
const ids = collectByType(nodes, 'reference').map((node: any) => String(node.id ?? ''))
|
|
|
|
expect(ids[0], `prefix(${i}): ${chunk}`).toBe('1')
|
|
}
|
|
})
|
|
|
|
it('still respects validateLink when the trailing markdown link is being recovered', () => {
|
|
const md = getMarkdown('numeric-reference-streaming-validate')
|
|
md.set?.({ validateLink: (url: string) => !/^\s*javascript:/i.test(url.trim()) })
|
|
|
|
const chunk = '随便输入一些文字 [1][百度](javascript:alert(1'
|
|
const nodes = parseMarkdownToStructure(chunk, md, { final: false })
|
|
const ids = collectByType(nodes, 'reference').map((node: any) => String(node.id ?? ''))
|
|
const links = collectByType(nodes, 'link')
|
|
|
|
expect(ids).toEqual(['1'])
|
|
expect(links).toHaveLength(1)
|
|
expect(String(links[0]?.text ?? '')).toBe('百度')
|
|
expect(String(links[0]?.href ?? '')).not.toMatch(/javascript:/i)
|
|
expect(Boolean(links[0]?.loading)).toBe(true)
|
|
expect(collectText(nodes)).toContain('百度')
|
|
})
|
|
})
|