@ecsplico/editor 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1
4
+
5
+ ### Changed
6
+
7
+ - The comment stores and `handleCommentsRequest` (`/comments/server`, `/comments/json`, `/comments/drizzle`) answer refused requests (`400`/`404`/`405`/`409`) with German `error` texts, like the editor's other server routes (e.g. `Kommentar-Thread „t1“ nicht gefunden`). Status codes and `CommentStoreError.code` are unchanged; clients that matched the old English texts must match the status or code instead.
8
+ - Errors thrown by `useEditorDocument` (`/adapters/vue`) and `createRestCommentsModel` (`/comments/client`) start with `@ecsplico/editor:` like the editor's other developer errors.
9
+
3
10
  ## 2.1.0
4
11
 
5
12
  ### Added
@@ -8,6 +15,18 @@
8
15
  - Optional peer dependency `drizzle-orm` (`^0.45.0`), needed only by `/adapters/drizzle` and `/comments/drizzle`.
9
16
  - Bundle-isolation fixtures `adapters-node`, `adapters-drizzle`, `adapters-nitro`, `adapters-vue` and `headless-comments-client`: Vue is reachable only from `/adapters/vue`, Nitro only from `/server/nitro`, drizzle only from the two drizzle entries, React from none.
10
17
  - Example `examples/vue-nitro-json` (`bun run proto:vue-nitro-json`): a Vue editor on a bare Nitro server with JSON-file storage.
18
+ - Framework-free models behind the Vue and React components: `createCompositeInspector` (`/composites`), `createAiSelectionFlow` (`/ai`) and the comments margin (`createMarginController`, `layoutMargin`, `marginCards` in `/comments`; orphaned threads sit at their deletion point). The document tree reports why a drop is refused and filters by title.
19
+ - Accepted children per Baustein kind, enforced in the editor: `AcceptedChildren`, `kindAccepts`, `moveIntoVerdict` (`/composites`).
20
+ - The block-lines module owning the `[id] …` line format the AI sees (`readBlockLines`, `BlockLines` and the `BLOCK_LINE_*` texts in `/ai` and `/ai/protocol`): IDs inside fenced code are ignored, `add` requires `afterId`, operations on a block inside a block another operation replaces or deletes are dropped (`AgentOpProblemReason` `inside`), and operations on blocks deleted since the document went to the model are reported as `stale` (`AgentOpOutcome`).
21
+
22
+ ### Changed (breaking)
23
+
24
+ These shipped in a minor release by mistake; the only known consumer (skaile-forge) is migrated.
25
+
26
+ - `documentBlockIds(text)` is removed from `/ai` and `/ai/protocol`: use `readBlockLines(text).ids`.
27
+ - `AgentOpContext` (`checkAgentOps`): `known: (id) => boolean` is replaced by `lines: BlockLines`; pass `lines: readBlockLines(documentChatText(editor))`.
28
+ - `CommentsSnapshot` has a required `orphanAt` (thread id → deletion position); a host's own `CommentsModel` must fill it (an empty map is valid).
29
+ - `AgentOpOutcome` and `AgentOpProblemReason` gained `stale` and `inside`; exhaustive maps over them need the new keys.
11
30
 
12
31
  ### Notes
13
32
 
@@ -138,7 +138,7 @@ function createRestCommentsModel(options) {
138
138
  });
139
139
  if (!response.ok) {
140
140
  const detail = await response.json().then(errorOf, () => void 0);
141
- throw new Error(`Comments request ${init?.method ?? "GET"} ${endpoint} failed: ${response.status}${typeof detail === "string" ? ` ${detail}` : ""}`);
141
+ throw new Error(`@ecsplico/editor: comments request ${init?.method ?? "GET"} ${endpoint} failed: ${response.status}${typeof detail === "string" ? ` ${detail}` : ""}`);
142
142
  }
143
143
  return response.json();
144
144
  }
@@ -328,7 +328,7 @@ function createRestCommentsModel(options) {
328
328
  }
