@pixerate/editor 0.21.0 → 0.22.0
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/README.md +20 -11
- package/dist/agent/index.cjs +8 -8
- package/dist/agent/index.d.cts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +2 -2
- package/dist/canvas/index.cjs +1269 -13
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +432 -6
- package/dist/canvas/index.d.ts +432 -6
- package/dist/canvas/index.js +1264 -8
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-U6NDQFOK.cjs → chunk-6M3BCTBD.cjs} +427 -138
- package/dist/chunk-6M3BCTBD.cjs.map +1 -0
- package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
- package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
- package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
- package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
- package/dist/{chunk-A7DKIA72.js → chunk-DENSCKFC.js} +642 -69
- package/dist/chunk-DENSCKFC.js.map +1 -0
- package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
- package/dist/chunk-GVSMRPHB.cjs.map +1 -0
- package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
- package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
- package/dist/chunk-MJZBNWG7.js.map +1 -0
- package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
- package/dist/chunk-QGHGJ56O.js.map +1 -0
- package/dist/{chunk-GVW4P7BG.js → chunk-RU52I7QO.js} +305 -16
- package/dist/chunk-RU52I7QO.js.map +1 -0
- package/dist/{chunk-JYRIOFEN.cjs → chunk-SAZRENEZ.cjs} +647 -74
- package/dist/chunk-SAZRENEZ.cjs.map +1 -0
- package/dist/{controller-OUdxPY-X.d.ts → controller-B7GIybc4.d.cts} +139 -7
- package/dist/{controller-OUdxPY-X.d.cts → controller-B7GIybc4.d.ts} +139 -7
- package/dist/extensions/index.cjs +14 -4
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +3 -3
- package/dist/extensions/index.d.ts +3 -3
- package/dist/extensions/index.js +13 -3
- package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
- package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
- package/dist/index.cjs +250 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -8
- package/dist/index.d.ts +294 -8
- package/dist/index.js +241 -7
- package/dist/index.js.map +1 -1
- package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
- package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
- package/dist/serializers/index.cjs +3 -3
- package/dist/serializers/index.d.cts +1 -1
- package/dist/serializers/index.d.ts +1 -1
- package/dist/serializers/index.js +2 -2
- package/dist/spreadsheet/index.cjs +7 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +33 -4
- package/dist/spreadsheet/index.d.ts +33 -4
- package/dist/spreadsheet/index.js +6 -2
- package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
- package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
- package/dist/{types-DMe0vFB1.d.cts → types-CJ4U-HHI.d.cts} +1 -1
- package/dist/{types-DMe0vFB1.d.ts → types-CJ4U-HHI.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3TYMNULT.js.map +0 -1
- package/dist/chunk-4QPLEUL2.cjs.map +0 -1
- package/dist/chunk-A7DKIA72.js.map +0 -1
- package/dist/chunk-CYFJCKC4.js.map +0 -1
- package/dist/chunk-GVW4P7BG.js.map +0 -1
- package/dist/chunk-JYRIOFEN.cjs.map +0 -1
- package/dist/chunk-TVRC4KVT.cjs.map +0 -1
- package/dist/chunk-U6NDQFOK.cjs.map +0 -1
- /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-TVRC4KVT.cjs","../src/extensions/comment-store.ts","../src/extensions/track-changes.ts","../src/serializers/plain-text.ts","../src/serializers/mentions.ts","../src/serializers/markdown.ts"],"names":["Slice","match"],"mappings":"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T;AACE;AACA;AACA;AAGK;AACP;AACE;AACA;AAIK;AACE;AAII;AACA;AAMA;AA4DA;AACH,EAAA;AACA,kBAAA;AAER,EAAA;AACO,IAAA;AACP,EAAA;AAEA,kBAAA;AACO,IAAA;AACL,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,kBAAA;AAA+C;AAG3C,EAAA;AACG,IAAA;AACA,IAAA;AACC,MAAA;AACF,QAAA;AACF,MAAA;AACE,QAAA;AACF,MAAA;AACD,IAAA;AACH,EAAA;AACF;AAea;AAsBJ;AACA,EAAA;AACT;AAEM;AACJ,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAEF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAKa;AACL,EAAA;AACN,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACE,QAAA;AAEF,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AAEE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AACD,MAAA;AACD,MAAA;AACF,IAAA;AACF,EAAA;AACD;AAMY;AACL,EAAA;AACN,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACE,QAAA;AAEF,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AAGE,IAAA;AACE,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AAEE,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AACD,MAAA;AACD,MAAA;AACF,IAAA;AACF,EAAA;AACD;AAEQ;AACF,EAAA;AACD,EAAA;AACA,E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AAA;AACA,EAAA;AACJ,EAAA;AACQ,EAAA;AACA,IAAA;AACD,IAAA;AACH,MAAA;AACF,IAAA;AACM,IAAA;AACF,IAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AACM,IAAA;AAEH,IAAA;AAEG,IAAA;AAEJ,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAgBgB;AACP,EAAA;AACT;AAES;AACH,EAAA;AACA,EAAA;AACA,EAAA;AACJ,EAAA;AACQ,EAAA;AACN,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEM;AACA;AACH;AAIA;AAgBa;AACT,EAAA;AACE,EAAA;AACL,IAAA;AACC,IAAA;AACC,MAAA;AACA,MAAA;AACA,MAAA;AAGA,MAAA;AACF,IAAA;AACF,EAAA;AACF;AAgCgB;AAIT,EAAA;AACD,EAAA;AACA,EAAA;AACF,IAAA;AACF,EAAA;AAEE,EAAA;AAIM,IAAA;AACJ,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACO,EAAA;AACT;AASgB;AACT,EAAA;AACC,EAAA;AACF,EAAA;AACI,IAAA;AACF,IAAA;AACF,MAAA;AACI,MAAA;AACN,IAAA;AACA,IAAA;AACF,EAAA;AACO,EAAA;AACT;ALglCU;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-TVRC4KVT.cjs","sourcesContent":[null,"/**\n * Who made a comment, reply or tracked change.\n */\nexport interface ReviewAuthor {\n id: string;\n name: string;\n color?: string;\n avatarUrl?: string;\n}\n\n/**\n * Where a comment thread is attached in the document. `from`/`to` are\n * ProseMirror positions and `quote` is the commented text at the time the\n * anchor was last synced. Positions are only meaningful for the current\n * document; the `comment` mark in the document is the source of truth.\n */\nexport interface CommentAnchor {\n from: number;\n to: number;\n quote: string;\n}\n\nexport interface CommentReply {\n id: string;\n author: ReviewAuthor;\n body: string;\n createdAt: number;\n editedAt?: number;\n}\n\nexport interface CommentThread {\n id: string;\n /** `null` while the thread has no text in the document (see `orphaned`). */\n anchor: CommentAnchor | null;\n author: ReviewAuthor;\n createdAt: number;\n /** An empty body marks a draft that has not been posted yet. */\n body: string;\n editedAt?: number;\n replies: CommentReply[];\n resolved: boolean;\n resolvedAt?: number;\n resolvedBy?: ReviewAuthor;\n /**\n * True when the commented text was deleted from the document. Undoing the\n * deletion re-attaches the thread.\n */\n orphaned: boolean;\n /** The quote the thread had before it was orphaned. */\n lastQuote?: string;\n}\n\nexport interface CommentStoreSnapshot {\n /** Threads in document order (anchored first), then orphaned threads. */\n threads: readonly CommentThread[];\n activeThreadId: string | null;\n}\n\n/** JSON shape produced by `CommentStore.toJSON()`. */\nexport interface SerializedComments {\n version: 1;\n threads: CommentThread[];\n}\n\nexport interface CommentStoreOptions {\n threads?: CommentThread[] | SerializedComments;\n generateId?: () => string;\n now?: () => number;\n}\n\nexport interface CreateThreadInput {\n id?: string;\n body: string;\n author: ReviewAuthor;\n anchor?: CommentAnchor | null;\n createdAt?: number;\n}\n\nexport type CommentStoreListener = (snapshot: CommentStoreSnapshot) => void;\n\nlet idCounter = 0;\n\n/**\n * Generates a reasonably unique id without depending on `crypto`.\n */\nexport function createReviewId(prefix = \"c\"): string {\n idCounter = (idCounter + 1) % 1e6;\n return `${prefix}-${Date.now().toString(36)}-${idCounter.toString(36)}-${Math.random()\n .toString(36)\n .slice(2, 8)}`;\n}\n\nfunction sameAnchor(a: CommentAnchor | null, b: CommentAnchor | null): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n return a.from === b.from && a.to === b.to && a.quote === b.quote;\n}\n\nfunction normalizeThread(raw: any): CommentThread {\n return {\n id: String(raw.id),\n anchor: raw.anchor ?? null,\n author: raw.author ?? { id: \"unknown\", name: \"Unknown\" },\n createdAt: Number(raw.createdAt ?? 0),\n body: String(raw.body ?? \"\"),\n ...(raw.editedAt !== undefined ? { editedAt: raw.editedAt } : {}),\n replies: Array.isArray(raw.replies) ? raw.replies.map((r: any) => ({ ...r })) : [],\n resolved: !!raw.resolved,\n ...(raw.resolvedAt !== undefined ? { resolvedAt: raw.resolvedAt } : {}),\n ...(raw.resolvedBy !== undefined ? { resolvedBy: raw.resolvedBy } : {}),\n orphaned: !!raw.orphaned,\n ...(raw.lastQuote !== undefined ? { lastQuote: String(raw.lastQuote) } : {}),\n };\n}\n\n/**\n * Headless, framework-agnostic store of comment threads.\n *\n * The store never talks to a server: persist `toJSON()` yourself and pass it\n * back via `new CommentStore({ threads })` or `load()`. Snapshots are\n * immutable and only replaced when something changes, so `getSnapshot` is safe\n * to use with React's `useSyncExternalStore`. `subscribe` calls the listener\n * immediately (Svelte store contract).\n */\nexport class CommentStore {\n private threads: CommentThread[] = [];\n private activeThreadId: string | null = null;\n private snapshot: CommentStoreSnapshot;\n private listeners = new Set<CommentStoreListener>();\n /** Deleted threads, kept so undoing a \"Delete Comment\" can restore them. */\n private deleted = new Map<string, CommentThread>();\n private generateId: () => string;\n private now: () => number;\n\n constructor(options: CommentStoreOptions = {}) {\n this.generateId = options.generateId ?? (() => createReviewId(\"comment\"));\n this.now = options.now ?? (() => Date.now());\n if (options.threads) {\n this.threads = CommentStore.parse(options.threads);\n }\n this.snapshot = this.computeSnapshot();\n }\n\n /** Parses `toJSON()` output (or a plain thread array). */\n static parse(json: string | CommentThread[] | SerializedComments): CommentThread[] {\n const value = typeof json === \"string\" ? JSON.parse(json) : json;\n const list = Array.isArray(value) ? value : value?.threads;\n if (!Array.isArray(list)) return [];\n return list.filter((t: any) => t && t.id != null).map(normalizeThread);\n }\n\n subscribe = (listener: CommentStoreListener): (() => void) => {\n this.listeners.add(listener);\n listener(this.snapshot);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): CommentStoreSnapshot => this.snapshot;\n\n getThread(id: string): CommentThread | undefined {\n return this.threads.find((t) => t.id === id);\n }\n\n createThread(input: CreateThreadInput): CommentThread {\n const thread: CommentThread = {\n id: input.id ?? this.generateId(),\n anchor: input.anchor ?? null,\n author: input.author,\n createdAt: input.createdAt ?? this.now(),\n body: input.body,\n replies: [],\n resolved: false,\n orphaned: false,\n };\n this.threads = [...this.threads.filter((t) => t.id !== thread.id), thread];\n this.deleted.delete(thread.id);\n this.emit();\n return thread;\n }\n\n /** Edits the thread's body, or one of its replies when `replyId` is given. */\n editComment(threadId: string, body: string, replyId?: string): boolean {\n const editedAt = this.now();\n return this.updateThread(threadId, (t) => {\n if (!replyId) return { ...t, body, editedAt };\n if (!t.replies.some((r) => r.id === replyId)) return t;\n return {\n ...t,\n replies: t.replies.map((r) => (r.id === replyId ? { ...r, body, editedAt } : r)),\n };\n });\n }\n\n addReply(\n threadId: string,\n input: { body: string; author: ReviewAuthor; id?: string },\n ): CommentReply | null {\n const reply: CommentReply = {\n id: input.id ?? this.generateId(),\n author: input.author,\n body: input.body,\n createdAt: this.now(),\n };\n const ok = this.updateThread(threadId, (t) => ({\n ...t,\n replies: [...t.replies, reply],\n // Replying to a resolved thread re-opens it, as in Word and Pages.\n resolved: false,\n resolvedAt: undefined,\n resolvedBy: undefined,\n }));\n return ok ? reply : null;\n }\n\n deleteReply(threadId: string, replyId: string): boolean {\n return this.updateThread(threadId, (t) =>\n t.replies.some((r) => r.id === replyId)\n ? { ...t, replies: t.replies.filter((r) => r.id !== replyId) }\n : t,\n );\n }\n\n resolveThread(id: string, resolved = true, by?: ReviewAuthor): boolean {\n return this.updateThread(id, (t) =>\n t.resolved === resolved\n ? t\n : resolved\n ? { ...t, resolved, resolvedAt: this.now(), ...(by ? { resolvedBy: by } : {}) }\n : { ...t, resolved, resolvedAt: undefined, resolvedBy: undefined },\n );\n }\n\n deleteThread(id: string): boolean {\n const thread = this.getThread(id);\n if (!thread) return false;\n this.deleted.set(id, thread);\n this.threads = this.threads.filter((t) => t.id !== id);\n if (this.activeThreadId === id) this.activeThreadId = null;\n this.emit();\n return true;\n }\n\n /** Deletes every thread whose text is no longer in the document. */\n removeOrphanedThreads(): string[] {\n const ids = this.threads.filter((t) => t.orphaned).map((t) => t.id);\n if (ids.length === 0) return ids;\n ids.forEach((id) => this.deleted.set(id, this.getThread(id)!));\n this.threads = this.threads.filter((t) => !t.orphaned);\n if (this.activeThreadId && ids.includes(this.activeThreadId)) {\n this.activeThreadId = null;\n }\n this.emit();\n return ids;\n }\n\n setActiveThread(id: string | null): void {\n const next = id && this.getThread(id) ? id : null;\n if (next === this.activeThreadId) return;\n this.activeThreadId = next;\n this.emit();\n }\n\n /**\n * Updates anchors from the document. Threads missing from `anchors` become\n * orphaned. Threads that were deleted but whose mark re-appeared (e.g. after\n * undoing \"Delete Comment\") are restored. Returns true if anything changed.\n */\n syncAnchors(anchors: Map<string, CommentAnchor>): boolean {\n let changed = false;\n const restored: CommentThread[] = [];\n anchors.forEach((_anchor, id) => {\n if (!this.getThread(id) && this.deleted.has(id)) {\n restored.push(this.deleted.get(id)!);\n this.deleted.delete(id);\n }\n });\n const next = [...this.threads, ...restored].map((t) => {\n const anchor = anchors.get(t.id) ?? null;\n const orphaned = anchor === null;\n if (sameAnchor(t.anchor, anchor) && t.orphaned === orphaned) return t;\n changed = true;\n // Keep the last known anchor quote for orphaned threads so the UI can\n // still show what was commented on.\n return orphaned\n ? { ...t, anchor: null, orphaned, ...(t.anchor ? { lastQuote: t.anchor.quote } : {}) }\n : { ...t, anchor, orphaned };\n });\n if (!changed && restored.length === 0) return false;\n this.threads = next;\n this.emit();\n return true;\n }\n\n /** Replaces all threads, e.g. with data loaded from a server. */\n load(json: string | CommentThread[] | SerializedComments): void {\n this.threads = CommentStore.parse(json);\n this.deleted.clear();\n if (this.activeThreadId && !this.getThread(this.activeThreadId)) {\n this.activeThreadId = null;\n }\n this.emit();\n }\n\n clear(): void {\n this.threads = [];\n this.deleted.clear();\n this.activeThreadId = null;\n this.emit();\n }\n\n toJSON(): SerializedComments {\n return {\n version: 1,\n threads: this.threads.map((t) => ({ ...t, replies: t.replies.map((r) => ({ ...r })) })),\n };\n }\n\n private updateThread(id: string, fn: (t: CommentThread) => CommentThread): boolean {\n const index = this.threads.findIndex((t) => t.id === id);\n if (index === -1) return false;\n const updated = fn(this.threads[index]);\n if (updated === this.threads[index]) return true;\n this.threads = this.threads.map((t, i) => (i === index ? updated : t));\n this.emit();\n return true;\n }\n\n private computeSnapshot(): CommentStoreSnapshot {\n const threads = [...this.threads].sort((a, b) => {\n if (a.anchor && b.anchor) {\n return a.anchor.from - b.anchor.from || a.createdAt - b.createdAt;\n }\n if (a.anchor) return -1;\n if (b.anchor) return 1;\n return a.createdAt - b.createdAt;\n });\n return { threads, activeThreadId: this.activeThreadId };\n }\n\n private emit(): void {\n this.snapshot = this.computeSnapshot();\n const snapshot = this.snapshot;\n this.listeners.forEach((listener) => {\n try {\n listener(snapshot);\n } catch (err) {\n console.error(\"[CommentStore] Error in listener:\", err);\n }\n });\n }\n}\n\nexport function createCommentStore(options?: CommentStoreOptions): CommentStore {\n return new CommentStore(options);\n}\n\n/**\n * Short, human-readable timestamp for review UIs (\"just now\", \"5m ago\",\n * \"3h ago\", then a locale date). Shared by the React and Svelte components.\n */\nexport function formatReviewTime(timestamp: number, now: number = Date.now()): string {\n const seconds = Math.round((now - timestamp) / 1000);\n if (seconds < 45) return \"just now\";\n const minutes = Math.round(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.round(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n return new Date(timestamp).toLocaleDateString();\n}\n","import { Extension, Mark, mergeAttributes, type Editor } from \"@tiptap/core\";\nimport {\n Plugin,\n PluginKey,\n TextSelection,\n type EditorState,\n type Transaction,\n} from \"@tiptap/pm/state\";\nimport {\n Fragment,\n Slice,\n type MarkType,\n type Node as ProsemirrorNode,\n type NodeType,\n} from \"@tiptap/pm/model\";\nimport { Mapping, ReplaceStep, Transform, type StepMap } from \"@tiptap/pm/transform\";\nimport type { EditorView } from \"@tiptap/pm/view\";\nimport { createReviewId, type ReviewAuthor } from \"./comment-store\";\n\nexport const INSERTION_MARK_NAME = \"insertion\";\nexport const DELETION_MARK_NAME = \"deletion\";\n\n/**\n * Set this transaction meta to `true` to apply a change without tracking it,\n * e.g. `editor.chain().command(({ tr }) => { tr.setMeta(TRACK_CHANGES_SKIP_META, true); return true; }).setContent(…).run()`.\n */\nexport const TRACK_CHANGES_SKIP_META = \"trackChangesSkip\";\n\nexport type TrackedChangeType = \"insertion\" | \"deletion\";\n\n/**\n * How tracked content is rendered by serializers:\n * - `accepted` (default): as if every change were accepted (deleted text is\n * dropped, inserted text kept).\n * - `original`: as if every change were rejected.\n * - `all`: both inserted and deleted text.\n */\nexport type TrackedChangesView = \"accepted\" | \"original\" | \"all\";\n\nexport interface TrackedChangeAttributes {\n changeId: string;\n authorId: string;\n authorName: string;\n authorColor: string | null;\n createdAt: number;\n}\n\nexport interface TrackedChange {\n id: string;\n type: TrackedChangeType;\n author: ReviewAuthor;\n createdAt: number;\n /** Start of the first range. */\n from: number;\n /** End of the last range. */\n to: number;\n /** A change can be split into several ranges by other marks or blocks. */\n ranges: { from: number; to: number }[];\n text: string;\n}\n\nexport interface TrackChangesSnapshot {\n enabled: boolean;\n author: ReviewAuthor;\n /** Changes in document order. */\n changes: readonly TrackedChange[];\n /** The change at the selection, if any. */\n currentChangeId: string | null;\n}\n\nexport interface TrackChangesOptions {\n /** Whether tracking is on when the editor is created. */\n enabled: boolean;\n author: ReviewAuthor;\n now: () => number;\n generateId: () => string;\n insertionHTMLAttributes: Record<string, any>;\n deletionHTMLAttributes: Record<string, any>;\n}\n\n/**\n * External store over the editor's tracked-change state. Snapshots only\n * change when the document, selection or tracking state changes, so they can\n * be used with `useSyncExternalStore`. `subscribe` calls the listener\n * immediately (Svelte store contract).\n */\nexport class TrackChangesStore {\n private snapshot: TrackChangesSnapshot;\n private listeners = new Set<(snapshot: TrackChangesSnapshot) => void>();\n\n constructor(initial: TrackChangesSnapshot) {\n this.snapshot = initial;\n }\n\n subscribe = (listener: (snapshot: TrackChangesSnapshot) => void): (() => void) => {\n this.listeners.add(listener);\n listener(this.snapshot);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): TrackChangesSnapshot => this.snapshot;\n\n /** @internal Called by the extension's plugin view. */\n set(snapshot: TrackChangesSnapshot): void {\n this.snapshot = snapshot;\n this.listeners.forEach((listener) => {\n try {\n listener(snapshot);\n } catch (err) {\n console.error(\"[TrackChangesStore] Error in listener:\", err);\n }\n });\n }\n}\n\nexport interface TrackChangesStorage {\n store: TrackChangesStore;\n}\n\ninterface TrackChangesPluginState {\n enabled: boolean;\n author: ReviewAuthor;\n}\n\ntype TrackChangesMeta =\n | { type: \"setEnabled\"; enabled: boolean }\n | { type: \"setAuthor\"; author: ReviewAuthor };\n\nexport const trackChangesPluginKey = new PluginKey<TrackChangesPluginState>(\"trackChanges\");\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n trackChanges: {\n setTrackChanges: (enabled: boolean) => ReturnType;\n toggleTrackChanges: () => ReturnType;\n setTrackChangesAuthor: (author: ReviewAuthor) => ReturnType;\n /** Accepts one change (single undo step). */\n acceptChange: (id: string) => ReturnType;\n /** Rejects one change (single undo step). */\n rejectChange: (id: string) => ReturnType;\n acceptAllChanges: () => ReturnType;\n rejectAllChanges: () => ReturnType;\n /** Selects the next change after the selection, wrapping around. */\n selectNextChange: () => ReturnType;\n /** Selects the previous change before the selection, wrapping around. */\n selectPreviousChange: () => ReturnType;\n };\n }\n}\n\nfunction changeAttributes(mark: { attrs: Record<string, any> }): TrackedChangeAttributes {\n return mark.attrs as TrackedChangeAttributes;\n}\n\nconst sharedAttributes = () => ({\n changeId: {\n default: null,\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-change-id\"),\n renderHTML: (attrs: any) => ({ \"data-change-id\": attrs.changeId }),\n },\n authorId: {\n default: \"\",\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-author-id\") ?? \"\",\n renderHTML: (attrs: any) => ({ \"data-author-id\": attrs.authorId }),\n },\n authorName: {\n default: \"\",\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-author-name\") ?? \"\",\n renderHTML: (attrs: any) => ({ \"data-author-name\": attrs.authorName }),\n },\n authorColor: {\n default: null,\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-author-color\"),\n renderHTML: (attrs: any) =>\n attrs.authorColor ? { \"data-author-color\": attrs.authorColor } : {},\n },\n createdAt: {\n default: 0,\n parseHTML: (el: HTMLElement) => Number(el.getAttribute(\"data-created-at\") ?? 0),\n renderHTML: (attrs: any) => ({ \"data-created-at\": String(attrs.createdAt) }),\n },\n});\n\n/**\n * Text inserted while tracking was on. Rendered as `<ins>`.\n */\nexport const TrackedInsertion = Mark.create<{ HTMLAttributes: Record<string, any> }>({\n name: INSERTION_MARK_NAME,\n inclusive: false,\n addOptions() {\n return {\n HTMLAttributes: {\n class:\n \"pixerate-insertion underline decoration-emerald-500 text-emerald-700 dark:text-emerald-400\",\n },\n };\n },\n addStorage() {\n // tiptap-markdown: inserted text is exported as plain text.\n return { markdown: { serialize: { open: \"\", close: \"\" }, parse: {} } };\n },\n addAttributes: sharedAttributes,\n parseHTML() {\n return [{ tag: 'ins[data-change-type=\"insertion\"]', priority: 100 }];\n },\n renderHTML({ HTMLAttributes }) {\n return [\n \"ins\",\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n \"data-change-type\": \"insertion\",\n }),\n 0,\n ];\n },\n});\n\n/**\n * Text deleted while tracking was on. The text stays in the document until\n * the change is accepted. Rendered as `<del>` (struck through).\n */\nexport const TrackedDeletion = Mark.create<{ HTMLAttributes: Record<string, any> }>({\n name: DELETION_MARK_NAME,\n inclusive: false,\n addOptions() {\n return {\n HTMLAttributes: {\n class:\n \"pixerate-deletion line-through decoration-red-500 text-red-600 dark:text-red-400\",\n },\n };\n },\n addStorage() {\n // Only reached when serializing a document that still has deletions\n // (`getEditorMarkdown(editor, { trackedChanges: \"all\" })`).\n return {\n markdown: {\n serialize: { open: \"~~\", close: \"~~\", mixable: true, expelEnclosingWhitespace: true },\n parse: {},\n },\n };\n },\n addAttributes: sharedAttributes,\n parseHTML() {\n // Higher priority than Strike, which also parses `<del>`.\n return [{ tag: 'del[data-change-type=\"deletion\"]', priority: 100 }];\n },\n renderHTML({ HTMLAttributes }) {\n return [\n \"del\",\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n \"data-change-type\": \"deletion\",\n }),\n 0,\n ];\n },\n});\n\nfunction toDoc(source: any): ProsemirrorNode | null {\n if (!source) return null;\n if (source.state?.doc) return source.state.doc;\n if (source.doc && typeof source.doc.descendants === \"function\") return source.doc;\n if (typeof source.descendants === \"function\") return source;\n return null;\n}\n\n/**\n * Lists tracked changes in document order. Accepts an editor, an editor\n * state or a ProseMirror document.\n */\nexport function getTrackedChanges(\n source: Editor | EditorState | ProsemirrorNode | null | undefined,\n): TrackedChange[] {\n const doc = toDoc(source);\n if (!doc) return [];\n const byKey = new Map<string, TrackedChange>();\n doc.descendants((node, pos) => {\n if (!node.isInline) return;\n node.marks.forEach((mark) => {\n const type = mark.type.name;\n if (type !== INSERTION_MARK_NAME && type !== DELETION_MARK_NAME) return;\n const attrs = changeAttributes(mark);\n if (!attrs.changeId) return;\n const key = `${type}:${attrs.changeId}`;\n const end = pos + node.nodeSize;\n const text = node.isText ? node.text ?? \"\" : \"\";\n let change = byKey.get(key);\n if (!change) {\n change = {\n id: attrs.changeId,\n type: type as TrackedChangeType,\n author: {\n id: attrs.authorId,\n name: attrs.authorName,\n ...(attrs.authorColor ? { color: attrs.authorColor } : {}),\n },\n createdAt: attrs.createdAt,\n from: pos,\n to: end,\n ranges: [],\n text: \"\",\n };\n byKey.set(key, change);\n }\n const last = change.ranges[change.ranges.length - 1];\n if (last && last.to === pos) {\n last.to = end;\n } else {\n change.ranges.push({ from: pos, to: end });\n }\n change.to = end;\n change.text += text;\n });\n });\n return Array.from(byKey.values()).sort((a, b) => a.from - b.from || a.to - b.to);\n}\n\n/** The change containing `pos` (defaults to the selection start). */\nexport function getTrackedChangeAt(\n state: EditorState,\n pos: number = state.selection.from,\n changes: readonly TrackedChange[] = getTrackedChanges(state.doc),\n): TrackedChange | null {\n const { from, to } = state.selection;\n // A selection exactly covering a change (e.g. after selectNextChange).\n const exact = changes.find((c) => c.from === from && c.to === to);\n if (exact) return exact;\n return (\n changes.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ??\n changes.find((c) => c.ranges.some((r) => r.from <= pos && pos <= r.to)) ??\n null\n );\n}\n\nexport function isTrackChangesEnabled(source: Editor | EditorState | null | undefined): boolean {\n const state: EditorState | undefined = (source as any)?.state ?? (source as any);\n if (!state || typeof (state as any).plugins === \"undefined\") return false;\n return trackChangesPluginKey.getState(state)?.enabled ?? false;\n}\n\n/** Returns the tracked-changes store of an editor using `TrackChanges`. */\nexport function getTrackChangesStore(editor: Editor | null | undefined): TrackChangesStore | null {\n return (editor?.storage as any)?.trackChanges?.store ?? null;\n}\n\n/**\n * Accepts or rejects changes on a transform. Inline marks are removed first,\n * then text is deleted from the end of the document backwards so positions\n * stay valid.\n */\nfunction applyDecision(\n tr: Transform,\n changes: readonly TrackedChange[],\n decision: \"accept\" | \"reject\",\n): void {\n const schema = tr.doc.type.schema;\n const deletions: { from: number; to: number }[] = [];\n changes.forEach((change) => {\n const markType = schema.marks[change.type];\n if (!markType) return;\n const keep = (change.type === \"insertion\") === (decision === \"accept\");\n change.ranges.forEach(({ from, to }) => {\n if (keep) {\n tr.removeMark(from, to, markType);\n } else {\n deletions.push({ from, to });\n }\n });\n });\n deletions\n .sort((a, b) => b.from - a.from)\n .forEach(({ from, to }) => {\n tr.delete(tr.mapping.map(from), tr.mapping.map(to));\n });\n}\n\n/**\n * Returns a copy of `doc` with tracked changes resolved for `view`\n * (`accepted` by default). The input document is not modified.\n */\nexport function resolveTrackedChanges(\n doc: ProsemirrorNode,\n view: TrackedChangesView = \"accepted\",\n): ProsemirrorNode {\n if (view === \"all\") return doc;\n const changes = getTrackedChanges(doc);\n if (changes.length === 0) return doc;\n const tr = new Transform(doc);\n applyDecision(tr, changes, view === \"accepted\" ? \"accept\" : \"reject\");\n return tr.doc;\n}\n\nfunction hasInlineContent(fragment: Fragment): boolean {\n let found = false;\n fragment.descendants((node) => {\n if (found) return false;\n if (node.isInline) found = true;\n return !found;\n });\n return found;\n}\n\n/**\n * Removes deleted text and insertion marks from a slice (used for paste), so\n * pasting tracked content pastes it as if all changes were accepted.\n */\nfunction acceptSlice(slice: Slice, insertion?: MarkType, deletion?: MarkType): Slice {\n if (!insertion && !deletion) return slice;\n const mapFragment = (fragment: Fragment): Fragment => {\n const children: ProsemirrorNode[] = [];\n fragment.forEach((child) => {\n if (child.isInline) {\n if (deletion && deletion.isInSet(child.marks)) return;\n children.push(insertion ? child.mark(insertion.removeFromSet(child.marks)) : child);\n } else {\n children.push(child.copy(mapFragment(child.content)));\n }\n });\n return Fragment.from(children);\n };\n return new Slice(mapFragment(slice.content), slice.openStart, slice.openEnd);\n}\n\ninterface TrackContext {\n author: ReviewAuthor;\n now: () => number;\n generateId: () => string;\n insertion: MarkType;\n deletion: MarkType;\n}\n\nfunction newAttributes(ctx: TrackContext): TrackedChangeAttributes {\n return {\n changeId: ctx.generateId(),\n authorId: ctx.author.id,\n authorName: ctx.author.name,\n authorColor: ctx.author.color ?? null,\n createdAt: ctx.now(),\n };\n}\n\n/**\n * Reuses the attributes of an adjacent change of the same type by the same\n * author, so typing (or backspacing) a word produces one change, not one per\n * character.\n */\nfunction adjacentAttributes(\n doc: ProsemirrorNode,\n from: number,\n to: number,\n type: MarkType,\n ctx: TrackContext,\n): TrackedChangeAttributes | null {\n const candidates = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];\n for (const node of candidates) {\n const mark = node && type.isInSet(node.marks);\n if (mark && mark.attrs.authorId === ctx.author.id && mark.attrs.changeId) {\n return changeAttributes(mark);\n }\n }\n return null;\n}\n\n/**\n * Marks deleted content with the deletion mark so it can be put back.\n * Text inserted by the same author (an untracked-in-effect draft) is dropped,\n * i.e. really deleted. Returns `null` if the slice holds content that cannot\n * carry the mark (e.g. code blocks), in which case the deletion is not\n * tracked.\n */\nfunction markDeletedSlice(\n slice: Slice,\n parentType: NodeType,\n attrs: TrackedChangeAttributes,\n ctx: TrackContext,\n): { slice: Slice; hasContent: boolean } | null {\n let ok = true;\n let hasContent = false;\n const deletionMark = ctx.deletion.create(attrs);\n const mapFragment = (fragment: Fragment, parent: NodeType): Fragment => {\n const children: ProsemirrorNode[] = [];\n fragment.forEach((child) => {\n if (child.isInline) {\n const insertion = ctx.insertion.isInSet(child.marks);\n if (insertion && insertion.attrs.authorId === ctx.author.id) return;\n hasContent = true;\n if (ctx.deletion.isInSet(child.marks)) {\n children.push(child);\n return;\n }\n if (!parent.allowsMarkType(ctx.deletion)) {\n ok = false;\n children.push(child);\n return;\n }\n children.push(child.mark(deletionMark.addToSet(child.marks)));\n } else {\n children.push(child.copy(mapFragment(child.content, child.type)));\n }\n });\n return Fragment.from(children);\n };\n const content = mapFragment(slice.content, parentType);\n if (!ok) return null;\n return { slice: new Slice(content, slice.openStart, slice.openEnd), hasContent };\n}\n\nfunction shouldSkip(tr: Transaction): boolean {\n return (\n !tr.docChanged ||\n tr.getMeta(TRACK_CHANGES_SKIP_META) === true ||\n !!tr.getMeta(trackChangesPluginKey) ||\n // Undo/redo replays already-tracked history.\n !!tr.getMeta(\"history$\") ||\n // Remote collaborative changes were tracked by their author.\n !!tr.getMeta(\"y-sync$\") ||\n // Housekeeping appended by other plugins (e.g. trailing nodes).\n !!tr.getMeta(\"appendedTransaction\")\n );\n}\n\ninterface PendingRestore {\n pos: number;\n slice: Slice;\n parentType: NodeType;\n /** Nothing was inserted in place of the deleted content. */\n pureDelete: boolean;\n backspace: boolean;\n}\n\n/**\n * Builds a transaction that turns the user's raw edits into tracked changes:\n * inserted content gets the insertion mark, deleted content is put back with\n * the deletion mark. Block structure changes (splitting/joining paragraphs,\n * wrapping, lifting) and formatting changes are not tracked.\n */\nfunction trackTransactions(\n transactions: readonly Transaction[],\n oldState: EditorState,\n newState: EditorState,\n ctx: TrackContext,\n): Transaction | null {\n const maps: StepMap[] = [];\n transactions.forEach((t) => maps.push(...(t.mapping.maps as StepMap[])));\n\n const inserted: { from: number; to: number }[] = [];\n const restores: PendingRestore[] = [];\n let mapIndex = 0;\n const single =\n transactions.length === 1 && transactions[0].steps.length === 1;\n\n transactions.forEach((t) => {\n const skip = shouldSkip(t);\n t.steps.forEach((step, i) => {\n const index = mapIndex + i;\n if (skip || !(step instanceof ReplaceStep)) return;\n const { from, to, slice } = step as unknown as { from: number; to: number; slice: Slice };\n const rest = new Mapping(maps.slice(index + 1));\n if (slice.size > 0 && hasInlineContent(slice.content)) {\n const start = rest.map(from, 1);\n const end = rest.map(from + slice.size, -1);\n if (end > start) inserted.push({ from: start, to: end });\n }\n if (to > from) {\n const docBefore = t.docs[i];\n const deleted = docBefore.slice(from, to);\n if (!hasInlineContent(deleted.content)) return;\n const $from = docBefore.resolve(from);\n const before = i === 0 ? t.before : null;\n const sel = before === oldState.doc ? oldState.selection : null;\n restores.push({\n pos: rest.map(from, -1),\n slice: deleted,\n parentType: $from.node(Math.max(0, $from.depth - deleted.openStart)).type,\n pureDelete: slice.size === 0,\n backspace:\n single && slice.size === 0 && !!sel && sel.empty && sel.from === to,\n });\n }\n });\n mapIndex += t.steps.length;\n });\n\n if (inserted.length === 0 && restores.length === 0) return null;\n\n const tr = newState.tr;\n let changed = false;\n\n inserted.forEach(({ from, to }) => {\n tr.removeMark(from, to, ctx.deletion);\n tr.removeMark(from, to, ctx.insertion);\n const attrs = adjacentAttributes(tr.doc, from, to, ctx.insertion, ctx) ?? newAttributes(ctx);\n tr.addMark(from, to, ctx.insertion.create(attrs));\n changed = true;\n });\n\n let cursor: number | null = null;\n restores\n .sort((a, b) => b.pos - a.pos)\n .forEach((restore) => {\n const pos = tr.mapping.map(restore.pos, -1);\n const attrs = adjacentAttributes(tr.doc, pos, pos, ctx.deletion, ctx) ?? newAttributes(ctx);\n const marked = markDeletedSlice(restore.slice, restore.parentType, attrs, ctx);\n if (!marked || !marked.hasContent) return;\n const sizeBefore = tr.doc.content.size;\n try {\n tr.replace(pos, pos, marked.slice);\n } catch {\n return;\n }\n changed = true;\n // Backspace leaves the caret before the struck-through text so the\n // next Backspace continues backwards; Delete leaves it after.\n if (restores.length === 1 && restore.pureDelete && newState.selection.empty) {\n cursor = restore.backspace ? pos : pos + (tr.doc.content.size - sizeBefore);\n }\n });\n\n if (!changed) return null;\n if (cursor !== null) {\n try {\n tr.setSelection(TextSelection.create(tr.doc, cursor));\n } catch {\n // Keep the mapped selection.\n }\n }\n tr.setMeta(TRACK_CHANGES_SKIP_META, true);\n return tr;\n}\n\nfunction buildSnapshot(state: EditorState, author: ReviewAuthor): TrackChangesSnapshot {\n const plugin = trackChangesPluginKey.getState(state);\n const changes = getTrackedChanges(state.doc);\n return {\n enabled: plugin?.enabled ?? false,\n author: plugin?.author ?? author,\n changes,\n currentChangeId: getTrackedChangeAt(state, state.selection.from, changes)?.id ?? null,\n };\n}\n\n/**\n * Suggestion mode / track changes. When enabled, typed and pasted content is\n * marked as an insertion and deleted content is kept but marked as a\n * deletion (struck through) until the change is accepted or rejected.\n *\n * Deleting your own insertion really deletes it. Formatting-only changes and\n * block-structure changes (splitting/joining paragraphs, lists) are not\n * tracked. IME composition is tracked once the composition ends.\n */\nexport const TrackChanges = Extension.create<TrackChangesOptions, TrackChangesStorage>({\n name: \"trackChanges\",\n\n addOptions() {\n return {\n enabled: false,\n author: { id: \"anonymous\", name: \"Anonymous\" },\n now: () => Date.now(),\n generateId: () => createReviewId(\"change\"),\n insertionHTMLAttributes: {},\n deletionHTMLAttributes: {},\n };\n },\n\n addStorage() {\n return {\n store: new TrackChangesStore({\n enabled: this.options.enabled,\n author: this.options.author,\n changes: [],\n currentChangeId: null,\n }),\n };\n },\n\n addExtensions() {\n const insertionAttrs = this.options?.insertionHTMLAttributes ?? {};\n const deletionAttrs = this.options?.deletionHTMLAttributes ?? {};\n return [\n Object.keys(insertionAttrs).length\n ? TrackedInsertion.configure({ HTMLAttributes: insertionAttrs })\n : TrackedInsertion,\n Object.keys(deletionAttrs).length\n ? TrackedDeletion.configure({ HTMLAttributes: deletionAttrs })\n : TrackedDeletion,\n ];\n },\n\n addCommands() {\n const decide =\n (decision: \"accept\" | \"reject\", id: string | null) =>\n ({ state, tr, dispatch }: { state: EditorState; tr: Transaction; dispatch?: any }) => {\n const all = getTrackedChanges(state.doc);\n const changes = id === null ? all : all.filter((c) => c.id === id);\n if (changes.length === 0) return false;\n if (dispatch) {\n applyDecision(tr, changes, decision);\n tr.setMeta(TRACK_CHANGES_SKIP_META, true);\n }\n return true;\n };\n\n const select =\n (direction: 1 | -1) =>\n ({ state, tr, dispatch }: { state: EditorState; tr: Transaction; dispatch?: any }) => {\n const changes = getTrackedChanges(state.doc);\n if (changes.length === 0) return false;\n const { from } = state.selection;\n const target =\n direction === 1\n ? changes.find((c) => c.from > from) ?? changes[0]\n : [...changes].reverse().find((c) => c.from < from) ?? changes[changes.length - 1];\n if (dispatch) {\n tr.setSelection(TextSelection.create(tr.doc, target.from, target.to)).scrollIntoView();\n }\n return true;\n };\n\n return {\n setTrackChanges:\n (enabled) =>\n ({ tr, dispatch }) => {\n if (dispatch) {\n tr.setMeta(trackChangesPluginKey, { type: \"setEnabled\", enabled });\n tr.setMeta(\"addToHistory\", false);\n }\n return true;\n },\n toggleTrackChanges:\n () =>\n ({ state, commands }) =>\n commands.setTrackChanges(!isTrackChangesEnabled(state)),\n setTrackChangesAuthor:\n (author) =>\n ({ tr, dispatch }) => {\n if (dispatch) {\n tr.setMeta(trackChangesPluginKey, { type: \"setAuthor\", author });\n tr.setMeta(\"addToHistory\", false);\n }\n return true;\n },\n acceptChange: (id) => decide(\"accept\", id),\n rejectChange: (id) => decide(\"reject\", id),\n acceptAllChanges: () => decide(\"accept\", null),\n rejectAllChanges: () => decide(\"reject\", null),\n selectNextChange: () => select(1),\n selectPreviousChange: () => select(-1),\n };\n },\n\n addProseMirrorPlugins() {\n const options = this.options;\n const store = this.storage.store;\n let editorView: EditorView | null = null;\n // Transactions applied during IME composition. Changing the document\n // around the composition would break the IME, so they are tracked\n // together once it ends.\n let pending: { oldState: EditorState; transactions: Transaction[] } | null = null;\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n\n const scheduleFlush = () => {\n if (flushTimer) clearTimeout(flushTimer);\n flushTimer = setTimeout(() => {\n flushTimer = null;\n const view = editorView;\n if (!view || !pending) return;\n if (view.composing) {\n scheduleFlush();\n return;\n }\n view.dispatch(view.state.tr.setMeta(\"trackChangesFlush\", true));\n }, 0);\n };\n\n return [\n new Plugin<TrackChangesPluginState>({\n key: trackChangesPluginKey,\n state: {\n init: () => ({ enabled: options.enabled, author: options.author }),\n apply: (tr, value) => {\n const meta = tr.getMeta(trackChangesPluginKey) as TrackChangesMeta | undefined;\n if (!meta) return value;\n if (meta.type === \"setEnabled\") {\n return meta.enabled === value.enabled ? value : { ...value, enabled: meta.enabled };\n }\n return { ...value, author: meta.author };\n },\n },\n appendTransaction: (transactions, oldState, newState) => {\n const plugin = trackChangesPluginKey.getState(oldState);\n const tracking = !!plugin?.enabled;\n if (!tracking && !pending) return null;\n\n if (editorView?.composing && transactions.some((t) => t.docChanged)) {\n if (tracking) {\n if (!pending) pending = { oldState, transactions: [] };\n pending.transactions.push(...transactions);\n }\n return null;\n }\n\n let trs: readonly Transaction[] = transactions;\n let fromState = oldState;\n if (pending) {\n trs = [...pending.transactions, ...transactions];\n fromState = pending.oldState;\n pending = null;\n }\n if (!tracking && trs === transactions) return null;\n const author = plugin?.author ?? options.author;\n return trackTransactions(trs, fromState, newState, {\n author,\n now: options.now,\n generateId: options.generateId,\n insertion: newState.schema.marks[INSERTION_MARK_NAME],\n deletion: newState.schema.marks[DELETION_MARK_NAME],\n });\n },\n props: {\n transformPasted: (slice, view) =>\n acceptSlice(\n slice,\n view.state.schema.marks[INSERTION_MARK_NAME],\n view.state.schema.marks[DELETION_MARK_NAME],\n ),\n handleDOMEvents: {\n compositionend: () => {\n if (pending) scheduleFlush();\n return false;\n },\n },\n },\n view: (view) => {\n editorView = view;\n store.set(buildSnapshot(view.state, options.author));\n return {\n update(view, prevState) {\n if (\n prevState.doc !== view.state.doc ||\n !prevState.selection.eq(view.state.selection) ||\n trackChangesPluginKey.getState(prevState) !==\n trackChangesPluginKey.getState(view.state)\n ) {\n store.set(buildSnapshot(view.state, options.author));\n }\n if (pending && !view.composing) scheduleFlush();\n },\n destroy() {\n editorView = null;\n pending = null;\n if (flushTimer) clearTimeout(flushTimer);\n },\n };\n },\n }),\n ];\n },\n});\n","import { Slice, Fragment, Node as ProsemirrorNode, type Schema } from \"@tiptap/pm/model\";\nimport type { TrackedChangesView } from \"../extensions/track-changes\";\n\nexport interface PlainTextOptions {\n /**\n * How tracked changes (`TrackChanges` extension) appear in the text.\n * Defaults to `\"accepted\"`: deleted text is dropped and inserted text kept,\n * so suggested deletions never leak into exported text. `\"original\"` drops\n * insertions and keeps deletions; `\"all\"` keeps both.\n */\n trackedChanges?: TrackedChangesView;\n}\n\n/**\n * Match tracked deletion / insertion elements. They never nest within\n * themselves, so a lazy match is enough.\n */\nconst TRACKED_DELETION_REGEX =\n /<del\\b(?=[^>]*\\bdata-change-type=[\"']deletion[\"'])[^>]*>[\\s\\S]*?<\\/del>/gi;\nconst TRACKED_INSERTION_REGEX =\n /<ins\\b(?=[^>]*\\bdata-change-type=[\"']insertion[\"'])[^>]*>[\\s\\S]*?<\\/ins>/gi;\n\nfunction stripTrackedChanges(html: string, view: TrackedChangesView): string {\n if (view === \"all\" || !html.includes(\"data-change-type\")) return html;\n return html.replace(\n view === \"accepted\" ? TRACKED_DELETION_REGEX : TRACKED_INSERTION_REGEX,\n \"\",\n );\n}\n\n/**\n * Escapes characters that have syntactic meaning in HTML text.\n */\nexport function escapeHtml(text: string): string {\n if (!text) return \"\";\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Matches strings that are already TipTap paragraph HTML (start with `<p>` and\n * end with `</p>`), as opposed to plain text that merely mentions a `<p>` tag.\n */\nconst PARAGRAPH_HTML_REGEX = /^\\s*<p[\\s>][\\s\\S]*<\\/p>\\s*$/i;\n\n/**\n * Converts a plain-text string with newlines into HTML paragraphs suitable\n * for TipTap initial content or setContent, preserving line breaks.\n * Input that is already paragraph HTML (`<p>…</p>`) is passed through as-is.\n */\nexport function plainTextToTipTapHtml(text: string): string {\n if (!text) return \"<p></p>\";\n if (PARAGRAPH_HTML_REGEX.test(text)) {\n return text;\n }\n return text\n .replace(/\\r\\n/g, \"\\n\")\n .replace(/\\r/g, \"\\n\")\n .split(\"\\n\")\n .map((line) => `<p>${escapeHtml(line)}</p>`)\n .join(\"\");\n}\n\n/**\n * Builds an open ProseMirror slice with one paragraph per line, so inserting it\n * splits the surrounding paragraph like typing Enter would. Use it for\n * plain-text paste handling (`clipboardTextParser`) and literal text insertion.\n */\nexport function plainTextToSlice(text: string, schema: Schema): Slice {\n const nodes: ProsemirrorNode[] = [];\n text\n .replace(/\\r\\n?/g, \"\\n\")\n .split(\"\\n\")\n .forEach((line) => {\n const nodeJson: any = { type: \"paragraph\" };\n if (line.length > 0) {\n nodeJson.content = [{ type: \"text\", text: line }];\n }\n try {\n nodes.push(ProsemirrorNode.fromJSON(schema, nodeJson));\n } catch {\n // Ignore invalid node\n }\n });\n return Slice.maxOpen(Fragment.fromArray(nodes));\n}\n\n/**\n * Block-level elements that start a new line in plain-text output.\n */\nconst BLOCK_TAGS = new Set([\n \"P\", \"DIV\", \"LI\", \"UL\", \"OL\", \"PRE\", \"BLOCKQUOTE\", \"TABLE\", \"TR\",\n \"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\", \"HR\",\n]);\n\n/**\n * Matches a TipTap mention span (including its content). The lookahead keeps\n * a single `[^>]*` scan per tag, avoiding catastrophic backtracking on\n * malformed input.\n */\nconst MENTION_SPAN_REGEX =\n /<span\\b(?=[^>]*\\bdata-type=[\"']mention[\"'])([^>]*)>[\\s\\S]*?<\\/span>/gi;\n\nfunction getHtmlAttribute(attrs: string, name: string): string | undefined {\n const match = new RegExp(`\\\\b${name}=(?:\"([^\"]*)\"|'([^']*)')`, \"i\").exec(\n attrs,\n );\n return match ? (match[1] ?? match[2]) : undefined;\n}\n\n/**\n * Parses HTML into an inert fragment. Content of a `<template>` element is not\n * rendered, so `<img onerror>` and similar handlers never run.\n */\nfunction parseHtmlFragment(html: string): DocumentFragment {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n return template.content;\n}\n\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/�*39;/g, \"'\")\n .replace(/ /g, \"\\u00a0\")\n .replace(/&/g, \"&\");\n}\n\n/**\n * Extracts plain text from an HTML string or TipTap HTML, preserving newlines between block tags.\n */\nexport function htmlToPlainText(html: string, options: PlainTextOptions = {}): string {\n if (!html) return \"\";\n\n // Normalize mentions so data-label is preserved as @label even if textContent is empty\n const mentionNormalized = stripTrackedChanges(\n html,\n options.trackedChanges ?? \"accepted\",\n ).replace(MENTION_SPAN_REGEX, (match, attrs) => {\n const label =\n getHtmlAttribute(attrs, \"data-label\") || getHtmlAttribute(attrs, \"data-id\");\n return label ? `@${label}` : match;\n });\n\n if (typeof document !== \"undefined\") {\n const fragment = parseHtmlFragment(mentionNormalized);\n fragment\n .querySelectorAll(\"br.ProseMirror-trailingBreak\")\n .forEach((br) => br.remove());\n\n // Walk the tree, emitting one line per innermost block so nested blocks\n // (e.g. TipTap's <li><p>…</p></li>) are not repeated.\n const lines: string[] = [];\n let current: string | null = null;\n const flush = () => {\n if (current !== null) {\n lines.push(current);\n current = null;\n }\n };\n const visit = (node: Node) => {\n if (node.nodeType === 3) {\n const value = node.nodeValue ?? \"\";\n // Skip formatting whitespace between blocks.\n if (current === null && value.trim() === \"\") return;\n current = (current ?? \"\") + value;\n return;\n }\n if (node.nodeType !== 1) return;\n const tag = (node as Element).tagName.toUpperCase();\n if (tag === \"BR\") {\n current = (current ?? \"\") + \"\\n\";\n return;\n }\n if (!BLOCK_TAGS.has(tag)) {\n node.childNodes.forEach(visit);\n return;\n }\n flush();\n const linesBefore = lines.length;\n node.childNodes.forEach(visit);\n flush();\n // An empty block (e.g. <p></p>) is an empty line.\n if (lines.length === linesBefore && tag !== \"HR\") {\n lines.push(\"\");\n }\n };\n fragment.childNodes.forEach(visit);\n flush();\n return lines.join(\"\\n\");\n }\n\n // Node.js fallback regex\n const text = mentionNormalized\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n // A closing block, plus any closing wrappers right after it, ends one line.\n .replace(\n /<\\/(?:p|div|li|pre|blockquote|h[1-6])>(?:\\s*<\\/(?:li|ul|ol|blockquote|div)>)*/gi,\n \"\\n\",\n )\n .replace(/<[^>]+>/g, \"\");\n return decodeHtmlEntities(text).replace(/\\n$/, \"\");\n}\n\n/**\n * Extracts plain text from a TipTap editor instance, preserving newline separators\n * between block nodes and converting mention nodes to @label. Tracked changes\n * are rendered as accepted unless `options.trackedChanges` says otherwise.\n */\nexport function getEditorText(editor: any, options: PlainTextOptions = {}): string {\n if (!editor) return \"\";\n const html = typeof editor.getHTML === \"function\" ? editor.getHTML() : \"\";\n if (html) {\n return htmlToPlainText(html, options);\n }\n if (typeof editor.getText === \"function\") {\n try {\n return editor.getText({ blockSeparator: \"\\n\" });\n } catch {\n return editor.getText();\n }\n }\n return \"\";\n}\n\nexport interface ExtractedMention {\n id: string;\n label: string;\n type?: string;\n avatarUrl?: string;\n color?: string;\n [key: string]: any;\n}\n\nimport { extractMentions } from \"./mentions\";\n\n/**\n * Traverses an editor instance, ProseMirror document, JSONContent, or HTML string\n * and extracts all mention attributes without needing regex.\n */\nexport function extractMentionsFromDoc(docOrEditor: any): ExtractedMention[] {\n if (!docOrEditor) return [];\n const mentions: ExtractedMention[] = [];\n\n // TipTap editor or ProseMirror Node with .descendants()\n const doc =\n docOrEditor?.state?.doc ||\n (typeof docOrEditor?.descendants === \"function\" ? docOrEditor : null);\n\n if (doc && typeof doc.descendants === \"function\") {\n doc.descendants((node: any) => {\n if (node.type?.name === \"mention\" && node.attrs) {\n mentions.push({\n id: node.attrs.id || \"\",\n label: node.attrs.label || \"\",\n type: node.attrs.type,\n avatarUrl: node.attrs.avatarUrl,\n color: node.attrs.color,\n ...node.attrs,\n });\n }\n });\n return mentions;\n }\n\n // JSONContent tree\n if (typeof docOrEditor === \"object\" && docOrEditor.content) {\n const walk = (nodes: any[]) => {\n for (const node of nodes) {\n if (node.type === \"mention\" && node.attrs) {\n mentions.push({\n id: node.attrs.id || \"\",\n label: node.attrs.label || \"\",\n type: node.attrs.type,\n avatarUrl: node.attrs.avatarUrl,\n color: node.attrs.color,\n ...node.attrs,\n });\n }\n if (Array.isArray(node.content)) {\n walk(node.content);\n }\n }\n };\n walk(docOrEditor.content);\n return mentions;\n }\n\n // HTML String\n if (typeof docOrEditor === \"string\") {\n if (typeof document !== \"undefined\") {\n parseHtmlFragment(docOrEditor).querySelectorAll('span[data-type=\"mention\"]').forEach((el) => {\n mentions.push({\n id: el.getAttribute(\"data-id\") || \"\",\n label: el.getAttribute(\"data-label\") || \"\",\n type: el.getAttribute(\"data-type-name\") || undefined,\n avatarUrl: el.getAttribute(\"data-avatar-url\") || undefined,\n color: el.getAttribute(\"data-color\") || undefined,\n });\n });\n } else {\n const spanRegex = /<span([^>]*data-type=[\"']mention[\"'][^>]*)>/gi;\n let match;\n while ((match = spanRegex.exec(docOrEditor)) !== null) {\n const attrStr = match[1];\n const getAttr = (name: string) => {\n const m = new RegExp(`${name}=[\"']([^\"']*)[\"']`, \"i\").exec(attrStr);\n return m ? m[1] : undefined;\n };\n mentions.push({\n id: getAttr(\"data-id\") || \"\",\n label: getAttr(\"data-label\") || \"\",\n type: getAttr(\"data-type-name\") || getAttr(\"data-entity-type\"),\n avatarUrl: getAttr(\"data-avatar-url\"),\n color: getAttr(\"data-color\"),\n });\n }\n }\n\n // Fallback: If no HTML mention tags were found, extract raw @mentions from text or markdown\n if (mentions.length === 0) {\n const rawHandles = extractMentions(docOrEditor);\n for (const handle of rawHandles) {\n mentions.push({\n id: handle,\n label: handle,\n type: \"user\",\n });\n }\n }\n }\n\n return mentions;\n}\n","import { escapeHtml } from \"./plain-text\";\nimport type { MentionEntity } from \"../extensions/mention\";\n\n/**\n * Canonical regex pattern for matching @mentions in plain text or markdown.\n * Supports:\n * - Standard unquoted handles: @username, @user.name, @team_alpha, @sa-1\n * - Quoted handles: @\"John Doe\", "Jane Doe"\n * Unquoted handles exclude trailing punctuation (e.g. @violet. matches \"violet\" without the dot).\n */\nexport const MENTION_REGEX =\n /(?:^|[\\s>(*_\"';\\\\/[\\]{}~`:]|"|>|<|&)@(?:\"([^\"]+)\"|"([^&]+)"|([a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)*))/g;\n\n/**\n * Split regex identifying markdown code blocks, inline code, URLs, and existing HTML tags.\n * Used by preprocessMarkdownMentions to ensure non-mention regions remain completely untouched.\n */\nexport const MARKDOWN_MENTION_SPLIT_REGEX =\n /(```[\\s\\S]*?```|~~~[\\s\\S]*?~~~|<pre[\\s\\S]*?<\\/pre>|<code[\\s\\S]*?<\\/code>|`[^`\\n]+`|<span\\b[^>]*\\b(?:data-type=[\"']mention[\"']|class=[\"'][^\"']*pixerate-mention-node[^\"']*[\"']|data-mention=[\"'][^\"']*[\"'])[^>]*>[\\s\\S]*?<\\/span>|<[^>]+>|\\]\\([^)]+\\)|(?:https?|file|vscode|cursor|mailto):\\/\\/[^\\s<)]+)/gi;\n\nexport interface MentionSegment {\n type: \"text\" | \"mention\";\n value: string;\n handle?: string;\n}\n\nexport interface PreprocessMarkdownMentionsOptions {\n /**\n * Known pool of mentionable entities (users, agents, teams, etc.).\n */\n mentionables?: MentionEntity[];\n /**\n * Default entity type assigned when a handle does not match any entry in mentionables.\n * Defaults to 'user'.\n */\n defaultType?: string;\n /**\n * Custom resolver callback to derive entity metadata for unmatched handles.\n */\n resolveMention?: (handle: string) => Partial<MentionEntity> | null | undefined;\n}\n\n/**\n * Strips code blocks and inline code from markdown or HTML content:\n * - Fenced code blocks: ```...``` and ~~~...~~~\n * - Inline backtick code: `...`\n * - HTML code/pre tags: <pre>...</pre>, <code>...</code>\n * Used to avoid false-positive mention parsing and keyword analysis within code snippets, terminal logs, or stack traces.\n */\nexport function stripMarkdownCode(content: string): string {\n if (!content || typeof content !== \"string\") return \"\";\n return content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/~~~[\\s\\S]*?~~~/g, \"\")\n .replace(/<pre[\\s\\S]*?<\\/pre>/gi, \"\")\n .replace(/<code[\\s\\S]*?<\\/code>/gi, \"\")\n .replace(/`[^`\\n]*`/g, \"\");\n}\n\n/**\n * Extracts unique mention handles from a text, markdown, or HTML string,\n * ignoring trailing punctuation, code blocks, URLs, and pre-existing HTML mention attributes.\n */\nexport function extractMentions(content: string): string[] {\n if (!content || typeof content !== \"string\") return [];\n\n // Normalize existing HTML mention nodes (e.g. from TipTap or rich text serializers)\n // so data-mention-suggestion-char=\"@\" does not trigger spurious quoted matches against attributes,\n // and ensure the actual mentioned label or ID is preserved cleanly.\n const normalizedMentionNodes = content.replace(\n /<span\\b[^>]*\\b(?:data-type=[\"']mention[\"']|class=[\"'][^\"']*pixerate-mention-node[^\"']*[\"'])[^>]*>([\\s\\S]*?)<\\/span>/gi,\n (match, inner) => {\n const labelMatch = match.match(/\\bdata-label=[\"']([^\"']+)[\"']/i);\n if (labelMatch) return ` @${labelMatch[1]} `;\n const idMatch = match.match(/\\bdata-id=[\"']([^\"']+)[\"']/i);\n if (idMatch) return ` @${idMatch[1]} `;\n return ` ${inner} `;\n }\n );\n\n const cleanContent = stripMarkdownCode(normalizedMentionNodes);\n // Strip URLs and markdown link destinations so @package or @handles in URLs are not extracted as mentions\n const withoutUrls = cleanContent\n .replace(/(?:https?|ftp|file|vscode|cursor|mailto):\\/\\/[^\\s<>()]+/gi, \"\")\n .replace(/\\]\\([^)]*\\)/g, \"]\");\n // Strip remaining HTML tags so attributes containing '@' or '>' aren't treated as mention triggers\n const textOnly = withoutUrls.replace(/<[^>]+>/g, \" \");\n\n const mentions: string[] = [];\n const seen = new Set<string>();\n\n const regex = new RegExp(MENTION_REGEX.source, \"g\");\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(textOnly)) !== null) {\n const handle = (match[1] ?? match[2] ?? match[3] ?? \"\").trim();\n if (handle && !seen.has(handle)) {\n seen.add(handle);\n mentions.push(handle);\n }\n }\n\n return mentions;\n}\n\n/**\n * Segments a text string into plain text parts and mention tokens.\n * Correctly leaves trailing punctuation (like \".\") as part of the following text segment.\n */\nexport function parseMentionSegments(content: string): MentionSegment[] {\n if (!content) return [];\n\n const segments: MentionSegment[] = [];\n const regex = new RegExp(MENTION_REGEX.source, \"g\");\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const fullMatch = match[0];\n const atIndex = fullMatch.indexOf(\"@\");\n const prefix = atIndex > 0 ? fullMatch.slice(0, atIndex) : \"\";\n const mentionToken = fullMatch.slice(atIndex);\n const handle = match[1] ?? match[2] ?? match[3];\n const matchStart = match.index + prefix.length;\n\n if (matchStart > lastIndex) {\n segments.push({\n type: \"text\",\n value: content.slice(lastIndex, matchStart),\n });\n }\n\n segments.push({\n type: \"mention\",\n value: mentionToken,\n handle,\n });\n\n lastIndex = matchStart + mentionToken.length;\n }\n\n if (lastIndex < content.length) {\n segments.push({\n type: \"text\",\n value: content.slice(lastIndex),\n });\n }\n\n return segments;\n}\n\n/**\n * Preprocesses markdown text by turning raw @mention handles into TipTap-compatible\n * mention HTML spans (<span class=\"pixerate-mention-node\" data-type=\"mention\" ...>),\n * ensuring code blocks, inline code, existing mention nodes, and URLs remain immune.\n */\nexport function preprocessMarkdownMentions(\n content: string,\n optionsOrMentionables: MentionEntity[] | PreprocessMarkdownMentionsOptions = []\n): string {\n if (!content || typeof content !== \"string\") return \"\";\n\n const options: PreprocessMarkdownMentionsOptions = Array.isArray(optionsOrMentionables)\n ? { mentionables: optionsOrMentionables }\n : optionsOrMentionables;\n\n const allMentionables = options.mentionables || [];\n const defaultType = options.defaultType || \"user\";\n const resolveMention = options.resolveMention;\n\n const parts = content.split(MARKDOWN_MENTION_SPLIT_REGEX);\n\n return parts\n .map((part) => {\n if (!part) return \"\";\n\n // Preserve code fences, code tags, inline backticks, existing mention spans,\n // HTML tags, markdown link targets, and raw URLs untouched.\n if (\n part.startsWith(\"```\") ||\n part.startsWith(\"~~~\") ||\n part.startsWith(\"`\") ||\n part.startsWith(\"](\") ||\n /^(?:https?|file|vscode|cursor|mailto):\\/\\//i.test(part) ||\n /^<pre[\\s>]/i.test(part) ||\n /^<code[\\s>]/i.test(part) ||\n /^<[^>]+>$/i.test(part) ||\n /^<span\\b[^>]*\\b(?:data-type=[\"']mention[\"']|class=[\"'][^\"']*pixerate-mention-node[^\"']*[\"']|data-mention=[\"'][^\"']*[\"'])/i.test(\n part\n )\n ) {\n return part;\n }\n\n const regex = new RegExp(MENTION_REGEX.source, \"g\");\n return part.replace(regex, (match, quotedHandle, escapedQuotedHandle, standardHandle) => {\n const atIndex = match.indexOf(\"@\");\n const prefix = atIndex > 0 ? match.slice(0, atIndex) : \"\";\n const rawHandle = (quotedHandle || escapedQuotedHandle || standardHandle || \"\").trim();\n if (!rawHandle) return match;\n\n const lowerHandle = rawHandle.toLowerCase();\n const entity = allMentionables.find((e) => {\n if (e.label?.toLowerCase() === lowerHandle) return true;\n if (e.handle?.toLowerCase() === lowerHandle) return true;\n if (e.name?.toLowerCase() === lowerHandle) return true;\n if (e.id?.toLowerCase() === lowerHandle) return true;\n if (e.type === \"agent\" && `agent${(e.handle || e.label || \"\").toLowerCase()}` === lowerHandle) return true;\n if (\n e.aliases?.some(\n (a: string) =>\n a.toLowerCase() === lowerHandle ||\n (e.type === \"agent\" && `agent${a.toLowerCase()}` === lowerHandle)\n )\n ) {\n return true;\n }\n return false;\n });\n\n let id = entity?.id;\n let label = entity?.label || entity?.handle || rawHandle;\n let type = entity?.type;\n let avatarUrl = entity?.avatarUrl;\n let color = entity?.color || entity?.hexColor;\n\n if (!entity) {\n const customResolved = resolveMention?.(rawHandle);\n if (customResolved) {\n id = customResolved.id ?? id;\n label = customResolved.label ?? customResolved.handle ?? label;\n type = customResolved.type ?? type;\n avatarUrl = customResolved.avatarUrl ?? avatarUrl;\n color = customResolved.color ?? customResolved.hexColor ?? color;\n } else {\n const stripped = lowerHandle.replace(/^agent[\\s\\-_]*/i, \"\");\n if (\n [\"supervisor\", \"coordinator\", \"planner\", \"validator\", \"executor\"].includes(stripped)\n ) {\n type = \"agent\";\n id = stripped;\n label = stripped;\n } else if (lowerHandle === \"agents\") {\n type = \"team\";\n id = \"agents\";\n label = \"agents\";\n } else {\n type = defaultType;\n id = rawHandle;\n label = rawHandle;\n }\n }\n }\n\n const avatarAttr = avatarUrl ? ` data-avatar-url=\"${escapeHtml(avatarUrl)}\"` : \"\";\n const colorAttr = color ? ` data-color=\"${escapeHtml(color)}\"` : \"\";\n\n return `${prefix}<span class=\"pixerate-mention-node\" data-type=\"mention\" data-id=\"${escapeHtml(\n id || label\n )}\" data-label=\"${escapeHtml(label)}\" data-mention-suggestion-char=\"@\" data-type-name=\"${escapeHtml(\n type || defaultType\n )}\"${avatarAttr}${colorAttr}>@${escapeHtml(label)}</span>`;\n });\n })\n .join(\"\");\n}\n","import MarkdownIt from \"markdown-it\";\nimport taskLists from \"markdown-it-task-lists\";\nimport { escapeHtml, getEditorText, type PlainTextOptions } from \"./plain-text\";\nimport { resolveTrackedChanges } from \"../extensions/track-changes\";\n\nconst md = new MarkdownIt({\n html: true,\n linkify: true,\n breaks: true,\n}).use(taskLists);\n\nconst defaultValidateLink = md.validateLink;\nmd.validateLink = (url: string) => {\n if (\n url.startsWith(\"file://\") ||\n url.startsWith(\"vscode://\") ||\n url.startsWith(\"cursor://\")\n ) {\n return true;\n }\n return defaultValidateLink.call(md, url);\n};\n\n/**\n * Tags allowed through from raw HTML embedded in markdown (e.g. mention spans\n * and file links that tiptap-markdown serializes as HTML). Anything else is\n * rendered as escaped text. `input` is limited to checkboxes, which\n * markdown-it-task-lists emits as raw HTML.\n */\nconst ALLOWED_HTML_TAGS = new Set([\n \"a\", \"abbr\", \"b\", \"blockquote\", \"br\", \"code\", \"del\", \"details\", \"div\", \"em\",\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\", \"hr\", \"i\", \"img\", \"input\", \"kbd\", \"li\", \"mark\",\n \"ol\", \"p\", \"pre\", \"s\", \"span\", \"strong\", \"sub\", \"summary\", \"sup\", \"table\",\n \"tbody\", \"td\", \"tfoot\", \"th\", \"thead\", \"tr\", \"u\", \"ul\",\n]);\n\nconst ALLOWED_HTML_ATTRS = new Set([\n \"alt\", \"align\", \"checked\", \"class\", \"colspan\", \"disabled\", \"height\", \"href\",\n \"rel\", \"rowspan\", \"src\", \"start\", \"target\", \"title\", \"type\", \"width\",\n]);\n\nconst HTML_TAG_REGEX =\n /<(\\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\\s+[^\\s\"'>\\/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s\"'=<>`]+))?)*)\\s*\\/?>/g;\nconst HTML_ATTR_REGEX =\n /([^\\s\"'>\\/=]+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g;\n\nfunction sanitizeTag(\n tagText: string,\n isClosing: string,\n rawTagName: string,\n rawAttrs: string,\n stripDisallowed: boolean,\n): string {\n const tagName = rawTagName.toLowerCase();\n const reject = () => (stripDisallowed ? \"\" : escapeHtml(tagText));\n if (!ALLOWED_HTML_TAGS.has(tagName)) return reject();\n if (isClosing) return `</${tagName}>`;\n if (tagName === \"input\" && !/\\stype\\s*=\\s*[\"']?checkbox[\"']?[\\s>/]/i.test(tagText)) {\n return reject();\n }\n\n let attrs = \"\";\n let attrMatch: RegExpExecArray | null;\n HTML_ATTR_REGEX.lastIndex = 0;\n while ((attrMatch = HTML_ATTR_REGEX.exec(rawAttrs)) !== null) {\n const name = attrMatch[1].toLowerCase();\n if (!ALLOWED_HTML_ATTRS.has(name) && !/^data-[a-z0-9_.-]+$/.test(name)) {\n continue;\n }\n const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];\n if (rawValue === undefined) {\n attrs += ` ${name}`;\n continue;\n }\n const value = md.utils.unescapeAll(rawValue);\n if (\n (name === \"href\" || name === \"src\") &&\n // Browsers ignore control characters and whitespace inside schemes.\n !md.validateLink(value.replace(/[\\u0000-\\u0020\\u007f]/g, \"\"))\n ) {\n continue;\n }\n attrs += ` ${name}=\"${escapeHtml(value)}\"`;\n }\n return `<${tagName}${attrs}>`;\n}\n\nexport interface SanitizeHtmlOptions {\n /**\n * What to do with tags outside the allowlist: `escape` (default) renders\n * them as literal text, `strip` removes the tag but keeps its text content.\n */\n disallowedTags?: \"escape\" | \"strip\";\n}\n\n/**\n * Sanitizes raw HTML with the same allowlist the markdown renderer applies to\n * embedded HTML: event handlers, `style` and unsafe URL schemes are dropped,\n * and disallowed tags are escaped (or removed with `disallowedTags: \"strip\"`).\n * It is string-based, so it works without a DOM and never runs handlers.\n */\nexport function sanitizeHtml(html: string, options: SanitizeHtmlOptions = {}): string {\n return sanitizeEmbeddedHtml(html, options.disallowedTags === \"strip\");\n}\n\nfunction sanitizeEmbeddedHtml(html: string, stripDisallowed = false): string {\n let result = \"\";\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n HTML_TAG_REGEX.lastIndex = 0;\n while ((match = HTML_TAG_REGEX.exec(html)) !== null) {\n result += html.slice(lastIndex, match.index).replace(/</g, \"<\");\n result += sanitizeTag(match[0], match[1], match[2], match[3], stripDisallowed);\n lastIndex = match.index + match[0].length;\n }\n return result + html.slice(lastIndex).replace(/</g, \"<\");\n}\n\nconst defaultHtmlInline = md.renderer.rules.html_inline;\nconst defaultHtmlBlock = md.renderer.rules.html_block;\nmd.renderer.rules.html_inline = (tokens, idx, options, env, self) =>\n env?.allowUnsafeHtml\n ? defaultHtmlInline!(tokens, idx, options, env, self)\n : sanitizeEmbeddedHtml(tokens[idx].content);\nmd.renderer.rules.html_block = (tokens, idx, options, env, self) =>\n env?.allowUnsafeHtml\n ? defaultHtmlBlock!(tokens, idx, options, env, self)\n : sanitizeEmbeddedHtml(tokens[idx].content);\n\nimport {\n preprocessMarkdownMentions,\n type PreprocessMarkdownMentionsOptions,\n} from \"./mentions\";\nimport type { MentionEntity } from \"../extensions/mention\";\n\n/**\n * Preprocesses file-protocol and editor-protocol links in markdown content:\n * converts [label](file://...), vscode://, and cursor:// links into anchor tags\n * annotated with data-file-link=\"true\", preserving code backticks in labels.\n */\nexport function preprocessMarkdownFileLinks(content: string): string {\n if (!content) return \"\";\n return content.replace(\n /\\[([^\\]]+)\\]\\(((?:file|vscode|cursor):\\/\\/[^)]+)\\)/g,\n (_match, rawLabel, href) => {\n const trimmed = rawLabel.trim();\n const codeMatch = trimmed.match(/^`([^`]+)`$/);\n const labelContent = codeMatch\n ? `<code>${escapeHtml(codeMatch[1])}</code>`\n : escapeHtml(trimmed);\n return `<a href=\"${escapeHtml(href)}\" data-file-link=\"true\" title=\"Click to copy file path\">${labelContent}</a>`;\n }\n );\n}\n\nexport interface MarkdownToHtmlOptions {\n /**\n * Preprocess file, vscode, and cursor links into clickable data-file-link tags.\n */\n preprocessFileLinks?: boolean;\n /**\n * Whether to preprocess @mentions into TipTap mention node HTML spans.\n */\n preprocessMentions?: boolean;\n /**\n * Pool of mentionables to pass to preprocessMarkdownMentions.\n */\n mentionables?: MentionEntity[];\n /**\n * Full options for preprocessMarkdownMentions.\n */\n preprocessOptions?: PreprocessMarkdownMentionsOptions;\n /**\n * Render raw HTML embedded in the markdown without sanitizing it. By default,\n * embedded HTML is restricted to an allowlist of tags and attributes (event\n * handlers, `style` and unsafe URL schemes are removed). Only enable this for\n * fully trusted input.\n */\n allowUnsafeHtml?: boolean;\n}\n\n/**\n * Converts a markdown string into HTML suitable for TipTap initial content or setContent.\n * Raw HTML embedded in the markdown is sanitized unless `allowUnsafeHtml` is set.\n */\nexport function markdownToTipTapHtml(\n markdown: string,\n options?: MarkdownToHtmlOptions\n): string {\n if (!markdown) return \"<p></p>\";\n let content = markdown;\n if (options?.preprocessFileLinks) {\n content = preprocessMarkdownFileLinks(content);\n }\n if (\n options?.preprocessMentions ||\n options?.mentionables ||\n options?.preprocessOptions\n ) {\n const mentionOpts = options.preprocessOptions || {\n mentionables: options.mentionables,\n };\n content = preprocessMarkdownMentions(content, mentionOpts);\n }\n return md.render(content, { allowUnsafeHtml: options?.allowUnsafeHtml });\n}\n\n/**\n * Extracts clean markdown from a TipTap editor instance.\n * If the editor has the Markdown extension configured, it uses editor.storage.markdown.getMarkdown().\n * Otherwise, it falls back to getEditorText(editor).\n * Tracked changes are exported as accepted unless `options.trackedChanges`\n * says otherwise (`\"all\"` renders deletions as `~~strikethrough~~`).\n */\nexport function getEditorMarkdown(editor: any, options: PlainTextOptions = {}): string {\n if (!editor) return \"\";\n const markdown = editor.storage?.markdown;\n if (typeof markdown?.getMarkdown === \"function\") {\n const doc = editor.state?.doc;\n if (doc && typeof markdown.serializer?.serialize === \"function\") {\n const resolved = resolveTrackedChanges(doc, options.trackedChanges ?? \"accepted\");\n if (resolved !== doc) return markdown.serializer.serialize(resolved);\n }\n return markdown.getMarkdown();\n }\n return getEditorText(editor, options);\n}\n"]}
|