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('

Angular

') expect(html).toContain('') 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('bold') 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: '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: `
\nMore\n

Body

\n
`, final: true, }) expect(html).toContain('
') expect(html).toContain('') expect(html).toContain('More') expect(html).toContain('

Body

') expect(html).toContain('
') }) it('keeps top-level thinking nodes intact while streaming html output', () => { const html = renderMarkdownToHtml({ content: '我们需要给出一个全面的回答', 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('3') expect(html).not.toContain('') }) it('sanitizes dangerous html while preserving safe attributes', () => { const html = sanitizeHtmlFragment('
SafeLink
') expect(html).toContain('
') expect(html).toContain('Safe') expect(html).toContain('Link') expect(html).not.toContain('onclick') expect(html).not.toContain('javascript:') expect(html).not.toContain(' { const structuredHtml = renderMarkdownNodeToHtml( { type: 'html_block', tag: 'span', raw: '', content: '', 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('') expect(structuredHtml).toContain('
  • alpha
  • beta
') const blockedHtml = renderMarkdownNodeToHtml( { type: 'html_block', tag: 'script', raw: '', content: '', children: [ { type: 'list', raw: '', ordered: false, items: [ { type: 'list_item', raw: '', children: [{ type: 'text', raw: '', content: 'alpha' }], }, ], }, ], } as any, ) expect(blockedHtml).not.toContain('
    ') expect(blockedHtml).not.toContain('
  • ') expect(blockedHtml).not.toContain('\n\n- alpha\n\n', content: '
    \n\n- alpha\n\n
    ', children: [ { type: 'list', raw: '', ordered: false, items: [ { type: 'list_item', raw: '', children: [{ type: 'text', raw: '', content: 'alpha' }], }, ], }, ], } as any, ) expect(literalHtml).not.toContain('