| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557 |
- /**
- * blockOperations.ts - Block操作工具函数
- *
- * 提供block的各种操作辅助函数
- *
- * @module utils/blockOperations
- */
- import type {
- DocumentBlock,
- BlockType,
- TableBlock,
- TableRow,
- TableCell,
- RichText,
- } from '../types/editor';
- // ══════════════════════════════════════════════════════════════════════════════
- // Block ID Generation
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 生成块ID
- *
- * @param type 块类型
- * @param index 索引(可选)
- * @returns 块ID
- *
- * @example
- * ```ts
- * generateBlockId('heading') // "block-h-1704096000000-0"
- * ```
- */
- export function generateBlockId(type: BlockType, index: number = 0): string {
- const typePrefix: Record<BlockType, string> = {
- heading: 'h',
- paragraph: 'p',
- table: 'tbl',
- image: 'img',
- };
- return `block-${typePrefix[type]}-${Date.now()}-${index}`;
- }
- // ══════════════════════════════════════════════════════════════════════════════
- // Block Order Operations
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 计算插入位置的block_order
- * 稀疏排序策略:在两个块之间找到中间值
- *
- * @param prevOrder 前一个块的order
- * @param nextOrder 后一个块的order
- * @returns 新的order,如果返回-1表示需要重排
- *
- * @example
- * ```ts
- * computeInsertOrder(100, 200) // 150
- * computeInsertOrder(100, 101) // -1 (需要重排)
- * ```
- */
- export function computeInsertOrder(prevOrder: number, nextOrder: number): number {
- const gap = nextOrder - prevOrder;
-
- if (gap > 1) {
- // 有间隙,直接取中间值
- return Math.floor((prevOrder + nextOrder) / 2);
- }
-
- // 间隙不足,需要重排
- return -1;
- }
- /**
- * 重新平衡block_order(稀疏排序,间隔100)
- *
- * @param blocks 块数组
- * @returns 重新排序后的块数组
- *
- * @example
- * ```ts
- * const rebalanced = rebalanceBlockOrders(blocks);
- * // blocks[0].block_order = 0
- * // blocks[1].block_order = 100
- * // blocks[2].block_order = 200
- * ```
- */
- export function rebalanceBlockOrders(blocks: DocumentBlock[]): DocumentBlock[] {
- const sorted = [...blocks].sort((a, b) => a.block_order - b.block_order);
- return sorted.map((block, index) => ({
- ...block,
- block_order: index * 100,
- }));
- }
- // ══════════════════════════════════════════════════════════════════════════════
- // Table Operations
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 创建空的表格单元格
- *
- * @returns 空单元格
- */
- export function createEmptyCell(): TableCell {
- return {
- text: '',
- rowspan: 1,
- colspan: 1,
- style: {},
- };
- }
- /**
- * 创建空的表格行
- *
- * @param cols 列数
- * @returns 表格行
- */
- export function createEmptyRow(cols: number): TableRow {
- return {
- cells: Array(cols).fill(null).map(() => createEmptyCell()),
- };
- }
- /**
- * 在表格中插入行
- *
- * @param table 表格块
- * @param afterRow 在此行之后插入
- * @returns 新的表格块
- *
- * @example
- * ```ts
- * const newTable = insertTableRow(table, 1); // 在第2行后插入
- * ```
- */
- export function insertTableRow(table: TableBlock, afterRow: number): TableBlock {
- const newRow = createEmptyRow(table.metadata.cols);
- const rows = [...table.content.rows];
- rows.splice(afterRow + 1, 0, newRow);
-
- return {
- ...table,
- content: { rows },
- metadata: {
- ...table.metadata,
- rows: rows.length,
- },
- };
- }
- /**
- * 在表格中插入列
- *
- * @param table 表格块
- * @param afterCol 在此列之后插入
- * @returns 新的表格块
- *
- * @example
- * ```ts
- * const newTable = insertTableColumn(table, 1); // 在第2列后插入
- * ```
- */
- export function insertTableColumn(table: TableBlock, afterCol: number): TableBlock {
- const rows = table.content.rows.map((row) => {
- const cells = [...row.cells];
- cells.splice(afterCol + 1, 0, createEmptyCell());
- return { cells };
- });
-
- const colWidths = [...table.metadata.col_widths];
- const avgWidth = colWidths.reduce((sum, w) => sum + w, 0) / colWidths.length;
- colWidths.splice(afterCol + 1, 0, avgWidth);
-
- return {
- ...table,
- content: { rows },
- metadata: {
- ...table.metadata,
- cols: table.metadata.cols + 1,
- col_widths: colWidths,
- },
- };
- }
- /**
- * 删除表格行
- *
- * @param table 表格块
- * @param rowIndex 行索引
- * @returns 新的表格块
- */
- export function deleteTableRow(table: TableBlock, rowIndex: number): TableBlock {
- if (table.content.rows.length <= 1) {
- throw new Error('表格至少需要一行');
- }
-
- const rows = table.content.rows.filter((_, i) => i !== rowIndex);
-
- return {
- ...table,
- content: { rows },
- metadata: {
- ...table.metadata,
- rows: rows.length,
- },
- };
- }
- /**
- * 删除表格列
- *
- * @param table 表格块
- * @param colIndex 列索引
- * @returns 新的表格块
- */
- export function deleteTableColumn(table: TableBlock, colIndex: number): TableBlock {
- if (table.metadata.cols <= 1) {
- throw new Error('表格至少需要一列');
- }
-
- const rows = table.content.rows.map((row) => ({
- cells: row.cells.filter((_, i) => i !== colIndex),
- }));
-
- const colWidths = table.metadata.col_widths.filter((_, i) => i !== colIndex);
-
- return {
- ...table,
- content: { rows },
- metadata: {
- ...table.metadata,
- cols: table.metadata.cols - 1,
- col_widths: colWidths,
- },
- };
- }
- /**
- * 合并单元格
- *
- * 支持横向合并(colspan)和纵向合并(rowspan)
- * 合并时会保存所有被合并单元格的原始内容和样式,以便拆分时恢复
- *
- * @param table 表格块
- * @param startRow 起始行
- * @param startCol 起始列
- * @param endRow 结束行
- * @param endCol 结束列
- * @returns 新的表格块
- *
- * @example
- * ```ts
- * // 横向合并: a + b (第0行,第0-1列)
- * mergeCells(table, 0, 0, 0, 1);
- * // 结果: a单元格 colspan=2, 保存b的内容和样式到 _mergedCells
- *
- * // 纵向合并: a + d (第0-1行,第0列)
- * mergeCells(table, 0, 0, 1, 0);
- * // 结果: a单元格 rowspan=2, 保存d的内容和样式到 _mergedCells
- * ```
- */
- export function mergeCells(
- table: TableBlock,
- startRow: number,
- startCol: number,
- endRow: number,
- endCol: number
- ): TableBlock {
- // 收集所有被合并单元格的内容和样式(保存位置偏移量)
- const mergedCells: Array<{
- rowOffset: number;
- colOffset: number;
- text: string | RichText[];
- style: any; // 保存原始样式
- }> = [];
-
- // 收集主单元格内容用于显示
- const displayTexts: string[] = [];
-
- table.content.rows.forEach((row, rowIdx) => {
- if (rowIdx >= startRow && rowIdx <= endRow) {
- row.cells.forEach((cell, colIdx) => {
- if (colIdx >= startCol && colIdx <= endCol) {
- const text = typeof cell.text === 'string'
- ? cell.text
- : cell.text.map(seg => seg.text).join('');
-
- // 保存所有单元格的原始数据和样式(包括主单元格)
- mergedCells.push({
- rowOffset: rowIdx - startRow,
- colOffset: colIdx - startCol,
- text: cell.text,
- style: { ...cell.style }, // 深拷贝样式
- });
-
- // 用于显示的文本
- if (text.trim()) {
- displayTexts.push(text.trim());
- }
- }
- });
- }
- });
-
- const rows = table.content.rows.map((row, rowIdx) => {
- if (rowIdx < startRow || rowIdx > endRow) {
- return row;
- }
-
- const cells = row.cells.map((cell, colIdx) => {
- if (colIdx < startCol || colIdx > endCol) {
- return cell;
- }
-
- if (rowIdx === startRow && colIdx === startCol) {
- // 主单元格,设置rowspan和colspan,保存原始数据
- return {
- ...cell,
- text: displayTexts.join(' ') || cell.text,
- rowspan: endRow - startRow + 1,
- colspan: endCol - startCol + 1,
- style: {
- ...cell.style,
- _mergedCells: mergedCells, // 保存所有单元格的原始内容和样式
- },
- };
- }
-
- // 被合并的单元格,标记为隐藏
- return {
- ...cell,
- text: '', // 清空显示内容
- rowspan: 0,
- colspan: 0,
- };
- });
-
- return { cells };
- });
-
- return {
- ...table,
- content: { rows },
- };
- }
- /**
- * 拆分单元格
- *
- * 将已合并的单元格拆分回独立单元格,并恢复原始内容和样式到对应位置
- *
- * @param table 表格块
- * @param rowIndex 单元格所在行
- * @param colIndex 单元格所在列
- * @returns 新的表格块
- *
- * @example
- * ```ts
- * // 拆分横向合并的单元格 (a+b)
- * splitCell(table, 0, 0);
- * // 结果: a单元格恢复为普通单元格(内容为原始a,样式为原始a样式), b单元格恢复(内容为原始b,样式为原始b样式)
- *
- * // 拆分纵向合并的单元格 (a+d)
- * splitCell(table, 0, 0);
- * // 结果: a单元格恢复为普通单元格(内容为原始a,样式为原始a样式), d单元格恢复(内容为原始d,样式为原始d样式)
- * ```
- */
- export function splitCell(
- table: TableBlock,
- rowIndex: number,
- colIndex: number
- ): TableBlock {
- const targetCell = table.content.rows[rowIndex]?.cells[colIndex];
-
- if (!targetCell) {
- throw new Error('单元格不存在');
- }
-
- // 如果单元格没有合并,无需拆分
- if (targetCell.rowspan <= 1 && targetCell.colspan <= 1) {
- throw new Error('此单元格未合并,无需拆分');
- }
-
- const rowspan = targetCell.rowspan || 1;
- const colspan = targetCell.colspan || 1;
-
- // 获取保存的原始单元格数据(包含text和style)
- const mergedCells = targetCell.style._mergedCells || [];
-
- // 创建一个映射,用于快速查找原始内容和样式
- const cellDataMap = new Map<string, { text: string | RichText[]; style: any }>();
- mergedCells.forEach(({ rowOffset, colOffset, text, style }) => {
- const key = `${rowOffset}-${colOffset}`;
- cellDataMap.set(key, { text, style: style || {} });
- });
-
- const rows = table.content.rows.map((row, rowIdx) => {
- // 不在合并范围内的行直接返回
- if (rowIdx < rowIndex || rowIdx >= rowIndex + rowspan) {
- return row;
- }
-
- const cells = row.cells.map((cell, colIdx) => {
- // 不在合并范围内的列直接返回
- if (colIdx < colIndex || colIdx >= colIndex + colspan) {
- return cell;
- }
-
- // 计算当前单元格在合并区域中的偏移量
- const rowOffset = rowIdx - rowIndex;
- const colOffset = colIdx - colIndex;
- const key = `${rowOffset}-${colOffset}`;
-
- // 从保存的数据中恢复原始内容和样式
- const cellData = cellDataMap.get(key);
- const originalText = cellData?.text || '';
- const originalStyle = cellData?.style || {};
-
- // 主单元格:恢复为普通单元格,清除合并标记
- if (rowIdx === rowIndex && colIdx === colIndex) {
- const newStyle = { ...originalStyle };
- delete newStyle._mergedCells; // 清除保存的合并数据
-
- return {
- ...cell,
- text: originalText,
- rowspan: 1,
- colspan: 1,
- style: newStyle, // 使用原始样式
- };
- }
-
- // 被合并的单元格:恢复为独立单元格,并恢复原始内容和样式
- const recoveredStyle = { ...originalStyle };
- delete recoveredStyle._mergedCells;
-
- return {
- ...createEmptyCell(),
- text: originalText,
- style: recoveredStyle, // 使用原始样式
- };
- });
-
- return { cells };
- });
-
- return {
- ...table,
- content: { rows },
- };
- }
- // ══════════════════════════════════════════════════════════════════════════════
- // Block Validation
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 验证块数据是否有效
- *
- * @param block 块数据
- * @returns 是否有效
- */
- export function validateBlock(block: Partial<DocumentBlock>): boolean {
- if (!block.type) return false;
- if (block.block_order === undefined) return false;
-
- switch (block.type) {
- case 'heading':
- return (
- typeof block.level === 'number' &&
- block.level >= 1 &&
- block.level <= 6
- );
-
- case 'paragraph':
- return true;
-
- case 'table':
- return !!(
- block.metadata?.cols &&
- block.metadata?.rows &&
- (block as any).content?.rows
- );
-
- case 'image':
- return !!(block.content && typeof block.content === 'string');
-
- default:
- return false;
- }
- }
- // ══════════════════════════════════════════════════════════════════════════════
- // Block Search and Filter
- // ══════════════════════════════════════════════════════════════════════════════
- /**
- * 在blocks中搜索文本
- *
- * @param blocks 块数组
- * @param query 搜索关键词
- * @returns 匹配的块ID数组
- */
- export function searchBlocks(blocks: DocumentBlock[], query: string): string[] {
- const lowerQuery = query.toLowerCase();
-
- return blocks
- .filter((block) => {
- switch (block.type) {
- case 'heading':
- case 'paragraph': {
- const content =
- typeof block.content === 'string'
- ? block.content
- : block.content.map((seg) => seg.text).join('');
- return content.toLowerCase().includes(lowerQuery);
- }
-
- case 'table': {
- return block.content.rows.some((row) =>
- row.cells.some((cell) => {
- const text =
- typeof cell.text === 'string'
- ? cell.text
- : cell.text.map((seg) => seg.text).join('');
- return text.toLowerCase().includes(lowerQuery);
- })
- );
- }
-
- case 'image': {
- return block.metadata.alt?.toLowerCase().includes(lowerQuery);
- }
-
- default:
- return false;
- }
- })
- .map((block) => block.id);
- }
- /**
- * 获取指定类型的所有块
- *
- * @param blocks 块数组
- * @param type 块类型
- * @returns 匹配类型的块数组
- */
- export function filterBlocksByType<T extends DocumentBlock>(
- blocks: DocumentBlock[],
- type: BlockType
- ): T[] {
- return blocks.filter((block) => block.type === type) as T[];
- }
|