329
329
  async function load() {
330
330
  const discussion = await request();
331
- if (!isDiscussion(discussion)) throw new Error(`Comments request GET ${endpoint} did not answer a discussion.`);
331
+ if (!isDiscussion(discussion)) throw new Error(`@ecsplico/editor: comments request GET ${endpoint} did not answer a discussion`);
332
332
  threads = discussion.threads;
333
333
  anchors = new Map(discussion.annotations.map((annotation) => [annotation.id, annotation.anchor]));
334
334
  persisted.clear();
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["EMPTY"],"sources":["../../../src/adapters/comments/client.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { type AuthorIdentity, createStore, releaseRanges, resolveRange } from '../../core'\nimport { cancelCommentDraft, COMMENT_LIMITS, type CommentReply, type CommentsModel, type CommentsSnapshot, type CommentThread as EditorThread, commentsKey, type DocRange, setActiveComment, startCommentDraft } from '../../features/comments'\nimport { EMPTY_SNAPSHOT as EMPTY, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from '../../features/comments/snapshot-equality'\nimport { captureAnchor, capturePoint, type PortableAnchor, resolveAnchor, mapAnchor } from '../anchors'\nimport type { Annotation, Comment, CommentAction, CommentThread, Discussion } from './types'\n\n// A `CommentsModel` for the editor's comments feature (`commentsFeature({ model })`) whose threads live behind a REST\n// endpoint (`handleCommentsRequest` serves it) instead of the document's Y.Doc. The text a thread hangs on is a\n// portable anchor (`../anchors`), not a `comment` mark: anchors are resolved against the document when the threads\n// load, follow every edit of the editor, and are written back to the endpoint when they moved.\n\n/** The part of a `Response` the model reads; `globalThis.fetch` fits. */\nexport interface CommentsFetchResponse {\n ok: boolean\n status: number\n json(): Promise<unknown>\n}\n\nexport type CommentsFetch = (url: string, init?: { method?: string, headers?: Record<string, string>, body?: string }) => Promise<CommentsFetchResponse>\n\nexport interface RestCommentsModelOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. */\n fetch: CommentsFetch\n /** The document the threads belong to. */\n documentId: string\n /** The document's comments endpoint (`GET` → `Discussion`, `POST` → `CommentAction`); default `/comments/<encoded documentId>`. */\n endpoint?: string\n /** Who writes: the author of new comments and the only one who may edit or delete them; `null` while nobody is signed in (no comments then). */\n user: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may compose and change comments now; readers of a read-only document may be allowed. */\n canComment: boolean | (() => boolean)\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Milliseconds a changed anchor waits before it is written back (typing moves many); default 500. `idle()` writes at once. */\n persistDelay?: number\n /** Called with every failed request; the model reloads the threads afterwards, so the local state falls back to the server's. */\n onError?: (error: Error) => void\n}\n\nexport interface RestCommentsModel extends CommentsModel {\n /** Fetches the threads again and resolves their anchors against the current document. */\n reload(): Promise<void>\n /** Resolves once every change made so far has been sent (and the threads loaded, if a load was requested). */\n idle(): Promise<void>\n}\n\n/** Neutral colour of authors that stored no colour. */\nconst FALLBACK_COLOR = '#6b7280'\n/** Characters of context that must agree for an orphan to be re-attached on load. */\nconst REATTACH_CONTEXT = 24\n\nconst newId = (prefix: string) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`\n/** The `error` message of a refused request's JSON body. */\nfunction errorOf(body: unknown): unknown {\n return typeof body === 'object' && body !== null && 'error' in body ? body.error : undefined\n}\n\nfunction isDiscussion(value: unknown): value is Discussion {\n return typeof value === 'object' && value !== null && 'annotations' in value && Array.isArray(value.annotations) && 'threads' in value && Array.isArray(value.threads)\n}\n\nconst sameJson = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)\n\nconst authorOf = (comment: Comment): EditorThread['author'] => ({ id: comment.authorId, name: comment.authorName ?? comment.authorId, color: comment.authorColor ?? FALLBACK_COLOR })\n\nfunction quoteOf(anchor: PortableAnchor | undefined): string {\n if (!anchor) return ''\n if (anchor.kind === 'range') return anchor.quote\n if (anchor.kind === 'orphan') return anchor.quote ?? (anchor.previous.kind === 'range' ? anchor.previous.quote : '')\n return ''\n}\n\nfunction viewOf(thread: CommentThread, anchor: PortableAnchor | undefined): EditorThread {\n const [first, ...replies] = thread.comments\n return {\n id: thread.id,\n author: first ? authorOf(first) : { id: '', name: '', color: FALLBACK_COLOR },\n text: first?.body ?? '',\n createdAt: first?.createdAt ?? '',\n ...(first?.editedAt !== undefined && { editedAt: first.editedAt }),\n quote: quoteOf(anchor),\n resolved: thread.status === 'resolved',\n replies: replies.map((reply): CommentReply => ({\n id: reply.id,\n author: authorOf(reply),\n text: reply.body,\n createdAt: reply.createdAt,\n ...(reply.editedAt !== undefined && { editedAt: reply.editedAt }),\n })),\n }\n}\n\nfunction rangeOf(anchor: PortableAnchor | undefined): DocRange | null {\n if (!anchor || anchor.kind === 'orphan') return null\n return anchor.kind === 'range' ? { from: anchor.from, to: anchor.to } : { from: anchor.position, to: anchor.position }\n}\n\n/** A document is \"empty\" while the host has not put its content in yet: nothing to resolve anchors against. */\nconst isBlank = (doc: { textContent: string }) => doc.textContent === ''\n\n/** Whether the text at `candidate` still reads like the context `previous` was written in (one side suffices). */\nfunction contextAgrees(candidate: PortableAnchor, previous: PortableAnchor): boolean {\n if (candidate.kind === 'orphan' || previous.kind === 'orphan') return false\n const before = previous.contextBefore?.slice(-REATTACH_CONTEXT)\n const after = previous.contextAfter?.slice(0, REATTACH_CONTEXT)\n if (!before && !after) return true\n return (!!before && !!candidate.contextBefore?.endsWith(before)) || (!!after && !!candidate.contextAfter?.startsWith(after))\n}\n\n/**\n * The comments of one editor over a REST endpoint, as a `CommentsModel` (the host passes `() => model` as the\n * `model` factory of `commentsFeature`/`commentsFeatureCore`; one instance serves the plugin and every hook).\n *\n * - **Load.** `refresh()` (called by the feature once the editor exists) fetches `GET endpoint`; each annotation's\n * anchor is resolved against the document (exact offsets, else quote and context search). An anchor that moved is\n * re-captured and written back; an anchor whose text is gone becomes an `orphan` (the thread stays, without a range)\n * and an orphan whose text is back with the same surroundings is re-attached. While the document is still empty the\n * resolving waits for its content.\n * - **Edits.** Every transaction carries all anchors with it (`mapAnchor`), so ranges, quotes and contexts follow the\n * text; changed anchors are written back after `persistDelay`. Text deleted under an anchor orphans it for good (it is\n * never silently re-attached to other text during editing; `reload()` re-attaches it when its text and context\n * are found again, e.g. after the content was replaced).\n * - **Writes** are applied locally at once and sent in order; a failed request is reported to `onError` and the threads\n * are reloaded. Without a `user` or while `canComment` is false nothing is written. Only the author may edit or\n * delete a comment.\n * - **Lifecycle.** The model follows the editor `getEditor` names until that editor is destroyed; `dispose()` writes\n * pending anchor changes and does not stop it (the feature and its hooks share the instance and dispose it on their\n * own schedule).\n */\nexport function createRestCommentsModel(options: RestCommentsModelOptions): RestCommentsModel {\n const { fetch, documentId, getEditor, onError } = options\n const endpoint = options.endpoint ?? `/comments/${encodeURIComponent(documentId)}`\n const persistDelay = options.persistDelay ?? 500\n const store = createStore<CommentsSnapshot>(EMPTY)\n\n const userNow = () => (typeof options.user === 'function' ? options.user() : options.user)\n const canCommentNow = () => (typeof options.canComment === 'function' ? options.canComment() : options.canComment)\n /** Editors that announced their destruction: their view is still alive while `destroy` is emitted. */\n const gone = new WeakSet<Editor>()\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed && !gone.has(instance) ? instance : undefined\n }\n\n // -- Data: what the endpoint holds (annotation anchors are the local truth until written back) -----------------------\n let threads: readonly CommentThread[] = []\n let anchors = new Map<string, PortableAnchor>()\n /** The anchor each annotation has on the server, serialised. */\n const persisted = new Map<string, string>()\n const dirty = new Set<string>()\n /**\n * Where the text of an annotation that became an orphan during this session was: the deletion point, mapped through\n * every later edit. Keyed by annotation id (`orphanAt` in the snapshot is keyed by thread id; `orphanPositions` translates).\n * Deletion points are session-local: `load()` clears them, and an anchor that arrives already orphaned has none, because\n * the server stores no deletion point. Such threads get no `orphanAt` entry and sit after the placed ones in the margin.\n */\n const deletedAt = new Map<string, number>()\n /** Anchors wait for the document content (resolve on load) before they get a range. */\n let unresolved = false\n /** A load was requested / the first one succeeded: writes wait for it, so a load never overwrites a newer local change. */\n let loaded = false\n let ready = false\n\n // -- Requests: one queue, so a reply never overtakes its thread's creation ---------------------------------------------\n let chain: Promise<void> = Promise.resolve()\n let timer: ReturnType<typeof setTimeout> | undefined\n\n function enqueue(task: () => Promise<void>, reloadOnError = true): void {\n chain = chain.then(task).catch((error: unknown) => {\n onError?.(error instanceof Error ? error : new Error(String(error)))\n if (reloadOnError) enqueue(load, false)\n })\n }\n\n async function request(init?: { method: string, body: CommentAction }): Promise<unknown> {\n const response = await fetch(endpoint, init && { method: init.method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(init.body) })\n if (!response.ok) {\n const detail = await response.json().then(errorOf, () => undefined)\n throw new Error(`Comments request ${init?.method ?? 'GET'} ${endpoint} failed: ${response.status}${typeof detail === 'string' ? ` ${detail}` : ''}`)\n }\n return response.json()\n }\n\n const send = (body: CommentAction) => enqueue(async () => void await request({ method: 'POST', body }))\n\n function writeAnchors(): void {\n timer = undefined\n if (!userNow() || !ready || !canCommentNow()) {\n dirty.clear()\n return\n }\n for (const id of dirty) {\n const anchor = anchors.get(id)\n const threadId = threads.find(thread => thread.annotationId === id)?.id\n if (!anchor || !threadId || persisted.get(id) === JSON.stringify(anchor)) continue\n persisted.set(id, JSON.stringify(anchor))\n send({ action: 'update-annotation', annotation: { id, threadId, anchor } })\n }\n dirty.clear()\n }\n\n function markDirty(id: string): void {\n dirty.add(id)\n if (timer === undefined) timer = setTimeout(writeAnchors, persistDelay)\n }\n\n function flush(): void {\n clearTimeout(timer)\n writeAnchors()\n }\n\n // -- Anchors ------------------------------------------------------------------------------------------------------------\n\n /** `anchor` read against `editor`'s document: the moved/re-captured anchor, an orphan, or the same object. */\n function resolveOne(editor: Editor, anchor: PortableAnchor): PortableAnchor {\n const live = anchor.kind === 'orphan' ? anchor.previous : anchor\n const found = resolveAnchor(editor.state.doc, live)\n if (!found) {\n return anchor.kind === 'orphan'\n ? anchor\n : { kind: 'orphan', previous: anchor, quote: quoteOf(anchor) || undefined, contextBefore: anchor.contextBefore, contextAfter: anchor.contextAfter, reason: 'unresolvable' }\n }\n const next = live.kind === 'range' ? captureAnchor(editor, found) : capturePoint(editor, found.from)\n if (anchor.kind === 'orphan' && !contextAgrees(next, live)) return anchor\n return sameJson(next, anchor) ? anchor : next\n }\n\n function resolveAll(editor: Editor): void {\n if (isBlank(editor.state.doc)) {\n unresolved = anchors.size > 0\n return\n }\n unresolved = false\n for (const [id, anchor] of anchors) {\n const next = resolveOne(editor, anchor)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind !== 'orphan') deletedAt.delete(id)\n markDirty(id)\n }\n }\n }\n\n function carry(transactions: readonly Transaction[]): void {\n for (const transaction of transactions) {\n if (!transaction.docChanged) continue\n for (const [id, position] of deletedAt) deletedAt.set(id, transaction.mapping.map(position, 1))\n for (const [id, anchor] of anchors) {\n const next = mapAnchor(anchor, transaction)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind === 'orphan' && anchor.kind !== 'orphan') {\n deletedAt.set(id, transaction.mapping.map(anchor.kind === 'range' ? anchor.from : anchor.position, 1))\n }\n markDirty(id)\n }\n }\n }\n }\n\n // -- Snapshot -----------------------------------------------------------------------------------------------------------\n\n function rangesOf(editor: Editor | undefined): ReadonlyMap<string, DocRange> {\n if (!editor || unresolved) return NO_RANGES\n const size = editor.state.doc.content.size\n const ranges = new Map<string, DocRange>()\n for (const thread of threads) {\n const range = rangeOf(anchors.get(thread.annotationId))\n if (range && range.from <= size) ranges.set(thread.id, { from: range.from, to: Math.min(range.to, size) })\n }\n return ranges\n }\n\n /**\n * The snapshot's `orphanAt`, keyed by thread id: where each open thread without a text range was, read from `deletedAt`\n * (keyed by annotation id). Empty when there is no tracked deletion point; threads loaded already orphaned never have one\n * (the server stores none) and are left out, so they sit after the placed ones in the margin.\n */\n function orphanPositions(editor: Editor | undefined, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n if (!editor || unresolved || deletedAt.size === 0) return NO_POSITIONS\n const size = editor.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.status === 'resolved' || ranges.has(thread.id)) continue\n const position = deletedAt.get(thread.annotationId)\n if (position !== undefined && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n function recompute(): void {\n const editor = current()\n const previous = store.get()\n const user = userNow()\n const available = user !== null\n const canComment = available && ready && canCommentNow()\n const views = threads.map(thread => viewOf(thread, anchors.get(thread.annotationId)))\n const nextRanges = rangesOf(editor)\n const ranges = sameRanges(nextRanges, previous.ranges) ? previous.ranges : nextRanges\n const nextOrphanAt = orphanPositions(editor, ranges)\n const orphanAt = sameOrphanAt(nextOrphanAt, previous.orphanAt) ? previous.orphanAt : nextOrphanAt\n const plugin = editor && commentsKey.getState(editor.state)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = editor && anchor ? resolveRange(editor.state, anchor) : null\n const draftQuote = editor && draft ? editor.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const sameViews = (views.length === previous.threads.length && views.every((view, index) => sameJson(view, previous.threads[index])))\n const threadsOut = sameViews ? previous.threads : views\n\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && threadsOut === previous.threads\n && ranges === previous.ranges && orphanAt === previous.orphanAt && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = threadsOut === previous.threads && ranges === previous.ranges\n store.set({\n available,\n canComment,\n user,\n threads: threadsOut,\n ranges,\n orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open: kept\n ? previous.open\n : threadsOut\n .filter(thread => !thread.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt)),\n resolved: threadsOut === previous.threads ? previous.resolved : threadsOut.filter(thread => thread.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt)),\n })\n }\n\n // -- Editor binding -----------------------------------------------------------------------------------------------------\n\n let bound: Editor | undefined\n\n function onTransaction({ transaction, appendedTransactions }: { transaction: Transaction, appendedTransactions?: readonly Transaction[] }): void {\n const editor = bound\n if (editor && anchors.size) {\n const all = [transaction, ...(appendedTransactions ?? [])]\n if (all.some(tr => tr.docChanged)) {\n if (isBlank(editor.state.doc)) unresolved = true\n else if (unresolved) resolveAll(editor)\n else carry(all)\n }\n }\n recompute()\n }\n\n function onDestroy(): void {\n if (bound) gone.add(bound)\n unbind()\n recompute()\n }\n\n function unbind(): void {\n bound?.off('transaction', onTransaction)\n bound?.off('update', recompute)\n bound?.off('destroy', onDestroy)\n bound = undefined\n }\n\n function refresh(): void {\n const editor = current()\n if (editor !== bound) {\n unbind()\n bound = editor\n editor?.on('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` may follow it.\n editor?.on('update', recompute)\n editor?.on('destroy', onDestroy)\n if (editor && loaded) resolveAll(editor)\n }\n if (!loaded) {\n loaded = true\n enqueue(load, false)\n }\n recompute()\n }\n\n function dispose(): void {\n flush()\n }\n\n async function load(): Promise<void> {\n const discussion = await request()\n if (!isDiscussion(discussion)) throw new Error(`Comments request GET ${endpoint} did not answer a discussion.`)\n threads = discussion.threads\n anchors = new Map(discussion.annotations.map((annotation: Annotation) => [annotation.id, annotation.anchor]))\n persisted.clear()\n for (const [id, anchor] of anchors) persisted.set(id, JSON.stringify(anchor))\n dirty.clear()\n deletedAt.clear()\n ready = true\n const editor = current()\n unresolved = anchors.size > 0\n if (editor) resolveAll(editor)\n recompute()\n }\n\n function reload(): Promise<void> {\n flush()\n loaded = true\n enqueue(load, false)\n return idle()\n }\n\n async function idle(): Promise<void> {\n flush()\n const seen = chain\n await seen\n // A request that failed queued a reload, a finished one may have moved anchors.\n if (chain !== seen || timer !== undefined || dirty.size > 0) await idle()\n }\n\n // -- Mutations ----------------------------------------------------------------------------------------------------------\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function writer(): AuthorIdentity | null {\n const user = userNow()\n return user && ready && canCommentNow() ? user : null\n }\n\n const commentOf = (user: AuthorIdentity, body: string): Comment => ({ id: newId('cm'), body, authorId: user.id, authorName: user.name, authorColor: user.color, createdAt: new Date().toISOString() })\n\n function setThreads(next: readonly CommentThread[]): void {\n threads = next\n recompute()\n }\n\n const threadById = (id: string) => threads.find(thread => thread.id === id)\n\n function replaceThread(id: string, change: (thread: CommentThread) => CommentThread): void {\n setThreads(threads.map(thread => (thread.id === id ? change(thread) : thread)))\n }\n\n function add(text: string): string | null {\n const editor = current()\n const user = writer()\n const body = clean(text)\n const draft = editor && commentsKey.getState(editor.state)?.draft\n const range = editor && draft ? resolveRange(editor.state, draft) : null\n if (!editor || !user || !draft || !range || range.from >= range.to || !body) return null\n const threadId = newId('ct')\n const annotation: Annotation = { id: newId('ca'), threadId, anchor: captureAnchor(editor, range) }\n const comment = commentOf(user, body)\n editor.view.dispatch(editor.state.tr.setMeta(commentsKey, { draft: null, active: threadId }).setMeta('addToHistory', false))\n releaseRanges(editor, [draft])\n anchors.set(annotation.id, annotation.anchor)\n persisted.set(annotation.id, JSON.stringify(annotation.anchor))\n threads = [...threads, { id: threadId, annotationId: annotation.id, status: 'open', comments: [comment] }]\n recompute()\n send({ action: 'create', annotation, thread: { id: threadId, annotationId: annotation.id, initialComment: comment } })\n return threadId\n }\n\n function reply(threadId: string, text: string): void {\n const user = writer()\n const body = clean(text)\n if (!user || !body || !threadById(threadId)) return\n const comment = commentOf(user, body)\n replaceThread(threadId, thread => ({ ...thread, comments: [...thread.comments, comment] }))\n send({ action: 'reply', threadId, comment })\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const user = writer()\n const body = clean(text)\n const thread = threadById(threadId)\n const target = thread && (replyId === null ? thread.comments[0] : thread.comments.find(comment => comment.id === replyId))\n if (!user || !body || !thread || !target || target.authorId !== user.id || target.body === body) return\n const editedAt = new Date().toISOString()\n replaceThread(threadId, item => ({ ...item, comments: item.comments.map(comment => (comment.id === target.id ? { ...comment, body, editedAt } : comment)) }))\n send({ action: 'edit', threadId, commentId: target.id, body, editedAt })\n }\n\n const activeThread = () => {\n const editor = current()\n return editor ? commentsKey.getState(editor.state)?.active ?? null : null\n }\n\n function setResolved(threadId: string, resolved: boolean): void {\n const thread = threadById(threadId)\n if (!writer() || !thread || (thread.status === 'resolved') === resolved) return\n const status = resolved ? 'resolved' : 'open'\n replaceThread(threadId, item => ({ ...item, status }))\n if (resolved && activeThread() === threadId) activate(null)\n send({ action: 'status', threadId, status })\n }\n\n function remove(threadId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n if (!user || !thread || thread.comments[0]?.authorId !== user.id) return\n if (activeThread() === threadId) activate(null)\n anchors.delete(thread.annotationId)\n persisted.delete(thread.annotationId)\n dirty.delete(thread.annotationId)\n deletedAt.delete(thread.annotationId)\n setThreads(threads.filter(item => item.id !== threadId))\n send({ action: 'remove', threadId })\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n const target = thread?.comments.slice(1).find(comment => comment.id === replyId)\n if (!user || !thread || !target || target.authorId !== user.id) return\n replaceThread(threadId, item => ({ ...item, comments: item.comments.filter(comment => comment.id !== replyId) }))\n send({ action: 'remove-comment', threadId, commentId: replyId })\n }\n\n function activate(id: string | null, activateOptions: { reveal?: boolean } = {}): void {\n const editor = current()\n if (editor) setActiveComment(editor, id, activateOptions)\n }\n\n function startDraft(): void {\n const editor = current()\n if (editor && writer()) startCommentDraft(editor)\n }\n\n function cancelDraft(): void {\n const editor = current()\n if (editor) cancelCommentDraft(editor)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft, reload, idle }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/adapters/comments/client.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { Transaction } from '@tiptap/pm/state'\nimport { type AuthorIdentity, createStore, releaseRanges, resolveRange } from '../../core'\nimport { cancelCommentDraft, COMMENT_LIMITS, type CommentReply, type CommentsModel, type CommentsSnapshot, type CommentThread as EditorThread, commentsKey, type DocRange, setActiveComment, startCommentDraft } from '../../features/comments'\nimport { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from '../../features/comments/snapshot-equality'\nimport { captureAnchor, capturePoint, type PortableAnchor, resolveAnchor, mapAnchor } from '../anchors'\nimport type { Annotation, Comment, CommentAction, CommentThread, Discussion } from './types'\n\n// A `CommentsModel` for the editor's comments feature (`commentsFeature({ model })`) whose threads live behind a REST\n// endpoint (`handleCommentsRequest` serves it) instead of the document's Y.Doc. The text a thread hangs on is a\n// portable anchor (`../anchors`), not a `comment` mark: anchors are resolved against the document when the threads\n// load, follow every edit of the editor, and are written back to the endpoint when they moved.\n\n/** The part of a `Response` the model reads; `globalThis.fetch` fits. */\nexport interface CommentsFetchResponse {\n ok: boolean\n status: number\n json(): Promise<unknown>\n}\n\nexport type CommentsFetch = (url: string, init?: { method?: string, headers?: Record<string, string>, body?: string }) => Promise<CommentsFetchResponse>\n\nexport interface RestCommentsModelOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. */\n fetch: CommentsFetch\n /** The document the threads belong to. */\n documentId: string\n /** The document's comments endpoint (`GET` → `Discussion`, `POST` → `CommentAction`); default `/comments/<encoded documentId>`. */\n endpoint?: string\n /** Who writes: the author of new comments and the only one who may edit or delete them; `null` while nobody is signed in (no comments then). */\n user: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may compose and change comments now; readers of a read-only document may be allowed. */\n canComment: boolean | (() => boolean)\n /** The editor the threads belong to; may return no editor yet or a destroyed one. */\n getEditor: () => Editor | undefined\n /** Milliseconds a changed anchor waits before it is written back (typing moves many); default 500. `idle()` writes at once. */\n persistDelay?: number\n /** Called with every failed request; the model reloads the threads afterwards, so the local state falls back to the server's. */\n onError?: (error: Error) => void\n}\n\nexport interface RestCommentsModel extends CommentsModel {\n /** Fetches the threads again and resolves their anchors against the current document. */\n reload(): Promise<void>\n /** Resolves once every change made so far has been sent (and the threads loaded, if a load was requested). */\n idle(): Promise<void>\n}\n\n/** Neutral colour of authors that stored no colour. */\nconst FALLBACK_COLOR = '#6b7280'\n/** Characters of context that must agree for an orphan to be re-attached on load. */\nconst REATTACH_CONTEXT = 24\n\nconst newId = (prefix: string) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`\n/** The `error` message of a refused request's JSON body. */\nfunction errorOf(body: unknown): unknown {\n return typeof body === 'object' && body !== null && 'error' in body ? body.error : undefined\n}\n\nfunction isDiscussion(value: unknown): value is Discussion {\n return typeof value === 'object' && value !== null && 'annotations' in value && Array.isArray(value.annotations) && 'threads' in value && Array.isArray(value.threads)\n}\n\nconst sameJson = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)\n\nconst authorOf = (comment: Comment): EditorThread['author'] => ({ id: comment.authorId, name: comment.authorName ?? comment.authorId, color: comment.authorColor ?? FALLBACK_COLOR })\n\nfunction quoteOf(anchor: PortableAnchor | undefined): string {\n if (!anchor) return ''\n if (anchor.kind === 'range') return anchor.quote\n if (anchor.kind === 'orphan') return anchor.quote ?? (anchor.previous.kind === 'range' ? anchor.previous.quote : '')\n return ''\n}\n\nfunction viewOf(thread: CommentThread, anchor: PortableAnchor | undefined): EditorThread {\n const [first, ...replies] = thread.comments\n return {\n id: thread.id,\n author: first ? authorOf(first) : { id: '', name: '', color: FALLBACK_COLOR },\n text: first?.body ?? '',\n createdAt: first?.createdAt ?? '',\n ...(first?.editedAt !== undefined && { editedAt: first.editedAt }),\n quote: quoteOf(anchor),\n resolved: thread.status === 'resolved',\n replies: replies.map((reply): CommentReply => ({\n id: reply.id,\n author: authorOf(reply),\n text: reply.body,\n createdAt: reply.createdAt,\n ...(reply.editedAt !== undefined && { editedAt: reply.editedAt }),\n })),\n }\n}\n\nfunction rangeOf(anchor: PortableAnchor | undefined): DocRange | null {\n if (!anchor || anchor.kind === 'orphan') return null\n return anchor.kind === 'range' ? { from: anchor.from, to: anchor.to } : { from: anchor.position, to: anchor.position }\n}\n\n/** A document is \"empty\" while the host has not put its content in yet: nothing to resolve anchors against. */\nconst isBlank = (doc: { textContent: string }) => doc.textContent === ''\n\n/** Whether the text at `candidate` still reads like the context `previous` was written in (one side suffices). */\nfunction contextAgrees(candidate: PortableAnchor, previous: PortableAnchor): boolean {\n if (candidate.kind === 'orphan' || previous.kind === 'orphan') return false\n const before = previous.contextBefore?.slice(-REATTACH_CONTEXT)\n const after = previous.contextAfter?.slice(0, REATTACH_CONTEXT)\n if (!before && !after) return true\n return (!!before && !!candidate.contextBefore?.endsWith(before)) || (!!after && !!candidate.contextAfter?.startsWith(after))\n}\n\n/**\n * The comments of one editor over a REST endpoint, as a `CommentsModel` (the host passes `() => model` as the\n * `model` factory of `commentsFeature`/`commentsFeatureCore`; one instance serves the plugin and every hook).\n *\n * - **Load.** `refresh()` (called by the feature once the editor exists) fetches `GET endpoint`; each annotation's\n * anchor is resolved against the document (exact offsets, else quote and context search). An anchor that moved is\n * re-captured and written back; an anchor whose text is gone becomes an `orphan` (the thread stays, without a range)\n * and an orphan whose text is back with the same surroundings is re-attached. While the document is still empty the\n * resolving waits for its content.\n * - **Edits.** Every transaction carries all anchors with it (`mapAnchor`), so ranges, quotes and contexts follow the\n * text; changed anchors are written back after `persistDelay`. Text deleted under an anchor orphans it for good (it is\n * never silently re-attached to other text during editing; `reload()` re-attaches it when its text and context\n * are found again, e.g. after the content was replaced).\n * - **Writes** are applied locally at once and sent in order; a failed request is reported to `onError` and the threads\n * are reloaded. Without a `user` or while `canComment` is false nothing is written. Only the author may edit or\n * delete a comment.\n * - **Lifecycle.** The model follows the editor `getEditor` names until that editor is destroyed; `dispose()` writes\n * pending anchor changes and does not stop it (the feature and its hooks share the instance and dispose it on their\n * own schedule).\n */\nexport function createRestCommentsModel(options: RestCommentsModelOptions): RestCommentsModel {\n const { fetch, documentId, getEditor, onError } = options\n const endpoint = options.endpoint ?? `/comments/${encodeURIComponent(documentId)}`\n const persistDelay = options.persistDelay ?? 500\n const store = createStore<CommentsSnapshot>(EMPTY_SNAPSHOT)\n\n const userNow = () => (typeof options.user === 'function' ? options.user() : options.user)\n const canCommentNow = () => (typeof options.canComment === 'function' ? options.canComment() : options.canComment)\n /** Editors that announced their destruction: their view is still alive while `destroy` is emitted. */\n const gone = new WeakSet<Editor>()\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed && !gone.has(instance) ? instance : undefined\n }\n\n // -- Data: what the endpoint holds (annotation anchors are the local truth until written back) -----------------------\n let threads: readonly CommentThread[] = []\n let anchors = new Map<string, PortableAnchor>()\n /** The anchor each annotation has on the server, serialised. */\n const persisted = new Map<string, string>()\n const dirty = new Set<string>()\n /**\n * Where the text of an annotation that became an orphan during this session was: the deletion point, mapped through\n * every later edit. Keyed by annotation id (`orphanAt` in the snapshot is keyed by thread id; `orphanPositions` translates).\n * Deletion points are session-local: `load()` clears them, and an anchor that arrives already orphaned has none, because\n * the server stores no deletion point. Such threads get no `orphanAt` entry and sit after the placed ones in the margin.\n */\n const deletedAt = new Map<string, number>()\n /** Anchors wait for the document content (resolve on load) before they get a range. */\n let unresolved = false\n /** A load was requested / the first one succeeded: writes wait for it, so a load never overwrites a newer local change. */\n let loaded = false\n let ready = false\n\n // -- Requests: one queue, so a reply never overtakes its thread's creation ---------------------------------------------\n let chain: Promise<void> = Promise.resolve()\n let timer: ReturnType<typeof setTimeout> | undefined\n\n function enqueue(task: () => Promise<void>, reloadOnError = true): void {\n chain = chain.then(task).catch((error: unknown) => {\n onError?.(error instanceof Error ? error : new Error(String(error)))\n if (reloadOnError) enqueue(load, false)\n })\n }\n\n async function request(init?: { method: string, body: CommentAction }): Promise<unknown> {\n const response = await fetch(endpoint, init && { method: init.method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(init.body) })\n if (!response.ok) {\n const detail = await response.json().then(errorOf, () => undefined)\n throw new Error(`@ecsplico/editor: comments request ${init?.method ?? 'GET'} ${endpoint} failed: ${response.status}${typeof detail === 'string' ? ` ${detail}` : ''}`)\n }\n return response.json()\n }\n\n const send = (body: CommentAction) => enqueue(async () => void await request({ method: 'POST', body }))\n\n function writeAnchors(): void {\n timer = undefined\n if (!userNow() || !ready || !canCommentNow()) {\n dirty.clear()\n return\n }\n for (const id of dirty) {\n const anchor = anchors.get(id)\n const threadId = threads.find(thread => thread.annotationId === id)?.id\n if (!anchor || !threadId || persisted.get(id) === JSON.stringify(anchor)) continue\n persisted.set(id, JSON.stringify(anchor))\n send({ action: 'update-annotation', annotation: { id, threadId, anchor } })\n }\n dirty.clear()\n }\n\n function markDirty(id: string): void {\n dirty.add(id)\n if (timer === undefined) timer = setTimeout(writeAnchors, persistDelay)\n }\n\n function flush(): void {\n clearTimeout(timer)\n writeAnchors()\n }\n\n // -- Anchors ------------------------------------------------------------------------------------------------------------\n\n /** `anchor` read against `editor`'s document: the moved/re-captured anchor, an orphan, or the same object. */\n function resolveOne(editor: Editor, anchor: PortableAnchor): PortableAnchor {\n const live = anchor.kind === 'orphan' ? anchor.previous : anchor\n const found = resolveAnchor(editor.state.doc, live)\n if (!found) {\n return anchor.kind === 'orphan'\n ? anchor\n : { kind: 'orphan', previous: anchor, quote: quoteOf(anchor) || undefined, contextBefore: anchor.contextBefore, contextAfter: anchor.contextAfter, reason: 'unresolvable' }\n }\n const next = live.kind === 'range' ? captureAnchor(editor, found) : capturePoint(editor, found.from)\n if (anchor.kind === 'orphan' && !contextAgrees(next, live)) return anchor\n return sameJson(next, anchor) ? anchor : next\n }\n\n function resolveAll(editor: Editor): void {\n if (isBlank(editor.state.doc)) {\n unresolved = anchors.size > 0\n return\n }\n unresolved = false\n for (const [id, anchor] of anchors) {\n const next = resolveOne(editor, anchor)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind !== 'orphan') deletedAt.delete(id)\n markDirty(id)\n }\n }\n }\n\n function carry(transactions: readonly Transaction[]): void {\n for (const transaction of transactions) {\n if (!transaction.docChanged) continue\n for (const [id, position] of deletedAt) deletedAt.set(id, transaction.mapping.map(position, 1))\n for (const [id, anchor] of anchors) {\n const next = mapAnchor(anchor, transaction)\n if (next !== anchor) {\n anchors.set(id, next)\n if (next.kind === 'orphan' && anchor.kind !== 'orphan') {\n deletedAt.set(id, transaction.mapping.map(anchor.kind === 'range' ? anchor.from : anchor.position, 1))\n }\n markDirty(id)\n }\n }\n }\n }\n\n // -- Snapshot -----------------------------------------------------------------------------------------------------------\n\n function rangesOf(editor: Editor | undefined): ReadonlyMap<string, DocRange> {\n if (!editor || unresolved) return NO_RANGES\n const size = editor.state.doc.content.size\n const ranges = new Map<string, DocRange>()\n for (const thread of threads) {\n const range = rangeOf(anchors.get(thread.annotationId))\n if (range && range.from <= size) ranges.set(thread.id, { from: range.from, to: Math.min(range.to, size) })\n }\n return ranges\n }\n\n /**\n * The snapshot's `orphanAt`, keyed by thread id: where each open thread without a text range was, read from `deletedAt`\n * (keyed by annotation id). Empty when there is no tracked deletion point; threads loaded already orphaned never have one\n * (the server stores none) and are left out, so they sit after the placed ones in the margin.\n */\n function orphanPositions(editor: Editor | undefined, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n if (!editor || unresolved || deletedAt.size === 0) return NO_POSITIONS\n const size = editor.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.status === 'resolved' || ranges.has(thread.id)) continue\n const position = deletedAt.get(thread.annotationId)\n if (position !== undefined && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n function recompute(): void {\n const editor = current()\n const previous = store.get()\n const user = userNow()\n const available = user !== null\n const canComment = available && ready && canCommentNow()\n const views = threads.map(thread => viewOf(thread, anchors.get(thread.annotationId)))\n const nextRanges = rangesOf(editor)\n const ranges = sameRanges(nextRanges, previous.ranges) ? previous.ranges : nextRanges\n const nextOrphanAt = orphanPositions(editor, ranges)\n const orphanAt = sameOrphanAt(nextOrphanAt, previous.orphanAt) ? previous.orphanAt : nextOrphanAt\n const plugin = editor && commentsKey.getState(editor.state)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = editor && anchor ? resolveRange(editor.state, anchor) : null\n const draftQuote = editor && draft ? editor.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const sameViews = (views.length === previous.threads.length && views.every((view, index) => sameJson(view, previous.threads[index])))\n const threadsOut = sameViews ? previous.threads : views\n\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && threadsOut === previous.threads\n && ranges === previous.ranges && orphanAt === previous.orphanAt && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = threadsOut === previous.threads && ranges === previous.ranges\n store.set({\n available,\n canComment,\n user,\n threads: threadsOut,\n ranges,\n orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open: kept\n ? previous.open\n : threadsOut\n .filter(thread => !thread.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt)),\n resolved: threadsOut === previous.threads ? previous.resolved : threadsOut.filter(thread => thread.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt)),\n })\n }\n\n // -- Editor binding -----------------------------------------------------------------------------------------------------\n\n let bound: Editor | undefined\n\n function onTransaction({ transaction, appendedTransactions }: { transaction: Transaction, appendedTransactions?: readonly Transaction[] }): void {\n const editor = bound\n if (editor && anchors.size) {\n const all = [transaction, ...(appendedTransactions ?? [])]\n if (all.some(tr => tr.docChanged)) {\n if (isBlank(editor.state.doc)) unresolved = true\n else if (unresolved) resolveAll(editor)\n else carry(all)\n }\n }\n recompute()\n }\n\n function onDestroy(): void {\n if (bound) gone.add(bound)\n unbind()\n recompute()\n }\n\n function unbind(): void {\n bound?.off('transaction', onTransaction)\n bound?.off('update', recompute)\n bound?.off('destroy', onDestroy)\n bound = undefined\n }\n\n function refresh(): void {\n const editor = current()\n if (editor !== bound) {\n unbind()\n bound = editor\n editor?.on('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` may follow it.\n editor?.on('update', recompute)\n editor?.on('destroy', onDestroy)\n if (editor && loaded) resolveAll(editor)\n }\n if (!loaded) {\n loaded = true\n enqueue(load, false)\n }\n recompute()\n }\n\n function dispose(): void {\n flush()\n }\n\n async function load(): Promise<void> {\n const discussion = await request()\n if (!isDiscussion(discussion)) throw new Error(`@ecsplico/editor: comments request GET ${endpoint} did not answer a discussion`)\n threads = discussion.threads\n anchors = new Map(discussion.annotations.map((annotation: Annotation) => [annotation.id, annotation.anchor]))\n persisted.clear()\n for (const [id, anchor] of anchors) persisted.set(id, JSON.stringify(anchor))\n dirty.clear()\n deletedAt.clear()\n ready = true\n const editor = current()\n unresolved = anchors.size > 0\n if (editor) resolveAll(editor)\n recompute()\n }\n\n function reload(): Promise<void> {\n flush()\n loaded = true\n enqueue(load, false)\n return idle()\n }\n\n async function idle(): Promise<void> {\n flush()\n const seen = chain\n await seen\n // A request that failed queued a reload, a finished one may have moved anchors.\n if (chain !== seen || timer !== undefined || dirty.size > 0) await idle()\n }\n\n // -- Mutations ----------------------------------------------------------------------------------------------------------\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function writer(): AuthorIdentity | null {\n const user = userNow()\n return user && ready && canCommentNow() ? user : null\n }\n\n const commentOf = (user: AuthorIdentity, body: string): Comment => ({ id: newId('cm'), body, authorId: user.id, authorName: user.name, authorColor: user.color, createdAt: new Date().toISOString() })\n\n function setThreads(next: readonly CommentThread[]): void {\n threads = next\n recompute()\n }\n\n const threadById = (id: string) => threads.find(thread => thread.id === id)\n\n function replaceThread(id: string, change: (thread: CommentThread) => CommentThread): void {\n setThreads(threads.map(thread => (thread.id === id ? change(thread) : thread)))\n }\n\n function add(text: string): string | null {\n const editor = current()\n const user = writer()\n const body = clean(text)\n const draft = editor && commentsKey.getState(editor.state)?.draft\n const range = editor && draft ? resolveRange(editor.state, draft) : null\n if (!editor || !user || !draft || !range || range.from >= range.to || !body) return null\n const threadId = newId('ct')\n const annotation: Annotation = { id: newId('ca'), threadId, anchor: captureAnchor(editor, range) }\n const comment = commentOf(user, body)\n editor.view.dispatch(editor.state.tr.setMeta(commentsKey, { draft: null, active: threadId }).setMeta('addToHistory', false))\n releaseRanges(editor, [draft])\n anchors.set(annotation.id, annotation.anchor)\n persisted.set(annotation.id, JSON.stringify(annotation.anchor))\n threads = [...threads, { id: threadId, annotationId: annotation.id, status: 'open', comments: [comment] }]\n recompute()\n send({ action: 'create', annotation, thread: { id: threadId, annotationId: annotation.id, initialComment: comment } })\n return threadId\n }\n\n function reply(threadId: string, text: string): void {\n const user = writer()\n const body = clean(text)\n if (!user || !body || !threadById(threadId)) return\n const comment = commentOf(user, body)\n replaceThread(threadId, thread => ({ ...thread, comments: [...thread.comments, comment] }))\n send({ action: 'reply', threadId, comment })\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const user = writer()\n const body = clean(text)\n const thread = threadById(threadId)\n const target = thread && (replyId === null ? thread.comments[0] : thread.comments.find(comment => comment.id === replyId))\n if (!user || !body || !thread || !target || target.authorId !== user.id || target.body === body) return\n const editedAt = new Date().toISOString()\n replaceThread(threadId, item => ({ ...item, comments: item.comments.map(comment => (comment.id === target.id ? { ...comment, body, editedAt } : comment)) }))\n send({ action: 'edit', threadId, commentId: target.id, body, editedAt })\n }\n\n const activeThread = () => {\n const editor = current()\n return editor ? commentsKey.getState(editor.state)?.active ?? null : null\n }\n\n function setResolved(threadId: string, resolved: boolean): void {\n const thread = threadById(threadId)\n if (!writer() || !thread || (thread.status === 'resolved') === resolved) return\n const status = resolved ? 'resolved' : 'open'\n replaceThread(threadId, item => ({ ...item, status }))\n if (resolved && activeThread() === threadId) activate(null)\n send({ action: 'status', threadId, status })\n }\n\n function remove(threadId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n if (!user || !thread || thread.comments[0]?.authorId !== user.id) return\n if (activeThread() === threadId) activate(null)\n anchors.delete(thread.annotationId)\n persisted.delete(thread.annotationId)\n dirty.delete(thread.annotationId)\n deletedAt.delete(thread.annotationId)\n setThreads(threads.filter(item => item.id !== threadId))\n send({ action: 'remove', threadId })\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const user = writer()\n const thread = threadById(threadId)\n const target = thread?.comments.slice(1).find(comment => comment.id === replyId)\n if (!user || !thread || !target || target.authorId !== user.id) return\n replaceThread(threadId, item => ({ ...item, comments: item.comments.filter(comment => comment.id !== replyId) }))\n send({ action: 'remove-comment', threadId, commentId: replyId })\n }\n\n function activate(id: string | null, activateOptions: { reveal?: boolean } = {}): void {\n const editor = current()\n if (editor) setActiveComment(editor, id, activateOptions)\n }\n\n function startDraft(): void {\n const editor = current()\n if (editor && writer()) startCommentDraft(editor)\n }\n\n function cancelDraft(): void {\n const editor = current()\n if (editor) cancelCommentDraft(editor)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft, reload, idle }\n}\n"],"mappings":"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"}
@@ -96,7 +96,7 @@ function createDrizzleCommentStore({ db, tables }) {
96
96
  }
