✨ feat(vscode): 代码阅读修改的卡片里标出改了哪儿(改值旧值划掉接新值、加行 + 淡绿底、删行 − 淡红底划掉、说明按词 / 字标增删)
This commit is contained in:
1 parent
3b7ce69842
commit
707d85c324
6 files changed
+283
-51
No files matched your search
@@ -2,12 +2,13 @@ import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
import type { DiffTree, PlacedRemoved } from './diffView'
|
||||
import type { QaState } from './qa'
|
||||
import type { PendingImage } from './qaImages'
|
||||
import type { BlockView } from './types'
|
||||
import type { BlockMarks, BlockView } from './types'
|
||||
import { ChevronDown, ChevronRight, EyeOff } from 'lucide-react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { AskThread } from './AskThread'
|
||||
import { BlockTools } from './BlockTools'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import { DiffText } from './DiffText'
|
||||
import { removedTitle } from './diffView'
|
||||
import { KindIcon } from './kindIcons'
|
||||
import { PropertyGrid, PropertyTable } from './PropertyGrid'
|
||||
@@ -147,20 +148,20 @@ function BlockQa({ anchor, qa }: { anchor: string, qa: QaTree }): ReactElement {
|
||||
|
||||
/**
|
||||
* 预览状态的内容:函数、类、枚举、导入、常量、变量块和库规则的卡片是属性表,其余是一行译文;
|
||||
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明。
|
||||
* 译文下面依次接着摊开实参的表(跨几行写的调用)、说明。标差异时 marks 是这块自己的内容改了哪些(页面上正是差异算的那一版才给)。
|
||||
*/
|
||||
function BlockPreview({ block, tree }: { block: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
function BlockPreview({ block, tree, marks }: { block: BlockView, tree: BlockTreeState, marks?: BlockMarks }): ReactElement {
|
||||
if ('properties' in block)
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} />
|
||||
return <PropertyGrid blockId={block.id} rows={block.properties} collapsed={tree.collapsed} onToggleFold={tree.onToggleFold} marks={marks?.rows} />
|
||||
return (
|
||||
<>
|
||||
<TextPreview text={block.translation} />
|
||||
{marks?.translation ? <DiffText diff={marks.translation} /> : <TextPreview text={block.translation} />}
|
||||
{block.table && (
|
||||
<div className="mt-[3px]">
|
||||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} />
|
||||
<PropertyTable parentId={tableId(block.id)} columns={block.table.columns} items={block.table.items} marks={marks?.table} />
|
||||
</div>
|
||||
)}
|
||||
{block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||||
{marks?.doc ? <DiffText diff={marks.doc} className="mt-0.5" /> : block.doc && <TextPreview text={block.doc} className="mt-0.5" />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -207,7 +208,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
||||
onExit={() => tree.onToggleEdit(block.id)}
|
||||
/>
|
||||
)
|
||||
: <BlockPreview block={block} tree={tree} />}
|
||||
: <BlockPreview block={block} tree={tree} marks={tree.diff?.current ? change?.marks : undefined} />}
|
||||
</div>
|
||||
{tree.temporary.has(block.id) && (
|
||||
<span className="cr-temporary flex h-5 shrink-0 items-center gap-1 rounded px-1.5 text-[11px] whitespace-nowrap">
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { InlinePart, TextDiff } from './types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { renderMarkdownDiff } from './markdown'
|
||||
import { SegmentText } from './SegmentText'
|
||||
|
||||
/**
|
||||
* 着色的词里的增删(同一行的值改了):没变的照常,删掉的一截红底划掉(旧值,不能跳定义),新加的一截绿底,
|
||||
* 整段改了就是旧值紧跟新值。放在 .cr-code 的段落里。
|
||||
*/
|
||||
export function DiffInline({ parts }: { parts: InlinePart[] }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, index) => part.change
|
||||
? (
|
||||
<span key={index} className="cr-diff-part" data-change={part.change}>
|
||||
<SegmentText segments={part.segments} />
|
||||
</span>
|
||||
)
|
||||
: <SegmentText key={index} segments={part.segments} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 预览里一段带增删的文字:着色的词逐截标(DiffInline),Markdown 说明照常排版、增删的字按词 / 字标(同 TextPreview 的排版)。 */
|
||||
export function DiffText({ diff, className }: { diff: TextDiff, className?: string }): ReactElement {
|
||||
if ('markdown' in diff)
|
||||
return <div className={cn('cr-markdown', className)} dangerouslySetInnerHTML={{ __html: renderMarkdownDiff(diff.markdown) }} />
|
||||
return (
|
||||
<p className={cn('cr-code', className)}>
|
||||
<DiffInline parts={diff.inline} />
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { Inline, PropertyItem, PropertyRow } from './types'
|
||||
import type { Inline, InlinePart, ItemDiff, ItemsDiff, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import { DiffInline, DiffText } from './DiffText'
|
||||
import { inlineText } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { subRowsLayout, tableCells, tableTemplate } from './subRows'
|
||||
@@ -22,6 +23,22 @@ interface Folding {
|
||||
onToggleFold: (id: string) => void
|
||||
}
|
||||
|
||||
/** 删掉的行下面的子行:总是展开,点了也不收(旧内容只是看看)。 */
|
||||
const FIXED: Folding = { collapsed: new Set(), onToggleFold: () => {} }
|
||||
|
||||
/** 表格行第一格前的记号:加的 +、删的 −,没变的空一格,列照样对齐。 */
|
||||
const SIGNS = { add: '+', del: '−', none: ' ' } as const
|
||||
|
||||
/** 删掉的行(子行)里排在新的第 after 行后面的那些,带着它在删掉的行里的序号(页面上认它用)。 */
|
||||
function removedAt<T>(removed: readonly Removed<T>[] | undefined, after: number): { value: T, index: number }[] {
|
||||
return (removed ?? []).flatMap((entry, index) => entry.after === after ? [{ value: entry.value, index }] : [])
|
||||
}
|
||||
|
||||
/** 一格的字:这一格的值改了就逐截标(DiffInline),否则照常。 */
|
||||
function CellText({ cell, parts }: { cell: Inline[], parts: InlinePart[] | undefined }): ReactElement {
|
||||
return parts ? <DiffInline parts={parts} /> : <SegmentText segments={cell} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||||
@@ -60,32 +77,53 @@ function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Inline[],
|
||||
/**
|
||||
* 展开在一行或一个子行(parentId)下面的一层子行:左边一条竖线,每个子行几列对齐。
|
||||
* 带下一层的子行(字典 / 对象的值又是字典 / 对象),下一层展开在它下面、再往右缩进一层,默认展开。
|
||||
* 标差异时(marks)新加的子行淡绿底、左边 +,删掉的放回原来的位置、淡红底划掉、左边 −,改了的格子逐截标。
|
||||
*/
|
||||
function PropertyItems({ parentId, items, folding }: { parentId: string, items: PropertyItem[], folding: Folding }): ReactElement {
|
||||
function PropertyItems({ parentId, items, folding, marks }: { parentId: string, items: PropertyItem[], folding: Folding, marks?: ItemsDiff }): ReactElement {
|
||||
const nested = items.some(item => (item.items?.length ?? 0) > 0)
|
||||
/** 一个子行:删掉的(change 是 del)不带 id、下面的子行总是展开 */
|
||||
const itemRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
|
||||
const children = item.items ?? []
|
||||
const folds = change === 'del' ? FIXED : folding
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="cr-prop-item grid gap-x-3"
|
||||
data-id={change === 'del' ? undefined : id}
|
||||
data-diff={change}
|
||||
style={{ gridTemplateColumns: gridTemplate(item.cells.length) }}
|
||||
>
|
||||
{item.cells.map((cell, i) => i === 0 && nested
|
||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
|
||||
: (
|
||||
<p key={i} className="cr-code min-w-0">
|
||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
|
||||
? (
|
||||
<div className="cr-diff-gone">
|
||||
<PropertyItems parentId={id} items={children} folding={folds} />
|
||||
</div>
|
||||
)
|
||||
: <PropertyItems parentId={id} items={children} folding={folds} marks={mark?.items} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||||
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{itemRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
|
||||
return (
|
||||
<div className="flex gap-2.5 pl-[5px]">
|
||||
<div className="cr-guide w-px shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
{removed(-1)}
|
||||
{items.map((item, index) => {
|
||||
const id = propertyItemId(parentId, index)
|
||||
const children = item.items ?? []
|
||||
const mark = marks?.changed[index]
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div
|
||||
className="cr-prop-item grid gap-x-3"
|
||||
data-id={id}
|
||||
style={{ gridTemplateColumns: gridTemplate(item.cells.length) }}
|
||||
>
|
||||
{item.cells.map((cell, i) => i === 0 && nested
|
||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0} folding={folding} />
|
||||
: (
|
||||
<p key={i} className="cr-code min-w-0">
|
||||
<SegmentText segments={cell} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{children.length > 0 && !folding.collapsed.has(id) && <PropertyItems parentId={id} items={children} folding={folding} />}
|
||||
{itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
|
||||
{removed(index)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
@@ -98,34 +136,49 @@ function PropertyItems({ parentId, items, folding }: { parentId: string, items:
|
||||
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
|
||||
* 行带 table(实参的值里跨几行写的调用)时,那张表排在最后一格里、文字下面,往右缩一点,一层层往下套。
|
||||
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
|
||||
*/
|
||||
export function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
|
||||
export function PropertyTable({ parentId, columns, items, marks }: { parentId: string, columns: string[], items: PropertyItem[], marks?: ItemsDiff }): ReactElement {
|
||||
// 有加行、删行的表每行第一格前留一个记号位
|
||||
const signed = marks !== undefined && (marks.removed.length > 0 || Object.values(marks.changed).some(mark => mark.added))
|
||||
/** 一行:删掉的(change 是 del)不带 id,套着的表不再标 */
|
||||
const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
|
||||
const cells = tableCells(item, columns)
|
||||
const text = (cell: Inline[], i: number): ReactElement => (
|
||||
<>
|
||||
{signed && i === 0 && <span className="cr-diff-sign" data-change={change} aria-hidden>{SIGNS[change ?? 'none']}</span>}
|
||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<div className="cr-table-row" role="row" data-id={change === 'del' ? undefined : id} data-diff={change}>
|
||||
{cells.map((cell, i) => item.table && i === cells.length - 1
|
||||
? (
|
||||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||||
<p className="cr-code">{text(cell, i)}</p>
|
||||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
|
||||
</div>
|
||||
)
|
||||
: <p key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</p>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||||
.map(({ value, index }) => <Fragment key={`removed-${index}`}>{tableRow(value, `${parentId}/x${index}`, 'del', undefined)}</Fragment>)
|
||||
return (
|
||||
<div className="pl-[5px]">
|
||||
<div className="cr-table" role="table" style={{ gridTemplateColumns: tableTemplate(columns.length) }}>
|
||||
<div className="cr-table-head" role="row">
|
||||
{columns.map((name, i) => <span key={i} className="cr-table-cell" role="columnheader">{name}</span>)}
|
||||
</div>
|
||||
{removed(-1)}
|
||||
{items.map((item, index) => {
|
||||
const id = propertyItemId(parentId, index)
|
||||
const cells = tableCells(item, columns)
|
||||
const mark = marks?.changed[index]
|
||||
return (
|
||||
<div key={index} className="cr-table-row" role="row" data-id={id}>
|
||||
{cells.map((cell, i) => item.table && i === cells.length - 1
|
||||
? (
|
||||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||||
<p className="cr-code">
|
||||
<SegmentText segments={cell} />
|
||||
</p>
|
||||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<p key={i} className="cr-table-cell cr-code min-w-0" role="cell">
|
||||
<SegmentText segments={cell} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<Fragment key={index}>
|
||||
{tableRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
|
||||
{removed(index)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -136,23 +189,30 @@ export function PropertyTable({ parentId, columns, items }: { parentId: string,
|
||||
/**
|
||||
* 函数、类、枚举、导入、常量、变量块和库规则的卡片的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
|
||||
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。
|
||||
* 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
|
||||
*/
|
||||
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks }: {
|
||||
blockId: string
|
||||
rows: PropertyRow[]
|
||||
collapsed: ReadonlySet<string>
|
||||
onToggleFold: (id: string) => void
|
||||
/** 标差异时这张属性表改了哪些行 */
|
||||
marks?: RowsDiff
|
||||
}): ReactElement {
|
||||
const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
|
||||
.map(({ value, index }) => <RemovedRow key={`removed-${index}`} id={`${blockId}/x${index}`} row={value} />)
|
||||
return (
|
||||
<div className="grid grid-cols-[minmax(76px,max-content)_minmax(0,1fr)] gap-x-3 gap-y-[3px] *:col-span-full">
|
||||
{removed(-1)}
|
||||
{rows.map((row, index) => {
|
||||
const layout = subRowsLayout(row)
|
||||
const id = propertyRowId(blockId, index)
|
||||
const folded = collapsed.has(id)
|
||||
const Chevron = folded ? ChevronRight : ChevronDown
|
||||
const mark = marks?.changed[index]
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<div className="cr-prop-row grid grid-cols-subgrid" data-id={id}>
|
||||
<div className="cr-prop-row grid grid-cols-subgrid" data-id={id} data-diff={mark?.added && 'add'}>
|
||||
{layout
|
||||
? (
|
||||
<button
|
||||
@@ -167,13 +227,34 @@ export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
|
||||
</button>
|
||||
)
|
||||
: <span className="cr-prop-label flex h-5 items-center">{row.label}</span>}
|
||||
<TextPreview text={row.value} className="min-w-0" />
|
||||
{mark?.value ? <DiffText diff={mark.value} className="min-w-0" /> : <TextPreview text={row.value} className="min-w-0" />}
|
||||
</div>
|
||||
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />}
|
||||
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} />}
|
||||
{!folded && layout === 'table' && <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} marks={mark?.items} />}
|
||||
{!folded && layout === 'list' && <PropertyItems parentId={id} items={row.items ?? []} folding={{ collapsed, onToggleFold }} marks={mark?.items} />}
|
||||
{removed(index)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 属性表里删掉的一行:整行淡红底、划掉,左边 −;它下面的子行照样摊开、划掉。 */
|
||||
function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {
|
||||
const layout = subRowsLayout(row)
|
||||
return (
|
||||
<>
|
||||
<div className="cr-prop-row grid grid-cols-subgrid" data-diff="del">
|
||||
<span className="cr-prop-label flex h-5 items-center">{row.label}</span>
|
||||
<TextPreview text={row.value} className="min-w-0" />
|
||||
</div>
|
||||
{layout && (
|
||||
<div className="cr-diff-gone">
|
||||
{layout === 'table'
|
||||
? <PropertyTable parentId={id} columns={row.columns ?? []} items={row.items ?? []} />
|
||||
: <PropertyItems parentId={id} items={row.items ?? []} folding={FIXED} />}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -931,3 +931,69 @@ body {
|
||||
.cr-new-file > svg {
|
||||
color: var(--cr-diff-add);
|
||||
}
|
||||
|
||||
/*
|
||||
* 卡片里的标记:改了的值旧值红底划掉、紧跟新值绿底(.cr-diff-part),说明里增删的字同样(.cr-diff-word,<ins> / <del>);
|
||||
* 新加的行、子行整行淡绿底、左边 +,删掉的整行淡红底、划掉、左边 −;表格的行记号写在第一格前(.cr-diff-sign)
|
||||
*/
|
||||
body {
|
||||
--cr-diff-add-text: var(--vscode-diffEditor-insertedTextBackground, color-mix(in srgb, var(--cr-diff-add) 32%, transparent));
|
||||
--cr-diff-del-text: var(--vscode-diffEditor-removedTextBackground, color-mix(in srgb, var(--cr-diff-del) 32%, transparent));
|
||||
}
|
||||
.cr-diff-part,
|
||||
.cr-diff-word {
|
||||
border-radius: 2px;
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
:is(.cr-diff-part, .cr-diff-word)[data-change="add"],
|
||||
ins.cr-diff-word {
|
||||
background: var(--cr-diff-add-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
:is(.cr-diff-part, .cr-diff-word)[data-change="del"],
|
||||
del.cr-diff-word {
|
||||
background: var(--cr-diff-del-text);
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
.cr-diff-part[data-change="del"] * {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-diff="add"] {
|
||||
background: color-mix(in srgb, var(--cr-diff-add) 14%, transparent);
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-diff="del"] {
|
||||
background: color-mix(in srgb, var(--cr-diff-del) 14%, transparent);
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-diff="del"] :not(.cr-diff-sign),
|
||||
.cr-diff-gone,
|
||||
.cr-diff-gone * {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item)[data-diff] {
|
||||
position: relative;
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item)[data-diff]::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9px;
|
||||
width: 8px;
|
||||
font-family: var(--vscode-editor-font-family, monospace);
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item)[data-diff="add"]::before {
|
||||
content: "+";
|
||||
color: var(--cr-diff-add);
|
||||
}
|
||||
:is(.cr-prop-row, .cr-prop-item)[data-diff="del"]::before {
|
||||
content: "−";
|
||||
color: var(--cr-diff-del);
|
||||
}
|
||||
.cr-diff-sign {
|
||||
display: inline-block;
|
||||
width: 1.4ch;
|
||||
white-space: pre;
|
||||
}
|
||||
.cr-diff-sign[data-change="add"] { color: var(--cr-diff-add); }
|
||||
.cr-diff-sign[data-change="del"] { color: var(--cr-diff-del); }
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { renderMarkdown } from './markdown'
|
||||
import { renderMarkdown, renderMarkdownDiff } from './markdown'
|
||||
|
||||
describe('renderMarkdown', () => {
|
||||
it('一行文字是一个段落;段内换行是软换行,空行分段', () => {
|
||||
@@ -45,3 +45,19 @@ describe('renderMarkdown', () => {
|
||||
expect(renderMarkdown('[文档](https://example.com "")')).toBe('<p><a href="https://example.com">文档</a></p>\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderMarkdownDiff', () => {
|
||||
it('照常排版,新加的字包进 <ins>、删掉的包进 <del>', () => {
|
||||
expect(renderMarkdownDiff([{ text: '放锁' }, { text: '前', change: 'del' }, { text: '都', change: 'add' }, { text: '必须比对 `token`' }]))
|
||||
.toBe('<p>放锁<del class="cr-diff-word">前</del><ins class="cr-diff-word">都</ins>必须比对 <code>token</code></p>\n')
|
||||
})
|
||||
|
||||
it('跨行的一截逐行包,新加的列表项留住列表记号', () => {
|
||||
expect(renderMarkdownDiff([{ text: '- a\n' }, { text: '- b\n- c', change: 'add' }]))
|
||||
.toBe('<ul>\n<li>a</li>\n<li><ins class="cr-diff-word">b</ins></li>\n<li><ins class="cr-diff-word">c</ins></li>\n</ul>\n')
|
||||
})
|
||||
|
||||
it('行中间的一截不把前面的字当块记号', () => {
|
||||
expect(renderMarkdownDiff([{ text: '共 ' }, { text: '# 3', change: 'add' }])).toBe('<p>共 <ins class="cr-diff-word"># 3</ins></p>\n')
|
||||
})
|
||||
})
|
||||
@@ -40,3 +40,37 @@ export function renderMarkdown(text: string): string {
|
||||
cache.delete(cache.keys().next().value!)
|
||||
return html
|
||||
}
|
||||
|
||||
/** 渲染带增删的 Markdown 时,新加 / 删掉的字前后夹的记号(Unicode 私用区的字,正文里不会有),渲染完换成 <ins> / <del>。 */
|
||||
const MARKS = {
|
||||
add: ['', '', '<ins class="cr-diff-word">', '</ins>'],
|
||||
del: ['', '', '<del class="cr-diff-word">', '</del>'],
|
||||
} as const
|
||||
|
||||
/** 一行开头的块记号(列表、标题、引用):新加的一行要把它留在记号外面,不然就不认成列表、标题了。 */
|
||||
const BLOCK_LEAD = /^\s*(?:(?:[\-*+>]|\d+[.)]|#{1,6})\s+)*/
|
||||
|
||||
/**
|
||||
* 带增删的 Markdown 说明 → HTML:照常排版,新加的字包进 <ins>、删掉的包进 <del>(样式见 .cr-diff-word)。
|
||||
* 一截跨了几行时逐行包,记号不跨段落、列表项。
|
||||
*/
|
||||
export function renderMarkdownDiff(parts: readonly { text: string, change?: 'add' | 'del' }[]): string {
|
||||
let text = ''
|
||||
for (const { text: piece, change } of parts) {
|
||||
if (!change) {
|
||||
text += piece
|
||||
continue
|
||||
}
|
||||
const [open, close] = MARKS[change]
|
||||
text += piece.split('\n').map((line, index) => {
|
||||
const atLineStart = index > 0 || text === '' || text.endsWith('\n')
|
||||
const lead = atLineStart ? BLOCK_LEAD.exec(line)?.[0] ?? '' : ''
|
||||
const rest = line.slice(lead.length)
|
||||
return rest ? `${lead}${open}${rest}${close}` : line
|
||||
}).join('\n')
|
||||
}
|
||||
let html = md.render(text)
|
||||
for (const [open, close, openTag, closeTag] of Object.values(MARKS))
|
||||
html = html.replaceAll(open, openTag).replaceAll(close, closeTag)
|
||||
return html
|
||||
}
|
||||
Reference in new issue
Block a user