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,266 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
renderMarkdownNodeToHtml,
|
||||
renderMarkdownToHtml,
|
||||
renderNestedMarkdownToHtml,
|
||||
} from '../packages/markstream-angular/src/renderMarkdownHtml'
|
||||
import { sanitizeHtmlContent as sanitizeHtmlFragment } from '../packages/markstream-angular/src/sanitizeHtmlContent'
|
||||
|
||||
describe('markstream-angular html renderer', () => {
|
||||
it('renders markdown content into stable html for baseline blocks', () => {
|
||||
const html = renderMarkdownToHtml({
|
||||
content: `# Angular\n\n- one\n- two\n\n\`\`\`ts\nconst value = 1\n\`\`\``,
|
||||
final: true,
|
||||
})
|
||||
|
||||
expect(html).toContain('<h1>Angular</h1>')
|
||||
expect(html).toContain('<ul><li><p>one</p></li><li><p>two</p></li></ul>')
|
||||
expect(html).toContain('data-markstream-code-block="1"')
|
||||
expect(html).toContain('data-markstream-language="ts"')
|
||||
expect(html).toContain('class="language-ts"')
|
||||
expect(html).toContain('const value = 1')
|
||||
})
|
||||
|
||||
it('preserves diff metadata on code fences for advanced enhancers', () => {
|
||||
const html = renderMarkdownToHtml({
|
||||
content: `\`\`\`diff ts
|
||||
-const value = 1
|
||||
+const value: number = 1
|
||||
\`\`\``,
|
||||
final: true,
|
||||
})
|
||||
|
||||
expect(html).toContain('data-markstream-diff="1"')
|
||||
expect(html).toContain('data-markstream-original="')
|
||||
expect(html).toContain('data-markstream-updated="')
|
||||
expect(html).toContain('data-markstream-language="ts"')
|
||||
})
|
||||
|
||||
it('renders nested custom nodes with merged classes', () => {
|
||||
const html = renderNestedMarkdownToHtml(
|
||||
{
|
||||
nodes: [
|
||||
{
|
||||
type: 'thinking',
|
||||
tag: 'thinking',
|
||||
raw: '',
|
||||
attrs: [['class', 'preset']],
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
raw: '',
|
||||
children: [
|
||||
{ type: 'text', raw: '', content: 'Inner ' },
|
||||
{
|
||||
type: 'strong',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'bold' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
customHtmlTags: ['thinking'],
|
||||
customNodeClass: 'thinking-node__nested',
|
||||
},
|
||||
)
|
||||
|
||||
expect(html).toContain('<strong>bold</strong>')
|
||||
expect(html).toContain('data-markstream-custom-tag="thinking"')
|
||||
expect(html).toContain('class="markstream-nested-custom markstream-nested-custom--thinking thinking-node__nested preset"')
|
||||
})
|
||||
|
||||
it('escapes incomplete html nodes instead of trusting partial markup', () => {
|
||||
const html = renderMarkdownNodeToHtml(
|
||||
{
|
||||
type: 'html_inline',
|
||||
raw: '',
|
||||
content: '<span>partial',
|
||||
loading: true,
|
||||
autoClosed: false,
|
||||
} as any,
|
||||
)
|
||||
|
||||
expect(html).toBe('<span>partial')
|
||||
})
|
||||
|
||||
it('preserves safe html structures like details and summary', () => {
|
||||
const html = renderMarkdownToHtml({
|
||||
content: `<details>\n<summary>More</summary>\n<p>Body</p>\n</details>`,
|
||||
final: true,
|
||||
})
|
||||
|
||||
expect(html).toContain('<details>')
|
||||
expect(html).toContain('<summary>')
|
||||
expect(html).toContain('More')
|
||||
expect(html).toContain('<p>Body</p>')
|
||||
expect(html).toContain('</details>')
|
||||
})
|
||||
|
||||
it('keeps top-level thinking nodes intact while streaming html output', () => {
|
||||
const html = renderMarkdownToHtml({
|
||||
content: '<thinking>我们需要给出一个全面的回答',
|
||||
final: false,
|
||||
customHtmlTags: ['thinking'],
|
||||
})
|
||||
|
||||
expect(html).toContain('data-markstream-custom-tag="thinking"')
|
||||
expect(html).toContain('全面的回答')
|
||||
expect(html).not.toContain('<thinking>')
|
||||
})
|
||||
|
||||
it('emits math source and render shells so KaTeX can refresh incrementally', () => {
|
||||
const html = renderMarkdownToHtml({
|
||||
content: '$x = a$\n\n$$f(x) = x^2$$',
|
||||
final: true,
|
||||
})
|
||||
|
||||
expect(html).toContain('markstream-nested-math__source')
|
||||
expect(html).toContain('markstream-nested-math__render')
|
||||
expect(html).toContain('markstream-nested-math-block__source')
|
||||
expect(html).toContain('markstream-nested-math-block__render')
|
||||
})
|
||||
|
||||
it('keeps softbreak text visible and renders references like the runtime renderer', () => {
|
||||
const html = renderMarkdownToHtml({
|
||||
content: '第一行\n第二行[3]',
|
||||
final: true,
|
||||
})
|
||||
|
||||
expect(html).toContain('class="markstream-angular-text-node"')
|
||||
expect(html).toContain('第一行\n第二行')
|
||||
expect(html).toContain('<span class="markstream-nested-reference">3</span>')
|
||||
expect(html).not.toContain('<sup class="markstream-nested-reference">')
|
||||
})
|
||||
|
||||
it('sanitizes dangerous html while preserving safe attributes', () => {
|
||||
const html = sanitizeHtmlFragment('<details open onclick="evil()"><summary>Safe</summary><a href="javascript:alert(1)" title="ok">Link</a><script>alert(1)</script></details>')
|
||||
|
||||
expect(html).toContain('<details open>')
|
||||
expect(html).toContain('<summary>Safe</summary>')
|
||||
expect(html).toContain('<a title="ok">Link</a>')
|
||||
expect(html).not.toContain('onclick')
|
||||
expect(html).not.toContain('javascript:')
|
||||
expect(html).not.toContain('<script')
|
||||
expect(html).not.toContain('alert(1)')
|
||||
})
|
||||
|
||||
it('renders structured html wrappers while leaving blocked tags on the sanitized fallback path', () => {
|
||||
const structuredHtml = renderMarkdownNodeToHtml(
|
||||
{
|
||||
type: 'html_block',
|
||||
tag: 'span',
|
||||
raw: '<span style="font-size: 12px;"></span>',
|
||||
content: '<span style="font-size: 12px;"></span>',
|
||||
attrs: [['style', 'font-size: 12px;']],
|
||||
children: [
|
||||
{
|
||||
type: 'list',
|
||||
raw: '',
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: 'list_item',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'alpha' }],
|
||||
},
|
||||
{
|
||||
type: 'list_item',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'beta' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
)
|
||||
|
||||
expect(structuredHtml).toContain('<span style="font-size: 12px;">')
|
||||
expect(structuredHtml).toContain('<ul><li>alpha</li><li>beta</li></ul>')
|
||||
|
||||
const blockedHtml = renderMarkdownNodeToHtml(
|
||||
{
|
||||
type: 'html_block',
|
||||
tag: 'script',
|
||||
raw: '<script>\n\n- alpha\n\n</script>',
|
||||
content: '<script>\n\n- alpha\n\n</script>',
|
||||
children: [
|
||||
{
|
||||
type: 'list',
|
||||
raw: '',
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: 'list_item',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'alpha' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
)
|
||||
|
||||
expect(blockedHtml).not.toContain('<ul>')
|
||||
expect(blockedHtml).not.toContain('<li>')
|
||||
expect(blockedHtml).not.toContain('<script')
|
||||
|
||||
const literalHtml = renderMarkdownNodeToHtml(
|
||||
{
|
||||
type: 'html_block',
|
||||
tag: 'pre',
|
||||
raw: '<pre>\n\n- alpha\n\n</pre>',
|
||||
content: '<pre>\n\n- alpha\n\n</pre>',
|
||||
children: [
|
||||
{
|
||||
type: 'list',
|
||||
raw: '',
|
||||
ordered: false,
|
||||
items: [
|
||||
{
|
||||
type: 'list_item',
|
||||
raw: '',
|
||||
children: [{ type: 'text', raw: '', content: 'alpha' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
)
|
||||
|
||||
expect(literalHtml).not.toContain('<ul>')
|
||||
expect(literalHtml).not.toContain('<li>')
|
||||
expect(literalHtml).toContain('<pre>')
|
||||
})
|
||||
|
||||
it('sanitizes dangerous attrs on structured html wrappers', () => {
|
||||
const html = renderMarkdownNodeToHtml(
|
||||
{
|
||||
type: 'html_block',
|
||||
tag: 'a',
|
||||
raw: '<a href="javascript:alert(1)" onclick="alert(1)" data-safe="ok"></a>',
|
||||
content: '<a href="javascript:alert(1)" onclick="alert(1)" data-safe="ok"></a>',
|
||||
attrs: [
|
||||
['href', 'javascript:alert(1)'],
|
||||
['onclick', 'alert(1)'],
|
||||
['data-safe', 'ok'],
|
||||
],
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
raw: 'safe child',
|
||||
children: [{ type: 'text', raw: 'safe child', content: 'safe child' }],
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
)
|
||||
|
||||
expect(html).toContain('<a data-safe="ok">')
|
||||
expect(html).toContain('<p>safe child</p>')
|
||||
expect(html).not.toContain('onclick=')
|
||||
expect(html).not.toContain('javascript:')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user