97
97
  async function requireThread(documentId, id) {
98
98
  const thread = await threadOf(documentId, id);
99
- if (!thread) throw notFound("comment thread", id);
99
+ if (!thread) throw notFound("Kommentar-Thread", id);
100
100
  return thread;
101
101
  }
102
102
  async function discussion(documentId) {
@@ -146,8 +146,8 @@ function createDrizzleCommentStore({ db, tables }) {
146
146
  createThread: (documentId, input) => write(async () => {
147
147
  assertNewThread(input);
148
148
  const { annotation, thread } = input;
149
- if (await db.select({ id: annotations.id }).from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()) throw conflict(`Annotation "${annotation.id}" already exists.`);
150
- if (await threadRow(documentId, thread.id)) throw conflict(`Comment thread "${thread.id}" already exists.`);
149
+ if (await db.select({ id: annotations.id }).from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()) throw conflict(`Annotation „${annotation.id}“ existiert bereits`);
150
+ if (await threadRow(documentId, thread.id)) throw conflict(`Kommentar-Thread „${thread.id}“ existiert bereits`);
151
151
  try {
152
152
  await db.insert(annotations).values({
153
153
  documentId,
@@ -177,12 +177,12 @@ function createDrizzleCommentStore({ db, tables }) {
177
177
  }),
178
178
  addComment: (documentId, threadId, comment) => write(async () => {
179
179
  await requireThread(documentId, threadId);
180
- if (await db.select({ id: comments.id }).from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, comment.id))).get()) throw conflict(`Comment "${comment.id}" already exists in thread "${threadId}".`);
180
+ if (await db.select({ id: comments.id }).from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, comment.id))).get()) throw conflict(`Kommentar „${comment.id}“ existiert bereits im Thread „${threadId}“`);
181
181
  await insertComment(documentId, threadId, comment);
182
182
  return requireThread(documentId, threadId);
183
183
  }),
184
184
  editComment: (documentId, threadId, commentId, body, editedAt) => write(async () => {
185
- if (!(await requireThread(documentId, threadId)).comments.some((comment) => comment.id === commentId)) throw notFound("comment", commentId);
185
+ if (!(await requireThread(documentId, threadId)).comments.some((comment) => comment.id === commentId)) throw notFound("Kommentar", commentId);
186
186
  await db.update(comments).set({
187
187
  body,
188
188
  editedAt
@@ -191,8 +191,8 @@ function createDrizzleCommentStore({ db, tables }) {
191
191
  }),
192
192
  removeComment: (documentId, threadId, commentId) => write(async () => {
193
193
  const index = (await requireThread(documentId, threadId)).comments.findIndex((comment) => comment.id === commentId);
194
- if (index < 0) throw notFound("comment", commentId);
195
- if (index === 0) throw invalid("The first comment of a thread cannot be removed on its own; remove the thread.");
194
+ if (index < 0) throw notFound("Kommentar", commentId);
195
+ if (index === 0) throw invalid("Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen");
196
196
  await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run();
197
197
  return requireThread(documentId, threadId);
198
198
  }),
@@ -203,8 +203,8 @@ function createDrizzleCommentStore({ db, tables }) {
203
203
  }),
204
204
  updateAnchor: (documentId, annotation) => write(async () => {
205
205
  const row = await db.select().from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get();
206
- if (!row) throw notFound("annotation", annotation.id);
207
- if (row.threadId !== annotation.threadId) throw invalid("An annotation cannot move to another thread.");
206
+ if (!row) throw notFound("Annotation", annotation.id);
207
+ if (row.threadId !== annotation.threadId) throw invalid("Eine Annotation kann nicht in einen anderen Thread wechseln");
208
208
  await db.update(annotations).set({ anchor: annotation.anchor }).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run();
209
209
  return structuredClone(annotation);
210
210
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle.js","names":[],"sources":["../../../src/adapters/comments/drizzle.ts"],"sourcesContent":["import { and, eq, type SQL, sql } from 'drizzle-orm'\nimport { integer, primaryKey, type SQLiteColumn, type SQLiteTable, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type { PortableAnchor } from '../anchors'\nimport type { EditorDb } from '../drizzle'\nimport { conflict, invalid, notFound } from './errors'\nimport { assertNewThread } from './record'\nimport type { Annotation, Comment, CommentStore, CommentThread, Discussion } from './types'\n\nconst COMMENT_TABLES = {\n annotations: sqliteTable('editor_comment_annotations', {\n documentId: text('document_id').notNull(),\n id: text('id').notNull(),\n threadId: text('thread_id').notNull(),\n seq: integer('seq').notNull(),\n anchor: text('anchor', { mode: 'json' }).$type<PortableAnchor>().notNull(),\n }, table => [primaryKey({ columns: [table.documentId, table.id] })]),\n threads: sqliteTable('editor_comment_threads', {\n documentId: text('document_id').notNull(),\n id: text('id').notNull(),\n annotationId: text('annotation_id').notNull(),\n status: text('status', { enum: ['open', 'resolved'] }).notNull(),\n seq: integer('seq').notNull(),\n }, table => [primaryKey({ columns: [table.documentId, table.id] })]),\n comments: sqliteTable('editor_comments', {\n documentId: text('document_id').notNull(),\n threadId: text('thread_id').notNull(),\n id: text('id').notNull(),\n seq: integer('seq').notNull(),\n body: text('body').notNull(),\n authorId: text('author_id').notNull(),\n authorName: text('author_name'),\n authorColor: text('author_color'),\n createdAt: text('created_at').notNull(),\n editedAt: text('edited_at'),\n }, table => [primaryKey({ columns: [table.documentId, table.threadId, table.id] })]),\n}\n\nexport type CommentTables = typeof COMMENT_TABLES\n\n/**\n * The tables of the comment store in a SQLite database:\n *\n * editor_comment_annotations(document_id, id, thread_id, seq, anchor json; PK (document_id, id))\n * editor_comment_threads(document_id, id, annotation_id, status, seq; PK (document_id, id))\n * editor_comments(document_id, thread_id, id, seq, body, author_id, author_name?, author_color?, created_at, edited_at?; PK (document_id, thread_id, id))\n *\n * `seq` orders rows within their document (annotations, threads) or thread (comments). Call it from the host's drizzle\n * schema (so `drizzle-kit` migrates the tables) or use `bootstrapCommentTables` in a host without migrations.\n */\nexport const commentTables = (): CommentTables => COMMENT_TABLES\n\n/** `CREATE TABLE IF NOT EXISTS` for the tables of `commentTables()`. */\nexport async function bootstrapCommentTables(db: EditorDb): Promise<void> {\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comment_annotations (document_id text NOT NULL, id text NOT NULL, thread_id text NOT NULL, seq integer NOT NULL, anchor text NOT NULL, PRIMARY KEY (document_id, id))`)\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comment_threads (document_id text NOT NULL, id text NOT NULL, annotation_id text NOT NULL, status text NOT NULL, seq integer NOT NULL, PRIMARY KEY (document_id, id))`)\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comments (document_id text NOT NULL, thread_id text NOT NULL, id text NOT NULL, seq integer NOT NULL, body text NOT NULL, author_id text NOT NULL, author_name text, author_color text, created_at text NOT NULL, edited_at text, PRIMARY KEY (document_id, thread_id, id))`)\n}\n\nexport interface DrizzleCommentStoreOptions {\n db: EditorDb\n tables: CommentTables\n}\n\n/** Writers on one database of one process run one after another: SQLite has one writer, and each operation is several statements. */\nconst queues = new WeakMap<object, Promise<unknown>>()\n\nfunction serialized<T>(db: object, action: () => Promise<T>): Promise<T> {\n const run = (queues.get(db) ?? Promise.resolve()).then(action)\n const tail = run.catch(() => {})\n queues.set(db, tail)\n void tail.then(() => {\n if (queues.get(db) === tail) queues.delete(db)\n })\n return run\n}\n\n/**\n * The comment store on drizzle tables. Operations are several single statements (no transaction, so it runs on sync and\n * async drivers) applied one after another per database; a failed `createThread` removes what it had written. A\n * unique key still protects against a second process writing the same ids. Unlike the JSON store it keeps only the\n * fields of `Comment`, `Annotation` and `CommentThread`.\n */\nexport function createDrizzleCommentStore({ db, tables }: DrizzleCommentStoreOptions): CommentStore {\n const { annotations, threads, comments } = tables\n type CommentRow = typeof comments.$inferSelect\n\n const commentOf = (row: CommentRow): Comment => ({\n id: row.id,\n body: row.body,\n authorId: row.authorId,\n createdAt: row.createdAt,\n ...(row.authorName !== null && { authorName: row.authorName }),\n ...(row.authorColor !== null && { authorColor: row.authorColor }),\n ...(row.editedAt !== null && { editedAt: row.editedAt }),\n })\n\n async function threadRow(documentId: string, id: string) {\n return db.select().from(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, id))).get()\n }\n\n async function threadOf(documentId: string, id: string): Promise<CommentThread | undefined> {\n const row = await threadRow(documentId, id)\n if (!row) return undefined\n const rows = await db.select().from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, id))).orderBy(comments.seq).all()\n return { id: row.id, annotationId: row.annotationId, status: row.status, comments: rows.map(commentOf) }\n }\n\n async function requireThread(documentId: string, id: string): Promise<CommentThread> {\n const thread = await threadOf(documentId, id)\n if (!thread) throw notFound('comment thread', id)\n return thread\n }\n\n async function discussion(documentId: string): Promise<Discussion> {\n const [annotationRows, threadRows, commentRows] = await Promise.all([\n db.select().from(annotations).where(eq(annotations.documentId, documentId)).orderBy(annotations.seq).all(),\n db.select().from(threads).where(eq(threads.documentId, documentId)).orderBy(threads.seq).all(),\n db.select().from(comments).where(eq(comments.documentId, documentId)).orderBy(comments.seq).all(),\n ])\n return {\n annotations: annotationRows.map(row => ({ id: row.id, threadId: row.threadId, anchor: row.anchor })),\n threads: threadRows.map(row => ({\n id: row.id,\n annotationId: row.annotationId,\n status: row.status,\n comments: commentRows.filter(comment => comment.threadId === row.id).map(commentOf),\n })),\n }\n }\n\n const nextSeq = async (column: SQLiteColumn, table: SQLiteTable, where: SQL | undefined): Promise<number> => {\n const row = await db.select({ value: sql<number>`coalesce(max(${column}), 0)` }).from(table).where(where).get()\n return (row?.value ?? 0) + 1\n }\n\n const insertComment = async (documentId: string, threadId: string, comment: Comment) => {\n await db.insert(comments).values({\n documentId,\n threadId,\n id: comment.id,\n seq: await nextSeq(comments.seq, comments, and(eq(comments.documentId, documentId), eq(comments.threadId, threadId))),\n body: comment.body,\n authorId: comment.authorId,\n authorName: comment.authorName ?? null,\n authorColor: comment.authorColor ?? null,\n createdAt: comment.createdAt,\n editedAt: comment.editedAt ?? null,\n }).run()\n }\n\n const write = <T>(action: () => Promise<T>) => serialized(db, action)\n\n return {\n getDiscussion: discussion,\n getThread: (documentId, threadId) => threadOf(documentId, threadId),\n async listThreads(documentId) {\n return (await discussion(documentId)).threads\n },\n\n createThread: (documentId, input) => write(async () => {\n assertNewThread(input)\n const { annotation, thread } = input\n if (await db.select({ id: annotations.id }).from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()) throw conflict(`Annotation \"${annotation.id}\" already exists.`)\n if (await threadRow(documentId, thread.id)) throw conflict(`Comment thread \"${thread.id}\" already exists.`)\n try {\n await db.insert(annotations).values({\n documentId,\n id: annotation.id,\n threadId: annotation.threadId,\n seq: await nextSeq(annotations.seq, annotations, eq(annotations.documentId, documentId)),\n anchor: annotation.anchor,\n }).run()\n await db.insert(threads).values({\n documentId,\n id: thread.id,\n annotationId: thread.annotationId,\n status: 'open',\n seq: await nextSeq(threads.seq, threads, eq(threads.documentId, documentId)),\n }).run()\n await insertComment(documentId, thread.id, thread.initialComment)\n }\n catch (error) {\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, thread.id))).run()\n await db.delete(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, thread.id))).run()\n await db.delete(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run()\n throw error\n }\n return { annotation: structuredClone(annotation), thread: await requireThread(documentId, thread.id) }\n }),\n\n addComment: (documentId, threadId, comment) => write(async () => {\n await requireThread(documentId, threadId)\n if (await db.select({ id: comments.id }).from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, comment.id))).get()) {\n throw conflict(`Comment \"${comment.id}\" already exists in thread \"${threadId}\".`)\n }\n await insertComment(documentId, threadId, comment)\n return requireThread(documentId, threadId)\n }),\n\n editComment: (documentId, threadId, commentId, body, editedAt) => write(async () => {\n const thread = await requireThread(documentId, threadId)\n if (!thread.comments.some(comment => comment.id === commentId)) throw notFound('comment', commentId)\n await db.update(comments).set({ body, editedAt }).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run()\n return requireThread(documentId, threadId)\n }),\n\n removeComment: (documentId, threadId, commentId) => write(async () => {\n const thread = await requireThread(documentId, threadId)\n const index = thread.comments.findIndex(comment => comment.id === commentId)\n if (index < 0) throw notFound('comment', commentId)\n if (index === 0) throw invalid('The first comment of a thread cannot be removed on its own; remove the thread.')\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run()\n return requireThread(documentId, threadId)\n }),\n\n setThreadStatus: (documentId, threadId, status) => write(async () => {\n await requireThread(documentId, threadId)\n await db.update(threads).set({ status }).where(and(eq(threads.documentId, documentId), eq(threads.id, threadId))).run()\n return requireThread(documentId, threadId)\n }),\n\n updateAnchor: (documentId, annotation: Annotation) => write(async () => {\n const row = await db.select().from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()\n if (!row) throw notFound('annotation', annotation.id)\n if (row.threadId !== annotation.threadId) throw invalid('An annotation cannot move to another thread.')\n await db.update(annotations).set({ anchor: annotation.anchor }).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run()\n return structuredClone(annotation)\n }),\n\n removeThread: (documentId, threadId) => write(async () => {\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId))).run()\n await db.delete(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, threadId))).run()\n await db.delete(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.threadId, threadId))).run()\n }),\n\n removeDocuments: prefix => write(async () => {\n const under = (column: { name: string }) => sql`substr(${sql.identifier(column.name)}, 1, ${prefix.length}) = ${prefix}`\n const documents = new Set<string>()\n for (const [table, column] of [[annotations, annotations.documentId], [threads, threads.documentId], [comments, comments.documentId]] as const) {\n for (const row of await db.selectDistinct({ documentId: column }).from(table).where(under(column)).all()) documents.add(row.documentId)\n }\n for (const [table, column] of [[comments, comments.documentId], [threads, threads.documentId], [annotations, annotations.documentId]] as const) {\n await db.delete(table).where(under(column)).run()\n }\n return documents.size\n }),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"drizzle.js","names":[],"sources":["../../../src/adapters/comments/drizzle.ts"],"sourcesContent":["import { and, eq, type SQL, sql } from 'drizzle-orm'\nimport { integer, primaryKey, type SQLiteColumn, type SQLiteTable, sqliteTable, text } from 'drizzle-orm/sqlite-core'\nimport type { PortableAnchor } from '../anchors'\nimport type { EditorDb } from '../drizzle'\nimport { conflict, invalid, notFound } from './errors'\nimport { assertNewThread } from './record'\nimport type { Annotation, Comment, CommentStore, CommentThread, Discussion } from './types'\n\nconst COMMENT_TABLES = {\n annotations: sqliteTable('editor_comment_annotations', {\n documentId: text('document_id').notNull(),\n id: text('id').notNull(),\n threadId: text('thread_id').notNull(),\n seq: integer('seq').notNull(),\n anchor: text('anchor', { mode: 'json' }).$type<PortableAnchor>().notNull(),\n }, table => [primaryKey({ columns: [table.documentId, table.id] })]),\n threads: sqliteTable('editor_comment_threads', {\n documentId: text('document_id').notNull(),\n id: text('id').notNull(),\n annotationId: text('annotation_id').notNull(),\n status: text('status', { enum: ['open', 'resolved'] }).notNull(),\n seq: integer('seq').notNull(),\n }, table => [primaryKey({ columns: [table.documentId, table.id] })]),\n comments: sqliteTable('editor_comments', {\n documentId: text('document_id').notNull(),\n threadId: text('thread_id').notNull(),\n id: text('id').notNull(),\n seq: integer('seq').notNull(),\n body: text('body').notNull(),\n authorId: text('author_id').notNull(),\n authorName: text('author_name'),\n authorColor: text('author_color'),\n createdAt: text('created_at').notNull(),\n editedAt: text('edited_at'),\n }, table => [primaryKey({ columns: [table.documentId, table.threadId, table.id] })]),\n}\n\nexport type CommentTables = typeof COMMENT_TABLES\n\n/**\n * The tables of the comment store in a SQLite database:\n *\n * editor_comment_annotations(document_id, id, thread_id, seq, anchor json; PK (document_id, id))\n * editor_comment_threads(document_id, id, annotation_id, status, seq; PK (document_id, id))\n * editor_comments(document_id, thread_id, id, seq, body, author_id, author_name?, author_color?, created_at, edited_at?; PK (document_id, thread_id, id))\n *\n * `seq` orders rows within their document (annotations, threads) or thread (comments). Call it from the host's drizzle\n * schema (so `drizzle-kit` migrates the tables) or use `bootstrapCommentTables` in a host without migrations.\n */\nexport const commentTables = (): CommentTables => COMMENT_TABLES\n\n/** `CREATE TABLE IF NOT EXISTS` for the tables of `commentTables()`. */\nexport async function bootstrapCommentTables(db: EditorDb): Promise<void> {\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comment_annotations (document_id text NOT NULL, id text NOT NULL, thread_id text NOT NULL, seq integer NOT NULL, anchor text NOT NULL, PRIMARY KEY (document_id, id))`)\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comment_threads (document_id text NOT NULL, id text NOT NULL, annotation_id text NOT NULL, status text NOT NULL, seq integer NOT NULL, PRIMARY KEY (document_id, id))`)\n await db.run(sql`CREATE TABLE IF NOT EXISTS editor_comments (document_id text NOT NULL, thread_id text NOT NULL, id text NOT NULL, seq integer NOT NULL, body text NOT NULL, author_id text NOT NULL, author_name text, author_color text, created_at text NOT NULL, edited_at text, PRIMARY KEY (document_id, thread_id, id))`)\n}\n\nexport interface DrizzleCommentStoreOptions {\n db: EditorDb\n tables: CommentTables\n}\n\n/** Writers on one database of one process run one after another: SQLite has one writer, and each operation is several statements. */\nconst queues = new WeakMap<object, Promise<unknown>>()\n\nfunction serialized<T>(db: object, action: () => Promise<T>): Promise<T> {\n const run = (queues.get(db) ?? Promise.resolve()).then(action)\n const tail = run.catch(() => {})\n queues.set(db, tail)\n void tail.then(() => {\n if (queues.get(db) === tail) queues.delete(db)\n })\n return run\n}\n\n/**\n * The comment store on drizzle tables. Operations are several single statements (no transaction, so it runs on sync and\n * async drivers) applied one after another per database; a failed `createThread` removes what it had written. A\n * unique key still protects against a second process writing the same ids. Unlike the JSON store it keeps only the\n * fields of `Comment`, `Annotation` and `CommentThread`.\n */\nexport function createDrizzleCommentStore({ db, tables }: DrizzleCommentStoreOptions): CommentStore {\n const { annotations, threads, comments } = tables\n type CommentRow = typeof comments.$inferSelect\n\n const commentOf = (row: CommentRow): Comment => ({\n id: row.id,\n body: row.body,\n authorId: row.authorId,\n createdAt: row.createdAt,\n ...(row.authorName !== null && { authorName: row.authorName }),\n ...(row.authorColor !== null && { authorColor: row.authorColor }),\n ...(row.editedAt !== null && { editedAt: row.editedAt }),\n })\n\n async function threadRow(documentId: string, id: string) {\n return db.select().from(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, id))).get()\n }\n\n async function threadOf(documentId: string, id: string): Promise<CommentThread | undefined> {\n const row = await threadRow(documentId, id)\n if (!row) return undefined\n const rows = await db.select().from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, id))).orderBy(comments.seq).all()\n return { id: row.id, annotationId: row.annotationId, status: row.status, comments: rows.map(commentOf) }\n }\n\n async function requireThread(documentId: string, id: string): Promise<CommentThread> {\n const thread = await threadOf(documentId, id)\n if (!thread) throw notFound('Kommentar-Thread', id)\n return thread\n }\n\n async function discussion(documentId: string): Promise<Discussion> {\n const [annotationRows, threadRows, commentRows] = await Promise.all([\n db.select().from(annotations).where(eq(annotations.documentId, documentId)).orderBy(annotations.seq).all(),\n db.select().from(threads).where(eq(threads.documentId, documentId)).orderBy(threads.seq).all(),\n db.select().from(comments).where(eq(comments.documentId, documentId)).orderBy(comments.seq).all(),\n ])\n return {\n annotations: annotationRows.map(row => ({ id: row.id, threadId: row.threadId, anchor: row.anchor })),\n threads: threadRows.map(row => ({\n id: row.id,\n annotationId: row.annotationId,\n status: row.status,\n comments: commentRows.filter(comment => comment.threadId === row.id).map(commentOf),\n })),\n }\n }\n\n const nextSeq = async (column: SQLiteColumn, table: SQLiteTable, where: SQL | undefined): Promise<number> => {\n const row = await db.select({ value: sql<number>`coalesce(max(${column}), 0)` }).from(table).where(where).get()\n return (row?.value ?? 0) + 1\n }\n\n const insertComment = async (documentId: string, threadId: string, comment: Comment) => {\n await db.insert(comments).values({\n documentId,\n threadId,\n id: comment.id,\n seq: await nextSeq(comments.seq, comments, and(eq(comments.documentId, documentId), eq(comments.threadId, threadId))),\n body: comment.body,\n authorId: comment.authorId,\n authorName: comment.authorName ?? null,\n authorColor: comment.authorColor ?? null,\n createdAt: comment.createdAt,\n editedAt: comment.editedAt ?? null,\n }).run()\n }\n\n const write = <T>(action: () => Promise<T>) => serialized(db, action)\n\n return {\n getDiscussion: discussion,\n getThread: (documentId, threadId) => threadOf(documentId, threadId),\n async listThreads(documentId) {\n return (await discussion(documentId)).threads\n },\n\n createThread: (documentId, input) => write(async () => {\n assertNewThread(input)\n const { annotation, thread } = input\n if (await db.select({ id: annotations.id }).from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()) throw conflict(`Annotation „${annotation.id}“ existiert bereits`)\n if (await threadRow(documentId, thread.id)) throw conflict(`Kommentar-Thread „${thread.id}“ existiert bereits`)\n try {\n await db.insert(annotations).values({\n documentId,\n id: annotation.id,\n threadId: annotation.threadId,\n seq: await nextSeq(annotations.seq, annotations, eq(annotations.documentId, documentId)),\n anchor: annotation.anchor,\n }).run()\n await db.insert(threads).values({\n documentId,\n id: thread.id,\n annotationId: thread.annotationId,\n status: 'open',\n seq: await nextSeq(threads.seq, threads, eq(threads.documentId, documentId)),\n }).run()\n await insertComment(documentId, thread.id, thread.initialComment)\n }\n catch (error) {\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, thread.id))).run()\n await db.delete(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, thread.id))).run()\n await db.delete(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run()\n throw error\n }\n return { annotation: structuredClone(annotation), thread: await requireThread(documentId, thread.id) }\n }),\n\n addComment: (documentId, threadId, comment) => write(async () => {\n await requireThread(documentId, threadId)\n if (await db.select({ id: comments.id }).from(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, comment.id))).get()) {\n throw conflict(`Kommentar „${comment.id}“ existiert bereits im Thread „${threadId}“`)\n }\n await insertComment(documentId, threadId, comment)\n return requireThread(documentId, threadId)\n }),\n\n editComment: (documentId, threadId, commentId, body, editedAt) => write(async () => {\n const thread = await requireThread(documentId, threadId)\n if (!thread.comments.some(comment => comment.id === commentId)) throw notFound('Kommentar', commentId)\n await db.update(comments).set({ body, editedAt }).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run()\n return requireThread(documentId, threadId)\n }),\n\n removeComment: (documentId, threadId, commentId) => write(async () => {\n const thread = await requireThread(documentId, threadId)\n const index = thread.comments.findIndex(comment => comment.id === commentId)\n if (index < 0) throw notFound('Kommentar', commentId)\n if (index === 0) throw invalid('Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen')\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId), eq(comments.id, commentId))).run()\n return requireThread(documentId, threadId)\n }),\n\n setThreadStatus: (documentId, threadId, status) => write(async () => {\n await requireThread(documentId, threadId)\n await db.update(threads).set({ status }).where(and(eq(threads.documentId, documentId), eq(threads.id, threadId))).run()\n return requireThread(documentId, threadId)\n }),\n\n updateAnchor: (documentId, annotation: Annotation) => write(async () => {\n const row = await db.select().from(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).get()\n if (!row) throw notFound('Annotation', annotation.id)\n if (row.threadId !== annotation.threadId) throw invalid('Eine Annotation kann nicht in einen anderen Thread wechseln')\n await db.update(annotations).set({ anchor: annotation.anchor }).where(and(eq(annotations.documentId, documentId), eq(annotations.id, annotation.id))).run()\n return structuredClone(annotation)\n }),\n\n removeThread: (documentId, threadId) => write(async () => {\n await db.delete(comments).where(and(eq(comments.documentId, documentId), eq(comments.threadId, threadId))).run()\n await db.delete(threads).where(and(eq(threads.documentId, documentId), eq(threads.id, threadId))).run()\n await db.delete(annotations).where(and(eq(annotations.documentId, documentId), eq(annotations.threadId, threadId))).run()\n }),\n\n removeDocuments: prefix => write(async () => {\n const under = (column: { name: string }) => sql`substr(${sql.identifier(column.name)}, 1, ${prefix.length}) = ${prefix}`\n const documents = new Set<string>()\n for (const [table, column] of [[annotations, annotations.documentId], [threads, threads.documentId], [comments, comments.documentId]] as const) {\n for (const row of await db.selectDistinct({ documentId: column }).from(table).where(under(column)).all()) documents.add(row.documentId)\n }\n for (const [table, column] of [[comments, comments.documentId], [threads, threads.documentId], [annotations, annotations.documentId]] as const) {\n await db.delete(table).where(under(column)).run()\n }\n return documents.size\n }),\n }\n}\n"],"mappings":";;;;;AAQA,MAAM,iBAAiB;CACrB,aAAa,YAAY,8BAA8B;EACrD,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;EACvB,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;EACpC,KAAK,QAAQ,KAAK,CAAC,CAAC,QAAQ;EAC5B,QAAQ,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,MAAsB,CAAC,CAAC,QAAQ;CAC3E,IAAG,UAAS,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,YAAY,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;CACnE,SAAS,YAAY,0BAA0B;EAC7C,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;EACvB,cAAc,KAAK,eAAe,CAAC,CAAC,QAAQ;EAC5C,QAAQ,KAAK,UAAU,EAAE,MAAM,CAAC,QAAQ,UAAU,EAAE,CAAC,CAAC,CAAC,QAAQ;EAC/D,KAAK,QAAQ,KAAK,CAAC,CAAC,QAAQ;CAC9B,IAAG,UAAS,CAAC,WAAW,EAAE,SAAS,CAAC,MAAM,YAAY,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;CACnE,UAAU,YAAY,mBAAmB;EACvC,YAAY,KAAK,aAAa,CAAC,CAAC,QAAQ;EACxC,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;EACpC,IAAI,KAAK,IAAI,CAAC,CAAC,QAAQ;EACvB,KAAK,QAAQ,KAAK,CAAC,CAAC,QAAQ;EAC5B,MAAM,KAAK,MAAM,CAAC,CAAC,QAAQ;EAC3B,UAAU,KAAK,WAAW,CAAC,CAAC,QAAQ;EACpC,YAAY,KAAK,aAAa;EAC9B,aAAa,KAAK,cAAc;EAChC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;EACtC,UAAU,KAAK,WAAW;CAC5B,IAAG,UAAS,CAAC,WAAW,EAAE,SAAS;EAAC,MAAM;EAAY,MAAM;EAAU,MAAM;CAAE,EAAE,CAAC,CAAC,CAAC;AACrF;;;;;;;;;;;AAcA,MAAa,sBAAqC;;AAGlD,eAAsB,uBAAuB,IAA6B;CACxE,MAAM,GAAG,IAAI,GAAG,yMAAyM;CACzN,MAAM,GAAG,IAAI,GAAG,yMAAyM;CACzN,MAAM,GAAG,IAAI,GAAG,+SAA+S;AACjU;;AAQA,MAAM,yBAAS,IAAI,QAAkC;AAErD,SAAS,WAAc,IAAY,QAAsC;CACvE,MAAM,OAAO,OAAO,IAAI,EAAE,KAAK,QAAQ,QAAQ,EAAA,CAAG,KAAK,MAAM;CAC7D,MAAM,OAAO,IAAI,YAAY,CAAC,CAAC;CAC/B,OAAO,IAAI,IAAI,IAAI;CACnB,KAAU,WAAW;EACnB,IAAI,OAAO,IAAI,EAAE,MAAM,MAAM,OAAO,OAAO,EAAE;CAC/C,CAAC;CACD,OAAO;AACT;;;;;;;AAQA,SAAgB,0BAA0B,EAAE,IAAI,UAAoD;CAClG,MAAM,EAAE,aAAa,SAAS,aAAa;CAG3C,MAAM,aAAa,SAA8B;EAC/C,IAAI,IAAI;EACR,MAAM,IAAI;EACV,UAAU,IAAI;EACd,WAAW,IAAI;EACf,GAAI,IAAI,eAAe,QAAQ,EAAE,YAAY,IAAI,WAAW;EAC5D,GAAI,IAAI,gBAAgB,QAAQ,EAAE,aAAa,IAAI,YAAY;EAC/D,GAAI,IAAI,aAAa,QAAQ,EAAE,UAAU,IAAI,SAAS;CACxD;CAEA,eAAe,UAAU,YAAoB,IAAY;EACvD,OAAO,GAAG,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;CAC1G;CAEA,eAAe,SAAS,YAAoB,IAAgD;EAC1F,MAAM,MAAM,MAAM,UAAU,YAAY,EAAE;EAC1C,IAAI,CAAC,KAAK,OAAO,KAAA;EACjB,MAAM,OAAO,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,SAAS,GAAG,CAAC,CAAC,IAAI;EACnJ,OAAO;GAAE,IAAI,IAAI;GAAI,cAAc,IAAI;GAAc,QAAQ,IAAI;GAAQ,UAAU,KAAK,IAAI,SAAS;EAAE;CACzG;CAEA,eAAe,cAAc,YAAoB,IAAoC;EACnF,MAAM,SAAS,MAAM,SAAS,YAAY,EAAE;EAC5C,IAAI,CAAC,QAAQ,MAAM,SAAS,oBAAoB,EAAE;EAClD,OAAO;CACT;CAEA,eAAe,WAAW,YAAyC;EACjE,MAAM,CAAC,gBAAgB,YAAY,eAAe,MAAM,QAAQ,IAAI;GAClE,GAAG,OAAO,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,GAAG,YAAY,YAAY,UAAU,CAAC,CAAC,CAAC,QAAQ,YAAY,GAAG,CAAC,CAAC,IAAI;GACzG,GAAG,OAAO,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,GAAG,QAAQ,YAAY,UAAU,CAAC,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,IAAI;GAC7F,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,GAAG,SAAS,YAAY,UAAU,CAAC,CAAC,CAAC,QAAQ,SAAS,GAAG,CAAC,CAAC,IAAI;EAClG,CAAC;EACD,OAAO;GACL,aAAa,eAAe,KAAI,SAAQ;IAAE,IAAI,IAAI;IAAI,UAAU,IAAI;IAAU,QAAQ,IAAI;GAAO,EAAE;GACnG,SAAS,WAAW,KAAI,SAAQ;IAC9B,IAAI,IAAI;IACR,cAAc,IAAI;IAClB,QAAQ,IAAI;IACZ,UAAU,YAAY,QAAO,YAAW,QAAQ,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,SAAS;GACpF,EAAE;EACJ;CACF;CAEA,MAAM,UAAU,OAAO,QAAsB,OAAoB,UAA4C;EAE3G,SAAQ,MADU,GAAG,OAAO,EAAE,OAAO,GAAW,gBAAgB,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,EAAA,EACjG,SAAS,KAAK;CAC7B;CAEA,MAAM,gBAAgB,OAAO,YAAoB,UAAkB,YAAqB;EACtF,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,OAAO;GAC/B;GACA;GACA,IAAI,QAAQ;GACZ,KAAK,MAAM,QAAQ,SAAS,KAAK,UAAU,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,CAAC,CAAC;GACpH,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,YAAY,QAAQ,cAAc;GAClC,aAAa,QAAQ,eAAe;GACpC,WAAW,QAAQ;GACnB,UAAU,QAAQ,YAAY;EAChC,CAAC,CAAC,CAAC,IAAI;CACT;CAEA,MAAM,SAAY,WAA6B,WAAW,IAAI,MAAM;CAEpE,OAAO;EACL,eAAe;EACf,YAAY,YAAY,aAAa,SAAS,YAAY,QAAQ;EAClE,MAAM,YAAY,YAAY;GAC5B,QAAQ,MAAM,WAAW,UAAU,EAAA,CAAG;EACxC;EAEA,eAAe,YAAY,UAAU,MAAM,YAAY;GACrD,gBAAgB,KAAK;GACrB,MAAM,EAAE,YAAY,WAAW;GAC/B,IAAI,MAAM,GAAG,OAAO,EAAE,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,MAAM,SAAS,eAAe,WAAW,GAAG,oBAAoB;GAC3N,IAAI,MAAM,UAAU,YAAY,OAAO,EAAE,GAAG,MAAM,SAAS,qBAAqB,OAAO,GAAG,oBAAoB;GAC9G,IAAI;IACF,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,OAAO;KAClC;KACA,IAAI,WAAW;KACf,UAAU,WAAW;KACrB,KAAK,MAAM,QAAQ,YAAY,KAAK,aAAa,GAAG,YAAY,YAAY,UAAU,CAAC;KACvF,QAAQ,WAAW;IACrB,CAAC,CAAC,CAAC,IAAI;IACP,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,OAAO;KAC9B;KACA,IAAI,OAAO;KACX,cAAc,OAAO;KACrB,QAAQ;KACR,KAAK,MAAM,QAAQ,QAAQ,KAAK,SAAS,GAAG,QAAQ,YAAY,UAAU,CAAC;IAC7E,CAAC,CAAC,CAAC,IAAI;IACP,MAAM,cAAc,YAAY,OAAO,IAAI,OAAO,cAAc;GAClE,SACO,OAAO;IACZ,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;IAChH,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;IACvG,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;IACvH,MAAM;GACR;GACA,OAAO;IAAE,YAAY,gBAAgB,UAAU;IAAG,QAAQ,MAAM,cAAc,YAAY,OAAO,EAAE;GAAE;EACvG,CAAC;EAED,aAAa,YAAY,UAAU,YAAY,MAAM,YAAY;GAC/D,MAAM,cAAc,YAAY,QAAQ;GACxC,IAAI,MAAM,GAAG,OAAO,EAAE,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,GAAG,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GACxK,MAAM,SAAS,cAAc,QAAQ,GAAG,iCAAiC,SAAS,EAAE;GAEtF,MAAM,cAAc,YAAY,UAAU,OAAO;GACjD,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,cAAc,YAAY,UAAU,WAAW,MAAM,aAAa,MAAM,YAAY;GAElF,IAAI,EAAC,MADgB,cAAc,YAAY,QAAQ,EAAA,CAC3C,SAAS,MAAK,YAAW,QAAQ,OAAO,SAAS,GAAG,MAAM,SAAS,aAAa,SAAS;GACrG,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,IAAI;IAAE;IAAM;GAAS,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI;GACnK,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,gBAAgB,YAAY,UAAU,cAAc,MAAM,YAAY;GAEpE,MAAM,SAAQ,MADO,cAAc,YAAY,QAAQ,EAAA,CAClC,SAAS,WAAU,YAAW,QAAQ,OAAO,SAAS;GAC3E,IAAI,QAAQ,GAAG,MAAM,SAAS,aAAa,SAAS;GACpD,IAAI,UAAU,GAAG,MAAM,QAAQ,kFAAkF;GACjH,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI;GAC3I,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,kBAAkB,YAAY,UAAU,WAAW,MAAM,YAAY;GACnE,MAAM,cAAc,YAAY,QAAQ;GACxC,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;GACtH,OAAO,cAAc,YAAY,QAAQ;EAC3C,CAAC;EAED,eAAe,YAAY,eAA2B,MAAM,YAAY;GACtE,MAAM,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;GAC1I,IAAI,CAAC,KAAK,MAAM,SAAS,cAAc,WAAW,EAAE;GACpD,IAAI,IAAI,aAAa,WAAW,UAAU,MAAM,QAAQ,6DAA6D;GACrH,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,IAAI,EAAE,QAAQ,WAAW,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI;GAC1J,OAAO,gBAAgB,UAAU;EACnC,CAAC;EAED,eAAe,YAAY,aAAa,MAAM,YAAY;GACxD,MAAM,GAAG,OAAO,QAAQ,CAAC,CAAC,MAAM,IAAI,GAAG,SAAS,YAAY,UAAU,GAAG,GAAG,SAAS,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;GAC/G,MAAM,GAAG,OAAO,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,QAAQ,YAAY,UAAU,GAAG,GAAG,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;GACtG,MAAM,GAAG,OAAO,WAAW,CAAC,CAAC,MAAM,IAAI,GAAG,YAAY,YAAY,UAAU,GAAG,GAAG,YAAY,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;EAC1H,CAAC;EAED,kBAAiB,WAAU,MAAM,YAAY;GAC3C,MAAM,SAAS,WAA6B,GAAG,UAAU,IAAI,WAAW,OAAO,IAAI,EAAE,OAAO,OAAO,OAAO,MAAM;GAChH,MAAM,4BAAY,IAAI,IAAY;GAClC,KAAK,MAAM,CAAC,OAAO,WAAW;IAAC,CAAC,aAAa,YAAY,UAAU;IAAG,CAAC,SAAS,QAAQ,UAAU;IAAG,CAAC,UAAU,SAAS,UAAU;GAAC,GAClI,KAAK,MAAM,OAAO,MAAM,GAAG,eAAe,EAAE,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,UAAU,IAAI,IAAI,UAAU;GAExI,KAAK,MAAM,CAAC,OAAO,WAAW;IAAC,CAAC,UAAU,SAAS,UAAU;IAAG,CAAC,SAAS,QAAQ,UAAU;IAAG,CAAC,aAAa,YAAY,UAAU;GAAC,GAClI,MAAM,GAAG,OAAO,KAAK,CAAC,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI;GAElD,OAAO,UAAU;EACnB,CAAC;CACH;AACF"}
@@ -10,7 +10,7 @@ var CommentStoreError = class extends Error {
10
10
  this.name = "CommentStoreError";
11
11
  }
