Merge pull request #5319 from deepseek-harness/turtle/frontmatter-render-9aa734baa39d
feat(client): render Markdown YAML frontmatter as fields in document preview
This commit is contained in:
commit
08f0721e9d
15 files changed
+292
-9
No files matched your search
@@ -1,3 +1,5 @@
|
||||
- separator
|
||||
- 'heading "name: policy-shared description: Available to both model and user invocation" [level=2]'
|
||||
- term: name
|
||||
- definition: policy-shared
|
||||
- term: description
|
||||
- definition: Available to both model and user invocation
|
||||
- heading "policy-shared" [level=1]
|
||||
@@ -170,6 +170,7 @@ describe('web e2e: skill invocation policy through the real host', () => {
|
||||
await skill.click()
|
||||
const preview = page.locator('[data-document-markdown]')
|
||||
await expect.poll(() => preview.textContent()).toContain('policy-shared')
|
||||
await preview.locator('[data-document-frontmatter] dl').waitFor()
|
||||
expect(await input.textContent()).toBe(draft)
|
||||
const snapshot = await captureStableAria(page, '[data-document-markdown]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'preview.expected.md'), snapshot, MODE)
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: 949156aa02ec8b07
|
||||
zh: d3675754e2a37b5d
|
||||
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/how-it-reads:
|
||||
en: 5cdbd4a218315e70
|
||||
zh: d5054f541e0e0275
|
||||
en: b056bbe0f74fe328
|
||||
zh: fd9ac88f08825afc
|
||||
/deepseek-ai-dsh-client-ui-sidebar-documentpreview/excel-preview:
|
||||
en: b99b0e0b8efc2853
|
||||
zh: 4327352d1334fc6b
|
||||
|
||||
@@ -65,6 +65,8 @@ Shared copy comes from `sidebarDocumentPreview`; each builtin renderer owns its
|
||||
|
||||
Initial reads and renderer preparation, including Office conversion, share a centered 28px ongoing `StateDot` with the visible localized status “Rendering document...”. The status exposes the same accessible name and respects reduced-motion preferences. Additional text pages keep a compact inline spinner while loaded content stays visible. The PDF body loads its package-local `client.pdf.js` chunk only when a PDF preview mounts; PDF.js, its Worker source, and embedded support data stay out of the startup `client.js`. PDF and converted Word/PowerPoint pages form a vertical sequence with 12px page gaps and a 12px outer inset showing the theme-specific document background; fit width reserves this inset, and pages render lazily near the viewport; an unrendered page holds its place as a quiet 3:4 placeholder block. PDF.js’s official TextLayerBuilder manages selection boundaries and normalized copying over an aligned text layer. Selections use translucent blue in both themes while the canvas retains the document text; blank line breaks remain unhighlighted; alignment accounts for PDF page units, page rotation, and viewport resizing, and page disposal cancels both layers. Image-only PDFs contain no selectable text. Code previews show a tertiary-colored language label and a copy icon with a tooltip; wrapping remains in the document toolbar. They show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code. Code sits on the pane's own background rather than the chat card's fill; its banner is adjacent to a full-height inner scrollport, so both scrollbars begin below the copy control.
|
||||
|
||||
A leading YAML frontmatter block, opened by `---` on the first line and closed by `---` or `...`, renders above the Markdown body as a key/value list. String values show their resolved text, including folded and multiline strings; other scalars, lists, mappings, and aliases keep their authored YAML. Invalid YAML and non-mapping documents show the block verbatim, and a block without content shows nothing. An unterminated block remains Markdown. The YAML parser loads in the package-local `client.frontmatter-fields.js` chunk only when a document has frontmatter; the verbatim block stands in while it loads or if it fails to load. The source file is unchanged.
|
||||
|
||||
<a id="excel-preview"></a>
|
||||
## Excel preview
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `<img>` 静态
|
||||
|
||||
首次读取及渲染器准备(包括 Office 转换)共用居中的 28px ongoing `StateDot`,下方显示本地化状态文字“文档渲染中...”。状态向辅助技术提供相同的名称,并遵循减少动态效果偏好。追加文本页时使用紧凑的行内转圈,同时保留已显示的内容。PDF 正文仅在 PDF 预览挂载时加载包内 `client.pdf.js` chunk;PDF.js、Worker 源码和内嵌支持数据不会进入启动 `client.js`。PDF 与转换后的 Word/PowerPoint 页面组成纵向序列,页间距与四周留白均为 12px,露出当前主题的文档背景色;适应宽度会预留此留白,页面在接近视口时惰性渲染;未渲染的页以安静的 3:4 占位块保持位置。PDF.js 官方 TextLayerBuilder 在与画面重合的文字层上管理选区边界和复制文本规范化。两种主题均使用半透明蓝色选区,画布保留文档文字,空白换行不高亮;对齐同时考虑 PDF 页面单位、页面旋转与视口宽度变化,页面释放时取消两层渲染。纯图片 PDF 不包含可选取的文字。代码预览使用 tertiary 色的语言名称和带提示的复制图标;换行仍由文档工具栏控制。默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;复制条与占满剩余高度的内部滚动区相邻,因此横纵滚动条都从复制控件下方开始。
|
||||
|
||||
文档开头的 YAML frontmatter(首行 `---` 开始,以 `---` 或 `...` 结束)在 Markdown 正文上方显示为键值列表。字符串值显示解析后的文本,包括折叠与多行字符串;其他标量、列表、映射与别名保留原始 YAML 写法。无效 YAML 或非映射文档原样显示整个块,没有内容的块不显示。未闭合的块仍按 Markdown 渲染。YAML 解析器位于包内 `client.frontmatter-fields.js` 分块,仅在文档含 frontmatter 时加载;加载期间或加载失败时显示原样块。源文件不会被修改。
|
||||
|
||||
<a id="excel-preview"></a>
|
||||
## Excel 预览
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@
|
||||
"pdfjs-dist": "6.3.289",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
|
||||
+24
@@ -5,3 +5,27 @@
|
||||
font-family: var(--dsw-font, inherit);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Leading YAML frontmatter: a bordered metadata box above the body, never heading text. */
|
||||
.frontmatter {
|
||||
margin: 0 0 16px;
|
||||
padding: 10px 12px;
|
||||
border: 0.5px solid var(--dsw-alias-border-l2);
|
||||
border-radius: var(--dsw-radius-md);
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
font: var(--dsw-font-markdown-small);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* A document without frontmatter content leaves the box empty. */
|
||||
.frontmatter:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.frontmatterSource {
|
||||
margin: 0;
|
||||
font-family: var(--ds-font-family-code);
|
||||
white-space: inherit;
|
||||
}
|
||||
@@ -1,18 +1,33 @@
|
||||
/** One retained Markdown renderer over the document owner's accumulated text. */
|
||||
import { useMemo } from 'react'
|
||||
import { lazy, Suspense, useMemo } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { MarkdownText, type MarkdownLabels, type MarkdownPathImages } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { DocumentPreviewProps } from '../document/contract.ts'
|
||||
import { splitFrontmatter } from './frontmatter.ts'
|
||||
import type { FrontmatterFieldsProps } from './frontmatter-fields.tsx'
|
||||
import { markdownImageUrl } from './path-images.ts'
|
||||
import type {} from './locales.ts'
|
||||
import css from './MarkdownBody.module.css'
|
||||
|
||||
/** Show the verbatim frontmatter when the field-list chunk cannot load. */
|
||||
function FrontmatterFallback({ fallback }: FrontmatterFieldsProps): ReactNode {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const FrontmatterFields = lazy(() => import('./frontmatter-fields.tsx').then(
|
||||
module => ({ default: module.FrontmatterFields }),
|
||||
// The failed chunk only removes the field layout; the body and verbatim metadata stay usable.
|
||||
() => ({ default: FrontmatterFallback }),
|
||||
))
|
||||
|
||||
/** Standard document inputs and this implementation's locale. */
|
||||
export type MarkdownBodyProps = DocumentPreviewProps & PropsLocale<'documentMarkdown'>
|
||||
|
||||
/**
|
||||
* Render one accumulated document; EOF completes the primitive's full parse.
|
||||
* A leading YAML frontmatter block renders as a field list instead of Markdown; its
|
||||
* verbatim source stands in while the field-list chunk loads or when it fails.
|
||||
* @param props - owner-loaded contents and localized primitive labels.
|
||||
* @returns Markdown content, or nothing for a non-text delivery.
|
||||
*/
|
||||
@@ -30,10 +45,27 @@ export function MarkdownBody({ content, resourceAddress, useResource, t }: Markd
|
||||
const labels = useMemo<MarkdownLabels>(() => ({
|
||||
code: { copyLabel, copiedLabel, toolbarLabels: { codeLabel, wrapLabel, unwrapLabel } }, footnotes,
|
||||
}), [copyLabel, copiedLabel, footnotes, codeLabel, wrapLabel, unwrapLabel])
|
||||
const text = content.kind === 'text' ? content.text : ''
|
||||
const split = useMemo(() => splitFrontmatter(text), [text])
|
||||
if (content.kind !== 'text') return null
|
||||
return (
|
||||
<div className={css.document} data-document-markdown>
|
||||
<MarkdownText text={content.text} streaming={!content.eof} labels={labels} pathImages={pathImages} />
|
||||
{split === undefined ? null : <FrontmatterBox source={split.source} />}
|
||||
<MarkdownText text={split?.body ?? text} streaming={!content.eof} labels={labels} pathImages={pathImages} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Frame one frontmatter block and choose its field list or verbatim source.
|
||||
* @param props - YAML between the frontmatter delimiters.
|
||||
* @returns the metadata box; it collapses when the document has no content.
|
||||
*/
|
||||
function FrontmatterBox({ source }: { source: string }): ReactNode {
|
||||
const verbatim = <pre className={css.frontmatterSource}>{source}</pre>
|
||||
return (
|
||||
<div className={css.frontmatter} data-document-frontmatter>
|
||||
<Suspense fallback={verbatim}><FrontmatterFields source={source} fallback={verbatim} /></Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/* Key/value grid inside the Markdown body's frontmatter box. */
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr);
|
||||
gap: 4px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.key {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font-family: var(--ds-font-family-code);
|
||||
}
|
||||
|
||||
.value,
|
||||
.block {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.block {
|
||||
font-family: var(--ds-font-family-code);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Field list for a Markdown document's leading YAML frontmatter. The Markdown
|
||||
* body loads this module lazily so the YAML parser stays out of the startup
|
||||
* chunk.
|
||||
*/
|
||||
import { useMemo, type ReactNode } from 'react'
|
||||
import { isMap, isNode, isScalar, parseDocument } from 'yaml'
|
||||
import css from './frontmatter-fields.module.css'
|
||||
|
||||
/** One top-level entry; `block` marks the authored YAML of a collection or alias value. */
|
||||
interface FrontmatterField {
|
||||
key: string
|
||||
value: string
|
||||
block: boolean
|
||||
}
|
||||
|
||||
/** Owner inputs for the lazily loaded field list. */
|
||||
export interface FrontmatterFieldsProps {
|
||||
/** YAML between the frontmatter delimiters. */
|
||||
source: string
|
||||
/** Verbatim block shown for invalid YAML and non-mapping documents. */
|
||||
fallback: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve top-level mapping fields from the YAML syntax tree.
|
||||
* @param source - YAML between the frontmatter delimiters.
|
||||
* @returns the fields, `[]` for a document without content, or `undefined` for invalid YAML and non-mapping documents.
|
||||
*/
|
||||
function parseFields(source: string): readonly FrontmatterField[] | undefined {
|
||||
const parsed = parseDocument(source)
|
||||
if (parsed.errors.length > 0) return undefined
|
||||
if (parsed.contents === null) return []
|
||||
if (!isMap(parsed.contents)) return undefined
|
||||
return parsed.contents.items.map(({ key, value }) => ({
|
||||
key: nodeText(source, key),
|
||||
value: nodeText(source, value),
|
||||
block: isNode(value) && !isScalar(value),
|
||||
}))
|
||||
}
|
||||
|
||||
// Strings show their resolved value, so folded and multiline text reads as prose; other
|
||||
// nodes keep their authored text, so `1.0` stays `1.0` and aliases stay unexpanded.
|
||||
function nodeText(source: string, node: unknown): string {
|
||||
if (isScalar(node) && typeof node.value === 'string') return node.value.trimEnd()
|
||||
if (!isNode(node)) return ''
|
||||
/* v8 ignore next -- parsed nodes always carry their source range. */
|
||||
const [start, end] = node.range ?? [0, 0]
|
||||
const indent = start - source.lastIndexOf('\n', start - 1) - 1
|
||||
return source.slice(start, end).replace(new RegExp(`\\n {0,${indent}}`, 'gu'), '\n').trimEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* Render frontmatter fields, or the owner's verbatim block when the YAML is not a mapping.
|
||||
* @param props - frontmatter source and its verbatim fallback.
|
||||
* @returns the field list, the fallback, or nothing for a document without content.
|
||||
*/
|
||||
export function FrontmatterFields({ source, fallback }: FrontmatterFieldsProps): ReactNode {
|
||||
const fields = useMemo(() => parseFields(source), [source])
|
||||
if (fields === undefined) return fallback
|
||||
if (fields.length === 0) return null
|
||||
return (
|
||||
<dl className={css.fields}>
|
||||
{fields.map((field, index) => (
|
||||
<div key={index} className={css.field}>
|
||||
<dt className={css.key}>{field.key}</dt>
|
||||
<dd className={field.block ? css.block : css.value}>{field.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Leading YAML frontmatter split from a Markdown document before Markdown parsing. */
|
||||
|
||||
/** Frontmatter source between its delimiters and the Markdown body after the closing delimiter. */
|
||||
export interface FrontmatterSplit {
|
||||
source: string
|
||||
body: string
|
||||
}
|
||||
|
||||
// Only an opening `---` on the first line (after an optional BOM) counts; the block ends at the
|
||||
// first closing `---` or `...` line, and `??` lets an empty block close on the next line.
|
||||
const FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n(?:([\s\S]*?)\r?\n)??(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/u
|
||||
|
||||
/**
|
||||
* Split a leading YAML frontmatter block from Markdown text without parsing its YAML.
|
||||
* @param text - Markdown source; an unterminated block is not frontmatter.
|
||||
* @returns frontmatter source and the remaining body, or `undefined` when the text has none.
|
||||
*/
|
||||
export function splitFrontmatter(text: string): FrontmatterSplit | undefined {
|
||||
const match = FRONTMATTER.exec(text)
|
||||
if (match === null) return undefined
|
||||
return { source: match[1] ?? '', body: text.slice(match[0].length) }
|
||||
}
|
||||
+16
-1
@@ -53,12 +53,13 @@ describe('published document preview licenses', () => {
|
||||
], packageRoot, task.timeout)) as { filename: string; files: { path: string }[] }
|
||||
expect(packed.files.map(file => file.path)).toContain('lib/client.js')
|
||||
expect(packed.files.map(file => file.path)).toContain('lib/client.pdf.js')
|
||||
expect(packed.files.map(file => file.path)).toContain('lib/client.frontmatter-fields.js')
|
||||
expect(packed.files.some(file => file.path.endsWith('pdfjs-NOTICES.txt'))).toBe(false)
|
||||
|
||||
const client = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.js'], packageRoot, task.timeout)
|
||||
const pdf = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.pdf.js'], packageRoot, task.timeout)
|
||||
expect([...client.matchAll(/require\.async\("(\.\/client[^"/]*\.js)"\)/gu)].map(match => match[1]))
|
||||
.toEqual(['./client.pdf.js', './client.excel.js'])
|
||||
.toEqual(['./client.frontmatter-fields.js', './client.pdf.js', './client.excel.js'])
|
||||
expect(client).not.toMatch(/\brequire\("\.\/client[^"/]*\.js"\)/u)
|
||||
expect([...pdf.matchAll(/require\("(\.\/client[^"/]*\.js)"\)/gu)].map(match => match[1]))
|
||||
.toEqual([])
|
||||
@@ -76,6 +77,20 @@ describe('published document preview licenses', () => {
|
||||
const license = readFileSync(join(root, 'LICENSE'), 'utf8').trimEnd()
|
||||
expect(excel).toContain(license.split('\n').map(line => `// ${line}`).join('\n'))
|
||||
}
|
||||
const frontmatter = run('tar', ['-xOf', resolve(packageRoot, packed.filename), 'package/lib/client.frontmatter-fields.js'], packageRoot, task.timeout)
|
||||
expect(frontmatter).not.toMatch(/\brequire\("\.\/client[^"/]*\.js"\)/u)
|
||||
let frontmatterInitialized = false
|
||||
runInNewContext(frontmatter, { window: { __ModuleLoader__: { load: (registration: {
|
||||
factory: (resolve: (specifier: string) => unknown) => { FrontmatterFields: unknown }
|
||||
}) => {
|
||||
const loaded = registration.factory((specifier) => {
|
||||
if (specifier === 'react' || specifier === 'react/jsx-runtime') return require(specifier)
|
||||
throw new Error(`Unexpected browser dependency: ${specifier}`)
|
||||
})
|
||||
expect(typeof loaded.FrontmatterFields).toBe('function')
|
||||
frontmatterInitialized = true
|
||||
} } } })
|
||||
expect(frontmatterInitialized).toBe(true)
|
||||
let initialized = false
|
||||
runInNewContext(excel, { window: { __ModuleLoader__: { load: (registration: {
|
||||
factory: (resolve: (specifier: string) => unknown) => { ExcelBody: unknown }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @vitest-environment jsdom
|
||||
/** Markdown preview uses one accumulated document across page arrivals and EOF. */
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { MarkdownBody, type MarkdownBodyProps } from '../src/client/markdown/MarkdownBody.tsx'
|
||||
import { en, zh } from '../src/client/markdown/locales.ts'
|
||||
@@ -129,6 +129,60 @@ describe('MarkdownBody', () => {
|
||||
expect(view.getByRole('heading', { name: '脚注' })).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders leading YAML frontmatter as fields instead of a heading', async () => {
|
||||
const text = [
|
||||
'---', 'name: pdf', 'description: >-', ' Extract text', ' from PDF files.',
|
||||
'metadata:', ' tags: [docs, pdf]', 'anchor: &list [a]', 'copy: *list', 'license:', 'version: 1.0', 'beta: true', '1: one', '? orphan',
|
||||
'---', '', '# Body',
|
||||
].join('\n')
|
||||
const view = render(<MarkdownBody {...props(content([text], true))} />)
|
||||
const fields = await vi.waitUntil(() => view.container.querySelector('[data-document-frontmatter] dl'))
|
||||
expect([...fields.querySelectorAll('dt')].map(node => node.textContent))
|
||||
.toEqual(['name', 'description', 'metadata', 'anchor', 'copy', 'license', 'version', 'beta', '1', 'orphan'])
|
||||
expect([...fields.querySelectorAll('dd')].map(node => node.textContent)).toEqual([
|
||||
'pdf', 'Extract text from PDF files.', 'tags: [docs, pdf]', '[a]', '*list', '', '1.0', 'true', 'one', '',
|
||||
])
|
||||
expect(view.getAllByRole('heading').map(node => node.textContent)).toEqual(['Body'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['invalid YAML', 'name: [unclosed'],
|
||||
['a scalar document', '~'],
|
||||
['a sequence document', '- first\n- second'],
|
||||
])('shows the verbatim frontmatter source for %s', async (_, source) => {
|
||||
const view = render(<MarkdownBody {...props(content([`---\r\n${source}\r\n...\r\nBody.`], true))} />)
|
||||
await act(async () => { await import('../src/client/markdown/frontmatter-fields.tsx') })
|
||||
const block = view.container.querySelector('[data-document-frontmatter]')
|
||||
expect(block?.querySelector('dl')).toBeNull()
|
||||
expect(block?.querySelector('pre')?.textContent).toBe(source)
|
||||
expect(view.getByText('Body.')).toBeDefined()
|
||||
})
|
||||
|
||||
it('collapses frontmatter without content and keeps an unterminated opening rule as Markdown', async () => {
|
||||
const view = render(<MarkdownBody {...props(content(['---\nname: pdf\n---\nBody.'], true))} />)
|
||||
const block = await vi.waitUntil(() => view.container.querySelector('[data-document-frontmatter]:has(dl)'))
|
||||
view.rerender(<MarkdownBody {...props(content(['---\n# comment\n---\nBody.'], true))} />)
|
||||
expect(view.container.querySelector('[data-document-frontmatter]')).toBe(block)
|
||||
expect(block.childElementCount).toBe(0)
|
||||
view.rerender(<MarkdownBody {...props(content(['---\n---\n# Next\n\n---\nTail'], true))} />)
|
||||
expect(view.container.querySelector('[data-document-frontmatter]')?.childElementCount).toBe(0)
|
||||
expect(view.getByRole('heading', { name: 'Next' })).toBeDefined()
|
||||
expect(view.getByText('Tail')).toBeDefined()
|
||||
view.rerender(<MarkdownBody {...props(content(['---\nname: pdf'], false))} />)
|
||||
expect(view.container.querySelector('[data-document-frontmatter]')).toBeNull()
|
||||
expect(view.container.querySelector('hr')).not.toBeNull()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a leading blank line', '\n---\nname: pdf\n---\nBody.'],
|
||||
['leading text', 'Intro.\n\n---\nname: pdf\n---\nBody.'],
|
||||
['an indented opening rule', ' ---\nname: pdf\n---\nBody.'],
|
||||
])('treats a delimiter pair after %s as Markdown', (_, text) => {
|
||||
const view = render(<MarkdownBody {...props(content([text], true))} />)
|
||||
expect(view.container.querySelector('[data-document-frontmatter]')).toBeNull()
|
||||
expect(view.getByText('Body.')).toBeDefined()
|
||||
})
|
||||
|
||||
it('renders empty text and leaves non-text deliveries to their selected implementation', () => {
|
||||
const view = render(<MarkdownBody {...props({ kind: 'text', text: '', pages: [], eof: true })} />)
|
||||
expect(view.container.querySelector('[data-document-markdown]')?.textContent).toBe('')
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// @vitest-environment jsdom
|
||||
/** A field-list chunk that fails to load leaves the verbatim frontmatter and the Markdown body visible. */
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { MarkdownBody, type MarkdownBodyProps } from '../src/client/markdown/MarkdownBody.tsx'
|
||||
import { en } from '../src/client/markdown/locales.ts'
|
||||
|
||||
vi.mock('../src/client/markdown/frontmatter-fields.tsx', () => {
|
||||
throw new Error('chunk unavailable')
|
||||
})
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('MarkdownBody frontmatter chunk failure', () => {
|
||||
it('keeps the verbatim frontmatter and the body', async () => {
|
||||
const text = '---\nname: pdf\n---\n# Body'
|
||||
const view = render(<MarkdownBody {...{
|
||||
resourceAddress: 'dsh-resource://file/session/markdown/SKILL.md', wrap: false, t: makeTranslate(en),
|
||||
content: { kind: 'text', text, pages: [{ offset: 1, text, lines: 4 }], eof: true },
|
||||
useResource: () => ({ status: 'loading', value: undefined, failure: undefined }),
|
||||
} as MarkdownBodyProps} />)
|
||||
await vi.waitFor(() => { expect(view.container.querySelector('[data-document-frontmatter] pre')?.textContent).toBe('name: pdf') })
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(view.container.querySelector('[data-document-frontmatter] pre')?.textContent).toBe('name: pdf')
|
||||
expect(view.getByRole('heading', { name: 'Body' })).toBeDefined()
|
||||
})
|
||||
})
|
||||
Generated
+3
@@ -5496,6 +5496,9 @@ importers:
|
||||
xlsx:
|
||||
specifier: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
|
||||
version: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
|
||||
yaml:
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0
|
||||
|
||||
packages/client/ui-sidebar-files:
|
||||
devDependencies:
|
||||
|
||||
Reference in new issue
Block a user