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:
@@ -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 "。"
|
||||
@@ -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 "。"
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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```'} />)
|
||||
|
||||
Reference in New Issue
Block a user