12
12
  };
13
- const notFound = (what, id) => new CommentStoreError(`Unknown ${what} "${id}".`, "not-found", 404);
13
+ const notFound = (what, id) => new CommentStoreError(`${what} „${id}“ nicht gefunden`, "not-found", 404);
14
14
  const conflict = (message) => new CommentStoreError(message, "conflict", 409);
15
15
  const invalid = (message) => new CommentStoreError(message, "invalid", 400);
16
16
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../../src/adapters/comments/errors.ts"],"sourcesContent":["/** A caller mistake in a comment store operation; `status` is the HTTP status `handleCommentsRequest` answers with. */\nexport class CommentStoreError extends Error {\n constructor(message: string, readonly code: 'not-found' | 'conflict' | 'invalid', readonly status: 404 | 409 | 400) {\n super(message)\n this.name = 'CommentStoreError'\n }\n}\n\nexport const notFound = (what: string, id: string) => new CommentStoreError(`Unknown ${what} \"${id}\".`, 'not-found', 404)\nexport const conflict = (message: string) => new CommentStoreError(message, 'conflict', 409)\nexport const invalid = (message: string) => new CommentStoreError(message, 'invalid', 400)\n"],"mappings":";;AACA,IAAa,oBAAb,cAAuC,MAAM;CACL;CAAqD;CAA3F,YAAY,SAAiB,MAAqD,QAAkC;EAClH,MAAM,OAAO;EADuB,KAAA,OAAA;EAAqD,KAAA,SAAA;EAEzF,KAAK,OAAO;CACd;AACF;AAEA,MAAa,YAAY,MAAc,OAAe,IAAI,kBAAkB,WAAW,KAAK,IAAI,GAAG,KAAK,aAAa,GAAG;AACxH,MAAa,YAAY,YAAoB,IAAI,kBAAkB,SAAS,YAAY,GAAG;AAC3F,MAAa,WAAW,YAAoB,IAAI,kBAAkB,SAAS,WAAW,GAAG"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../../src/adapters/comments/errors.ts"],"sourcesContent":["/** A caller mistake in a comment store operation; `status` is the HTTP status `handleCommentsRequest` answers with. */\nexport class CommentStoreError extends Error {\n constructor(message: string, readonly code: 'not-found' | 'conflict' | 'invalid', readonly status: 404 | 409 | 400) {\n super(message)\n this.name = 'CommentStoreError'\n }\n}\n\n// i18n-ignore: HTTP error body of the comments route (server messages stay German, like src/server)\nexport const notFound = (what: string, id: string) => new CommentStoreError(`${what} „${id}“ nicht gefunden`, 'not-found', 404)\nexport const conflict = (message: string) => new CommentStoreError(message, 'conflict', 409)\nexport const invalid = (message: string) => new CommentStoreError(message, 'invalid', 400)\n"],"mappings":";;AACA,IAAa,oBAAb,cAAuC,MAAM;CACL;CAAqD;CAA3F,YAAY,SAAiB,MAAqD,QAAkC;EAClH,MAAM,OAAO;EADuB,KAAA,OAAA;EAAqD,KAAA,SAAA;EAEzF,KAAK,OAAO;CACd;AACF;AAGA,MAAa,YAAY,MAAc,OAAe,IAAI,kBAAkB,GAAG,KAAK,IAAI,GAAG,mBAAmB,aAAa,GAAG;AAC9H,MAAa,YAAY,YAAoB,IAAI,kBAAkB,SAAS,YAAY,GAAG;AAC3F,MAAa,WAAW,YAAoB,IAAI,kBAAkB,SAAS,WAAW,GAAG"}
@@ -6,18 +6,18 @@ const emptyDiscussion = () => ({
6
6
  });
7
7
  function requireThread(record, id) {
8
8
  const thread = record.threads.find((item) => item.id === id);
9
- if (!thread) throw notFound("comment thread", id);
9
+ if (!thread) throw notFound("Kommentar-Thread", id);
10
10
  return thread;
11
11
  }
12
12
  /** Checks the parts of a new thread that do not depend on stored data. */
