299 lines
7.9 KiB
TypeScript
299 lines
7.9 KiB
TypeScript
import { Workspace, DocEntity, TodoEntity, Entity } from "./types";
|
|
import { OutputData } from "@editorjs/editorjs";
|
|
|
|
/**
|
|
* Convert EditorJS OutputData to Markdown
|
|
*/
|
|
export function docToMarkdown(content: OutputData): string {
|
|
return content.blocks
|
|
.map((block) => {
|
|
switch (block.type) {
|
|
case "header":
|
|
return `${"#".repeat((block.data as any).level)} ${(block.data as any).text}`;
|
|
|
|
case "paragraph":
|
|
return `${(block.data as any).text}\n`;
|
|
|
|
case "list":
|
|
if ((block.data as any).style === "ordered") {
|
|
return ((block.data as any).items as Array<{ content: string }>)
|
|
.map((item, i) => `${i + 1}. ${item.content}`)
|
|
.join("\n");
|
|
} else {
|
|
return ((block.data as any).items as Array<{ content: string }>)
|
|
.map((item) => `- ${item.content}`)
|
|
.join("\n");
|
|
}
|
|
|
|
case "code":
|
|
return `\`\`\`\n${(block.data as any).code}\n\`\`\``;
|
|
|
|
case "quote":
|
|
return `> ${(block.data as any).text}`;
|
|
|
|
case "delimiter":
|
|
return `---`;
|
|
|
|
case "image":
|
|
return `.file.url})`;
|
|
|
|
default:
|
|
return "";
|
|
}
|
|
})
|
|
.join("\n\n");
|
|
}
|
|
|
|
/**
|
|
* Convert EditorJS OutputData to HTML
|
|
*/
|
|
export function docToHtml(content: OutputData): string {
|
|
const htmlBlocks = content.blocks.map((block) => {
|
|
switch (block.type) {
|
|
case "header":
|
|
return `<h${(block.data as any).level}>${(block.data as any).text}</h${(block.data as any).level}>`;
|
|
|
|
case "paragraph":
|
|
return `<p>${(block.data as any).text}</p>`;
|
|
|
|
case "list":
|
|
const items = ((block.data as any).items as Array<{ content: string }>)
|
|
.map((item) => `<li>${item.content}</li>`)
|
|
.join("\n");
|
|
|
|
if ((block.data as any).style === "ordered") {
|
|
return `<ol>\n${items}\n</ol>`;
|
|
} else {
|
|
return `<ul>\n${items}\n</ul>`;
|
|
}
|
|
|
|
case "code":
|
|
return `<pre><code>${(block.data as any).code}</code></pre>`;
|
|
|
|
case "quote":
|
|
return `<blockquote>${(block.data as any).text}</blockquote>`;
|
|
|
|
case "delimiter":
|
|
return `<hr />`;
|
|
|
|
case "image":
|
|
return `<img src="${(block.data as any).file.url}" alt="${(block.data as any).caption || ""}" />`;
|
|
|
|
default:
|
|
return "";
|
|
}
|
|
});
|
|
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px; }
|
|
h1, h2, h3 { margin-top: 20px; }
|
|
code { background-color: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
|
|
pre { background-color: #f0f0f0; padding: 10px; border-radius: 5px; overflow-x: auto; }
|
|
blockquote { border-left: 4px solid #ddd; padding-left: 10px; color: #666; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
${htmlBlocks.join("\n")}
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/**
|
|
* Export a Doc entity as JSON
|
|
*/
|
|
export function exportDocAsJson(doc: DocEntity): string {
|
|
return JSON.stringify(
|
|
{
|
|
name: doc.name,
|
|
type: "doc",
|
|
content: doc.content,
|
|
createdAt: new Date(doc.createdAt).toISOString(),
|
|
updatedAt: new Date(doc.updatedAt).toISOString(),
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Export a Doc entity as Markdown
|
|
*/
|
|
export function exportDocAsMarkdown(doc: DocEntity): string {
|
|
const header = `# ${doc.name}\n\nCreated: ${new Date(doc.createdAt).toISOString()}\nUpdated: ${new Date(doc.updatedAt).toISOString()}\n\n---\n\n`;
|
|
const content = docToMarkdown(doc.content);
|
|
return header + content;
|
|
}
|
|
|
|
/**
|
|
* Export a Doc entity as HTML
|
|
*/
|
|
export function exportDocAsHtml(doc: DocEntity): string {
|
|
const html = docToHtml(doc.content);
|
|
const headerHtml = `<h1>${doc.name}</h1><p>Created: ${new Date(doc.createdAt).toISOString()}\nUpdated: ${new Date(doc.updatedAt).toISOString()}</p><hr />`;
|
|
|
|
// Insert header before the first body element
|
|
return html
|
|
.replace("</head>", `<title>${doc.name}</title>\n</head>`)
|
|
.replace("<body>", `<body>\n${headerHtml}\n`);
|
|
}
|
|
|
|
/**
|
|
* Export a Todo entity as JSON
|
|
*/
|
|
export function exportTodoAsJson(todo: TodoEntity): string {
|
|
return JSON.stringify(
|
|
{
|
|
name: todo.name,
|
|
type: "todo",
|
|
config: todo.config,
|
|
tags: todo.tags,
|
|
tasks: todo.tasks,
|
|
createdAt: new Date(todo.createdAt).toISOString(),
|
|
updatedAt: new Date(todo.updatedAt).toISOString(),
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Export a Todo entity as CSV
|
|
*/
|
|
export function exportTodoAsCsv(todo: TodoEntity): string {
|
|
const headers = [
|
|
"Title",
|
|
"Description",
|
|
"Status",
|
|
"Tags",
|
|
"Created",
|
|
"Updated",
|
|
];
|
|
const rows = Object.values(todo.tasks).map((task) => [
|
|
task.title,
|
|
task.description || "",
|
|
todo.config.statuses.find((s) => s.id === task.statusId)?.name || "",
|
|
task.tagIds.map((id) => todo.tags[id]?.name || "").join(";"),
|
|
new Date(task.createdAt).toISOString(),
|
|
new Date(task.updatedAt).toISOString(),
|
|
]);
|
|
|
|
const csvContent = [
|
|
headers.join(","),
|
|
...rows.map((row) =>
|
|
row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(","),
|
|
),
|
|
].join("\n");
|
|
|
|
return csvContent;
|
|
}
|
|
|
|
/**
|
|
* Export a Todo entity as Markdown
|
|
*/
|
|
export function exportTodoAsMarkdown(todo: TodoEntity): string {
|
|
let markdown = `# ${todo.name}\n\nCreated: ${new Date(todo.createdAt).toISOString()}\nUpdated: ${new Date(todo.updatedAt).toISOString()}\n\n`;
|
|
|
|
// Group tasks by status
|
|
const tasksByStatus: Record<string, typeof todo.tasks> = {};
|
|
for (const status of todo.config.statuses) {
|
|
tasksByStatus[status.id] = {};
|
|
}
|
|
|
|
for (const [taskId, task] of Object.entries(todo.tasks)) {
|
|
const statusId = task.statusId;
|
|
if (!tasksByStatus[statusId]) {
|
|
tasksByStatus[statusId] = {};
|
|
}
|
|
tasksByStatus[statusId][taskId] = task;
|
|
}
|
|
|
|
// Add tasks by status
|
|
for (const status of todo.config.statuses) {
|
|
markdown += `## ${status.name}\n\n`;
|
|
const tasks = tasksByStatus[status.id];
|
|
if (Object.keys(tasks).length === 0) {
|
|
markdown += "- (No tasks)\n\n";
|
|
} else {
|
|
for (const task of Object.values(tasks)) {
|
|
const tags = task.tagIds
|
|
.map((id) => todo.tags[id]?.name || "")
|
|
.filter((n) => n)
|
|
.join(", ");
|
|
markdown += `- **${task.title}**${tags ? ` (${tags})` : ""}`;
|
|
if (task.description) {
|
|
markdown += `\n - ${task.description}`;
|
|
}
|
|
markdown += "\n";
|
|
}
|
|
}
|
|
markdown += "\n";
|
|
}
|
|
|
|
return markdown;
|
|
}
|
|
|
|
/**
|
|
* Export entire workspace as JSON
|
|
*/
|
|
export function exportWorkspaceAsJson(workspace: Workspace): string {
|
|
return JSON.stringify(workspace, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Trigger download of a file
|
|
*/
|
|
export function downloadFile(
|
|
content: string,
|
|
filename: string,
|
|
mimeType: string = "text/plain",
|
|
): void {
|
|
const blob = new Blob([content], { type: mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
/**
|
|
* Export any entity based on its type
|
|
*/
|
|
export function exportEntity(
|
|
entity: Entity,
|
|
format: "json" | "markdown" | "html" | "csv",
|
|
): string {
|
|
if (entity.type === "doc") {
|
|
switch (format) {
|
|
case "json":
|
|
return exportDocAsJson(entity);
|
|
case "markdown":
|
|
return exportDocAsMarkdown(entity);
|
|
case "html":
|
|
return exportDocAsHtml(entity);
|
|
default:
|
|
return exportDocAsJson(entity);
|
|
}
|
|
} else if (entity.type === "todo") {
|
|
switch (format) {
|
|
case "json":
|
|
return exportTodoAsJson(entity);
|
|
case "markdown":
|
|
return exportTodoAsMarkdown(entity);
|
|
case "csv":
|
|
return exportTodoAsCsv(entity);
|
|
default:
|
|
return exportTodoAsJson(entity);
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|