fix(ui-primitives): preserve raw HTML in text extraction
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||||
# after editing either side, bring the other along and re-record with:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
|
# pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
|
||||||
README.md: 29d7b5e20dbc3a53fe4b85e219b0c957c05ed873
|
README.md: 99d5f8b238ce78d83de0b5247d194b308dd6fac0
|
||||||
README.zh.md: 92d256f7f1d413a9792aa54f50317c2a15dd09a0
|
README.zh.md: 468aa75e88580d6ed5e78666ad34c18b22a85dd3
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/
|
|||||||
|
|
||||||
## Markdown rendering
|
## Markdown rendering
|
||||||
|
|
||||||
`MarkdownText` renders GFM from untrusted assistant output through React elements. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders image alt text without loading remote resources; `MessageText` remains the literal-text primitive for user-authored content. Element spacing, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars).
|
`MarkdownText` renders GFM from untrusted assistant output through React elements. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders image alt text without loading remote resources; `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars).
|
||||||
|
|
||||||
## Model Experience
|
## Model Experience
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Markdown 渲染
|
## Markdown 渲染
|
||||||
|
|
||||||
`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并只渲染图片 alt 文本而不加载远程资源;`MessageText` 仍是用户创作内容使用的字面文本原语。元素间距、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。
|
`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并只渲染图片 alt 文本而不加载远程资源;`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。
|
||||||
|
|
||||||
## 模型体验
|
## 模型体验
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Markdown-to-plain-text projection for compact summaries and labels.
|
* Markdown-to-plain-text projection for compact summaries and labels.
|
||||||
* Parsing shares the renderer's GFM grammar; raw HTML is omitted, links
|
* Parsing shares the renderer's GFM grammar; raw HTML stays literal, links
|
||||||
* keep their labels, images keep alt text, and code keeps its source text.
|
* keep their labels, images keep alt text, and code keeps its source text.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ function inlineText(node: MarkdownNode): string {
|
|||||||
case 'break':
|
case 'break':
|
||||||
return '\n'
|
return '\n'
|
||||||
case 'html':
|
case 'html':
|
||||||
return ''
|
return node.value ?? ''
|
||||||
default:
|
default:
|
||||||
return node.children?.map(inlineText).join('') ?? ''
|
return node.children?.map(inlineText).join('') ?? ''
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ function blockText(node: MarkdownNode): string {
|
|||||||
case 'tableCell':
|
case 'tableCell':
|
||||||
return compactInline(inlineText(node))
|
return compactInline(inlineText(node))
|
||||||
case 'html':
|
case 'html':
|
||||||
return ''
|
return node.value ?? ''
|
||||||
case 'thematicBreak':
|
case 'thematicBreak':
|
||||||
case 'definition':
|
case 'definition':
|
||||||
return ''
|
return ''
|
||||||
@@ -98,7 +98,7 @@ function fullText(root: MarkdownNode): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse GFM Markdown and project its user-visible plain text.
|
* Parse GFM Markdown, remove its presentation markup, and preserve raw HTML literally.
|
||||||
* @param markdown - Markdown source.
|
* @param markdown - Markdown source.
|
||||||
* @param options - Optional extraction boundary.
|
* @param options - Optional extraction boundary.
|
||||||
* @returns Plain text for the whole document, first visible line, or first semantic paragraph.
|
* @returns Plain text for the whole document, first visible line, or first semantic paragraph.
|
||||||
|
|||||||
@@ -34,10 +34,18 @@ describe('extractMarkdownPlainText', () => {
|
|||||||
.toBe('First paragraph with a link and diagram.')
|
.toBe('First paragraph with a link and diagram.')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('omits raw HTML and returns a useful fallback when no paragraph exists', () => {
|
it('preserves raw HTML while removing Markdown presentation markup', () => {
|
||||||
const markdown = '<script>alert(1)</script>\n\n## Safe heading'
|
const block = [
|
||||||
expect(extractMarkdownPlainText(markdown)).toBe('Safe heading')
|
'<background-task-complete id="trajectory-ui-watch">',
|
||||||
expect(extractMarkdownPlainText(markdown, { mode: 'first-paragraph' })).toBe('Safe heading')
|
'Command: pnpm test',
|
||||||
|
'Exit code: 0',
|
||||||
|
'</background-task-complete>',
|
||||||
|
].join('\n')
|
||||||
|
expect(extractMarkdownPlainText(block)).toBe(block)
|
||||||
|
expect(extractMarkdownPlainText('**Status:** <span data-state="ok">ready</span>'))
|
||||||
|
.toBe('Status: <span data-state="ok">ready</span>')
|
||||||
|
expect(extractMarkdownPlainText(block, { mode: 'first-paragraph' }))
|
||||||
|
.toBe('<background-task-complete id="trajectory-ui-watch">')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('projects GFM tables, references, hard breaks, and block structure', () => {
|
it('projects GFM tables, references, hard breaks, and block structure', () => {
|
||||||
@@ -54,7 +62,7 @@ describe('extractMarkdownPlainText', () => {
|
|||||||
'[asset]: diagram.png',
|
'[asset]: diagram.png',
|
||||||
].join('\n')
|
].join('\n')
|
||||||
expect(extractMarkdownPlainText(markdown)).toBe([
|
expect(extractMarkdownPlainText(markdown)).toBe([
|
||||||
'first second with diagram and visible',
|
'first second with diagram and <span>visible</span>',
|
||||||
'',
|
'',
|
||||||
'Name\tValue',
|
'Name\tValue',
|
||||||
'alpha\t1',
|
'alpha\t1',
|
||||||
|
|||||||
@@ -83,6 +83,34 @@ describe('TrajectoryTable', () => {
|
|||||||
expect(screen.getByText('15 tok')).toBeTruthy()
|
expect(screen.getByText('15 tok')).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps raw HTML tags in a Markdown-derived context preview', () => {
|
||||||
|
const html = [
|
||||||
|
'<background-task-complete id="trajectory-ui-watch">',
|
||||||
|
'Command: pnpm test',
|
||||||
|
'Exit code: 0',
|
||||||
|
'</background-task-complete>',
|
||||||
|
].join('\n')
|
||||||
|
const turns: readonly TrajectoryTurnModel[] = [{
|
||||||
|
turn: 1,
|
||||||
|
groups: [{
|
||||||
|
title: 'Message',
|
||||||
|
cells: [{
|
||||||
|
index: 1,
|
||||||
|
kind: 'context',
|
||||||
|
text: '',
|
||||||
|
inputDetail: html,
|
||||||
|
timeSeconds: 0,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
|
||||||
|
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
||||||
|
|
||||||
|
expect(screen.getByText(
|
||||||
|
'<background-task-complete id="trajectory-ui-watch"> Command: pnpm test Exit code: 0 </background-task-complete>',
|
||||||
|
)).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps running and failure semantics distinct from record roles', () => {
|
it('keeps running and failure semantics distinct from record roles', () => {
|
||||||
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
|
||||||
expect(view.container.querySelector('tr[data-kind="tool"][data-running="true"]')).toBeTruthy()
|
expect(view.container.querySelector('tr[data-kind="tool"][data-running="true"]')).toBeTruthy()
|
||||||
|
|||||||
Reference in New Issue
Block a user