首次发布git
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import TurndownService from "turndown";
|
||||
import JSZip from "jszip";
|
||||
import { saveAs } from "file-saver";
|
||||
import { Page } from "./store";
|
||||
import { useSettingsStore } from "./settings-store";
|
||||
|
||||
import { gfm } from "turndown-plugin-gfm";
|
||||
|
||||
// Initialize Turndown service
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
bulletListMarker: "-",
|
||||
});
|
||||
|
||||
// Use GFM plugin for tables
|
||||
turndownService.use(gfm);
|
||||
|
||||
// Configure Turndown to handle some common elements better if needed
|
||||
turndownService.addRule('strikethrough', {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
filter: ['del', 's', 'strike'] as any,
|
||||
replacement: function (content: string) {
|
||||
return '~' + content + '~';
|
||||
}
|
||||
});
|
||||
|
||||
// Task List Support (Simple)
|
||||
turndownService.addRule('taskListItems', {
|
||||
filter: function (node: HTMLElement) {
|
||||
return node.nodeName === 'LI' && node.className.includes('task-list-item');
|
||||
},
|
||||
replacement: function (content: string, node: HTMLElement) {
|
||||
const isChecked = node.getAttribute('data-checked') === 'true';
|
||||
return (isChecked ? '- [x] ' : '- [ ] ') + content.replace(/^- /, '') + '\n';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts HTML content to Markdown
|
||||
*/
|
||||
export function htmlToMarkdown(html: string): string {
|
||||
if (!html) return "";
|
||||
return turndownService.turndown(html);
|
||||
}
|
||||
|
||||
// Helper to sanitize filenames
|
||||
function sanitizeFilename(name: string): string {
|
||||
return name.replace(/[<>:"/\\|?*]/g, '_').trim();
|
||||
}
|
||||
|
||||
// Helper to escape YAML strings
|
||||
function escapeYamlString(str: string): string {
|
||||
return str.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a page to Markdown with Frontmatter
|
||||
*/
|
||||
export function pageToMarkdown(page: Page): string {
|
||||
const rawMarkdown = htmlToMarkdown(page.content || "");
|
||||
const tags = page.tags ? JSON.parse(typeof page.tags === 'string' ? page.tags : JSON.stringify(page.tags)) : [];
|
||||
|
||||
// Format Date to Local Time with Timezone
|
||||
const dateObj = page.updatedAt ? new Date(page.updatedAt) : new Date();
|
||||
|
||||
// Get timezone from store
|
||||
const { timezone } = useSettingsStore.getState();
|
||||
|
||||
// Use Intl to format date in the target timezone
|
||||
// We want format: YYYY-MM-DDTHH:mm:ss
|
||||
// Since Intl isn't extremely flexible with custom formats without parts, we can use "sv-SE" locale which is usually ISO-like (YYYY-MM-DD HH:mm:ss)
|
||||
// Or just construct it using parts.
|
||||
|
||||
let localDate;
|
||||
try {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
timeZone: timezone || "Asia/Shanghai",
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
// sv-SE outputs YYYY-MM-DD HH:mm:ss
|
||||
const parts = new Intl.DateTimeFormat('sv-SE', options).formatToParts(dateObj);
|
||||
const getPart = (type: string) => parts.find(p => p.type === type)?.value || "";
|
||||
localDate = `${getPart('year')}-${getPart('month')}-${getPart('day')}T${getPart('hour')}:${getPart('minute')}:${getPart('second')}`;
|
||||
} catch (e) {
|
||||
// Fallback if timezone is invalid
|
||||
localDate = dateObj.toISOString().slice(0, 19);
|
||||
}
|
||||
|
||||
// Construct Frontmatter
|
||||
const frontmatter = [
|
||||
"---",
|
||||
`title: "${escapeYamlString(page.title)}"`,
|
||||
`id: "${page.id}"`,
|
||||
`date: "${localDate}"`,
|
||||
`tags: [${tags.map((t: string) => `"${escapeYamlString(t)}"`).join(", ")}]`,
|
||||
`order: ${page.order || 0}`,
|
||||
"---",
|
||||
"",
|
||||
""
|
||||
].join("\n");
|
||||
|
||||
return frontmatter + rawMarkdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a single page as a Markdown file
|
||||
*/
|
||||
export function exportPageAsMarkdown(page: Page) {
|
||||
const markdown = pageToMarkdown(page);
|
||||
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
|
||||
const filename = sanitizeFilename(page.title || "Untitled");
|
||||
saveAs(blob, `${filename}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a folder and its contents as a Zip file
|
||||
*/
|
||||
export async function exportFolderAsZip(folderId: string, allPages: Page[], folderName: string) {
|
||||
const zip = new JSZip();
|
||||
const cleanFolderName = sanitizeFilename(folderName);
|
||||
const folder = zip.folder(cleanFolderName);
|
||||
|
||||
if (!folder) {
|
||||
console.error("Failed to create zip folder");
|
||||
return;
|
||||
}
|
||||
|
||||
addPagesToZipRecursive(folder, folderId, allPages);
|
||||
|
||||
try {
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
saveAs(content, `${folderName}.zip`);
|
||||
} catch (e) {
|
||||
console.error("Failed to generate zip", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports ALL pages as a Zip backup
|
||||
*/
|
||||
export async function exportAllPagesAsZip(allPages: Page[]) {
|
||||
const zip = new JSZip();
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 16); // YYYY-MM-DDTHH-mm
|
||||
const zipName = `noteai-backup-${timestamp}`;
|
||||
|
||||
// Root level pages (parentId is null)
|
||||
addPagesToZipRecursive(zip, null, allPages);
|
||||
|
||||
try {
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
saveAs(content, `${zipName}.zip`);
|
||||
} catch (e) {
|
||||
console.error("Failed to generate backup zip", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to recursively add pages to zip
|
||||
function addPagesToZipRecursive(currentFolder: JSZip, parentId: string | null, allPages: Page[]) {
|
||||
const children = allPages.filter(p => p.parentId === parentId);
|
||||
|
||||
children.forEach(page => {
|
||||
const cleanTitle = sanitizeFilename(page.title);
|
||||
|
||||
if (page.type === 'folder') {
|
||||
const subFolder = currentFolder.folder(cleanTitle);
|
||||
if (subFolder) {
|
||||
// Determine if we should look for children with this page's ID
|
||||
addPagesToZipRecursive(subFolder, page.id, allPages);
|
||||
}
|
||||
} else {
|
||||
const markdown = pageToMarkdown(page);
|
||||
// Handle duplicate filenames by appending ID if necessary?
|
||||
// For simplicity, we assume titles are unique enough or zip handles overwrite (last wins).
|
||||
// Better: just append a counter if exists, but JSZip might overwrite.
|
||||
currentFolder.file(`${cleanTitle}.md`, markdown);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user