/** * 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 = { 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(); 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): 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( blocks: DocumentBlock[], type: BlockType ): T[] { return blocks.filter((block) => block.type === type) as T[]; }