import { get, writable } from "svelte/store"; import { createFragment as createFragmentCommand, deleteFragment as deleteFragmentCommand, listFragments, updateFragment as updateFragmentCommand, type FragmentDto, } from "$lib/backend/fragments"; export type FragmentItem = { id: string; label: string; initialContent: string; }; export type ParsedFragment = { title: string; type: string; tags: string[]; body: string; }; const initialFragments: FragmentItem[] = []; export const fragmentsStore = writable(initialFragments); export const fragmentsBusyStore = writable(false); function toStoreId(id: string): string { return `fragments/${id}`; } function toBackendId(id: string): string | null { const prefix = "fragments/"; if (!id.startsWith(prefix)) return null; const backendId = id.slice(prefix.length).trim(); return backendId || null; } function splitDescription(description: string): { title: string; body: string; } { const normalized = description.trim(); if (!normalized) { return { title: "Untitled Fragment", body: "" }; } const separator = normalized.indexOf("\n\n"); if (separator === -1) { return { title: normalized, body: "" }; } const title = normalized.slice(0, separator).trim() || "Untitled Fragment"; const body = normalized.slice(separator + 2).trim(); return { title, body }; } function composeDescription(title: string, body: string): string { const resolvedTitle = title.trim() || "Untitled Fragment"; const resolvedBody = body.trim() || "Add details for this fragment."; return `${resolvedTitle}\n\n${resolvedBody}`; } function dtoToItem(dto: FragmentDto): FragmentItem { const parsed = splitDescription(dto.description); return { id: toStoreId(dto.id), label: parsed.title, initialContent: serializeFragment({ title: parsed.title, type: dto.type, tags: dto.tags ?? [], body: parsed.body, }), }; } function upsertById(items: FragmentItem[], next: FragmentItem): FragmentItem[] { const idx = items.findIndex((item) => item.id === next.id); if (idx === -1) { return [next, ...items]; } const clone = [...items]; clone[idx] = next; return clone; } export function createFragmentId(title: string): string { const slug = title .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 40); return `fragments/${slug || "fragment"}-${Date.now()}`; } export function serializeFragment(payload: ParsedFragment): string { const title = payload.title.trim() || "Untitled Fragment"; const type = payload.type.trim(); const tagsLine = payload.tags.length ? payload.tags.map((tag) => `#${tag}`).join(" ") : "(none)"; const body = payload.body.trim() || "Add details for this fragment."; return `# ${title}\n\nType: ${type}\n\nTags: ${tagsLine}\n\n${body}`; } export function parseFragmentContent( content: string, fallbackTitle = "Untitled Fragment", ): ParsedFragment { const headingMatch = content.match(/^#\s+(.+)$/m); const typeMatch = content.match(/^Type:\s*(.+)$/m); const tagsMatch = content.match(/^Tags:\s*(.+)$/m); const bodyMatch = content.match(/^#.*\n\nType:.*\n\nTags:.*\n\n([\s\S]*)$/); const rawTags = tagsMatch?.[1]?.trim() ?? "(none)"; const tags = rawTags.toLowerCase() === "(none)" ? [] : rawTags .split(/\s+/) .map((tag) => tag.replace(/^#/, "").trim()) .filter(Boolean); return { title: headingMatch?.[1]?.trim() || fallbackTitle, type: typeMatch?.[1]?.trim() || "", tags, body: bodyMatch?.[1]?.trim() || "", }; } export function createFragmentDraft(): FragmentItem { const id = `fragments/new-${Date.now()}`; return { id, label: "New Fragment", initialContent: "# New Fragment\n\nType: \n\nTags: (none)\n\n", }; } export function createFragmentItem( title: string, content: string, ): FragmentItem { return { id: createFragmentId(title), label: title.trim() || "Untitled Fragment", initialContent: content, }; } export function updateFragmentItem( items: FragmentItem[], id: string, title: string, content: string, ): FragmentItem[] { return items.map((item) => item.id === id ? { ...item, label: title.trim() || "Untitled Fragment", initialContent: content, } : item, ); } export function prependFragmentItem( items: FragmentItem[], item: FragmentItem, ): FragmentItem[] { return [item, ...items]; } export function removeFragmentItem( items: FragmentItem[], id: string, ): FragmentItem[] { return items.filter((item) => item.id !== id); } export async function hydrateFragments(): Promise { fragmentsBusyStore.set(true); try { const items = await listFragments(); fragmentsStore.set(items.map(dtoToItem)); } catch (error) { console.error("[fragments] hydrate:error", error); throw error; } finally { fragmentsBusyStore.set(false); } } export async function createFragmentFromParsed( payload: ParsedFragment, ): Promise { const created = await createFragmentCommand({ type: payload.type.trim(), description: composeDescription(payload.title, payload.body), tags: payload.tags, }); const item = dtoToItem(created); fragmentsStore.update((items) => prependFragmentItem(items, item)); return item; } export async function updateFragmentFromParsed( storeId: string, payload: ParsedFragment, ): Promise { const backendId = toBackendId(storeId); if (!backendId) return null; const ok = await updateFragmentCommand(backendId, { type: payload.type.trim(), description: composeDescription(payload.title, payload.body), tags: payload.tags, }); if (!ok) return null; const item: FragmentItem = { id: storeId, label: payload.title.trim() || "Untitled Fragment", initialContent: serializeFragment(payload), }; fragmentsStore.update((items) => upsertById(items, item)); return item; } export async function deleteFragmentByStoreId( storeId: string, ): Promise { const backendId = toBackendId(storeId); if (!backendId) return false; const ok = await deleteFragmentCommand(backendId); if (!ok) return false; fragmentsStore.update((items) => removeFragmentItem(items, storeId)); return true; } export function hasFragment(storeId: string): boolean { return get(fragmentsStore).some((item) => item.id === storeId); }