Add native task metadata, working-day calendar settings, dependency scheduling and rich note editing. Support complete-sheet PNG export with resolution and transparency controls, preserve XMind extensions and update browser regressions.
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
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<string, unknown> =>
|
|
value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {};
|
|
|
|
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<TopicDetails>
|
|
) {
|
|
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;
|
|
}
|
|
}
|