From aecbe6ae8b3dbc7bcdcf33f1c8c086dafb77eb24 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 30 Aug 2025 15:26:02 +0000 Subject: fix: handle list with slashes in their names and truncate long list names. fixes #1597 --- packages/shared/import-export/exporters.ts | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/shared/import-export/exporters.ts (limited to 'packages/shared/import-export/exporters.ts') 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 { + 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 = ` + + +Bookmarks +

Bookmarks

+

`; + + const footer = `

`; + + 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 `

${encodedTitle}`; + }) + .filter(Boolean) + .join("\n"); + + return `${header}\n${bookmarkEntries}\n${footer}`; +} + +function escapeHtml(input: string): string { + const escapeMap: Record = { + "&": "&", + "'": "'", + "`": "`", + '"': """, + "<": "<", + ">": ">", + }; + + return input.replace(/[&'`"<>]/g, (match) => escapeMap[match] || ""); +} -- cgit v1.2.3-70-g09d2