13
13
  function assertNewThread({ annotation, thread }) {
14
- if (!annotation.id || !thread.id) throw invalid("Annotation and thread need an id.");
15
- if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid("The annotation and the thread must point to each other.");
14
+ if (!annotation.id || !thread.id) throw invalid("Annotation und Thread brauchen eine ID");
15
+ if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid("Annotation und Thread müssen aufeinander verweisen");
16
16
  }
17
17
  function createThread(record, input) {
18
18
  assertNewThread(input);
19
- if (record.annotations.some((item) => item.id === input.annotation.id)) throw conflict(`Annotation "${input.annotation.id}" already exists.`);
20
- if (record.threads.some((item) => item.id === input.thread.id)) throw conflict(`Comment thread "${input.thread.id}" already exists.`);
19
+ if (record.annotations.some((item) => item.id === input.annotation.id)) throw conflict(`Annotation „${input.annotation.id}“ existiert bereits`);
20
+ if (record.threads.some((item) => item.id === input.thread.id)) throw conflict(`Kommentar-Thread „${input.thread.id}“ existiert bereits`);
21
21
  const annotation = structuredClone(input.annotation);
22
22
  const thread = {
23
23
  id: input.thread.id,
@@ -34,14 +34,14 @@ function createThread(record, input) {
34
34
  }
35
35
  function addComment(record, threadId, comment) {
36
36
  const thread = requireThread(record, threadId);
37
- if (thread.comments.some((existing) => existing.id === comment.id)) throw conflict(`Comment "${comment.id}" already exists in thread "${threadId}".`);
37
+ if (thread.comments.some((existing) => existing.id === comment.id)) throw conflict(`Kommentar „${comment.id}“ existiert bereits im Thread „${threadId}“`);
38
38
  thread.comments.push(structuredClone(comment));
39
39
  return thread;
40
40
  }
41
41
  function editComment(record, threadId, commentId, body, editedAt) {
42
42
  const thread = requireThread(record, threadId);
43
43
  const comment = thread.comments.find((existing) => existing.id === commentId);
44
- if (!comment) throw notFound("comment", commentId);
44
+ if (!comment) throw notFound("Kommentar", commentId);
45
45
  comment.body = body;
46
46
  comment.editedAt = editedAt;
47
47
  return thread;
@@ -49,8 +49,8 @@ function editComment(record, threadId, commentId, body, editedAt) {
49
49
  function removeComment(record, threadId, commentId) {
50
50
  const thread = requireThread(record, threadId);
51
51
  const index = thread.comments.findIndex((existing) => existing.id === commentId);
52
- if (index < 0) throw notFound("comment", commentId);
53
- if (index === 0) throw invalid("The first comment of a thread cannot be removed on its own; remove the thread.");
52
+ if (index < 0) throw notFound("Kommentar", commentId);
53
+ if (index === 0) throw invalid("Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen");
54
54
  thread.comments.splice(index, 1);
55
55
  return thread;
56
56
  }
@@ -62,8 +62,8 @@ function setThreadStatus(record, threadId, status) {
62
62
  function updateAnchor(record, annotation) {
63
63
  const index = record.annotations.findIndex((item) => item.id === annotation.id);
64
64
  const existing = record.annotations[index];
65
- if (!existing) throw notFound("annotation", annotation.id);
66
- if (existing.threadId !== annotation.threadId) throw invalid("An annotation cannot move to another thread.");
65
+ if (!existing) throw notFound("Annotation", annotation.id);
66
+ if (existing.threadId !== annotation.threadId) throw invalid("Eine Annotation kann nicht in einen anderen Thread wechseln");
67
67
  const stored = structuredClone(annotation);
68
68
  record.annotations[index] = stored;
69
69
  return stored;
@@ -1 +1 @@
1
- {"version":3,"file":"record.js","names":[],"sources":["../../../src/adapters/comments/record.ts"],"sourcesContent":["import { conflict, invalid, notFound } from './errors'\nimport type { Annotation, Comment, CommentThread, Discussion, NewThread, ThreadStatus } from './types'\n\n// The semantics of every `CommentStore` operation on one document's `Discussion`, for stores that hold the whole\n// record in memory (the JSON file). They mutate `record` in place and throw `CommentStoreError` before changing anything.\n\nexport const emptyDiscussion = (): Discussion => ({ annotations: [], threads: [] })\n\nexport function requireThread(record: Discussion, id: string): CommentThread {\n const thread = record.threads.find(item => item.id === id)\n if (!thread) throw notFound('comment thread', id)\n return thread\n}\n\n/** Checks the parts of a new thread that do not depend on stored data. */\nexport function assertNewThread({ annotation, thread }: NewThread): void {\n if (!annotation.id || !thread.id) throw invalid('Annotation and thread need an id.')\n if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid('The annotation and the thread must point to each other.')\n}\n\nexport function createThread(record: Discussion, input: NewThread): { annotation: Annotation, thread: CommentThread } {\n assertNewThread(input)\n if (record.annotations.some(item => item.id === input.annotation.id)) throw conflict(`Annotation \"${input.annotation.id}\" already exists.`)\n if (record.threads.some(item => item.id === input.thread.id)) throw conflict(`Comment thread \"${input.thread.id}\" already exists.`)\n const annotation = structuredClone(input.annotation)\n const thread: CommentThread = { id: input.thread.id, annotationId: input.thread.annotationId, status: 'open', comments: [structuredClone(input.thread.initialComment)] }\n record.annotations.push(annotation)\n record.threads.push(thread)\n return { annotation, thread }\n}\n\nexport function addComment(record: Discussion, threadId: string, comment: Comment): CommentThread {\n const thread = requireThread(record, threadId)\n if (thread.comments.some(existing => existing.id === comment.id)) throw conflict(`Comment \"${comment.id}\" already exists in thread \"${threadId}\".`)\n thread.comments.push(structuredClone(comment))\n return thread\n}\n\nexport function editComment(record: Discussion, threadId: string, commentId: string, body: string, editedAt: string): CommentThread {\n const thread = requireThread(record, threadId)\n const comment = thread.comments.find(existing => existing.id === commentId)\n if (!comment) throw notFound('comment', commentId)\n comment.body = body\n comment.editedAt = editedAt\n return thread\n}\n\nexport function removeComment(record: Discussion, threadId: string, commentId: string): CommentThread {\n const thread = requireThread(record, threadId)\n const index = thread.comments.findIndex(existing => existing.id === commentId)\n if (index < 0) throw notFound('comment', commentId)\n if (index === 0) throw invalid('The first comment of a thread cannot be removed on its own; remove the thread.')\n thread.comments.splice(index, 1)\n return thread\n}\n\nexport function setThreadStatus(record: Discussion, threadId: string, status: ThreadStatus): CommentThread {\n const thread = requireThread(record, threadId)\n thread.status = status\n return thread\n}\n\nexport function updateAnchor(record: Discussion, annotation: Annotation): Annotation {\n const index = record.annotations.findIndex(item => item.id === annotation.id)\n const existing = record.annotations[index]\n if (!existing) throw notFound('annotation', annotation.id)\n if (existing.threadId !== annotation.threadId) throw invalid('An annotation cannot move to another thread.')\n const stored = structuredClone(annotation)\n record.annotations[index] = stored\n return stored\n}\n\nexport function removeThread(record: Discussion, threadId: string): void {\n record.threads = record.threads.filter(thread => thread.id !== threadId)\n record.annotations = record.annotations.filter(annotation => annotation.threadId !== threadId)\n}\n"],"mappings":";;AAMA,MAAa,yBAAqC;CAAE,aAAa,CAAC;CAAG,SAAS,CAAC;AAAE;AAEjF,SAAgB,cAAc,QAAoB,IAA2B;CAC3E,MAAM,SAAS,OAAO,QAAQ,MAAK,SAAQ,KAAK,OAAO,EAAE;CACzD,IAAI,CAAC,QAAQ,MAAM,SAAS,kBAAkB,EAAE;CAChD,OAAO;AACT;;AAGA,SAAgB,gBAAgB,EAAE,YAAY,UAA2B;CACvE,IAAI,CAAC,WAAW,MAAM,CAAC,OAAO,IAAI,MAAM,QAAQ,mCAAmC;CACnF,IAAI,OAAO,iBAAiB,WAAW,MAAM,WAAW,aAAa,OAAO,IAAI,MAAM,QAAQ,yDAAyD;AACzJ;AAEA,SAAgB,aAAa,QAAoB,OAAqE;CACpH,gBAAgB,KAAK;CACrB,IAAI,OAAO,YAAY,MAAK,SAAQ,KAAK,OAAO,MAAM,WAAW,EAAE,GAAG,MAAM,SAAS,eAAe,MAAM,WAAW,GAAG,kBAAkB;CAC1I,IAAI,OAAO,QAAQ,MAAK,SAAQ,KAAK,OAAO,MAAM,OAAO,EAAE,GAAG,MAAM,SAAS,mBAAmB,MAAM,OAAO,GAAG,kBAAkB;CAClI,MAAM,aAAa,gBAAgB,MAAM,UAAU;CACnD,MAAM,SAAwB;EAAE,IAAI,MAAM,OAAO;EAAI,cAAc,MAAM,OAAO;EAAc,QAAQ;EAAQ,UAAU,CAAC,gBAAgB,MAAM,OAAO,cAAc,CAAC;CAAE;CACvK,OAAO,YAAY,KAAK,UAAU;CAClC,OAAO,QAAQ,KAAK,MAAM;CAC1B,OAAO;EAAE;EAAY;CAAO;AAC9B;AAEA,SAAgB,WAAW,QAAoB,UAAkB,SAAiC;CAChG,MAAM,SAAS,cAAc,QAAQ,QAAQ;CAC7C,IAAI,OAAO,SAAS,MAAK,aAAY,SAAS,OAAO,QAAQ,EAAE,GAAG,MAAM,SAAS,YAAY,QAAQ,GAAG,8BAA8B,SAAS,GAAG;CAClJ,OAAO,SAAS,KAAK,gBAAgB,OAAO,CAAC;CAC7C,OAAO;AACT;AAEA,SAAgB,YAAY,QAAoB,UAAkB,WAAmB,MAAc,UAAiC;CAClI,MAAM,SAAS,cAAc,QAAQ,QAAQ;CAC7C,MAAM,UAAU,OAAO,SAAS,MAAK,aAAY,SAAS,OAAO,SAAS;CAC1E,IAAI,CAAC,SAAS,MAAM,SAAS,WAAW,SAAS;CACjD,QAAQ,OAAO;CACf,QAAQ,WAAW;CACnB,OAAO;AACT;AAEA,SAAgB,cAAc,QAAoB,UAAkB,WAAkC;CACpG,MAAM,SAAS,cAAc,QAAQ,QAAQ;CAC7C,MAAM,QAAQ,OAAO,SAAS,WAAU,aAAY,SAAS,OAAO,SAAS;CAC7E,IAAI,QAAQ,GAAG,MAAM,SAAS,WAAW,SAAS;CAClD,IAAI,UAAU,GAAG,MAAM,QAAQ,gFAAgF;CAC/G,OAAO,SAAS,OAAO,OAAO,CAAC;CAC/B,OAAO;AACT;AAEA,SAAgB,gBAAgB,QAAoB,UAAkB,QAAqC;CACzG,MAAM,SAAS,cAAc,QAAQ,QAAQ;CAC7C,OAAO,SAAS;CAChB,OAAO;AACT;AAEA,SAAgB,aAAa,QAAoB,YAAoC;CACnF,MAAM,QAAQ,OAAO,YAAY,WAAU,SAAQ,KAAK,OAAO,WAAW,EAAE;CAC5E,MAAM,WAAW,OAAO,YAAY;CACpC,IAAI,CAAC,UAAU,MAAM,SAAS,cAAc,WAAW,EAAE;CACzD,IAAI,SAAS,aAAa,WAAW,UAAU,MAAM,QAAQ,8CAA8C;CAC3G,MAAM,SAAS,gBAAgB,UAAU;CACzC,OAAO,YAAY,SAAS;CAC5B,OAAO;AACT;AAEA,SAAgB,aAAa,QAAoB,UAAwB;CACvE,OAAO,UAAU,OAAO,QAAQ,QAAO,WAAU,OAAO,OAAO,QAAQ;CACvE,OAAO,cAAc,OAAO,YAAY,QAAO,eAAc,WAAW,aAAa,QAAQ;AAC/F"}
1
+ {"version":3,"file":"record.js","names":[],"sources":["../../../src/adapters/comments/record.ts"],"sourcesContent":["import { conflict, invalid, notFound } from './errors'\nimport type { Annotation, Comment, CommentThread, Discussion, NewThread, ThreadStatus } from './types'\n\n// The semantics of every `CommentStore` operation on one document's `Discussion`, for stores that hold the whole\n// record in memory (the JSON file). They mutate `record` in place and throw `CommentStoreError` before changing anything.\n\nexport const emptyDiscussion = (): Discussion => ({ annotations: [], threads: [] })\n\nexport function requireThread(record: Discussion, id: string): CommentThread {\n const thread = record.threads.find(item => item.id === id)\n if (!thread) throw notFound('Kommentar-Thread', id)\n return thread\n}\n\n/** Checks the parts of a new thread that do not depend on stored data. */\nexport function assertNewThread({ annotation, thread }: NewThread): void {\n if (!annotation.id || !thread.id) throw invalid('Annotation und Thread brauchen eine ID')\n if (thread.annotationId !== annotation.id || annotation.threadId !== thread.id) throw invalid('Annotation und Thread müssen aufeinander verweisen')\n}\n\nexport function createThread(record: Discussion, input: NewThread): { annotation: Annotation, thread: CommentThread } {\n assertNewThread(input)\n if (record.annotations.some(item => item.id === input.annotation.id)) throw conflict(`Annotation „${input.annotation.id}“ existiert bereits`)\n if (record.threads.some(item => item.id === input.thread.id)) throw conflict(`Kommentar-Thread „${input.thread.id}“ existiert bereits`)\n const annotation = structuredClone(input.annotation)\n const thread: CommentThread = { id: input.thread.id, annotationId: input.thread.annotationId, status: 'open', comments: [structuredClone(input.thread.initialComment)] }\n record.annotations.push(annotation)\n record.threads.push(thread)\n return { annotation, thread }\n}\n\nexport function addComment(record: Discussion, threadId: string, comment: Comment): CommentThread {\n const thread = requireThread(record, threadId)\n if (thread.comments.some(existing => existing.id === comment.id)) throw conflict(`Kommentar „${comment.id}“ existiert bereits im Thread „${threadId}“`)\n thread.comments.push(structuredClone(comment))\n return thread\n}\n\nexport function editComment(record: Discussion, threadId: string, commentId: string, body: string, editedAt: string): CommentThread {\n const thread = requireThread(record, threadId)\n const comment = thread.comments.find(existing => existing.id === commentId)\n if (!comment) throw notFound('Kommentar', commentId)\n comment.body = body\n comment.editedAt = editedAt\n return thread\n}\n\nexport function removeComment(record: Discussion, threadId: string, commentId: string): CommentThread {\n const thread = requireThread(record, threadId)\n const index = thread.comments.findIndex(existing => existing.id === commentId)\n if (index < 0) throw notFound('Kommentar', commentId)\n if (index === 0) throw invalid('Der erste Kommentar eines Threads lässt sich nur mit dem ganzen Thread entfernen')\n thread.comments.splice(index, 1)\n return thread\n}\n\nexport function setThreadStatus(record: Discussion, threadId: string, status: ThreadStatus): CommentThread {\n const thread = requireThread(record, threadId)\n thread.status = status\n return thread\n}\n\nexport function updateAnchor(record: Discussion, annotation: Annotation): Annotation {\n const index = record.annotations.findIndex(item => item.id === annotation.id)\n const existing = record.annotations[index]\n if (!existing) throw notFound('Annotation', annotation.id)\n if (existing.threadId !== annotation.threadId) throw invalid('Eine Annotation kann nicht in einen anderen Thread wechseln')\n const stored = structuredClone(annotation)\n record.annotations[index] = stored\n return stored\n}\n\nexport function removeThread(record: Discussion, threadId: string): void {\n record.threads = record.threads.filter(thread => thread.id !== threadId)\n record.annotations = record.annotations.filter(annotation => annotation.threadId !== threadId)\n}\n"],"mappings":"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"}
@@ -5,12 +5,12 @@ const isRecord = (value) => typeof value === "object" && value !== null && !Arra
5
5
  const isText = (value) => typeof value === "string";
6
6
  const isId = (value) => isText(value) && value !== "";
7
7
  function commentOf(value) {
8
- if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid("A comment needs id, body, authorId and createdAt.");
8
+ if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid("Ein Kommentar braucht id, body, authorId und createdAt");
9
9
  for (const key of [
10
10
  "authorName",
11
11
  "authorColor",
12
12
  "editedAt"
13
- ]) if (value[key] !== void 0 && !isText(value[key])) throw invalid(`Comment ${key} must be text.`);
13
+ ]) if (value[key] !== void 0 && !isText(value[key])) throw invalid(`Kommentar-Feld „${key}“ muss Text sein`);
14
14
  const { id, body, authorId, createdAt } = value;
15
15
  const { authorName, authorColor, editedAt } = value;
16
16
  return {
@@ -24,7 +24,7 @@ function commentOf(value) {
24
24
  };
25
25
  }
26
26
  function annotationOf(value) {
27
- if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid("An annotation needs id, threadId and a valid anchor.");
27
+ if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid("Eine Annotation braucht id, threadId und einen gültigen anchor");
28
28
  return {
29
29
  id: value.id,
30
30
  threadId: value.threadId,
@@ -32,7 +32,7 @@ function annotationOf(value) {
32
32
  };
33
33
  }
34
34
  function threadInputOf(value) {
35
- if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid("A thread needs id, annotationId and initialComment.");
35
+ if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid("Ein Thread braucht id, annotationId und initialComment");
36
36
  return {
37
37
  id: value.id,
38
38
  annotationId: value.annotationId,
@@ -41,15 +41,15 @@ function threadInputOf(value) {
41
41
  }
42
42
  function idOf(value, key) {
43
43
  const id = value[key];
44
- if (!isId(id)) throw invalid(`"${key}" must be a non-empty string.`);
44
+ if (!isId(id)) throw invalid(`„${key}“ muss ein nicht leerer String sein`);
45
45
  return id;
46
46
  }
47
47
  function statusOf(value) {
48
- if (value !== "open" && value !== "resolved") throw invalid("\"status\" must be \"open\" or \"resolved\".");
48
+ if (value !== "open" && value !== "resolved") throw invalid("„status“ muss „open“ oder „resolved“ sein");
49
49
  return value;
50
50
  }
51
51
  async function perform(store, documentId, body) {
52
- if (!isRecord(body)) throw invalid("The request body must be a JSON object with an \"action\".");
52
+ if (!isRecord(body)) throw invalid("Der Request-Body muss ein JSON-Objekt mit „action“ sein");
53
53
  switch (body.action) {
54
54
  case "create": return store.createThread(documentId, {
55
55
  annotation: annotationOf(body.annotation),
@@ -62,10 +62,10 @@ async function perform(store, documentId, body) {
62
62
  await store.removeThread(documentId, idOf(body, "threadId"));
63
63
  return { ok: true };
64
64
  case "edit":
65
- if (!isText(body.body)) throw invalid("\"body\" must be text.");
65
+ if (!isText(body.body)) throw invalid("„body“ muss Text sein");
66
66
  return { thread: await store.editComment(documentId, idOf(body, "threadId"), idOf(body, "commentId"), body.body, isId(body.editedAt) ? body.editedAt : (/* @__PURE__ */ new Date()).toISOString()) };
67
67
  case "remove-comment": return { thread: await store.removeComment(documentId, idOf(body, "threadId"), idOf(body, "commentId")) };
68
- default: throw invalid("Unknown comment action.");
68
+ default: throw invalid("Unbekannte Kommentar-Aktion");
69
69
  }
70
70
  }
71
71
  /**
@@ -86,7 +86,7 @@ async function handleCommentsRequest(store, request) {
86
86
  };
87
87
  return {
88
88
  status: 405,
89
- body: { error: `Method ${request.method} is not allowed.` }
89
+ body: { error: `Methode ${request.method} ist nicht erlaubt` }
90
90
  };
91
91
  } catch (error) {
92
92
  if (error instanceof CommentStoreError) return {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../../../src/adapters/comments/server.ts"],"sourcesContent":["import { isPortableAnchor } from '../anchors'\nimport { CommentStoreError, invalid } from './errors'\nimport type { Annotation, Comment, CommentStore, CreateThreadInput, ThreadStatus } from './types'\n\n// Framework-free REST handler of a document's comments endpoint: the host routes `GET`/`POST` of\n// `<base>/<documentId>` here after its own authentication and authorisation (the handler trusts the caller, including\n// the `authorId` of new comments), and answers with `status` and `body`. Nitro, Express or a test's fetch stub all fit.\n\nexport type { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from './types'\nexport { CommentStoreError } from './errors'\n\nexport interface CommentsRequest {\n method: string\n documentId: string\n /** The parsed JSON body of a `POST`. */\n body?: unknown\n}\n\nexport interface CommentsResponse {\n status: number\n /** JSON; `{ error }` for a refused request. */\n body: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst isText = (value: unknown): value is string => typeof value === 'string'\nconst isId = (value: unknown): value is string => isText(value) && value !== ''\n\nfunction commentOf(value: unknown): Comment {\n if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid('A comment needs id, body, authorId and createdAt.')\n for (const key of ['authorName', 'authorColor', 'editedAt']) {\n if (value[key] !== undefined && !isText(value[key])) throw invalid(`Comment ${key} must be text.`)\n }\n const { id, body, authorId, createdAt } = value\n const { authorName, authorColor, editedAt } = value as Record<string, string | undefined>\n return { id, body, authorId, createdAt, ...(authorName !== undefined && { authorName }), ...(authorColor !== undefined && { authorColor }), ...(editedAt !== undefined && { editedAt }) }\n}\n\nfunction annotationOf(value: unknown): Annotation {\n if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid('An annotation needs id, threadId and a valid anchor.')\n return { id: value.id, threadId: value.threadId, anchor: value.anchor }\n}\n\nfunction threadInputOf(value: unknown): CreateThreadInput {\n if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid('A thread needs id, annotationId and initialComment.')\n return { id: value.id, annotationId: value.annotationId, initialComment: commentOf(value.initialComment) }\n}\n\nfunction idOf(value: Record<string, unknown>, key: string): string {\n const id = value[key]\n if (!isId(id)) throw invalid(`\"${key}\" must be a non-empty string.`)\n return id\n}\n\nfunction statusOf(value: unknown): ThreadStatus {\n if (value !== 'open' && value !== 'resolved') throw invalid('\"status\" must be \"open\" or \"resolved\".')\n return value\n}\n\nasync function perform(store: CommentStore, documentId: string, body: unknown): Promise<unknown> {\n if (!isRecord(body)) throw invalid('The request body must be a JSON object with an \"action\".')\n switch (body.action) {\n case 'create':\n return store.createThread(documentId, { annotation: annotationOf(body.annotation), thread: threadInputOf(body.thread) })\n case 'reply':\n return { thread: await store.addComment(documentId, idOf(body, 'threadId'), commentOf(body.comment)) }\n case 'status':\n return { thread: await store.setThreadStatus(documentId, idOf(body, 'threadId'), statusOf(body.status)) }\n case 'update-annotation':\n return { annotation: await store.updateAnchor(documentId, annotationOf(body.annotation)) }\n case 'remove':\n await store.removeThread(documentId, idOf(body, 'threadId'))\n return { ok: true }\n case 'edit':\n if (!isText(body.body)) throw invalid('\"body\" must be text.')\n return { thread: await store.editComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId'), body.body, isId(body.editedAt) ? body.editedAt : new Date().toISOString()) }\n case 'remove-comment':\n return { thread: await store.removeComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId')) }\n default:\n throw invalid('Unknown comment action.')\n }\n}\n\n/**\n * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`\n * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`\n * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.\n */\nexport async function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse> {\n const method = request.method.toUpperCase()\n try {\n if (method === 'GET') return { status: 200, body: await store.getDiscussion(request.documentId) }\n if (method === 'POST') return { status: 200, body: await perform(store, request.documentId, request.body) }\n return { status: 405, body: { error: `Method ${request.method} is not allowed.` } } // i18n-ignore: HTTP error body of the comments route (server messages are not localised, like src/server)\n }\n catch (error) {\n if (error instanceof CommentStoreError) return { status: error.status, body: { error: error.message } }\n throw error\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../../../src/adapters/comments/server.ts"],"sourcesContent":["import { isPortableAnchor } from '../anchors'\nimport { CommentStoreError, invalid } from './errors'\nimport type { Annotation, Comment, CommentStore, CreateThreadInput, ThreadStatus } from './types'\n\n// Framework-free REST handler of a document's comments endpoint: the host routes `GET`/`POST` of\n// `<base>/<documentId>` here after its own authentication and authorisation (the handler trusts the caller, including\n// the `authorId` of new comments), and answers with `status` and `body`. Nitro, Express or a test's fetch stub all fit.\n\nexport type { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from './types'\nexport { CommentStoreError } from './errors'\n\nexport interface CommentsRequest {\n method: string\n documentId: string\n /** The parsed JSON body of a `POST`. */\n body?: unknown\n}\n\nexport interface CommentsResponse {\n status: number\n /** JSON; `{ error }` for a refused request. */\n body: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst isText = (value: unknown): value is string => typeof value === 'string'\nconst isId = (value: unknown): value is string => isText(value) && value !== ''\n\nfunction commentOf(value: unknown): Comment {\n if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid('Ein Kommentar braucht id, body, authorId und createdAt')\n for (const key of ['authorName', 'authorColor', 'editedAt']) {\n if (value[key] !== undefined && !isText(value[key])) throw invalid(`Kommentar-Feld „${key}“ muss Text sein`)\n }\n const { id, body, authorId, createdAt } = value\n const { authorName, authorColor, editedAt } = value as Record<string, string | undefined>\n return { id, body, authorId, createdAt, ...(authorName !== undefined && { authorName }), ...(authorColor !== undefined && { authorColor }), ...(editedAt !== undefined && { editedAt }) }\n}\n\nfunction annotationOf(value: unknown): Annotation {\n if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid('Eine Annotation braucht id, threadId und einen gültigen anchor')\n return { id: value.id, threadId: value.threadId, anchor: value.anchor }\n}\n\nfunction threadInputOf(value: unknown): CreateThreadInput {\n if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid('Ein Thread braucht id, annotationId und initialComment')\n return { id: value.id, annotationId: value.annotationId, initialComment: commentOf(value.initialComment) }\n}\n\nfunction idOf(value: Record<string, unknown>, key: string): string {\n const id = value[key]\n if (!isId(id)) throw invalid(`„${key}“ muss ein nicht leerer String sein`)\n return id\n}\n\nfunction statusOf(value: unknown): ThreadStatus {\n if (value !== 'open' && value !== 'resolved') throw invalid('„status“ muss „open“ oder „resolved“ sein')\n return value\n}\n\nasync function perform(store: CommentStore, documentId: string, body: unknown): Promise<unknown> {\n if (!isRecord(body)) throw invalid('Der Request-Body muss ein JSON-Objekt mit „action“ sein')\n switch (body.action) {\n case 'create':\n return store.createThread(documentId, { annotation: annotationOf(body.annotation), thread: threadInputOf(body.thread) })\n case 'reply':\n return { thread: await store.addComment(documentId, idOf(body, 'threadId'), commentOf(body.comment)) }\n case 'status':\n return { thread: await store.setThreadStatus(documentId, idOf(body, 'threadId'), statusOf(body.status)) }\n case 'update-annotation':\n return { annotation: await store.updateAnchor(documentId, annotationOf(body.annotation)) }\n case 'remove':\n await store.removeThread(documentId, idOf(body, 'threadId'))\n return { ok: true }\n case 'edit':\n if (!isText(body.body)) throw invalid('„body“ muss Text sein')\n return { thread: await store.editComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId'), body.body, isId(body.editedAt) ? body.editedAt : new Date().toISOString()) }\n case 'remove-comment':\n return { thread: await store.removeComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId')) }\n default:\n throw invalid('Unbekannte Kommentar-Aktion')\n }\n}\n\n/**\n * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`\n * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`\n * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.\n */\nexport async function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse> {\n const method = request.method.toUpperCase()\n try {\n if (method === 'GET') return { status: 200, body: await store.getDiscussion(request.documentId) }\n if (method === 'POST') return { status: 200, body: await perform(store, request.documentId, request.body) }\n return { status: 405, body: { error: `Methode ${request.method} ist nicht erlaubt` } } // i18n-ignore: HTTP error body of the comments route (server messages stay German, like src/server)\n }\n catch (error) {\n if (error instanceof CommentStoreError) return { status: error.status, body: { error: error.message } }\n throw error\n }\n}\n"],"mappings":"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"}
@@ -113,7 +113,7 @@ function useEditorDocument(id, options) {
113
113
  if (mine !== generation) return;
114
114
  modelValue.value = content;
115
115
  savedJson.value = fingerprint(content);
116
- } else throw new Error(`Loading "${documentId}" failed (${response.status}).`);
116
+ } else throw new Error(`@ecsplico/editor: loading "${documentId}" failed (${response.status})`);
117
117
  ready.value = true;
118
118
  } catch (cause) {
119
119
  if (mine === generation) error.value = errorOf(cause);
@@ -135,7 +135,7 @@ function useEditorDocument(id, options) {
135
135
  body: json
136
136
  });
137
137
  if (!response.ok) {
138
- const failure = /* @__PURE__ */ new Error(`Saving "${documentId}" failed (${response.status}).`);
138
+ const failure = /* @__PURE__ */ new Error(`@ecsplico/editor: saving "${documentId}" failed (${response.status})`);
139
139
  error.value = failure;
140
140
  throw failure;
141
141
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vue.js","names":[],"sources":["../../src/adapters/vue.ts"],"sourcesContent":["import { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type { AuthorIdentity, CollaborationSession } from '../core'\nimport { computed, type ComputedRef, getCurrentScope, type MaybeRefOrGetter, onScopeDispose, ref, type Ref, shallowRef, type ShallowRef, toValue, watch } from 'vue'\nimport * as Y from 'yjs'\nimport { type CommentsFetch, createRestCommentsModel, type RestCommentsModel } from './comments/client'\nimport { commentsUrl, documentUrl } from './routes'\n\n// The Vue side of an editor document: load it from the host's document endpoint, keep track of unsaved edits, save it,\n// and hand the editor surface what it needs (v-model, collaboration session, comments model). The server side is\n// `installEditorServer` (`./server/nitro`); any server that answers the same two routes works.\n\nexport type { CommentsFetch } from './comments/client'\n\nexport interface EditorDocumentOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. Used for the content and the comments. */\n fetch: CommentsFetch\n /**\n * Live collaboration: the collaboration server's websocket URL (`ws://host/collab`; the document id is appended as the\n * provider's `name`) and the optional token its `EditorAdapter.authenticate` receives. With it the Y.Doc is the document:\n * the server loads and stores the content, so the composable neither fetches nor saves it (`modelValue` stays\n * `undefined`, `dirty` stays `false`, `save()` resolves at once).\n */\n collaboration?: { url: string, token?: string | (() => string) | (() => Promise<string>) }\n /** Who writes: labels remote carets and is the author of new comments; `null` or omitted = nobody (no comments). */\n user?: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may comment now (a reader of a read-only document may be refused); default `true`. */\n canComment?: boolean | (() => boolean)\n}\n\n/** The props and events of the editor surface (`SimpleEditor`, `AdvancedEditor`, `EditorSurface`) that `useEditorDocument` fills. */\nexport interface EditorDocumentBinding {\n /** The document (standalone mode); `undefined` in collaboration mode and until it has loaded. */\n modelValue: JSONContent | undefined\n 'onUpdate:modelValue': (content: JSONContent) => void\n /** Hands the live editor to the comments model. */\n onReady: (editor: Editor) => void\n collaboration: CollaborationSession | undefined\n}\n\nexport interface EditorDocument {\n /** The document's TipTap JSON: the surface's `v-model`. `undefined` until loaded, and always in collaboration mode. */\n modelValue: Ref<JSONContent | undefined>\n /** `true` once the surface can be mounted: the content has loaded (a missing document starts empty), or the collaboration session exists. Mount with `:key` = the id, so a changed id remounts it. */\n ready: Ref<boolean>\n /** Edits since the content was loaded or last saved. */\n dirty: ComputedRef<boolean>\n /** Stores the current content (`PUT`); several calls run one after another and an edit made while one is in flight stays dirty. Rejects with the server's refusal. */\n save(): Promise<void>\n /** Fetches the content again and drops unsaved edits. */\n reload(): Promise<void>\n /** The last load or save error, `null` while all is well. */\n error: Ref<Error | null>\n /** The collaboration session for the surface's `collaboration` prop; `undefined` without the `collaboration` option. */\n collaboration: ShallowRef<CollaborationSession | undefined>\n /** The comments model of this document (`commentsFeature({ model: () => comments.value })`), threads behind the host's comments endpoint. A new one per id. */\n comments: Readonly<ShallowRef<RestCommentsModel>>\n /** The live editor, once the surface reported it. */\n editor: ShallowRef<Editor | undefined>\n /** `v-bind` target for the surface: `<AdvancedEditor v-if=\"ready\" :key=\"id\" v-bind=\"bind\" />`. */\n bind: ComputedRef<EditorDocumentBinding>\n /** Closes the collaboration session and the comments model; runs with the effect scope that called `useEditorDocument`. */\n dispose(): void\n}\n\nconst EMPTY_DOCUMENT: JSONContent = { type: 'doc', content: [{ type: 'paragraph' }] }\n\n/**\n * The document as compared for \"unsaved changes\": without block ids. The editor gives every block of loaded content an\n * id the moment it mounts (stored files have none yet); that is not an edit the user made, so it must not mark a\n * freshly opened document dirty. Saving still writes the ids.\n */\nconst withoutBlockIds = (node: JSONContent): JSONContent => {\n const { attrs, content, ...rest } = node\n const kept = attrs && Object.fromEntries(Object.entries(attrs).filter(([name]) => name !== 'id'))\n return {\n ...rest,\n ...(kept && Object.keys(kept).length > 0 && { attrs: kept }),\n ...(content && { content: content.map(withoutBlockIds) }),\n }\n}\nconst fingerprint = (content: JSONContent): string => JSON.stringify(withoutBlockIds(content))\n\n/** Document ids are percent-encoded into one path segment; the collab provider's `name` is the plain id. */\nfunction sessionOf(id: string, { url, token }: NonNullable<EditorDocumentOptions['collaboration']>, user: AuthorIdentity | null): { session: CollaborationSession, close: () => void } {\n const ydoc = new Y.Doc()\n const provider = new HocuspocusProvider({ url: `${url.replace(/\\/+$/, '')}/${encodeURIComponent(id)}`, name: id, document: ydoc, ...(token !== undefined && { token }) })\n return {\n session: { ydoc, provider, ...(user && { user }) },\n close() {\n provider.destroy()\n ydoc.destroy()\n },\n }\n}\n\nconst errorOf = (cause: unknown) => cause instanceof Error ? cause : new Error(String(cause))\n\n/**\n * Loads document `id` from `GET /api/editor/documents/<id>` (a `404` starts an empty document that the first `save()`\n * creates), tracks edits, saves with `PUT`, and prepares the collaboration session and comments model for the editor\n * surface. `id` may be reactive: a change loads the other document and replaces the session and the comments model.\n * Call it in a component's `setup`; the collaboration session is created synchronously, so the surface mounts in the same tick.\n */\nexport function useEditorDocument(id: MaybeRefOrGetter<string>, options: EditorDocumentOptions): EditorDocument {\n const userOf = () => toValue(options.user) ?? null\n const modelValue = ref<JSONContent>()\n const ready = ref(false)\n const error = ref<Error | null>(null)\n const collaboration = shallowRef<CollaborationSession>()\n const editor = shallowRef<Editor>()\n /** Fingerprint (`fingerprint`) of the JSON the server holds (the empty document while it does not exist), `null` before the first load. */\n const savedJson = ref<string | null>(null)\n /** The document does not exist on the server yet: the first save creates it even if nothing was typed. */\n let missing = false\n let closeSession: (() => void) | undefined\n let generation = 0\n let tail: Promise<unknown> = Promise.resolve()\n\n const commentsFor = (documentId: string) => createRestCommentsModel({\n fetch: options.fetch,\n documentId,\n endpoint: commentsUrl(documentId),\n user: userOf,\n canComment: () => toValue(options.canComment) ?? true,\n getEditor: () => editor.value,\n onError: (cause) => { error.value = cause },\n })\n const comments = shallowRef(commentsFor(toValue(id)))\n let commentsId = toValue(id)\n\n async function load(documentId: string): Promise<void> {\n const mine = ++generation\n ready.value = false\n error.value = null\n modelValue.value = undefined\n savedJson.value = null\n missing = false\n editor.value = undefined\n closeSession?.()\n closeSession = undefined\n collaboration.value = undefined\n if (commentsId !== documentId) {\n comments.value.dispose()\n comments.value = commentsFor(documentId)\n commentsId = documentId\n }\n\n if (options.collaboration) {\n const { session, close } = sessionOf(documentId, options.collaboration, userOf())\n closeSession = close\n collaboration.value = session\n ready.value = true\n return\n }\n try {\n const response = await options.fetch(documentUrl(documentId))\n if (mine !== generation) return\n if (response.status === 404) {\n modelValue.value = structuredClone(EMPTY_DOCUMENT)\n savedJson.value = fingerprint(EMPTY_DOCUMENT)\n missing = true\n }\n else if (response.ok) {\n const content = await response.json() as JSONContent\n if (mine !== generation) return\n modelValue.value = content\n savedJson.value = fingerprint(content)\n }\n else {\n throw new Error(`Loading \"${documentId}\" failed (${response.status}).`)\n }\n ready.value = true\n }\n catch (cause) {\n if (mine === generation) error.value = errorOf(cause)\n }\n }\n\n const dirty = computed(() => !options.collaboration && modelValue.value !== undefined && savedJson.value !== null && fingerprint(modelValue.value) !== savedJson.value)\n\n function save(): Promise<void> {\n if (options.collaboration) return Promise.resolve()\n const run = async () => {\n const documentId = toValue(id)\n const content = modelValue.value\n if (content === undefined || !ready.value) return\n const saved = fingerprint(content)\n if (saved === savedJson.value && !missing) return\n const json = JSON.stringify(content)\n const response = await options.fetch(documentUrl(documentId), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: json })\n if (!response.ok) {\n const failure = new Error(`Saving \"${documentId}\" failed (${response.status}).`)\n error.value = failure\n throw failure\n }\n error.value = null\n // Only if the document did not change to another id while the request was in flight.\n if (documentId === toValue(id)) {\n savedJson.value = saved\n missing = false\n }\n }\n const result = tail.then(run, run)\n tail = result.catch(() => {})\n return result\n }\n\n function dispose() {\n generation++\n closeSession?.()\n closeSession = undefined\n collaboration.value = undefined\n comments.value.dispose()\n }\n\n watch(() => toValue(id), documentId => void load(documentId), { immediate: true })\n if (getCurrentScope()) onScopeDispose(dispose)\n\n const bind = computed<EditorDocumentBinding>(() => ({\n modelValue: modelValue.value,\n 'onUpdate:modelValue': (content) => { modelValue.value = content },\n onReady: (instance) => { editor.value = instance },\n collaboration: collaboration.value,\n }))\n\n return { modelValue, ready, dirty, save, reload: () => load(toValue(id)), error, collaboration, comments, editor, bind, dispose }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"vue.js","names":[],"sources":["../../src/adapters/vue.ts"],"sourcesContent":["import { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { Editor, JSONContent } from '@tiptap/core'\nimport type { AuthorIdentity, CollaborationSession } from '../core'\nimport { computed, type ComputedRef, getCurrentScope, type MaybeRefOrGetter, onScopeDispose, ref, type Ref, shallowRef, type ShallowRef, toValue, watch } from 'vue'\nimport * as Y from 'yjs'\nimport { type CommentsFetch, createRestCommentsModel, type RestCommentsModel } from './comments/client'\nimport { commentsUrl, documentUrl } from './routes'\n\n// The Vue side of an editor document: load it from the host's document endpoint, keep track of unsaved edits, save it,\n// and hand the editor surface what it needs (v-model, collaboration session, comments model). The server side is\n// `installEditorServer` (`./server/nitro`); any server that answers the same two routes works.\n\nexport type { CommentsFetch } from './comments/client'\n\nexport interface EditorDocumentOptions {\n /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. Used for the content and the comments. */\n fetch: CommentsFetch\n /**\n * Live collaboration: the collaboration server's websocket URL (`ws://host/collab`; the document id is appended as the\n * provider's `name`) and the optional token its `EditorAdapter.authenticate` receives. With it the Y.Doc is the document:\n * the server loads and stores the content, so the composable neither fetches nor saves it (`modelValue` stays\n * `undefined`, `dirty` stays `false`, `save()` resolves at once).\n */\n collaboration?: { url: string, token?: string | (() => string) | (() => Promise<string>) }\n /** Who writes: labels remote carets and is the author of new comments; `null` or omitted = nobody (no comments). */\n user?: AuthorIdentity | null | (() => AuthorIdentity | null)\n /** Whether the user may comment now (a reader of a read-only document may be refused); default `true`. */\n canComment?: boolean | (() => boolean)\n}\n\n/** The props and events of the editor surface (`SimpleEditor`, `AdvancedEditor`, `EditorSurface`) that `useEditorDocument` fills. */\nexport interface EditorDocumentBinding {\n /** The document (standalone mode); `undefined` in collaboration mode and until it has loaded. */\n modelValue: JSONContent | undefined\n 'onUpdate:modelValue': (content: JSONContent) => void\n /** Hands the live editor to the comments model. */\n onReady: (editor: Editor) => void\n collaboration: CollaborationSession | undefined\n}\n\nexport interface EditorDocument {\n /** The document's TipTap JSON: the surface's `v-model`. `undefined` until loaded, and always in collaboration mode. */\n modelValue: Ref<JSONContent | undefined>\n /** `true` once the surface can be mounted: the content has loaded (a missing document starts empty), or the collaboration session exists. Mount with `:key` = the id, so a changed id remounts it. */\n ready: Ref<boolean>\n /** Edits since the content was loaded or last saved. */\n dirty: ComputedRef<boolean>\n /** Stores the current content (`PUT`); several calls run one after another and an edit made while one is in flight stays dirty. Rejects with the server's refusal. */\n save(): Promise<void>\n /** Fetches the content again and drops unsaved edits. */\n reload(): Promise<void>\n /** The last load or save error, `null` while all is well. */\n error: Ref<Error | null>\n /** The collaboration session for the surface's `collaboration` prop; `undefined` without the `collaboration` option. */\n collaboration: ShallowRef<CollaborationSession | undefined>\n /** The comments model of this document (`commentsFeature({ model: () => comments.value })`), threads behind the host's comments endpoint. A new one per id. */\n comments: Readonly<ShallowRef<RestCommentsModel>>\n /** The live editor, once the surface reported it. */\n editor: ShallowRef<Editor | undefined>\n /** `v-bind` target for the surface: `<AdvancedEditor v-if=\"ready\" :key=\"id\" v-bind=\"bind\" />`. */\n bind: ComputedRef<EditorDocumentBinding>\n /** Closes the collaboration session and the comments model; runs with the effect scope that called `useEditorDocument`. */\n dispose(): void\n}\n\nconst EMPTY_DOCUMENT: JSONContent = { type: 'doc', content: [{ type: 'paragraph' }] }\n\n/**\n * The document as compared for \"unsaved changes\": without block ids. The editor gives every block of loaded content an\n * id the moment it mounts (stored files have none yet); that is not an edit the user made, so it must not mark a\n * freshly opened document dirty. Saving still writes the ids.\n */\nconst withoutBlockIds = (node: JSONContent): JSONContent => {\n const { attrs, content, ...rest } = node\n const kept = attrs && Object.fromEntries(Object.entries(attrs).filter(([name]) => name !== 'id'))\n return {\n ...rest,\n ...(kept && Object.keys(kept).length > 0 && { attrs: kept }),\n ...(content && { content: content.map(withoutBlockIds) }),\n }\n}\nconst fingerprint = (content: JSONContent): string => JSON.stringify(withoutBlockIds(content))\n\n/** Document ids are percent-encoded into one path segment; the collab provider's `name` is the plain id. */\nfunction sessionOf(id: string, { url, token }: NonNullable<EditorDocumentOptions['collaboration']>, user: AuthorIdentity | null): { session: CollaborationSession, close: () => void } {\n const ydoc = new Y.Doc()\n const provider = new HocuspocusProvider({ url: `${url.replace(/\\/+$/, '')}/${encodeURIComponent(id)}`, name: id, document: ydoc, ...(token !== undefined && { token }) })\n return {\n session: { ydoc, provider, ...(user && { user }) },\n close() {\n provider.destroy()\n ydoc.destroy()\n },\n }\n}\n\nconst errorOf = (cause: unknown) => cause instanceof Error ? cause : new Error(String(cause))\n\n/**\n * Loads document `id` from `GET /api/editor/documents/<id>` (a `404` starts an empty document that the first `save()`\n * creates), tracks edits, saves with `PUT`, and prepares the collaboration session and comments model for the editor\n * surface. `id` may be reactive: a change loads the other document and replaces the session and the comments model.\n * Call it in a component's `setup`; the collaboration session is created synchronously, so the surface mounts in the same tick.\n */\nexport function useEditorDocument(id: MaybeRefOrGetter<string>, options: EditorDocumentOptions): EditorDocument {\n const userOf = () => toValue(options.user) ?? null\n const modelValue = ref<JSONContent>()\n const ready = ref(false)\n const error = ref<Error | null>(null)\n const collaboration = shallowRef<CollaborationSession>()\n const editor = shallowRef<Editor>()\n /** Fingerprint (`fingerprint`) of the JSON the server holds (the empty document while it does not exist), `null` before the first load. */\n const savedJson = ref<string | null>(null)\n /** The document does not exist on the server yet: the first save creates it even if nothing was typed. */\n let missing = false\n let closeSession: (() => void) | undefined\n let generation = 0\n let tail: Promise<unknown> = Promise.resolve()\n\n const commentsFor = (documentId: string) => createRestCommentsModel({\n fetch: options.fetch,\n documentId,\n endpoint: commentsUrl(documentId),\n user: userOf,\n canComment: () => toValue(options.canComment) ?? true,\n getEditor: () => editor.value,\n onError: (cause) => { error.value = cause },\n })\n const comments = shallowRef(commentsFor(toValue(id)))\n let commentsId = toValue(id)\n\n async function load(documentId: string): Promise<void> {\n const mine = ++generation\n ready.value = false\n error.value = null\n modelValue.value = undefined\n savedJson.value = null\n missing = false\n editor.value = undefined\n closeSession?.()\n closeSession = undefined\n collaboration.value = undefined\n if (commentsId !== documentId) {\n comments.value.dispose()\n comments.value = commentsFor(documentId)\n commentsId = documentId\n }\n\n if (options.collaboration) {\n const { session, close } = sessionOf(documentId, options.collaboration, userOf())\n closeSession = close\n collaboration.value = session\n ready.value = true\n return\n }\n try {\n const response = await options.fetch(documentUrl(documentId))\n if (mine !== generation) return\n if (response.status === 404) {\n modelValue.value = structuredClone(EMPTY_DOCUMENT)\n savedJson.value = fingerprint(EMPTY_DOCUMENT)\n missing = true\n }\n else if (response.ok) {\n const content = await response.json() as JSONContent\n if (mine !== generation) return\n modelValue.value = content\n savedJson.value = fingerprint(content)\n }\n else {\n throw new Error(`@ecsplico/editor: loading \"${documentId}\" failed (${response.status})`)\n }\n ready.value = true\n }\n catch (cause) {\n if (mine === generation) error.value = errorOf(cause)\n }\n }\n\n const dirty = computed(() => !options.collaboration && modelValue.value !== undefined && savedJson.value !== null && fingerprint(modelValue.value) !== savedJson.value)\n\n function save(): Promise<void> {\n if (options.collaboration) return Promise.resolve()\n const run = async () => {\n const documentId = toValue(id)\n const content = modelValue.value\n if (content === undefined || !ready.value) return\n const saved = fingerprint(content)\n if (saved === savedJson.value && !missing) return\n const json = JSON.stringify(content)\n const response = await options.fetch(documentUrl(documentId), { method: 'PUT', headers: { 'content-type': 'application/json' }, body: json })\n if (!response.ok) {\n const failure = new Error(`@ecsplico/editor: saving \"${documentId}\" failed (${response.status})`)\n error.value = failure\n throw failure\n }\n error.value = null\n // Only if the document did not change to another id while the request was in flight.\n if (documentId === toValue(id)) {\n savedJson.value = saved\n missing = false\n }\n }\n const result = tail.then(run, run)\n tail = result.catch(() => {})\n return result\n }\n\n function dispose() {\n generation++\n closeSession?.()\n closeSession = undefined\n collaboration.value = undefined\n comments.value.dispose()\n }\n\n watch(() => toValue(id), documentId => void load(documentId), { immediate: true })\n if (getCurrentScope()) onScopeDispose(dispose)\n\n const bind = computed<EditorDocumentBinding>(() => ({\n modelValue: modelValue.value,\n 'onUpdate:modelValue': (content) => { modelValue.value = content },\n onReady: (instance) => { editor.value = instance },\n collaboration: collaboration.value,\n }))\n\n return { modelValue, ready, dirty, save, reload: () => load(toValue(id)), error, collaboration, comments, editor, bind, dispose }\n}\n"],"mappings":"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"}
@@ -12,16 +12,16 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
12
12
  view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
13
13
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
14
  "update:modelValue": (text: string) => any;
15
- ready: (view: EditorView) => any;
16
15
  save: (text: string) => any;
16
+ ready: (view: EditorView) => any;
17
17
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
18
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
19
- onReady?: ((view: EditorView) => any) | undefined;
20
19
  onSave?: ((text: string) => any) | undefined;
20
+ onReady?: ((view: EditorView) => any) | undefined;
21
21
  }>, {
22
22
  editable: boolean;
23
- modelValue: string;
24
23
  wordWrap: boolean;
24
+ modelValue: string;
25
25
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
26
  declare const _default: typeof __VLS_export;
27
27
  //#endregion
@@ -33,16 +33,16 @@ type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_13>]?: (props: typeof _
33
33
  };
34
34
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
35
35
  "update:modelValue": (text: string) => any;
36
- ready: (target: DocumentSurfaceTarget) => any;
37
36
  save: (text: string) => any;
37
+ ready: (target: DocumentSurfaceTarget) => any;
38
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
39
39
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
40
- onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
41
40
  onSave?: ((text: string) => any) | undefined;
41
+ onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
42
42
  }>, {
43
43
  editable: boolean;
44
- modelValue: string;
45
44
  wordWrap: boolean;
45
+ modelValue: string;
46
46
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
47
47
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
48
48
  declare const _default: typeof __VLS_export;
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","names":["EMPTY"],"sources":["../../../src/features/comments/model.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport * as Y from 'yjs'\nimport type { AuthorIdentity, FeatureContext } from '../../core'\nimport { anchorResolverOf, editorEntryOf, releaseRanges, resolveRange } from '../../core'\nimport { newBlockId } from '../../core/schema'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport { cancelCommentDraft, commentsDoc, commentsKey, type CommentsMeta, commentsUser, setActiveComment, startCommentDraft } from './comments-plugin'\nimport { COMMENT_LIMITS, COMMENTS_FIELD, type CommentAnchor, type CommentReply, type CommentThread } from './schema'\nimport { EMPTY_SNAPSHOT as EMPTY, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from './snapshot-equality'\nimport { captureThreadAnchor, resolveThreadAnchor } from './thread-anchor'\n\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n\n/** A thread's `Y.Map` as plain data; `replies` converted explicitly so it is always a plain array. */\nfunction snapshotOf(thread: Y.Map<unknown>): CommentThread {\n const replies = thread.get('replies')\n return { ...(thread.toJSON() as CommentThread), replies: replies instanceof Y.Array ? replies.toArray() as CommentReply[] : [] }\n}\n\n/**\n * The comment threads of a collaborative editor, stored in its Y.Doc (`COMMENTS_FIELD`, one nested `Y.Map` per thread,\n * replies in a nested `Y.Array`), so they sync live with the document and persist with its Yjs state. Keeps the\n * `Comments` plugin's `known`/`hidden` thread ids in step with the store (highlights), exposes the plugin's ranges,\n * active thread and draft, and the mutations. Mutations are no-ops without collaboration or user; editing and\n * deleting are limited to the user's own comments and replies (a client-side rule: the Y.Doc itself has no ACL).\n * `getEditor` names the editor to work on (it may change over time and may return a destroyed or no editor).\n * Each thread also keeps `anchor` (a Yjs relative position of its text's start, see `CommentAnchor`): `orphanAt` resolves\n * it for the threads whose text is gone. Threads from before anchors get theirs written once they are seen with a range.\n */\nexport function createCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY)\n\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed ? instance : undefined\n }\n\n // The editor whose transactions are followed, and the Y.Map of threads observed.\n let bound: Editor | undefined\n let observed: { doc: Y.Doc, map: Y.Map<unknown> } | undefined\n let threads: readonly CommentThread[] = NO_THREADS\n let syncQueued = false\n // The thread list the plugin was last told about, for the deactivation rule.\n let told: readonly CommentThread[] = NO_THREADS\n // Threads already tried for an anchor (written, or not capturable), so a failed capture is not repeated on every change.\n const anchorTried = new Set<string>()\n let healQueued = false\n\n function unobserve() {\n observed?.map.unobserveDeep(onThreadsChanged)\n observed = undefined\n }\n\n function onThreadsChanged() {\n if (!observed) return\n threads = [...observed.map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n recompute()\n queueSync()\n }\n\n function onTransaction() {\n recompute()\n }\n\n function unbind() {\n bound?.off('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` follows `isEditable`.\n bound?.off('update', onTransaction)\n bound = undefined\n }\n\n /** The snapshot of the editor's current state; the previous object when nothing changed. */\n function recompute() {\n const instance = current()\n if (!instance) {\n store.set(EMPTY)\n return\n }\n const previous = store.get()\n const ydoc = commentsDoc(instance)\n const user = commentsUser(instance)\n const plugin = commentsKey.getState(instance.state)\n const ranges = plugin?.ranges ?? NO_RANGES\n const orphanAt = orphanPositions(instance, ranges)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = anchor ? resolveRange(instance.state, anchor) : null\n const draftQuote = draft ? instance.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const available = !!ydoc && !!user\n const canComment = available && instance.isEditable\n\n const sameThreads = threads === previous.threads\n const sameRangeMap = sameRanges(ranges, previous.ranges)\n const sameOrphanMap = sameOrphanAt(orphanAt, previous.orphanAt)\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && sameThreads && sameRangeMap && sameOrphanMap\n && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = sameThreads && sameRangeMap\n const open = kept\n ? previous.open\n : threads\n .filter(t => !t.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt))\n const resolved = sameThreads\n ? previous.resolved\n : threads.filter(t => t.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n store.set({\n available,\n canComment,\n user,\n threads,\n ranges: sameRangeMap ? previous.ranges : ranges,\n orphanAt: sameOrphanMap ? previous.orphanAt : orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open,\n resolved,\n })\n if (available) queueHeal()\n }\n\n /** Where each unresolved thread without a text range was, from its anchor; empty without a Yjs session. */\n function orphanPositions(instance: Editor, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n const resolver = anchorResolverOf(instance)\n if (!resolver) return NO_POSITIONS\n const size = instance.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.resolved || ranges.has(thread.id) || !thread.anchor) continue\n const position = resolveThreadAnchor(instance.state, resolver, thread.anchor)\n if (position !== null && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n // Threads from before anchors get theirs once they are seen with their text in the document (any client with a user\n // writes it; the positions are equivalent, so concurrent writes agree). Queued: not written from inside a Yjs\n // observer or a transaction listener.\n function queueHeal() {\n if (healQueued || !threads.some(t => !t.anchor && !anchorTried.has(t.id))) return\n healQueued = true\n queueMicrotask(() => {\n healQueued = false\n healAnchors()\n })\n }\n\n function healAnchors() {\n const ctx = writable()\n const resolver = ctx && anchorResolverOf(ctx.editor)\n if (!ctx || !resolver || ctx.editor !== bound) return\n const { ranges } = store.get()\n const written: [Y.Map<unknown>, CommentAnchor][] = []\n for (const { id, anchor } of threads) {\n const range = ranges.get(id)\n if (anchor || anchorTried.has(id) || !range) continue\n anchorTried.add(id)\n const thread = threadMap(ctx.map, id)\n const captured = thread && captureThreadAnchor(ctx.editor.state, resolver, range)\n if (thread && captured) written.push([thread, captured])\n }\n if (written.length) ctx.ydoc.transact(() => written.forEach(([thread, captured]) => thread.set('anchor', captured)))\n }\n\n // Highlights appear and disappear on every client as threads arrive, get resolved or deleted. The active thread\n // stays highlighted even when resolved (a resolved thread activated from its card), so a thread that turns resolved\n // or disappears while active is deactivated here, on every client, not only on the one that resolved or deleted it.\n // Queued (not dispatched from inside the Yjs observer or a transaction listener).\n function queueSync() {\n if (syncQueued) return\n syncQueued = true\n queueMicrotask(() => {\n syncQueued = false\n syncPlugin()\n })\n }\n\n function syncPlugin() {\n const instance = current()\n if (!instance || instance !== bound) return\n const value = commentsKey.getState(instance.state)\n if (!value) return\n const known = new Set(threads.map(t => t.id))\n const hidden = new Set(threads.filter(t => t.resolved).map(t => t.id))\n const meta: CommentsMeta = { known, hidden }\n const before = value.active ? told.find(t => t.id === value.active) : undefined\n if (value.active && before && !before.resolved && (!known.has(value.active) || hidden.has(value.active))) meta.active = null\n told = threads\n instance.view.dispatch(instance.state.tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n }\n\n /** Follows the editor `getEditor` names now, and the Y.Doc of its collaboration session. */\n function refresh() {\n const instance = current()\n if (instance !== bound) {\n unbind()\n unobserve()\n bound = instance\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n instance?.on('transaction', onTransaction)\n instance?.on('update', onTransaction)\n // The new editor's plugin knows no threads yet.\n queueSync()\n }\n const ydoc = instance ? commentsDoc(instance) : null\n if ((observed?.doc ?? null) !== ydoc) {\n unobserve()\n if (ydoc) {\n const map = ydoc.getMap<unknown>(COMMENTS_FIELD)\n observed = { doc: ydoc, map }\n map.observeDeep(onThreadsChanged)\n threads = [...map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n }\n else {\n threads = NO_THREADS\n }\n queueSync()\n }\n recompute()\n }\n\n function dispose() {\n unbind()\n unobserve()\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n store.set(EMPTY)\n }\n\n /** Editor, thread map and user while comments are available; null otherwise. */\n function writable() {\n const instance = current()\n const ydoc = instance && commentsDoc(instance)\n const author = instance && commentsUser(instance)\n if (!instance || !ydoc || !author) return null\n return { editor: instance, ydoc, map: ydoc.getMap<unknown>(COMMENTS_FIELD), user: { id: author.id, name: author.name, color: author.color } }\n }\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function threadMap(map: Y.Map<unknown>, threadId: string): Y.Map<unknown> | null {\n const thread = map.get(threadId)\n return thread instanceof Y.Map ? thread : null\n }\n\n function repliesOf(thread: Y.Map<unknown>): Y.Array<CommentReply> | null {\n const replies = thread.get('replies')\n return replies instanceof Y.Array ? replies as Y.Array<CommentReply> : null\n }\n\n function add(text: string): string | null {\n const ctx = writable()\n const body = clean(text)\n const anchor = ctx && commentsKey.getState(ctx.editor.state)?.draft\n const range = ctx && anchor ? resolveRange(ctx.editor.state, anchor) : null\n const type = ctx?.editor.schema.marks.comment\n if (!ctx || !anchor || !range || range.from >= range.to || !body || !type) return null\n const id = newBlockId()\n const { state } = ctx.editor\n const resolver = anchorResolverOf(ctx.editor)\n const threadAnchor = resolver ? captureThreadAnchor(state, resolver, range) : null\n const quote = state.doc.textBetween(range.from, range.to, ' ').slice(0, COMMENT_LIMITS.quote)\n const tr = state.tr.addMark(range.from, range.to, type.create({ id }))\n // Text that takes no marks (code block, inline code): no thread that would start out without a range.\n if (!tr.docChanged) return null\n // Not undoable: the mark is the thread's anchor, not an edit of the text.\n const meta: CommentsMeta = { draft: null, active: id }\n ctx.editor.view.dispatch(tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n releaseRanges(ctx.editor, [anchor])\n const thread = new Y.Map<unknown>()\n thread.set('id', id)\n thread.set('author', ctx.user)\n thread.set('text', body)\n thread.set('createdAt', new Date().toISOString())\n thread.set('quote', quote)\n thread.set('resolved', false)\n if (threadAnchor) thread.set('anchor', threadAnchor)\n thread.set('replies', new Y.Array<CommentReply>())\n ctx.map.set(id, thread)\n return id\n }\n\n function reply(threadId: string, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies || !body) return\n replies.push([{ id: newBlockId(), author: ctx.user, text: body, createdAt: new Date().toISOString() }])\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || !body) return\n const now = new Date().toISOString()\n if (replyId === null) {\n if ((thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id || thread.get('text') === body) return\n ctx.ydoc.transact(() => {\n thread.set('text', body)\n thread.set('editedAt', now)\n })\n return\n }\n const replies = repliesOf(thread)\n const index = replies ? replies.toArray().findIndex(r => r.id === replyId) : -1\n const previous = replies && index >= 0 ? replies.get(index) : undefined\n if (!replies || !previous || previous.author.id !== ctx.user.id || previous.text === body) return\n ctx.ydoc.transact(() => {\n replies.delete(index, 1)\n replies.insert(index, [{ ...previous, text: body, editedAt: now }])\n })\n }\n\n function setResolved(threadId: string, value: boolean): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread) return\n thread.set('resolved', value)\n if (value && commentsKey.getState(ctx.editor.state)?.active === threadId) setActiveComment(ctx.editor, null)\n }\n\n function remove(threadId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || (thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id) return\n const { state } = ctx.editor\n const tr = state.tr\n state.doc.descendants((node, pos) => {\n if (!node.isText) return\n const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.id === threadId)\n if (mark) tr.removeMark(pos, pos + node.nodeSize, mark)\n })\n if (commentsKey.getState(state)?.active === threadId) tr.setMeta(commentsKey, { active: null } satisfies CommentsMeta)\n if (tr.docChanged || tr.getMeta(commentsKey)) ctx.editor.view.dispatch(tr.setMeta('addToHistory', false))\n ctx.map.delete(threadId)\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies) return\n const index = replies.toArray().findIndex(r => r.id === replyId)\n if (index >= 0 && replies.get(index).author.id === ctx.user.id) replies.delete(index, 1)\n }\n\n function activate(id: string | null, options: { reveal?: boolean } = {}): void {\n const instance = current()\n if (instance) setActiveComment(instance, id, options)\n }\n\n function startDraft(): void {\n const ctx = writable()\n if (ctx) startCommentDraft(ctx.editor)\n }\n\n function cancelDraft(): void {\n const instance = current()\n if (instance) cancelCommentDraft(instance)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft }\n}\n\n/** Creates the comments model of one editor, as a host supplies it to `commentsFeatureCore`. */\nexport type CommentsModelFactory = (getEditor: () => Editor | undefined) => CommentsModel\n\n// The host's model factory per surface context, set by the comments feature while the surface builds its extensions.\nconst modelFactories = new WeakMap<FeatureContext, CommentsModelFactory>()\n\n/** Registers the host model factory of `ctx` (the comments feature does this; hosts pass `model` to the feature). */\nexport function provideCommentsModel(ctx: FeatureContext, factory: CommentsModelFactory): void {\n modelFactories.set(ctx, factory)\n}\n\n/** The host model factory the comments feature of `ctx` was configured with; undefined for the default Y.Doc model. */\nexport function commentsModelOf(ctx: FeatureContext): CommentsModelFactory | undefined {\n return modelFactories.get(ctx)\n}\n\n/**\n * The comments model the framework hooks (`useComments`) use: the model of the surface `getEditor` belongs to\n * (`commentsModelOf` of its feature context: the host's `model` factory) and `createCommentsModel` for every other\n * editor. It follows the editor over time: when the editor changes to one of a surface with another model, the inner\n * model is replaced (disposed, created, refreshed) and `state` carries on with the new one. The factory runs once per\n * consumer of this function, so a host model that must share state across them keeps it outside the factory.\n */\nexport function createEditorCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY)\n let inner: { model: CommentsModel, factory: CommentsModelFactory, stop: () => void } | undefined\n\n function release() {\n inner?.stop()\n inner?.model.dispose()\n inner = undefined\n }\n\n /** The inner model for the editor now; a replacement when its surface brings another factory. */\n function current(): CommentsModel {\n const entry = editorEntryOf(getEditor())\n const factory = (entry && commentsModelOf(entry.ctx)) ?? createCommentsModel\n if (inner?.factory !== factory) {\n release()\n const model = factory(getEditor)\n inner = { model, factory, stop: model.state.subscribe(snapshot => store.set(snapshot)) }\n store.set(model.state.get())\n }\n return inner!.model\n }\n\n return {\n state: store,\n refresh: () => current().refresh(),\n dispose: () => {\n release()\n store.set(EMPTY)\n },\n add: text => current().add(text),\n reply: (threadId, text) => current().reply(threadId, text),\n edit: (threadId, replyId, text) => current().edit(threadId, replyId, text),\n setResolved: (threadId, resolved) => current().setResolved(threadId, resolved),\n remove: threadId => current().remove(threadId),\n removeReply: (threadId, replyId) => current().removeReply(threadId, replyId),\n activate: (id, options) => current().activate(id, options),\n startDraft: () => current().startDraft(),\n cancelDraft: () => current().cancelDraft(),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.js","names":[],"sources":["../../../src/features/comments/model.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport * as Y from 'yjs'\nimport type { AuthorIdentity, FeatureContext } from '../../core'\nimport { anchorResolverOf, editorEntryOf, releaseRanges, resolveRange } from '../../core'\nimport { newBlockId } from '../../core/schema'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport { cancelCommentDraft, commentsDoc, commentsKey, type CommentsMeta, commentsUser, setActiveComment, startCommentDraft } from './comments-plugin'\nimport { COMMENT_LIMITS, COMMENTS_FIELD, type CommentAnchor, type CommentReply, type CommentThread } from './schema'\nimport { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser } from './snapshot-equality'\nimport { captureThreadAnchor, resolveThreadAnchor } from './thread-anchor'\n\nexport type DocRange = { from: number, to: number }\n\n/** What a comments UI renders: replaced (a new object) whenever any part of it changes, so a store subscriber re-renders only then. */\nexport interface CommentsSnapshot {\n /** Comments need a collaborative editor whose `Comments` extension has a user (a host model decides for itself). */\n available: boolean\n /**\n * Whether the user may compose a comment now: the default model says `available` and the editor is editable, a host\n * model may allow readers of a read-only document to comment.\n */\n canComment: boolean\n user: AuthorIdentity | null\n /** Plain snapshots of all threads in the Y.Doc, replaced on every change. */\n threads: readonly CommentThread[]\n /** Current text range per thread id (threads without one are orphans). */\n ranges: ReadonlyMap<string, DocRange>\n /**\n * Where the text of an orphaned open thread was: the document position of the deletion point, resolved from the\n * thread's stored anchor (so it is the same place for every collaborator and follows edits). Absent for orphans\n * without a usable anchor (threads from before anchors, or whose block is gone for good).\n */\n orphanAt: ReadonlyMap<string, number>\n active: string | null\n /** Current range of the draft a new comment is being written for. */\n draft: DocRange | null\n /** The draft's text as the new thread will keep it (`quote`). */\n draftQuote: string\n /** Unresolved threads in document order, orphans last; resolved ones newest first. */\n open: readonly CommentThread[]\n resolved: readonly CommentThread[]\n}\n\n/** The mutations of the comments of one editor; no-ops without collaboration or a user. */\nexport interface CommentsActions {\n /** Creates a thread on the draft range; its id, or null without a (non-empty) draft or text. */\n add: (text: string) => string | null\n reply: (threadId: string, text: string) => void\n /** Changes the text of the user's own thread (`replyId` null) or reply. */\n edit: (threadId: string, replyId: string | null, text: string) => void\n /** Resolves or reopens a thread (anyone may). */\n setResolved: (threadId: string, resolved: boolean) => void\n /** Deletes the user's own thread with its marks / the user's own reply. */\n remove: (threadId: string) => void\n removeReply: (threadId: string, replyId: string) => void\n /** Activates a thread (`null`: none); `reveal` selects and scrolls to its text. */\n activate: (id: string | null, options?: { reveal?: boolean }) => void\n /** Starts a comment on the current selection (the layout shows the composer while `draft` is set). */\n startDraft: () => void\n cancelDraft: () => void\n}\n\n/**\n * The comments of one editor, framework-free: `state` is the observable snapshot, `refresh()` brings it up to date\n * (the framework hook calls it when the editor it watches changes; the model itself listens to that editor's\n * transactions from then on), `dispose()` detaches the editor and Y.Doc listeners and may be followed by `refresh()` again\n * (StrictMode mounts twice).\n */\nexport interface CommentsModel extends CommentsActions {\n readonly state: ReadonlyStore<CommentsSnapshot>\n refresh: () => void\n dispose: () => void\n}\n\n/** A thread's `Y.Map` as plain data; `replies` converted explicitly so it is always a plain array. */\nfunction snapshotOf(thread: Y.Map<unknown>): CommentThread {\n const replies = thread.get('replies')\n return { ...(thread.toJSON() as CommentThread), replies: replies instanceof Y.Array ? replies.toArray() as CommentReply[] : [] }\n}\n\n/**\n * The comment threads of a collaborative editor, stored in its Y.Doc (`COMMENTS_FIELD`, one nested `Y.Map` per thread,\n * replies in a nested `Y.Array`), so they sync live with the document and persist with its Yjs state. Keeps the\n * `Comments` plugin's `known`/`hidden` thread ids in step with the store (highlights), exposes the plugin's ranges,\n * active thread and draft, and the mutations. Mutations are no-ops without collaboration or user; editing and\n * deleting are limited to the user's own comments and replies (a client-side rule: the Y.Doc itself has no ACL).\n * `getEditor` names the editor to work on (it may change over time and may return a destroyed or no editor).\n * Each thread also keeps `anchor` (a Yjs relative position of its text's start, see `CommentAnchor`): `orphanAt` resolves\n * it for the threads whose text is gone. Threads from before anchors get theirs written once they are seen with a range.\n */\nexport function createCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY_SNAPSHOT)\n\n const current = () => {\n const instance = getEditor()\n return instance && !instance.isDestroyed ? instance : undefined\n }\n\n // The editor whose transactions are followed, and the Y.Map of threads observed.\n let bound: Editor | undefined\n let observed: { doc: Y.Doc, map: Y.Map<unknown> } | undefined\n let threads: readonly CommentThread[] = NO_THREADS\n let syncQueued = false\n // The thread list the plugin was last told about, for the deactivation rule.\n let told: readonly CommentThread[] = NO_THREADS\n // Threads already tried for an anchor (written, or not capturable), so a failed capture is not repeated on every change.\n const anchorTried = new Set<string>()\n let healQueued = false\n\n function unobserve() {\n observed?.map.unobserveDeep(onThreadsChanged)\n observed = undefined\n }\n\n function onThreadsChanged() {\n if (!observed) return\n threads = [...observed.map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n recompute()\n queueSync()\n }\n\n function onTransaction() {\n recompute()\n }\n\n function unbind() {\n bound?.off('transaction', onTransaction)\n // `setEditable` emits `update` only, and `canComment` follows `isEditable`.\n bound?.off('update', onTransaction)\n bound = undefined\n }\n\n /** The snapshot of the editor's current state; the previous object when nothing changed. */\n function recompute() {\n const instance = current()\n if (!instance) {\n store.set(EMPTY_SNAPSHOT)\n return\n }\n const previous = store.get()\n const ydoc = commentsDoc(instance)\n const user = commentsUser(instance)\n const plugin = commentsKey.getState(instance.state)\n const ranges = plugin?.ranges ?? NO_RANGES\n const orphanAt = orphanPositions(instance, ranges)\n const active = plugin?.active ?? null\n const anchor = plugin?.draft\n const draft = anchor ? resolveRange(instance.state, anchor) : null\n const draftQuote = draft ? instance.state.doc.textBetween(draft.from, draft.to, ' ').slice(0, COMMENT_LIMITS.quote) : ''\n const available = !!ydoc && !!user\n const canComment = available && instance.isEditable\n\n const sameThreads = threads === previous.threads\n const sameRangeMap = sameRanges(ranges, previous.ranges)\n const sameOrphanMap = sameOrphanAt(orphanAt, previous.orphanAt)\n if (\n available === previous.available && canComment === previous.canComment && sameUser(user, previous.user) && sameThreads && sameRangeMap && sameOrphanMap\n && active === previous.active && sameRange(draft, previous.draft) && draftQuote === previous.draftQuote\n ) return\n\n const kept = sameThreads && sameRangeMap\n const open = kept\n ? previous.open\n : threads\n .filter(t => !t.resolved)\n .sort((a, b) => (ranges.get(a.id)?.from ?? Infinity) - (ranges.get(b.id)?.from ?? Infinity) || a.createdAt.localeCompare(b.createdAt))\n const resolved = sameThreads\n ? previous.resolved\n : threads.filter(t => t.resolved).sort((a, b) => b.createdAt.localeCompare(a.createdAt))\n store.set({\n available,\n canComment,\n user,\n threads,\n ranges: sameRangeMap ? previous.ranges : ranges,\n orphanAt: sameOrphanMap ? previous.orphanAt : orphanAt,\n active,\n draft: sameRange(draft, previous.draft) ? previous.draft : draft,\n draftQuote,\n open,\n resolved,\n })\n if (available) queueHeal()\n }\n\n /** Where each unresolved thread without a text range was, from its anchor; empty without a Yjs session. */\n function orphanPositions(instance: Editor, ranges: ReadonlyMap<string, DocRange>): ReadonlyMap<string, number> {\n const resolver = anchorResolverOf(instance)\n if (!resolver) return NO_POSITIONS\n const size = instance.state.doc.content.size\n let found: Map<string, number> | undefined\n for (const thread of threads) {\n if (thread.resolved || ranges.has(thread.id) || !thread.anchor) continue\n const position = resolveThreadAnchor(instance.state, resolver, thread.anchor)\n if (position !== null && position <= size) (found ??= new Map()).set(thread.id, position)\n }\n return found ?? NO_POSITIONS\n }\n\n // Threads from before anchors get theirs once they are seen with their text in the document (any client with a user\n // writes it; the positions are equivalent, so concurrent writes agree). Queued: not written from inside a Yjs\n // observer or a transaction listener.\n function queueHeal() {\n if (healQueued || !threads.some(t => !t.anchor && !anchorTried.has(t.id))) return\n healQueued = true\n queueMicrotask(() => {\n healQueued = false\n healAnchors()\n })\n }\n\n function healAnchors() {\n const ctx = writable()\n const resolver = ctx && anchorResolverOf(ctx.editor)\n if (!ctx || !resolver || ctx.editor !== bound) return\n const { ranges } = store.get()\n const written: [Y.Map<unknown>, CommentAnchor][] = []\n for (const { id, anchor } of threads) {\n const range = ranges.get(id)\n if (anchor || anchorTried.has(id) || !range) continue\n anchorTried.add(id)\n const thread = threadMap(ctx.map, id)\n const captured = thread && captureThreadAnchor(ctx.editor.state, resolver, range)\n if (thread && captured) written.push([thread, captured])\n }\n if (written.length) ctx.ydoc.transact(() => written.forEach(([thread, captured]) => thread.set('anchor', captured)))\n }\n\n // Highlights appear and disappear on every client as threads arrive, get resolved or deleted. The active thread\n // stays highlighted even when resolved (a resolved thread activated from its card), so a thread that turns resolved\n // or disappears while active is deactivated here, on every client, not only on the one that resolved or deleted it.\n // Queued (not dispatched from inside the Yjs observer or a transaction listener).\n function queueSync() {\n if (syncQueued) return\n syncQueued = true\n queueMicrotask(() => {\n syncQueued = false\n syncPlugin()\n })\n }\n\n function syncPlugin() {\n const instance = current()\n if (!instance || instance !== bound) return\n const value = commentsKey.getState(instance.state)\n if (!value) return\n const known = new Set(threads.map(t => t.id))\n const hidden = new Set(threads.filter(t => t.resolved).map(t => t.id))\n const meta: CommentsMeta = { known, hidden }\n const before = value.active ? told.find(t => t.id === value.active) : undefined\n if (value.active && before && !before.resolved && (!known.has(value.active) || hidden.has(value.active))) meta.active = null\n told = threads\n instance.view.dispatch(instance.state.tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n }\n\n /** Follows the editor `getEditor` names now, and the Y.Doc of its collaboration session. */\n function refresh() {\n const instance = current()\n if (instance !== bound) {\n unbind()\n unobserve()\n bound = instance\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n instance?.on('transaction', onTransaction)\n instance?.on('update', onTransaction)\n // The new editor's plugin knows no threads yet.\n queueSync()\n }\n const ydoc = instance ? commentsDoc(instance) : null\n if ((observed?.doc ?? null) !== ydoc) {\n unobserve()\n if (ydoc) {\n const map = ydoc.getMap<unknown>(COMMENTS_FIELD)\n observed = { doc: ydoc, map }\n map.observeDeep(onThreadsChanged)\n threads = [...map.values()].filter((t): t is Y.Map<unknown> => t instanceof Y.Map).map(snapshotOf)\n }\n else {\n threads = NO_THREADS\n }\n queueSync()\n }\n recompute()\n }\n\n function dispose() {\n unbind()\n unobserve()\n threads = NO_THREADS\n told = NO_THREADS\n anchorTried.clear()\n store.set(EMPTY_SNAPSHOT)\n }\n\n /** Editor, thread map and user while comments are available; null otherwise. */\n function writable() {\n const instance = current()\n const ydoc = instance && commentsDoc(instance)\n const author = instance && commentsUser(instance)\n if (!instance || !ydoc || !author) return null\n return { editor: instance, ydoc, map: ydoc.getMap<unknown>(COMMENTS_FIELD), user: { id: author.id, name: author.name, color: author.color } }\n }\n\n /** Trimmed and cut to the limit; '' when nothing is left. */\n const clean = (text: string) => text.trim().slice(0, COMMENT_LIMITS.text)\n\n function threadMap(map: Y.Map<unknown>, threadId: string): Y.Map<unknown> | null {\n const thread = map.get(threadId)\n return thread instanceof Y.Map ? thread : null\n }\n\n function repliesOf(thread: Y.Map<unknown>): Y.Array<CommentReply> | null {\n const replies = thread.get('replies')\n return replies instanceof Y.Array ? replies as Y.Array<CommentReply> : null\n }\n\n function add(text: string): string | null {\n const ctx = writable()\n const body = clean(text)\n const anchor = ctx && commentsKey.getState(ctx.editor.state)?.draft\n const range = ctx && anchor ? resolveRange(ctx.editor.state, anchor) : null\n const type = ctx?.editor.schema.marks.comment\n if (!ctx || !anchor || !range || range.from >= range.to || !body || !type) return null\n const id = newBlockId()\n const { state } = ctx.editor\n const resolver = anchorResolverOf(ctx.editor)\n const threadAnchor = resolver ? captureThreadAnchor(state, resolver, range) : null\n const quote = state.doc.textBetween(range.from, range.to, ' ').slice(0, COMMENT_LIMITS.quote)\n const tr = state.tr.addMark(range.from, range.to, type.create({ id }))\n // Text that takes no marks (code block, inline code): no thread that would start out without a range.\n if (!tr.docChanged) return null\n // Not undoable: the mark is the thread's anchor, not an edit of the text.\n const meta: CommentsMeta = { draft: null, active: id }\n ctx.editor.view.dispatch(tr.setMeta(commentsKey, meta).setMeta('addToHistory', false))\n releaseRanges(ctx.editor, [anchor])\n const thread = new Y.Map<unknown>()\n thread.set('id', id)\n thread.set('author', ctx.user)\n thread.set('text', body)\n thread.set('createdAt', new Date().toISOString())\n thread.set('quote', quote)\n thread.set('resolved', false)\n if (threadAnchor) thread.set('anchor', threadAnchor)\n thread.set('replies', new Y.Array<CommentReply>())\n ctx.map.set(id, thread)\n return id\n }\n\n function reply(threadId: string, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies || !body) return\n replies.push([{ id: newBlockId(), author: ctx.user, text: body, createdAt: new Date().toISOString() }])\n }\n\n function edit(threadId: string, replyId: string | null, text: string): void {\n const ctx = writable()\n const body = clean(text)\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || !body) return\n const now = new Date().toISOString()\n if (replyId === null) {\n if ((thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id || thread.get('text') === body) return\n ctx.ydoc.transact(() => {\n thread.set('text', body)\n thread.set('editedAt', now)\n })\n return\n }\n const replies = repliesOf(thread)\n const index = replies ? replies.toArray().findIndex(r => r.id === replyId) : -1\n const previous = replies && index >= 0 ? replies.get(index) : undefined\n if (!replies || !previous || previous.author.id !== ctx.user.id || previous.text === body) return\n ctx.ydoc.transact(() => {\n replies.delete(index, 1)\n replies.insert(index, [{ ...previous, text: body, editedAt: now }])\n })\n }\n\n function setResolved(threadId: string, value: boolean): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread) return\n thread.set('resolved', value)\n if (value && commentsKey.getState(ctx.editor.state)?.active === threadId) setActiveComment(ctx.editor, null)\n }\n\n function remove(threadId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n if (!ctx || !thread || (thread.get('author') as CommentThread['author'] | undefined)?.id !== ctx.user.id) return\n const { state } = ctx.editor\n const tr = state.tr\n state.doc.descendants((node, pos) => {\n if (!node.isText) return\n const mark = node.marks.find(m => m.type.name === 'comment' && m.attrs.id === threadId)\n if (mark) tr.removeMark(pos, pos + node.nodeSize, mark)\n })\n if (commentsKey.getState(state)?.active === threadId) tr.setMeta(commentsKey, { active: null } satisfies CommentsMeta)\n if (tr.docChanged || tr.getMeta(commentsKey)) ctx.editor.view.dispatch(tr.setMeta('addToHistory', false))\n ctx.map.delete(threadId)\n }\n\n function removeReply(threadId: string, replyId: string): void {\n const ctx = writable()\n const thread = ctx && threadMap(ctx.map, threadId)\n const replies = thread && repliesOf(thread)\n if (!ctx || !replies) return\n const index = replies.toArray().findIndex(r => r.id === replyId)\n if (index >= 0 && replies.get(index).author.id === ctx.user.id) replies.delete(index, 1)\n }\n\n function activate(id: string | null, options: { reveal?: boolean } = {}): void {\n const instance = current()\n if (instance) setActiveComment(instance, id, options)\n }\n\n function startDraft(): void {\n const ctx = writable()\n if (ctx) startCommentDraft(ctx.editor)\n }\n\n function cancelDraft(): void {\n const instance = current()\n if (instance) cancelCommentDraft(instance)\n }\n\n return { state: store, refresh, dispose, add, reply, edit, setResolved, remove, removeReply, activate, startDraft, cancelDraft }\n}\n\n/** Creates the comments model of one editor, as a host supplies it to `commentsFeatureCore`. */\nexport type CommentsModelFactory = (getEditor: () => Editor | undefined) => CommentsModel\n\n// The host's model factory per surface context, set by the comments feature while the surface builds its extensions.\nconst modelFactories = new WeakMap<FeatureContext, CommentsModelFactory>()\n\n/** Registers the host model factory of `ctx` (the comments feature does this; hosts pass `model` to the feature). */\nexport function provideCommentsModel(ctx: FeatureContext, factory: CommentsModelFactory): void {\n modelFactories.set(ctx, factory)\n}\n\n/** The host model factory the comments feature of `ctx` was configured with; undefined for the default Y.Doc model. */\nexport function commentsModelOf(ctx: FeatureContext): CommentsModelFactory | undefined {\n return modelFactories.get(ctx)\n}\n\n/**\n * The comments model the framework hooks (`useComments`) use: the model of the surface `getEditor` belongs to\n * (`commentsModelOf` of its feature context: the host's `model` factory) and `createCommentsModel` for every other\n * editor. It follows the editor over time: when the editor changes to one of a surface with another model, the inner\n * model is replaced (disposed, created, refreshed) and `state` carries on with the new one. The factory runs once per\n * consumer of this function, so a host model that must share state across them keeps it outside the factory.\n */\nexport function createEditorCommentsModel(getEditor: () => Editor | undefined): CommentsModel {\n const store = createStore<CommentsSnapshot>(EMPTY_SNAPSHOT)\n let inner: { model: CommentsModel, factory: CommentsModelFactory, stop: () => void } | undefined\n\n function release() {\n inner?.stop()\n inner?.model.dispose()\n inner = undefined\n }\n\n /** The inner model for the editor now; a replacement when its surface brings another factory. */\n function current(): CommentsModel {\n const entry = editorEntryOf(getEditor())\n const factory = (entry && commentsModelOf(entry.ctx)) ?? createCommentsModel\n if (inner?.factory !== factory) {\n release()\n const model = factory(getEditor)\n inner = { model, factory, stop: model.state.subscribe(snapshot => store.set(snapshot)) }\n store.set(model.state.get())\n }\n return inner!.model\n }\n\n return {\n state: store,\n refresh: () => current().refresh(),\n dispose: () => {\n release()\n store.set(EMPTY_SNAPSHOT)\n },\n add: text => current().add(text),\n reply: (threadId, text) => current().reply(threadId, text),\n edit: (threadId, replyId, text) => current().edit(threadId, replyId, text),\n setResolved: (threadId, resolved) => current().setResolved(threadId, resolved),\n remove: threadId => current().remove(threadId),\n removeReply: (threadId, replyId) => current().removeReply(threadId, replyId),\n activate: (id, options) => current().activate(id, options),\n startDraft: () => current().startDraft(),\n cancelDraft: () => current().cancelDraft(),\n }\n}\n"],"mappings":"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"}
@@ -40,29 +40,29 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
40
40
  settingsOpen: import("vue").WritableComputedRef<boolean, boolean>;
41
41
  settingsTab: import("vue").WritableComputedRef<string | undefined, string | undefined>;
42
42
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
43
+ "update:modelValue": (markdown: string) => any;
44
+ ready: (editor: Editor) => any;
43
45
  settingsOpen: (tab?: string | undefined) => any;
44
46
  "update:sidebarOpen": (value: boolean) => any;
45
47
  "update:commentsOpen": (value: boolean) => any;
46
- "update:modelValue": (markdown: string) => any;
47
48
  frontmatter: (frontmatter: string | null) => any;
48
- ready: (editor: Editor) => any;
49
49
  metadataCreate: (list: string, item: MetadataItem) => any;
50
50
  contentError: (error: Error) => any;
51
51
  diagnostics: (diagnostics: MarkdownDiagnostic[]) => any;
52
52
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
53
+ "onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
54
+ onReady?: ((editor: Editor) => any) | undefined;
53
55
  onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
54
56
  "onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
55
57
  "onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
56
- "onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
57
58
  onFrontmatter?: ((frontmatter: string | null) => any) | undefined;
58
- onReady?: ((editor: Editor) => any) | undefined;
59
59
  onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
60
60
  onContentError?: ((error: Error) => any) | undefined;
61
61
  onDiagnostics?: ((diagnostics: MarkdownDiagnostic[]) => any) | undefined;
62
62
  }>, {
63
63
  editable: boolean;
64
- settingsUi: "internal" | "external";
65
64
  modelValue: string;
65
+ settingsUi: "internal" | "external";
66
66
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
67
67
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
68
68
  declare const _default: typeof __VLS_export;
@@ -46,19 +46,19 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
46
46
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
47
47
  editor: import("vue").ComputedRef<Editor | undefined>;
48
48
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
49
+ "update:modelValue": (content: JSONContent) => any;
50
+ ready: (editor: Editor) => any;
49
51
  settingsOpen: (tab?: string | undefined) => any;
50
52
  "update:sidebarOpen": (value: boolean) => any;
51
53
  "update:commentsOpen": (value: boolean) => any;
52
- "update:modelValue": (content: JSONContent) => any;
53
- ready: (editor: Editor) => any;
54
54
  metadataCreate: (list: string, item: MetadataItem) => any;
55
55
  contentError: (error: Error) => any;
56
56
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
57
+ "onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
58
+ onReady?: ((editor: Editor) => any) | undefined;
57
59
  onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
58
60
  "onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
59
61
  "onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
60
- "onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
61
- onReady?: ((editor: Editor) => any) | undefined;
62
62
  onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
63
63
  onContentError?: ((error: Error) => any) | undefined;
64
64
  }>, {
@@ -43,19 +43,19 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
43
43
  settingsOpen: import("vue").Ref<boolean, boolean>;
44
44
  settingsTab: import("vue").Ref<string | undefined, string | undefined>;
45
45
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
46
+ "update:modelValue": (content: JSONContent) => any;
47
+ ready: (editor: Editor) => any;
46
48
  settingsOpen: (tab?: string | undefined) => any;
47
49
  "update:sidebarOpen": (value: boolean) => any;
48
50
  "update:commentsOpen": (value: boolean) => any;
49
- "update:modelValue": (content: JSONContent) => any;
50
- ready: (editor: Editor) => any;
51
51
  metadataCreate: (list: string, item: MetadataItem) => any;
52
52
  contentError: (error: Error) => any;
53
53
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
54
+ "onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
55
+ onReady?: ((editor: Editor) => any) | undefined;
54
56
  onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
55
57
  "onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
56
58
  "onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
57
- "onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
58
- onReady?: ((editor: Editor) => any) | undefined;
59
59
  onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
60
60
  onContentError?: ((error: Error) => any) | undefined;
61
61
  }>, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecsplico/editor",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "Vue and React editor library on TipTap v3 (Vue: Nuxt UI; React: shadcn/ui): framework-free features with Vue and React bindings, simple and advanced presets, tree-shakeable entries. Built ESM with type declarations.",
6
6
  "license": "MIT",