Files
mnote/wolai-frontend/src/lib/blocks.ts
T

115 lines
3.7 KiB
TypeScript
Raw Normal View History

2026-01-17 10:12:53 +08:00
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;
};