', () => {
expect(renderMarkdownDiff([{ text: '放锁' }, { text: '前', change: 'del' }, { text: '都', change: 'add' }, { text: '必须比对 `token`' }]))
.toBe('放锁前都必须比对 token
\n')
})
it('跨行的一截逐行包,新加的列表项留住列表记号', () => {
expect(renderMarkdownDiff([{ text: '- a\n' }, { text: '- b\n- c', change: 'add' }]))
.toBe('\n')
})
it('行中间的一截不把前面的字当块记号', () => {
expect(renderMarkdownDiff([{ text: '共 ' }, { text: '# 3', change: 'add' }])).toBe('共 # 3
\n')
})
})
describe('mermaid 代码块', () => {
const source = 'flowchart TD\n A["读 <配置> & 写"] --> B{"成功?"}\n'
const escaped = 'flowchart TD\n A["读 <配置> & 写"] --> B{"成功?"}\n'
const codeBlock = `${escaped}
\n`
const placeholder = `${codeBlock}
\n`
it('出占位:转义后的源码记在 data-source,里面先放原样的代码块', () => {
expect(renderMarkdown(`说明\n\n\`\`\`mermaid\n${source}\`\`\``)).toBe(`说明
\n${placeholder}`)
expect(renderMarkdown(`\`\`\`mermaid\n${source}\`\`\``, 'peek')).toBe(placeholder)
expect(renderMarkdown(`\`\`\`mermaid\n${source}\`\`\``, 'answer')).toBe(placeholder)
})
it('别的代码块照旧', () => {
expect(renderMarkdown('```ts\nload(\'a\')\n```')).toBe('load(\'a\')\n
\n')
expect(renderMarkdown('```\nflowchart TD\n```')).toBe('flowchart TD\n
\n')
expect(renderMarkdown('```mermaidx\nA\n```')).not.toContain('cr-mermaid')
})
it('带增删的说明、还在逐字长的回答里仍是普通代码块', () => {
expect(renderMarkdownDiff([{ text: `\`\`\`mermaid\n${source}\`\`\`` }])).toBe(codeBlock)
expect(renderMarkdownOnce(`\`\`\`mermaid\n${source}\`\`\``)).toBe(codeBlock)
})
})
describe('问 AI 的回答里写成行内代码的代码位置', () => {
it('整段是代码位置的行内代码包成仓库链接,逐字长着的回答也一样', () => {
expect(renderMarkdown('见 `backend/app/routes.py:96-135`。', 'answer')).toBe('见 backend/app/routes.py:96-135。
\n')
expect(renderMarkdown('`web/src/a.ts`', 'answer')).toBe('web/src/a.ts
\n')
expect(renderMarkdownOnce('见 `backend/app/routes.py:96`')).toBe('见 backend/app/routes.py:96
\n')
})
it('不像代码位置的、已经在链接里的不包;注释里的不包', () => {
expect(renderMarkdown('`12:30` 和 `a:b`、`token`', 'answer')).toBe('12:30 和 a:b、token
\n')
expect(renderMarkdown('[`backend/app/routes.py:96`](backend/app/routes.py#L96)', 'answer')).toBe('backend/app/routes.py:96
\n')
expect(renderMarkdown('见 `backend/app/routes.py:96-135`')).toBe('见 backend/app/routes.py:96-135
\n')
})
})
describe('「了解一块代码」的回答', () => {
/** 回答里各个链接的地址,按页面认仓库链接的办法拆开 */
const links = (html: string): unknown[] => [...html.matchAll(//g)].map(([, href]) => parseRepoLink(href.replaceAll('&', '&')))
it('调用树画成嵌套列表,节点是仓库链接:路径带成对的括号(Next.js 的路由分组)也认成链接,via 是行内代码', () => {
const answer = featureAnswer({ paths: [{ name: '首页发出首句', nodes: [
{ id: '1', folder: 'web', path: 'src/app/(main)/chat/code/page.tsx', symbol: 'ChatCodePage', role: '会话页' },
{ id: '2', parent: '1', folder: 'server', path: 'app/routes.py', symbol: 'create_app', via: 'POST /apps', role: '建应用' },
{ id: '3', parent: '2', folder: 'server', path: 'app/models.py', role: '应用表' },
] }] }, ['web', 'server'])
const html = renderMarkdown(answer, 'answer')
expect(html).toBe([
'1. 首页发出首句
',
'',
'',
].join('\n'))
expect(links(html)).toEqual([
{ path: 'web/src/app/(main)/chat/code/page.tsx', symbol: 'ChatCodePage' },
{ path: 'server/app/routes.py', symbol: 'create_app' },
{ path: 'server/app/models.py' },
])
})
it('路径里有空白、百分号、不成对的括号:编码后照样认成链接,拆出来是原来的路径', () => {
const html = renderMarkdown([codeLink('a', 'docs/a b.md'), codeLink('b', 'docs/100%.md'), codeLink('c', 'docs/a(b.md', 'Doc')].join(' '), 'answer')
expect(links(html)).toEqual([{ path: 'docs/a b.md' }, { path: 'docs/100%.md' }, { path: 'docs/a(b.md', symbol: 'Doc' }])
})
})