import type { PlaitElement } from "@plait/core"; import { getNoteDocument, noteDocumentText, serializeNoteDocument, type NoteDocument } from "./topic-notes"; export type TopicAttachment = { name: string; url: string; mimeType: string }; export type TopicDetails = { notes: string; notesDocument?: NoteDocument; labels: string[]; href: string; topicLinkId: string; attachment: TopicAttachment | null; }; export type TopicEntry = { element: PlaitElement; title: string; depth: number; path: number[]; }; const record = (value: unknown): Record => value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; export function getTopicTitle(element: PlaitElement): string { const visit = (value: unknown): string => { const item = record(value); if (typeof item.text === "string") return item.text; return Array.isArray(item.children) ? item.children.map(visit).join("") : ""; }; return visit(record(element.data).topic) || "未命名主题"; } export function getTopicEntries( elements: PlaitElement[], depth = 0, parentPath: number[] = [] ): TopicEntry[] { return elements.flatMap((element, index) => { if (!["mind", "mindmap", "mind_child"].includes(String(element.type))) return []; return [ { element, title: getTopicTitle(element), depth, path: [...parentPath, index], }, ...getTopicEntries(element.children || [], depth + 1, [ ...parentPath, index, ]), ]; }); } export function getTopicDetails(element: PlaitElement | null): TopicDetails { const data = record(element?.xmind); const notes = typeof data.notes === "string" ? data.notes : record(record(data.notes).plain).content; const labels = Array.isArray(data.labels) ? data.labels : typeof data.labels === "string" ? [data.labels] : []; const htmlText = typeof notes === "string" ? "" : noteDocumentText(getNoteDocument(element)); const attachment = record(data.attachment); const safeAttachment = typeof attachment.url === "string" && /^data:[a-z0-9.+/-]+;base64,[a-z0-9+/=]*$/i.test(attachment.url) && typeof attachment.name === "string" ? (attachment as TopicAttachment) : null; return { attachment: safeAttachment, notes: typeof notes === "string" ? notes : htmlText, labels: [ ...new Set( labels .filter( (label): label is string => typeof label === "string" && Boolean(label.trim()) ) .map((label) => label.trim()) ), ], href: typeof data.href === "string" ? data.href : "", topicLinkId: typeof data.topicLinkId === "string" ? data.topicLinkId : "", }; } export function patchTopicDetails( element: PlaitElement, patch: Partial ) { const data = { ...record(element.xmind) }; for (const [key, value] of Object.entries(patch)) { if (key === "notesDocument") data.notes = serializeNoteDocument(value as NoteDocument); else if (key === "notes") data.notes = value ? { plain: { content: value } } : undefined; else data[key] = value; } return data; } export function getSafeTopicHref(href: string): string | null { if (!href.trim()) return null; try { const url = new URL(href.trim()); return ["http:", "https:", "mailto:", "tel:"].includes(url.protocol) ? url.href : null; } catch { return null; } }