diff options
Diffstat (limited to 'packages/shared/import-export/exporters.ts')
| -rw-r--r-- | packages/shared/import-export/exporters.ts | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/packages/shared/import-export/exporters.ts b/packages/shared/import-export/exporters.ts new file mode 100644 index 00000000..967937a4 --- /dev/null +++ b/packages/shared/import-export/exporters.ts @@ -0,0 +1,111 @@ +import { z } from "zod"; + +import { BookmarkTypes, ZBookmark } from "../types/bookmarks"; + +export const zExportBookmarkSchema = z.object({ + createdAt: z.number(), + title: z.string().nullable(), + tags: z.array(z.string()), + content: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal(BookmarkTypes.LINK), + url: z.string(), + }), + z.object({ + type: z.literal(BookmarkTypes.TEXT), + text: z.string(), + }), + ]) + .nullable(), + note: z.string().nullable(), + archived: z.boolean().optional().default(false), +}); + +export const zExportSchema = z.object({ + bookmarks: z.array(zExportBookmarkSchema), +}); + +export function toExportFormat( + bookmark: ZBookmark, +): z.infer<typeof zExportBookmarkSchema> { + let content = null; + switch (bookmark.content.type) { + case BookmarkTypes.LINK: { + content = { + type: bookmark.content.type, + url: bookmark.content.url, + }; + break; + } + case BookmarkTypes.TEXT: { + content = { + type: bookmark.content.type, + text: bookmark.content.text, + }; + break; + } + // Exclude asset types for now + } + return { + createdAt: Math.floor(bookmark.createdAt.getTime() / 1000), + title: + bookmark.title ?? + (bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.title ?? null) + : null), + tags: bookmark.tags.map((t) => t.name), + content, + note: bookmark.note ?? null, + archived: bookmark.archived, + }; +} + +export function toNetscapeFormat(bookmarks: ZBookmark[]): string { + const header = `<!DOCTYPE NETSCAPE-Bookmark-file-1> +<!-- This is an automatically generated file. + It will be read and overwritten. + DO NOT EDIT! --> +<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> +<TITLE>Bookmarks</TITLE> +<H1>Bookmarks</H1> +<DL><p>`; + + const footer = `</DL><p>`; + + const bookmarkEntries = bookmarks + .map((bookmark) => { + if (bookmark.content?.type !== BookmarkTypes.LINK) { + return ""; + } + const addDate = bookmark.createdAt + ? `ADD_DATE="${Math.floor(bookmark.createdAt.getTime() / 1000)}"` + : ""; + + const tagNames = bookmark.tags.map((t) => t.name).join(","); + const tags = tagNames.length > 0 ? `TAGS="${tagNames}"` : ""; + + const encodedUrl = encodeURI(bookmark.content.url); + const displayTitle = bookmark.title ?? bookmark.content.url; + const encodedTitle = escapeHtml(displayTitle); + + return ` <DT><A HREF="${encodedUrl}" ${addDate} ${tags}>${encodedTitle}</A>`; + }) + .filter(Boolean) + .join("\n"); + + return `${header}\n${bookmarkEntries}\n${footer}`; +} + +function escapeHtml(input: string): string { + const escapeMap: Record<string, string> = { + "&": "&", + "'": "'", + "`": "`", + '"': """, + "<": "<", + ">": ">", + }; + + return input.replace(/[&'`"<>]/g, (match) => escapeMap[match] || ""); +} |
