Merge remote-tracking branch 'origin/master' into feat/md-incre-2

Port the two master-side markdown fixes onto the mdast renderer:
cjkFriendlyStrong joins both parse.ts grammars as a micromark extension
(ex remark plugin), and inline-code HTTP(S) URLs gain the safe anchor in
render.tsx without normalizeUri. Re-record the two new web-e2e goldens
whose footer clock spacing diverged from the nine established goldens,
and drop the timing-flaky LLM duration segment one of them captured.
This commit is contained in:
07akioni
2026-08-06 14:37:56 +08:00
430 changed files with 11455 additions and 2427 deletions

View File

@@ -0,0 +1,20 @@
<div class="_markdown_404681">
<p>
<strong>
#text "注意:"
#text "内容在标点后直接闭合。"
<p>
#text "**Notice:**text keeps upstream parsing."
<p>
#text "*提醒!*单星号也保持上游行为。"
<p>
<code>
<a href="https://example.com/preview?q=one%20two#result" rel="noopener noreferrer" target="_blank">
#text "https://example.com/preview?q=one%20two#result"
#text " 与 "
<code>
#text "curl http://127.0.0.1:3199/"
#text " 以及 "
<code>
#text "javascript:alert(1)"
#text "。"

View File

@@ -0,0 +1,20 @@
<div class="_markdown_404681">
<p>
<strong>
#text "注意:"
#text "内容在标点后直接闭合。"
<p>
#text "**Notice:**text keeps upstream parsing."
<p>
#text "*提醒!*单星号也保持上游行为。"
<p>
<code>
<a href="https://example.com/preview?q=one%20two#result" rel="noopener noreferrer" target="_blank">
#text "https://example.com/preview?q=one%20two#result"
#text " 与 "
<code>
#text "curl http://127.0.0.1:3199/"
#text " 以及 "
<code>
#text "javascript:alert(1)"
#text "。"

View File

@@ -216,6 +216,15 @@ const CORPUS: Record<string, string> = {
'```',
].join('\n'),
'gfm-strikethrough-and-literals': 'Mixed ~~gone~~ text with www.example.com literal and user@example.com email.',
'cjk-strong-and-inline-code-url': [
'**注意:**内容在标点后直接闭合。',
'',
'**Notice:**text keeps upstream parsing.',
'',
'*提醒!*单星号也保持上游行为。',
'',
'`https://example.com/preview?q=one%20two#result` 与 `curl http://127.0.0.1:3199/` 以及 `javascript:alert(1)`。',
].join('\n'),
'definition-only': '[unused]: https://example.com/unused',
'streaming-typical-partial': '## Streaming\n\n- first\n- **unfinished',
}

View File

@@ -2,6 +2,7 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { JsonBlock, MarkdownText, MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
import { cjkFriendlyStrong } from '../src/markdown/cjkFriendlyStrong.ts'
import { mathCompatibility } from '../src/markdown/mathCompatibility.ts'
afterEach(cleanup)
@@ -67,6 +68,100 @@ describe('MarkdownText', () => {
expect(screen.getByRole('link', { name: 'https://deepseek.com' })).toBeTruthy()
})
it('closes punctuation-terminated strong emphasis before adjacent CJK text', () => {
const cases = [
['**注意:**内容', '注意:'],
['**Notice:**内容', 'Notice:'],
['**事件中间件(waterfall)**实现', '事件中间件(waterfall)'],
['**事件中间件(waterfall)**实现', '事件中间件(waterfall)'],
['**句号。**后续', '句号。'],
['**Period.**后续', 'Period.'],
['**提醒!**继续', '提醒!'],
['**Warning!**继续', 'Warning!'],
] as const
const source = cases.map(([markdown]) => markdown).join('\n\n')
for (const streaming of [false, true]) {
const rendered = render(<MarkdownText text={source} streaming={streaming} />)
expect([...rendered.container.querySelectorAll('strong')].map(node => node.textContent))
.toEqual(cases.map(([, strong]) => strong))
rendered.unmount()
}
})
it('keeps the CJK strong extension out of escaped, code, math, and ASCII contexts', () => {
const source = [
String.raw`\**注意:**内容`,
'`**注意:**内容`',
'**Notice:**text',
'*提醒!*继续',
'$**注意:**内容$',
'```md',
'**注意:**内容',
'```',
'**普通**内容',
'*普通*内容',
].join('\n\n')
const { container } = render(<MarkdownText text={source} />)
expect([...container.querySelectorAll('strong')].map(node => node.textContent)).toEqual(['普通'])
expect([...container.querySelectorAll('em')].map(node => node.textContent)).toEqual(['普通'])
expect(container.querySelector('code')?.textContent).toBe('**注意:**内容')
expect(container.querySelector('.katex annotation')?.textContent).toBe('**注意:**内容')
expect(container.querySelector('pre code')?.textContent).toContain('**注意:**内容')
expect(container.textContent).toContain('**Notice:**text')
expect(container.textContent).toContain('*提醒!*继续')
expect(container.textContent).toContain('**注意:**内容')
})
it('links complete HTTP(S) inline code without promoting commands, unsafe schemes, or fences', () => {
const localUrl = 'http://127.0.0.1:3199/?demo=1'
const remoteUrl = 'https://example.com/preview?q=one%20two#result'
const source = [
`\`${localUrl}\``,
`\`${remoteUrl}\``,
'`curl http://127.0.0.1:3199/?demo=1`',
'`javascript:alert(1)`',
'`mailto:dev@example.com`',
`\` ${localUrl} \``,
'```',
localUrl,
'```',
].join('\n\n')
const { container } = render(<MarkdownText text={source} />)
const links = screen.getAllByRole('link')
expect(links.map(link => link.getAttribute('href'))).toEqual([localUrl, remoteUrl])
for (const link of links) {
expect(link.closest('code')).not.toBeNull()
expect(link.getAttribute('target')).toBe('_blank')
expect(link.getAttribute('rel')).toBe('noopener noreferrer')
}
links[0]?.focus()
expect(document.activeElement).toBe(links[0])
expect(screen.getByText('curl http://127.0.0.1:3199/?demo=1').closest('a')).toBeNull()
expect(screen.getByText('javascript:alert(1)').closest('a')).toBeNull()
expect(screen.getByText('mailto:dev@example.com').closest('a')).toBeNull()
const paddedCode = [...container.querySelectorAll('code')]
.find(code => code.textContent === ` ${localUrl} `)
expect(paddedCode?.querySelector('a')).toBeNull()
expect(container.querySelector('pre code a')).toBeNull()
})
it('exposes the CJK strong syntax as a micromark extension needing CommonMark attention markers', () => {
const extension = cjkFriendlyStrong()
expect(cjkFriendlyStrong()).toBe(extension)
const construct = extension.text?.[42]
const tokenizer = Array.isArray(construct) ? construct[0]?.tokenize : construct?.tokenize
expect(tokenizer).toBeTypeOf('function')
expect(() => tokenizer?.call({
parser: { constructs: { attentionMarkers: {} } },
previous: null,
} as never, {} as never, () => undefined, () => undefined)).toThrow(
'micromark CommonMark attention markers are unavailable',
)
})
it('a fence labeled with an inherited object key renders plain, never crashing shiki', () => {
for (const label of ['constructor', '__proto__', 'toString', 'hasOwnProperty']) {
const { container, unmount } = render(<MarkdownText text={'```' + label + '\ncode body\n```'} />)