115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import type { Json } from "@/types/supabase";
|
|
|
|
type BlockLike = {
|
|
id: string;
|
|
type?: string;
|
|
props?: Record<string, unknown>;
|
|
content?: unknown;
|
|
children?: BlockLike[];
|
|
};
|
|
|
|
const asBlockArray = (value: unknown): BlockLike[] => {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.filter((b) => b && typeof b === "object" && typeof (b as { id?: unknown }).id === "string") as BlockLike[];
|
|
};
|
|
|
|
export const findBlockInTree = (
|
|
blocks: BlockLike[],
|
|
blockId: string,
|
|
): { block: BlockLike; parent: BlockLike | null; index: number } | null => {
|
|
const stack: Array<{ list: BlockLike[]; parent: BlockLike | null }> = [{ list: blocks, parent: null }];
|
|
while (stack.length) {
|
|
const item = stack.pop()!;
|
|
const list = item.list;
|
|
for (let i = 0; i < list.length; i += 1) {
|
|
const b = list[i]!;
|
|
if (b.id === blockId) {
|
|
return { block: b, parent: item.parent, index: i };
|
|
}
|
|
if (Array.isArray(b.children) && b.children.length > 0) {
|
|
stack.push({ list: b.children, parent: b });
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const cloneBlock = (block: BlockLike): BlockLike => {
|
|
return {
|
|
...block,
|
|
props: block.props ? { ...block.props } : undefined,
|
|
content: Array.isArray(block.content) ? [...block.content] : block.content,
|
|
children: Array.isArray(block.children) ? block.children.map(cloneBlock) : block.children,
|
|
};
|
|
};
|
|
|
|
export const removeBlockSubtree = (
|
|
blocks: BlockLike[],
|
|
blockId: string,
|
|
): { removed: BlockLike | null; nextBlocks: BlockLike[] } => {
|
|
// 说明:这里不直接修改入参 blocks,返回新的 nextBlocks。
|
|
const nextTop = blocks.map(cloneBlock);
|
|
const hit = findBlockInTree(nextTop, blockId);
|
|
if (!hit) return { removed: null, nextBlocks: nextTop };
|
|
|
|
if (hit.parent) {
|
|
const parent = hit.parent;
|
|
const nextChildren = asBlockArray(parent.children).map(cloneBlock);
|
|
const removed = nextChildren.splice(hit.index, 1)[0] ?? null;
|
|
parent.children = nextChildren;
|
|
return { removed, nextBlocks: nextTop };
|
|
}
|
|
|
|
const removed = nextTop.splice(hit.index, 1)[0] ?? null;
|
|
return { removed, nextBlocks: nextTop };
|
|
};
|
|
|
|
export const replaceBlockInTree = (
|
|
blocks: BlockLike[],
|
|
blockId: string,
|
|
nextBlock: BlockLike,
|
|
): { ok: boolean; nextBlocks: BlockLike[] } => {
|
|
const nextTop = blocks.map(cloneBlock);
|
|
const hit = findBlockInTree(nextTop, blockId);
|
|
if (!hit) return { ok: false, nextBlocks: nextTop };
|
|
|
|
const normalized = cloneBlock({ ...nextBlock, id: blockId });
|
|
if (hit.parent) {
|
|
const parent = hit.parent;
|
|
const nextChildren = asBlockArray(parent.children).map(cloneBlock);
|
|
nextChildren[hit.index] = normalized;
|
|
parent.children = nextChildren;
|
|
return { ok: true, nextBlocks: nextTop };
|
|
}
|
|
|
|
nextTop[hit.index] = normalized;
|
|
return { ok: true, nextBlocks: nextTop };
|
|
};
|
|
|
|
export const extractBlockText = (block: BlockLike): string => {
|
|
const inline = Array.isArray(block.content) ? (block.content as Array<{ text?: unknown }>) : [];
|
|
const text = inline.map((n) => (typeof n?.text === "string" ? n.text : "")).join("");
|
|
return text.trim();
|
|
};
|
|
|
|
export const getBlocksFromDocumentContent = (content: unknown): BlockLike[] => {
|
|
if (Array.isArray(content)) return asBlockArray(content);
|
|
if (content && typeof content === "object") {
|
|
const blocks = (content as { blocks?: unknown }).blocks;
|
|
return asBlockArray(blocks);
|
|
}
|
|
return [];
|
|
};
|
|
|
|
export const withBlocksWrittenBack = (content: unknown, blocks: BlockLike[]): Json => {
|
|
// 复用现有结构:数组或 {blocks: []}
|
|
if (Array.isArray(content)) {
|
|
return blocks as unknown as Json;
|
|
}
|
|
if (content && typeof content === "object") {
|
|
return { ...(content as Record<string, unknown>), blocks } as Json;
|
|
}
|
|
return { blocks } as Json;
|
|
};
|
|
|