import type { Bookmark, BookmarkId, Tag, TaggedBookmark } from "@markone/core" import type { User } from "@markone/core/user" import { Readability } from "@mozilla/readability" import { JSDOM } from "jsdom" import { db } from "~/database.ts" import { findTagsByNames, insertTags } from "~/tag/tag.js" import { DEMO_BOOKMARKS } from "./demo-bookmarks.ts" class LinkUnreachable {} class UnsupportedLink {} interface CachedContent { title: string mimeType: string data: Buffer | null } function insertDemoBookmarks(user: User) { const query = db.query(` INSERT OR IGNORE INTO bookmarks (id, user_id, title, url, mime_type, content) VALUES ${Array(DEMO_BOOKMARKS.length).fill("(?,?,?,?,NULL,NULL)").join(",")} `) const args: Parameters = [] for (const bookmark of DEMO_BOOKMARKS) { args.push(bookmark.id, user.id, bookmark.title, bookmark.url) } query.run(...args) } function insertBookmark(bookmark: Bookmark, cachedContent: CachedContent | null, user: User) { const query = db.query(` INSERT INTO bookmarks (id, user_id, title, url, mime_type, content) VALUES ($id, $userId, $title, $url, $mimeType, $content) `) query.run({ id: bookmark.id, userId: user.id, title: bookmark.title, url: bookmark.url, mimeType: cachedContent?.mimeType || null, content: cachedContent?.data ?? null, }) } function findBookmarkCachedContent(id: string, user: User): Buffer | null { const query = db.query<{ content: Buffer | null }, { id: string; userId: string }>( "SELECT content FROM bookmarks WHERE id = $id AND user_id = $userId", ) const row = query.get({ id, userId: user.id }) return row?.content ?? null } function findBookmark(id: string, user: User): Bookmark | null { const bookmarkQuery = db.query( "SELECT id, title, url FROM bookmarks WHERE id = $id AND user_id = $userId", ) const bookmark = bookmarkQuery.get({ id, userId: user.id }) if (!bookmark) { return null } const tagsQuery = db.query(` SELECT tags.id, tags.name FROM tags INNER JOIN bookmark_tags ON bookmark_tags.tag_id = tags.id AND bookmark_tags.bookmark_id = $bookmarkId `) const tags = tagsQuery.all({ bookmarkId: id }) bookmark.tags = tags return bookmark } function updateBookmarkTitle(bookmark: Bookmark, newTitle: string, user: User) { const query = db.query("UPDATE bookmarks SET title = $title WHERE id = $id AND user_id = $userId") query.run({ title: newTitle, id: bookmark.id, userId: user.id, }) } function deleteBookmark(id: BookmarkId, user: User) { db.query("DELETE FROM bookmarks WHERE user_id = $userId AND id = $id").run({ id, userId: user.id, }) db.query("DELETE FROM bookmark_tags WHERE bookmark_id = ?").run(id) } async function cacheContent(url: string): Promise { const res = await fetch(url).catch(() => { throw new LinkUnreachable() }) let contentType = res.headers.get("Content-Type") if (!contentType) { contentType = "application/octet-stream" } if (contentType.startsWith("text/html")) { const matches = contentType.match(/charset=([^; ]+)/gi) let charset: string if (!matches || matches.length <= 2) { charset = "utf-8" } else { charset = matches[1] || "utf-8" } const websiteText = await res.text().catch(() => { throw new UnsupportedLink() }) const dom = new JSDOM(websiteText, { url }) const reader = new Readability(dom.window.document) const article = reader.parse() if (!article) { return null } if (article.content) { const newDom = new JSDOM(article.content, { url }) const doc = newDom.window.document const lightStyleLink = doc.createElement("link") lightStyleLink.rel = "stylesheet" lightStyleLink.href = "/reader-styles/sakura.css" lightStyleLink.media = "screen" const darkStyleLink = doc.createElement("link") darkStyleLink.rel = "stylesheet" darkStyleLink.href = "/reader-styles/sakura-dark.css" darkStyleLink.media = "screen and (prefers-color-scheme: dark)" doc.head.appendChild(lightStyleLink) doc.head.appendChild(darkStyleLink) article.content = newDom.serialize() } return { title: article.title || "Untitled", mimeType: "text/html", data: article.content ? Buffer.from(article.content, charset as BufferEncoding) : null, } } throw new UnsupportedLink() } function assignTagsToBookmark(tags: Tag[], bookmark: Bookmark) { const query = db.query(` INSERT OR IGNORE INTO bookmark_tags (tag_id, bookmark_id) VALUES ${Array(tags.length).fill("(?,?)").join(",")} `) const args: Parameters = [] for (const tag of tags) { args.push(tag.id) args.push(bookmark.id) } query.run(...args) } function findBookmarkTags(bookmark: Bookmark): Tag[] { const query = db.query(` SELECT tags.name as name, tags.id as id FROM bookmark_tags INNER JOIN tags ON tags.id = bookmark_tags.tag_id WHERE bookmark_tags.bookmark_id = $bookmarkId `) const tags = query.all({ bookmarkId: bookmark.id }) return tags } function updateBookmarkTags(bookmark: Bookmark, tagNames: string[], user: User) { const tags = findTagsByNames(tagNames, user) const existingTagNames = new Set() for (const tag of tags) { existingTagNames.add(tag.name) } const newTagNames: string[] = [] for (const name of tagNames) { if (!existingTagNames.has(name)) { newTagNames.push(name) } } if (newTagNames.length > 0) { const newTags = insertTags(newTagNames, user) tags.push(...newTags) } assignTagsToBookmark(tags, bookmark) return tags } export { insertDemoBookmarks, insertBookmark, updateBookmarkTitle, deleteBookmark, findBookmark, findBookmarkCachedContent, cacheContent, assignTagsToBookmark, findBookmarkTags, updateBookmarkTags, } export { LinkUnreachable, UnsupportedLink }