@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-AA4ZEVCC.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;AACQ;AACR;AACA;AAMK;AACP;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEK;AAIM;AACA;AAEA;AAEA;AAOP;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAMY;AA4HA;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;AAES;AACF,EAAA;AACD,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACF;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;AAOY;AACL,EAAA;AACN,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACE,QAAA;AAEF,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACK,MAAA;AACH,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,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;AAMK;AACE,EAAA;AACE,EAAA;AACR,EAAA;AACQ,EAAA;AACF,EAAA;AACA,EAAA;AACN,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAEM;AACJ,EAAA;AACA,EAAA;AACO,EAAA;AACP,EAAA;AACA,EAAA;AACF;AAEM;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAES;AACD,EAAA;AAIC,EAAA;AACT;AAES;AACH,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACC,EAAA;AAKT;AAEM;AAEG;AACH,EAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACH,MAAA;AACH,IAAA;AACA,IAAA;AACF,EAAA;AACQ,EAAA;AACV;AAES;AACD,EAAA;AACA,EAAA;AAEG,IAAA;AACN,EAAA;AACG,EAAA;AACE,IAAA;AACD,IAAA;AAAyC,IAAA;AAE/C,EAAA;AACM,EAAA;AACA,IAAA;AACN,EAAA;AACM,EAAA;AACT;AAES;AACL,EAAA;AAAkD,IAAA;AAE5C,MA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F,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;AL8zDU;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;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-AA4ZEVCC.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 Mark as ProsemirrorMark,\n NodeRange,\n Slice,\n type ResolvedPos,\n type MarkType,\n type Node as ProsemirrorNode,\n type NodeType,\n type Schema,\n} from \"@tiptap/pm/model\";\nimport {\n AddMarkStep,\n AttrStep,\n Mapping,\n RemoveMarkStep,\n ReplaceAroundStep,\n ReplaceStep,\n Transform,\n canJoin,\n liftTarget,\n type StepMap,\n} 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/** Mark recording a tracked formatting change (mark added/removed on existing text). */\nexport const FORMAT_CHANGE_MARK_NAME = \"formatChange\";\n/** Node attribute recording a tracked block change (type, attributes or wrapping). */\nexport const BLOCK_CHANGE_ATTRIBUTE = \"blockChange\";\n\n/**\n * Marks that are never treated as formatting: the review marks themselves,\n * comments (`comment`) and bookmarks (`bookmark`). Names are used instead of\n * imports to avoid module cycles.\n */\nconst REVIEW_MARK_NAMES = new Set([\n INSERTION_MARK_NAME,\n DELETION_MARK_NAME,\n FORMAT_CHANGE_MARK_NAME,\n \"comment\",\n \"bookmark\",\n]);\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\" | \"format\" | \"block\";\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, new formatting and block types kept).\n * - `original`: as if every change were rejected.\n * - `all`: both inserted and deleted text, current formatting and blocks.\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\n/** A serializable mark (`mark.toJSON()`). */\nexport interface TrackedMark {\n type: string;\n attrs?: Record<string, any>;\n}\n\n/** Attributes of the `formatChange` mark. */\nexport interface TrackedFormatChangeAttributes extends TrackedChangeAttributes {\n /** Formatting marks before the change. */\n before: TrackedMark[];\n /** Formatting marks after the change. */\n after: TrackedMark[];\n}\n\n/** A textblock's type, attributes and enclosing wrappers (lists, quotes). */\nexport interface TrackedBlockSnapshot {\n type: string;\n attrs: Record<string, any>;\n /** Ancestors from the outermost (below the document) to the parent. */\n wrappers: { type: string; attrs: Record<string, any> }[];\n}\n\n/** Value of the `blockChange` node attribute. */\nexport interface TrackedBlockChangeAttributes extends TrackedChangeAttributes {\n previous: TrackedBlockSnapshot;\n}\n\nexport interface TrackedFormatSummary {\n /** Labels of added formatting, e.g. `[\"Bold\"]`. */\n added: string[];\n /** Labels of removed formatting. */\n removed: string[];\n /** Labels of changed formatting (same mark, different attributes), e.g. `[\"Font size\"]`. */\n changed: string[];\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 /**\n * A change can be split into several ranges by other marks or blocks.\n * Block changes cover whole textblock nodes.\n */\n ranges: { from: number; to: number }[];\n text: string;\n /** Human-readable summary, e.g. `Formatted: Bold` or `Changed Paragraph to Heading 2`. */\n description: string;\n /** Formatting changes (`type: \"format\"`). */\n format?: TrackedFormatSummary;\n /** Block changes (`type: \"block\"`): the first block's previous and current state. */\n block?: { previous: TrackedBlockSnapshot; current: TrackedBlockSnapshot };\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 /** HTML attributes (e.g. `class`) of tracked formatting. Replaces the default dotted underline. */\n formatChangeHTMLAttributes: Record<string, any>;\n /** Class added to blocks with a tracked block change (margin bar by default). */\n blockChangeClass: string;\n /** Track mark changes (bold, links, font size, …) on existing text. Default `true`. */\n trackFormatting: boolean;\n /** Track textblock type/attribute and wrapping (list, quote) changes. Default `true`. */\n trackBlocks: boolean;\n /**\n * Track insertions and deletions inside code blocks by allowing the\n * insertion/deletion marks there. Default `true`.\n */\n trackCodeBlocks: boolean;\n /** Textblock node types that can record block changes. */\n blockTypes: string[];\n /** Extra mark names not tracked as formatting. */\n ignoredMarks: string[];\n /** Node attributes ignored when comparing blocks (e.g. generated ids). */\n ignoredBlockAttributes: string[];\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\nfunction parseJSON<T>(value: string | null, fallback: T): T {\n if (!value) return fallback;\n try {\n return JSON.parse(value) as T;\n } catch {\n return fallback;\n }\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\n/**\n * Existing text whose formatting changed while tracking was on. Stores the\n * formatting before and after the change, rendered as\n * `<span data-change-type=\"format\">` (dotted underline by default).\n */\nexport const TrackedFormatChange = Mark.create<{ HTMLAttributes: Record<string, any> }>({\n name: FORMAT_CHANGE_MARK_NAME,\n inclusive: false,\n addOptions() {\n return {\n HTMLAttributes: {\n class:\n \"pixerate-format-change underline decoration-dotted decoration-2 decoration-sky-500 underline-offset-4\",\n },\n };\n },\n addStorage() {\n return { markdown: { serialize: { open: \"\", close: \"\" }, parse: {} } };\n },\n addAttributes() {\n return {\n ...sharedAttributes(),\n before: {\n default: [],\n parseHTML: (el: HTMLElement) => parseJSON(el.getAttribute(\"data-format-before\"), []),\n renderHTML: (attrs: any) => ({ \"data-format-before\": JSON.stringify(attrs.before ?? []) }),\n },\n after: {\n default: [],\n parseHTML: (el: HTMLElement) => parseJSON(el.getAttribute(\"data-format-after\"), []),\n renderHTML: (attrs: any) => ({ \"data-format-after\": JSON.stringify(attrs.after ?? []) }),\n },\n };\n },\n parseHTML() {\n return [{ tag: 'span[data-change-type=\"format\"]', priority: 100 }];\n },\n renderHTML({ HTMLAttributes }) {\n return [\n \"span\",\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n \"data-change-type\": \"format\",\n }),\n 0,\n ];\n },\n});\n\n// ---------------------------------------------------------------------------\n// Descriptions\n// ---------------------------------------------------------------------------\n\nconst MARK_LABELS: Record<string, string> = {\n bold: \"Bold\",\n italic: \"Italic\",\n underline: \"Underline\",\n strike: \"Strikethrough\",\n code: \"Code\",\n link: \"Link\",\n highlight: \"Highlight\",\n subscript: \"Subscript\",\n superscript: \"Superscript\",\n textStyle: \"Text style\",\n};\n\nconst TEXT_STYLE_LABELS: Record<string, string> = {\n fontSize: \"Font size\",\n fontFamily: \"Font\",\n color: \"Color\",\n backgroundColor: \"Background color\",\n lineHeight: \"Line height\",\n};\n\nconst BLOCK_LABELS: Record<string, string> = {\n paragraph: \"Paragraph\",\n codeBlock: \"Code block\",\n blockquote: \"Quote\",\n bulletList: \"Bulleted list\",\n orderedList: \"Numbered list\",\n taskList: \"Task list\",\n listItem: \"List item\",\n taskItem: \"Task item\",\n};\n\nfunction humanize(name: string): string {\n const words = name\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .toLowerCase();\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === undefined) return \"null\";\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .filter((k) => obj[k] !== undefined)\n .sort()\n .map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)\n .join(\",\")}}`;\n}\n\nconst sameValue = (a: unknown, b: unknown) => stableStringify(a) === stableStringify(b);\n\nfunction markLabels(mark: TrackedMark, other?: TrackedMark): string[] {\n if (mark.type === \"textStyle\") {\n const attrs = mark.attrs ?? {};\n const otherAttrs = other?.attrs ?? {};\n const keys = Array.from(new Set([...Object.keys(attrs), ...Object.keys(otherAttrs)])).filter(\n (key) => (other ? !sameValue(attrs[key], otherAttrs[key]) : attrs[key] != null),\n );\n return keys.length ? keys.map((key) => TEXT_STYLE_LABELS[key] ?? humanize(key)) : [\"Text style\"];\n }\n return [MARK_LABELS[mark.type] ?? humanize(mark.type)];\n}\n\nfunction diffFormatting(before: TrackedMark[], after: TrackedMark[]): TrackedFormatSummary {\n const summary: TrackedFormatSummary = { added: [], removed: [], changed: [] };\n const push = (list: string[], labels: string[]) =>\n labels.forEach((label) => {\n if (!list.includes(label)) list.push(label);\n });\n after.forEach((mark) => {\n const old = before.find((m) => m.type === mark.type);\n if (!old) push(summary.added, markLabels(mark));\n else if (!sameValue(old.attrs ?? {}, mark.attrs ?? {})) push(summary.changed, markLabels(mark, old));\n });\n before.forEach((mark) => {\n if (!after.some((m) => m.type === mark.type)) push(summary.removed, markLabels(mark));\n });\n return summary;\n}\n\nfunction mergeSummary(target: TrackedFormatSummary, source: TrackedFormatSummary): void {\n ([\"added\", \"removed\", \"changed\"] as const).forEach((key) =>\n source[key].forEach((label) => {\n if (!target[key].includes(label)) target[key].push(label);\n }),\n );\n}\n\n/** Describes a formatting change, e.g. `Formatted: Bold; Removed formatting: Italic`. */\nexport function describeFormatChange(summary: TrackedFormatSummary): string {\n const parts: string[] = [];\n if (summary.added.length) parts.push(`Formatted: ${summary.added.join(\", \")}`);\n if (summary.removed.length) parts.push(`Removed formatting: ${summary.removed.join(\", \")}`);\n if (summary.changed.length) parts.push(`Changed formatting: ${summary.changed.join(\", \")}`);\n return parts.join(\"; \") || \"Formatted\";\n}\n\nfunction blockLabel(type: string, attrs?: Record<string, any>): string {\n if (type === \"heading\" && attrs?.level) return `Heading ${attrs.level}`;\n return BLOCK_LABELS[type] ?? humanize(type);\n}\n\nconst containerLabels = (wrappers: TrackedBlockSnapshot[\"wrappers\"]) =>\n wrappers\n .filter((w) => w.type !== \"listItem\" && w.type !== \"taskItem\")\n .map((w) => blockLabel(w.type, w.attrs));\n\nconst isPrefix = (a: string[], b: string[]) => a.every((label, i) => b[i] === label);\n\n/** Describes a block change, e.g. `Changed Paragraph to Heading 2`. */\nexport function describeBlockChange(\n previous: TrackedBlockSnapshot,\n current: TrackedBlockSnapshot,\n ignoredAttributes: readonly string[] = [\"id\"],\n): string {\n const parts: string[] = [];\n const before = blockLabel(previous.type, previous.attrs);\n const after = blockLabel(current.type, current.attrs);\n if (before !== after) {\n parts.push(`Changed ${before} to ${after}`);\n } else {\n const keys = Array.from(\n new Set([...Object.keys(previous.attrs ?? {}), ...Object.keys(current.attrs ?? {})]),\n ).filter(\n (key) =>\n key !== BLOCK_CHANGE_ATTRIBUTE &&\n !ignoredAttributes.includes(key) &&\n !(current.type === \"heading\" && key === \"level\") &&\n !sameValue(previous.attrs?.[key], current.attrs?.[key]),\n );\n keys.forEach((key) => {\n if (key === \"textAlign\") {\n parts.push(`Changed alignment to ${humanize(String(current.attrs?.textAlign ?? \"default\"))}`);\n } else {\n parts.push(`Changed ${humanize(key).toLowerCase()} of ${after}`);\n }\n });\n }\n const oldTypes = previous.wrappers.map((w) => w.type).join(\"/\");\n const newTypes = current.wrappers.map((w) => w.type).join(\"/\");\n if (oldTypes !== newTypes) {\n const was = containerLabels(previous.wrappers);\n const now = containerLabels(current.wrappers);\n const last = (list: string[]) => list[list.length - 1];\n if (was.length === 0 && now.length > 0) parts.push(`Wrapped in ${last(now)}`);\n else if (now.length === 0 && was.length > 0) parts.push(`Removed from ${last(was)}`);\n else if (now.length > was.length && isPrefix(was, now)) parts.push(`Indented in ${last(now)}`);\n else if (now.length < was.length && isPrefix(now, was)) parts.push(`Outdented from ${last(was)}`);\n else if (was.length && now.length) parts.push(`Changed ${last(was)} to ${last(now)}`);\n else parts.push(\"Changed nesting\");\n }\n return parts.join(\"; \") || `Changed ${after}`;\n}\n\nfunction truncate(text: string, max = 40): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\n// ---------------------------------------------------------------------------\n// Reading changes\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\nfunction stripBlockChange(attrs: Record<string, any>): Record<string, any> {\n const copy = { ...attrs };\n delete copy[BLOCK_CHANGE_ATTRIBUTE];\n return copy;\n}\n\n/** Snapshot of the textblock at `pos` (position before the node). */\nfunction blockSnapshot(doc: ProsemirrorNode, pos: number): TrackedBlockSnapshot {\n const $pos = doc.resolve(pos);\n const node = $pos.nodeAfter!;\n const wrappers: TrackedBlockSnapshot[\"wrappers\"] = [];\n for (let depth = 1; depth <= $pos.depth; depth++) {\n const wrapper = $pos.node(depth);\n wrappers.push({ type: wrapper.type.name, attrs: stripBlockChange(wrapper.attrs) });\n }\n return { type: node.type.name, attrs: stripBlockChange(node.attrs), wrappers };\n}\n\nfunction sameBlock(\n a: TrackedBlockSnapshot,\n b: TrackedBlockSnapshot,\n ignored: ReadonlySet<string>,\n): boolean {\n if (a.type !== b.type) return false;\n const filter = (attrs: Record<string, any>) =>\n Object.fromEntries(\n Object.entries(attrs ?? {}).filter(\n ([key]) => key !== BLOCK_CHANGE_ATTRIBUTE && !ignored.has(key),\n ),\n );\n if (!sameValue(filter(a.attrs), filter(b.attrs))) return false;\n if (a.wrappers.length !== b.wrappers.length) return false;\n return a.wrappers.every((w, i) => w.type === b.wrappers[i].type);\n}\n\nconst isMarkChange = (type: string) =>\n type === INSERTION_MARK_NAME || type === DELETION_MARK_NAME || type === FORMAT_CHANGE_MARK_NAME;\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\n const add = (\n key: string,\n type: TrackedChangeType,\n attrs: TrackedChangeAttributes,\n from: number,\n to: number,\n text: string,\n ): TrackedChange => {\n let change = byKey.get(key);\n if (!change) {\n change = {\n id: attrs.changeId,\n type,\n author: {\n id: attrs.authorId,\n name: attrs.authorName,\n ...(attrs.authorColor ? { color: attrs.authorColor } : {}),\n },\n createdAt: attrs.createdAt,\n from,\n to,\n ranges: [],\n text: \"\",\n description: \"\",\n };\n byKey.set(key, change);\n }\n const last = change.ranges[change.ranges.length - 1];\n if (last && last.to === from) {\n last.to = to;\n } else {\n change.ranges.push({ from, to });\n }\n change.to = to;\n change.text += text;\n return change;\n };\n\n doc.descendants((node, pos) => {\n const block = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n if (block?.changeId && node.isTextblock) {\n const change = add(\n `block:${block.changeId}`,\n \"block\",\n block,\n pos,\n pos + node.nodeSize,\n node.textContent,\n );\n if (!change.block) {\n change.block = { previous: block.previous, current: blockSnapshot(doc, pos) };\n }\n }\n if (!node.isInline) return;\n node.marks.forEach((mark) => {\n const markType = mark.type.name;\n if (!isMarkChange(markType)) return;\n const attrs = changeAttributes(mark);\n if (!attrs.changeId) return;\n const type: TrackedChangeType = markType === FORMAT_CHANGE_MARK_NAME ? \"format\" : (markType as TrackedChangeType);\n const change = add(\n `${type}:${attrs.changeId}`,\n type,\n attrs,\n pos,\n pos + node.nodeSize,\n node.isText ? node.text ?? \"\" : \"\",\n );\n if (type === \"format\") {\n const summary = diffFormatting(mark.attrs.before ?? [], mark.attrs.after ?? []);\n if (!change.format) change.format = { added: [], removed: [], changed: [] };\n mergeSummary(change.format, summary);\n }\n });\n });\n\n const changes = Array.from(byKey.values());\n changes.forEach((change) => {\n switch (change.type) {\n case \"insertion\":\n change.description = change.text ? `Inserted \"${truncate(change.text)}\"` : \"Inserted content\";\n break;\n case \"deletion\":\n change.description = change.text ? `Deleted \"${truncate(change.text)}\"` : \"Deleted content\";\n break;\n case \"format\":\n change.description = describeFormatChange(change.format!);\n break;\n case \"block\":\n change.description = describeBlockChange(change.block!.previous, change.block!.current);\n break;\n }\n });\n return changes.sort((a, b) => a.from - b.from || a.to - b.to);\n}\n\n/** The change containing `pos` (defaults to the selection start). Inline changes win over block changes. */\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) => {\n const [start, end] = selectionRange(c);\n return start === from && end === to;\n });\n if (exact) return exact;\n const inline = changes.filter((c) => c.type !== \"block\");\n const blocks = changes.filter((c) => c.type === \"block\");\n return (\n inline.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ??\n inline.find((c) => c.ranges.some((r) => r.from <= pos && pos <= r.to)) ??\n blocks.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ??\n null\n );\n}\n\n/** Text positions selected for a change (block changes select their content). */\nfunction selectionRange(change: TrackedChange): [number, number] {\n return change.type === \"block\" ? [change.from + 1, change.to - 1] : [change.from, change.to];\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// Accept / reject\n// ---------------------------------------------------------------------------\n\nfunction marksFromJSON(schema: Schema, marks: TrackedMark[] | undefined): ProsemirrorMark[] {\n const result: ProsemirrorMark[] = [];\n (marks ?? []).forEach((json) => {\n try {\n if (schema.marks[json.type]) result.push(schema.markFromJSON(json));\n } catch {\n // Unknown or invalid mark: skip.\n }\n });\n return result;\n}\n\nfunction decideFormat(\n tr: Transform,\n changes: readonly TrackedChange[],\n decision: \"accept\" | \"reject\",\n): void {\n const schema = tr.doc.type.schema;\n const formatType = schema.marks[FORMAT_CHANGE_MARK_NAME];\n if (!formatType) return;\n const ids = new Set(changes.map((c) => c.id));\n const segments: { from: number; to: number; node: ProsemirrorNode; mark: ProsemirrorMark }[] = [];\n changes.forEach((change) =>\n change.ranges.forEach(({ from, to }) =>\n tr.doc.nodesBetween(from, to, (node, pos) => {\n if (!node.isInline) return true;\n const mark = formatType.isInSet(node.marks);\n if (mark && ids.has(mark.attrs.changeId)) {\n segments.push({ from: Math.max(pos, from), to: Math.min(pos + node.nodeSize, to), node, mark });\n }\n return false;\n }),\n ),\n );\n segments.forEach(({ from, to, node, mark }) => {\n tr.removeMark(from, to, formatType);\n if (decision === \"accept\") return;\n const before: TrackedMark[] = mark.attrs.before ?? [];\n const after: TrackedMark[] = mark.attrs.after ?? [];\n const involved = new Set([...before, ...after].map((m) => m.type));\n node.marks.forEach((current) => {\n if (involved.has(current.type.name)) tr.removeMark(from, to, current);\n });\n marksFromJSON(schema, before).forEach((restored) => tr.addMark(from, to, restored));\n });\n}\n\nconst LIST_ITEM_TYPES = new Set([\"listItem\", \"taskItem\"]);\n\n/**\n * The textblock at `contentPos` when it is the first child of a list item:\n * its resolved position and the item's depth.\n */\nfunction listItemAt(\n tr: Transform,\n contentPos: number,\n): { $pos: ResolvedPos; itemDepth: number } | null {\n const $pos = tr.doc.resolve(contentPos);\n const itemDepth = $pos.depth - 1;\n if (itemDepth < 2) return null;\n if (!LIST_ITEM_TYPES.has($pos.node(itemDepth).type.name) || $pos.index(itemDepth) !== 0) {\n return null;\n }\n return { $pos, itemDepth };\n}\n\n/** Nests a list item into its previous sibling (prosemirror-schema-list `sinkListItem`). */\nfunction sinkItem(tr: Transform, contentPos: number): boolean {\n const info = listItemAt(tr, contentPos);\n if (!info) return false;\n const { $pos, itemDepth } = info;\n const list = $pos.node(itemDepth - 1);\n const index = $pos.index(itemDepth - 1);\n const itemType = $pos.node(itemDepth).type;\n if (index === 0 || list.child(index - 1).type !== itemType) return false;\n const nodeBefore = list.child(index - 1);\n const nestedBefore = nodeBefore.lastChild?.type === list.type;\n const inner = Fragment.from(nestedBefore ? itemType.create() : null);\n const slice = new Slice(\n Fragment.from(itemType.create(null, Fragment.from(list.type.create(null, inner)))),\n nestedBefore ? 3 : 1,\n 0,\n );\n const before = $pos.before(itemDepth);\n const after = $pos.after(itemDepth);\n tr.step(\n new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true),\n );\n return true;\n}\n\n/**\n * Moves a nested list item out to the enclosing list (prosemirror-schema-list\n * `liftToOuterList`); following siblings become its children.\n */\nfunction outdentItem(tr: Transform, contentPos: number): boolean {\n const info = listItemAt(tr, contentPos);\n if (!info) return false;\n const { $pos, itemDepth } = info;\n const listDepth = itemDepth - 1;\n if (listDepth < 2 || !LIST_ITEM_TYPES.has($pos.node(listDepth - 1).type.name)) return false;\n const itemType = $pos.node(itemDepth).type;\n let range = new NodeRange(\n tr.doc.resolve($pos.before(itemDepth)),\n tr.doc.resolve($pos.after(itemDepth)),\n listDepth,\n );\n const end = range.end;\n const endOfList = range.$to.end(range.depth);\n if (end < endOfList) {\n tr.step(\n new ReplaceAroundStep(\n end - 1,\n endOfList,\n end,\n endOfList,\n new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0),\n 1,\n true,\n ),\n );\n range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);\n }\n const target = liftTarget(range);\n if (target == null) return false;\n tr.lift(range, target);\n return true;\n}\n\n/** Lifts/wraps the textblock whose content starts at `contentPos` to match `wrappers`. */\nfunction restoreWrappers(\n tr: Transform,\n contentPos: number,\n wrappers: TrackedBlockSnapshot[\"wrappers\"],\n): void {\n const schema = tr.doc.type.schema;\n /** Runs `fn`, maps `contentPos` and reports whether it added steps. */\n const step = (fn: () => unknown): boolean => {\n const index = tr.steps.length;\n const result = fn();\n contentPos = tr.mapping.slice(index).map(contentPos);\n return result !== false && tr.steps.length > index;\n };\n const chain = () => {\n const $pos = tr.doc.resolve(contentPos);\n const types: string[] = [];\n for (let depth = 1; depth < $pos.depth; depth++) types.push($pos.node(depth).type.name);\n return types;\n };\n const target = wrappers.map((w) => w.type);\n\n for (let guard = 0; guard < 24; guard++) {\n const current = chain();\n if (current.join(\"/\") === target.join(\"/\")) return;\n\n // Same nesting, different wrapper types (e.g. bulleted → numbered list).\n if (current.length === target.length) {\n const index = current.findIndex((type, i) => type !== target[i]);\n const type = schema.nodes[target[index]];\n if (type && LIST_ITEM_TYPES.has(type.name) === LIST_ITEM_TYPES.has(current[index])) {\n const $pos = tr.doc.resolve(contentPos);\n if (step(() => tr.setNodeMarkup($pos.before(index + 1), type, wrappers[index].attrs))) {\n continue;\n }\n }\n }\n\n let common = 0;\n while (common < current.length && common < target.length && current[common] === target[common]) {\n common++;\n }\n\n if (current.length > common) {\n // Leave wrappers that were not there before: list items move to the\n // outer list, anything else is lifted.\n if (step(() => outdentItem(tr, contentPos))) continue;\n const $from = tr.doc.resolve(contentPos);\n const range = $from.blockRange(tr.doc.resolve($from.end()));\n const lift = range ? liftTarget(range) : null;\n if (!range || lift == null || !step(() => tr.lift(range, lift))) return;\n continue;\n }\n\n // Enter the wrappers that were removed: a list item nests into its\n // previous sibling, anything else is wrapped.\n const missing = wrappers.slice(current.length);\n if (\n missing.length >= 2 &&\n LIST_ITEM_TYPES.has(missing[1].type) &&\n step(() => sinkItem(tr, contentPos))\n ) {\n continue;\n }\n const types = missing.map((w) => ({ type: schema.nodes[w.type], attrs: w.attrs }));\n if (types.some((t) => !t.type)) return;\n const $from = tr.doc.resolve(contentPos);\n const range = $from.blockRange(tr.doc.resolve($from.end()));\n if (!range) return;\n const start = range.start;\n step(() => tr.wrap(range, types as { type: NodeType; attrs: Record<string, any> }[]));\n // Re-join with neighbouring wrappers of the same type (restoring one list).\n const wrapper = tr.doc.nodeAt(start);\n if (!wrapper) return;\n const end = start + wrapper.nodeSize;\n if (tr.doc.resolve(end).nodeAfter?.type === wrapper.type && canJoin(tr.doc, end)) {\n step(() => tr.join(end));\n }\n if (tr.doc.resolve(start).nodeBefore?.type === wrapper.type && canJoin(tr.doc, start)) {\n step(() => tr.join(start));\n }\n return;\n }\n}\n\nfunction decideBlocks(\n tr: Transform,\n changes: readonly TrackedChange[],\n decision: \"accept\" | \"reject\",\n): void {\n const ids = new Set(changes.map((c) => c.id));\n const items: { contentPos: number; record: TrackedBlockChangeAttributes }[] = [];\n tr.doc.descendants((node, pos) => {\n const record = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n if (node.isTextblock && record?.changeId && ids.has(record.changeId)) {\n items.push({ contentPos: pos + 1, record });\n }\n return !node.isTextblock;\n });\n const schema = tr.doc.type.schema;\n const startIndex = tr.steps.length;\n // Last block first, so structural changes do not shift earlier blocks.\n items.reverse().forEach(({ contentPos: original, record }) => {\n const contentPos = tr.mapping.slice(startIndex).map(original);\n const $pos = tr.doc.resolve(contentPos);\n if (!$pos.parent.isTextblock || $pos.parentOffset !== 0) return;\n const pos = $pos.before();\n if (decision === \"accept\") {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n return;\n }\n const previous = record.previous;\n const type = previous && schema.nodes[previous.type];\n if (!type) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n return;\n }\n const attrs = { ...previous.attrs, [BLOCK_CHANGE_ATTRIBUTE]: null };\n try {\n tr.setNodeMarkup(pos, type, attrs);\n } catch {\n // The content does not fit the previous type (e.g. marks in a code\n // block): convert while dropping incompatible content.\n try {\n tr.setBlockType(pos + 1, pos + 1, type, attrs);\n } catch {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n }\n }\n try {\n restoreWrappers(tr, tr.mapping.slice(startIndex).map(original), previous.wrappers ?? []);\n } catch {\n // Best effort: the block keeps its current wrappers.\n }\n });\n}\n\n/**\n * Accepts or rejects changes on a transform. Formatting is resolved first,\n * then inline marks are removed and text is deleted from the end of the\n * document backwards so positions stay valid; block changes come last.\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 format = changes.filter((c) => c.type === \"format\");\n if (format.length) decideFormat(tr, format, decision);\n\n const deletions: { from: number; to: number }[] = [];\n changes.forEach((change) => {\n if (change.type !== \"insertion\" && change.type !== \"deletion\") return;\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 const blocks = changes.filter((c) => c.type === \"block\");\n if (blocks.length) decideBlocks(tr, blocks, decision);\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\n// ---------------------------------------------------------------------------\n// Tracking\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 review marks/attributes from a slice (used for\n * paste), so pasting tracked content pastes it as if all changes were\n * accepted.\n */\nfunction acceptSlice(slice: Slice, schema: Schema): Slice {\n const insertion = schema.marks[INSERTION_MARK_NAME];\n const deletion = schema.marks[DELETION_MARK_NAME];\n const format = schema.marks[FORMAT_CHANGE_MARK_NAME];\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 let marks = child.marks;\n if (insertion) marks = insertion.removeFromSet(marks);\n if (format) marks = format.removeFromSet(marks);\n children.push(marks === child.marks ? child : child.mark(marks));\n } else if (child.attrs?.[BLOCK_CHANGE_ATTRIBUTE]) {\n children.push(\n child.type.create(\n { ...child.attrs, [BLOCK_CHANGE_ATTRIBUTE]: null },\n mapFragment(child.content),\n child.marks,\n ),\n );\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\n/** Copy of `fragment` without inline marks (adjacent text is merged). */\nfunction stripMarks(fragment: Fragment): Fragment {\n const children: ProsemirrorNode[] = [];\n fragment.forEach((child) => {\n children.push(child.isInline ? child.mark(ProsemirrorMark.none) : child.copy(stripMarks(child.content)));\n });\n return Fragment.fromArray(children);\n}\n\ninterface TrackContext {\n author: ReviewAuthor;\n now: () => number;\n generateId: () => string;\n insertion: MarkType;\n deletion: MarkType;\n /** Present when formatting is tracked. */\n format?: MarkType;\n trackBlocks: boolean;\n ignoredMarks: ReadonlySet<string>;\n ignoredAttributes: ReadonlySet<string>;\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\nfunction baseAttributes(attrs: TrackedChangeAttributes): TrackedChangeAttributes {\n return {\n changeId: attrs.changeId,\n authorId: attrs.authorId,\n authorName: attrs.authorName,\n authorColor: attrs.authorColor ?? null,\n createdAt: attrs.createdAt,\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, in which case the deletion is not 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\ninterface PendingBlock {\n previous: TrackedBlockSnapshot;\n existing: TrackedBlockChangeAttributes | null;\n}\n\n/** Formatting marks of a node, as JSON (review and ignored marks excluded). */\nfunction formattingOf(marks: readonly ProsemirrorMark[], ctx: TrackContext): TrackedMark[] {\n return marks\n .filter((m) => !REVIEW_MARK_NAMES.has(m.type.name) && !ctx.ignoredMarks.has(m.type.name))\n .map((m) => m.toJSON() as TrackedMark);\n}\n\nfunction mergeRanges(ranges: { from: number; to: number }[]): { from: number; to: number }[] {\n const sorted = ranges.filter((r) => r.to > r.from).sort((a, b) => a.from - b.from);\n const merged: { from: number; to: number }[] = [];\n sorted.forEach((range) => {\n const last = merged[merged.length - 1];\n if (last && range.from <= last.to) last.to = Math.max(last.to, range.to);\n else merged.push({ ...range });\n });\n return merged;\n}\n\n/**\n * Records formatting changes on `ranges` (final-document positions) by\n * comparing each piece of text with the same text in `oldDoc`.\n */\nfunction trackFormatting(\n tr: Transaction,\n ranges: { from: number; to: number }[],\n oldDoc: ProsemirrorNode,\n back: Mapping,\n inserted: { from: number; to: number }[],\n ctx: TrackContext,\n): boolean {\n const format = ctx.format!;\n const doc = tr.doc;\n let changed = false;\n let batch: TrackedChangeAttributes | null = null;\n\n mergeRanges(ranges).forEach(({ from: a, to: b }) => {\n const segments: { from: number; to: number; oldNode: ProsemirrorNode; node: ProsemirrorNode }[] = [];\n doc.nodesBetween(a, b, (node, pos, parent) => {\n if (!node.isInline) return true;\n const s = Math.max(a, pos);\n const e = Math.min(b, pos + node.nodeSize);\n if (e <= s || !parent || !parent.type.allowsMarkType(format)) return false;\n // Formatting a tracked insertion is part of the insertion.\n if (ctx.insertion.isInSet(node.marks)) return false;\n if (inserted.some((r) => s < r.to && e > r.from)) return false;\n const os = back.map(s, 1);\n const oe = back.map(e, -1);\n if (oe - os !== e - s) return false;\n oldDoc.nodesBetween(os, oe, (oldNode, oldPos) => {\n if (!oldNode.isInline) return true;\n const ss = Math.max(os, oldPos);\n const ee = Math.min(oe, oldPos + oldNode.nodeSize);\n if (ee > ss && !ctx.insertion.isInSet(oldNode.marks)) {\n segments.push({ from: s + (ss - os), to: s + (ee - os), oldNode, node });\n }\n return false;\n });\n return false;\n });\n\n segments.forEach(({ from, to, oldNode, node }) => {\n const existing = format.isInSet(oldNode.marks);\n const original: TrackedMark[] = existing\n ? (existing.attrs.before as TrackedMark[]) ?? []\n : formattingOf(oldNode.marks, ctx);\n const current = formattingOf(node.marks, ctx);\n if (sameValue(original, current)) {\n if (existing || format.isInSet(node.marks)) {\n tr.removeMark(from, to, format);\n changed = true;\n }\n return;\n }\n let attrs: TrackedChangeAttributes;\n if (existing && existing.attrs.authorId === ctx.author.id && existing.attrs.changeId) {\n attrs = baseAttributes(changeAttributes(existing));\n } else {\n if (!batch) {\n const description = describeFormatChange(diffFormatting(original, current));\n const neighbours = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];\n for (const neighbour of neighbours) {\n const mark = neighbour && format.isInSet(neighbour.marks);\n if (\n mark &&\n mark.attrs.authorId === ctx.author.id &&\n mark.attrs.changeId &&\n describeFormatChange(diffFormatting(mark.attrs.before ?? [], mark.attrs.after ?? [])) ===\n description\n ) {\n batch = baseAttributes(changeAttributes(mark));\n break;\n }\n }\n batch = batch ?? newAttributes(ctx);\n }\n attrs = batch;\n }\n tr.addMark(from, to, format.create({ ...attrs, before: original, after: current }));\n changed = true;\n });\n });\n return changed;\n}\n\nfunction trackBlocks(\n tr: Transaction,\n pending: Map<number, PendingBlock>,\n ctx: TrackContext,\n): boolean {\n let changed = false;\n let batch: TrackedChangeAttributes | null = null;\n pending.forEach(({ previous, existing }, contentPos) => {\n if (contentPos < 0 || contentPos > tr.doc.content.size) return;\n const $pos = tr.doc.resolve(contentPos);\n const node = $pos.parent;\n if (!node.isTextblock || $pos.parentOffset !== 0 || $pos.depth === 0) return;\n if (!node.type.spec.attrs || !(BLOCK_CHANGE_ATTRIBUTE in node.type.spec.attrs)) return;\n const pos = $pos.before();\n const original = existing?.previous ?? previous;\n const current = blockSnapshot(tr.doc, pos);\n if (sameBlock(original, current, ctx.ignoredAttributes)) {\n if (node.attrs[BLOCK_CHANGE_ATTRIBUTE]) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n changed = true;\n }\n return;\n }\n const base =\n existing && existing.authorId === ctx.author.id && existing.changeId\n ? baseAttributes(existing)\n : (batch = batch ?? newAttributes(ctx));\n const value: TrackedBlockChangeAttributes = { ...base, previous: original };\n if (!sameValue(node.attrs[BLOCK_CHANGE_ATTRIBUTE], value)) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, value);\n changed = true;\n }\n });\n return changed;\n}\n\n/** Records textblocks whose type, attributes or wrappers changed in a step. */\nfunction collectBlockChanges(\n step: ReplaceAroundStep | AttrStep,\n docBefore: ProsemirrorNode,\n docAfter: ProsemirrorNode,\n rest: Mapping,\n pending: Map<number, PendingBlock>,\n ctx: TrackContext,\n): void {\n const record = (oldPos: number, newPos: number) => {\n const key = rest.map(newPos + 1);\n if (pending.has(key)) return;\n const oldNode = docBefore.nodeAt(oldPos)!;\n const previous = blockSnapshot(docBefore, oldPos);\n if (sameBlock(previous, blockSnapshot(docAfter, newPos), ctx.ignoredAttributes)) return;\n pending.set(key, {\n previous,\n existing: (oldNode.attrs[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes) ?? null,\n });\n };\n\n if (step instanceof AttrStep) {\n const node = docBefore.nodeAt(step.pos);\n if (\n node?.isTextblock &&\n step.attr !== BLOCK_CHANGE_ATTRIBUTE &&\n !ctx.ignoredAttributes.has(step.attr)\n ) {\n record(step.pos, step.pos);\n }\n return;\n }\n\n const map = step.getMap();\n const back = map.invert();\n const start = map.map(step.from, -1);\n const end = map.map(step.to, 1);\n docAfter.nodesBetween(start, end, (node, pos) => {\n if (!node.isTextblock) return true;\n const oldContent = back.map(pos + 1);\n if (oldContent < 1 || oldContent > docBefore.content.size) return false;\n const $old = docBefore.resolve(oldContent);\n if (\n $old.parent.isTextblock &&\n $old.parentOffset === 0 &&\n $old.parent.content.size === node.content.size\n ) {\n record($old.before(), pos);\n }\n return false;\n });\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, formatting changes on existing text get the\n * `formatChange` mark and textblock type/wrapping changes get the\n * `blockChange` attribute. Paragraph splits and joins 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 const formatRanges: { from: number; to: number }[] = [];\n const blocks = new Map<number, PendingBlock>();\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) return;\n const rest = new Mapping(maps.slice(index + 1));\n const docBefore = t.docs[i];\n\n if (step instanceof AddMarkStep || step instanceof RemoveMarkStep) {\n const name = step.mark.type.name;\n if (ctx.format && !REVIEW_MARK_NAMES.has(name) && !ctx.ignoredMarks.has(name)) {\n formatRanges.push({ from: rest.map(step.from, 1), to: rest.map(step.to, -1) });\n }\n return;\n }\n if (step instanceof ReplaceAroundStep || step instanceof AttrStep) {\n if (ctx.trackBlocks) {\n const docAfter = t.docs[i + 1] ?? t.doc;\n collectBlockChanges(step, docBefore, docAfter, rest, blocks, ctx);\n }\n return;\n }\n if (!(step instanceof ReplaceStep)) return;\n const { from, to, slice } = step as unknown as { from: number; to: number; slice: Slice };\n\n // Replacing content with the same content: only marks may differ.\n if (to > from && slice.size === to - from) {\n const deleted = docBefore.slice(from, to);\n if (\n slice.openStart === deleted.openStart &&\n slice.openEnd === deleted.openEnd &&\n stripMarks(slice.content).eq(stripMarks(deleted.content))\n ) {\n if (ctx.format && !slice.content.eq(deleted.content)) {\n formatRanges.push({ from: rest.map(from, 1), to: rest.map(to, -1) });\n }\n return;\n }\n }\n\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 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 (\n inserted.length === 0 &&\n restores.length === 0 &&\n formatRanges.length === 0 &&\n blocks.size === 0\n ) {\n return null;\n }\n\n const tr = newState.tr;\n let changed = false;\n\n // Formatting and block attributes first: they do not move positions.\n if (formatRanges.length && ctx.format) {\n const back = new Mapping(maps).invert();\n changed = trackFormatting(tr, formatRanges, oldState.doc, back, inserted, ctx) || changed;\n }\n if (blocks.size) {\n changed = trackBlocks(tr, blocks, ctx) || changed;\n }\n\n inserted.forEach(({ from, to }) => {\n tr.removeMark(from, to, ctx.deletion);\n tr.removeMark(from, to, ctx.insertion);\n if (ctx.format) tr.removeMark(from, to, ctx.format);\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 * Lets code blocks (`marks: \"\"`) carry the insertion and deletion marks.\n * The node's own `marks` spec wins over `extendNodeSchema`, so the allowed\n * mark set of the built schema is extended instead.\n */\nfunction allowTrackingInCodeBlocks(schema: Schema): void {\n const marks = [schema.marks[INSERTION_MARK_NAME], schema.marks[DELETION_MARK_NAME]].filter(\n Boolean,\n ) as MarkType[];\n Object.values(schema.nodes).forEach((type) => {\n const markSet = type.markSet;\n if (!type.isTextblock || !type.spec.code || markSet === null) return;\n const missing = marks.filter((mark) => !markSet.includes(mark));\n if (missing.length) {\n (type as { markSet: readonly MarkType[] | null }).markSet = [...markSet, ...missing];\n }\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 * Formatting changes on existing text are recorded with the `formatChange`\n * mark, and textblock type/attribute/wrapping changes with the `blockChange`\n * node attribute.\n *\n * Deleting your own insertion really deletes it. Paragraph splits and joins\n * are not 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 formatChangeHTMLAttributes: {},\n blockChangeClass: \"pixerate-block-change border-l-2 border-amber-400 pl-2 dark:border-amber-500\",\n trackFormatting: true,\n trackBlocks: true,\n trackCodeBlocks: true,\n blockTypes: [\"paragraph\", \"heading\", \"codeBlock\"],\n ignoredMarks: [],\n ignoredBlockAttributes: [\"id\"],\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 addGlobalAttributes() {\n if (this.options?.trackBlocks === false) return [];\n const blockClass = this.options?.blockChangeClass ?? \"\";\n return [\n {\n types: this.options?.blockTypes ?? [\"paragraph\", \"heading\", \"codeBlock\"],\n attributes: {\n [BLOCK_CHANGE_ATTRIBUTE]: {\n default: null,\n keepOnSplit: false,\n parseHTML: (el: HTMLElement) => parseJSON(el.getAttribute(\"data-block-change\"), null),\n renderHTML: (attrs: Record<string, any>) => {\n const value = attrs[BLOCK_CHANGE_ATTRIBUTE];\n if (!value) return {};\n return {\n \"data-block-change\": JSON.stringify(value),\n ...(blockClass ? { class: blockClass } : {}),\n };\n },\n },\n },\n },\n ];\n },\n\n addExtensions() {\n const insertionAttrs = this.options?.insertionHTMLAttributes ?? {};\n const deletionAttrs = this.options?.deletionHTMLAttributes ?? {};\n const formatAttrs = this.options?.formatChangeHTMLAttributes ?? {};\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 Object.keys(formatAttrs).length\n ? TrackedFormatChange.configure({ HTMLAttributes: formatAttrs })\n : TrackedFormatChange,\n ];\n },\n\n onBeforeCreate() {\n if (this.options.trackCodeBlocks) allowTrackingInCodeBlocks(this.editor.schema);\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 start = (c: TrackedChange) => selectionRange(c)[0];\n const target =\n direction === 1\n ? changes.find((c) => start(c) > from) ?? changes[0]\n : [...changes].reverse().find((c) => start(c) < from) ?? changes[changes.length - 1];\n if (dispatch) {\n const [anchor, head] = selectionRange(target);\n tr.setSelection(TextSelection.create(tr.doc, anchor, head)).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 const ignoredMarks = new Set(options.ignoredMarks ?? []);\n const ignoredAttributes = new Set(options.ignoredBlockAttributes ?? []);\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 format:\n options.trackFormatting !== false\n ? newState.schema.marks[FORMAT_CHANGE_MARK_NAME]\n : undefined,\n trackBlocks: options.trackBlocks !== false,\n ignoredMarks,\n ignoredAttributes,\n });\n },\n props: {\n transformPasted: (slice, view) => acceptSlice(slice, view.state.schema),\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"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkAA4ZEVCCcjs = require('./chunk-AA4ZEVCC.cjs');
|
|
5
5
|
|
|
6
6
|
// src/serializers/html.ts
|
|
7
7
|
var DROP_WITH_CONTENT_REGEX = /<(script|style|template|noscript|iframe|object|embed|head|title|svg|math|textarea|select|button)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
|
|
@@ -15,12 +15,12 @@ function sanitizeImportedHtml(html) {
|
|
|
15
15
|
const body = BODY_REGEX.exec(content);
|
|
16
16
|
if (body) content = body[1];
|
|
17
17
|
content = content.replace(DROP_WITH_CONTENT_REGEX, "").replace(UNCLOSED_RAW_TEXT_REGEX, "").replace(NAMESPACED_TAG_REGEX, "");
|
|
18
|
-
const sanitized =
|
|
18
|
+
const sanitized = _chunkAA4ZEVCCcjs.sanitizeHtml.call(void 0, content, { disallowedTags: "strip" }).trim();
|
|
19
19
|
return sanitized || "<p></p>";
|
|
20
20
|
}
|
|
21
21
|
function plainTextToParagraphHtml(text) {
|
|
22
22
|
if (!text) return "<p></p>";
|
|
23
|
-
return text.replace(/\r\n?/g, "\n").split("\n").map((line) => `<p>${
|
|
23
|
+
return text.replace(/\r\n?/g, "\n").split("\n").map((line) => `<p>${_chunkAA4ZEVCCcjs.escapeHtml.call(void 0, line)}</p>`).join("");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// src/serializers/rtf.ts
|
|
@@ -296,4 +296,4 @@ function readRichClipboard(event, schema, mimeType = PIXERATE_DOCUMENT_MIME) {
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
exports.sanitizeImportedHtml = sanitizeImportedHtml; exports.plainTextToParagraphHtml = plainTextToParagraphHtml; exports.rtfToPlainText = rtfToPlainText; exports.PIXERATE_DOCUMENT_MIME = PIXERATE_DOCUMENT_MIME; exports.getSchemaSignature = getSchemaSignature; exports.serializeRichClipboardPayload = serializeRichClipboardPayload; exports.parseRichClipboardPayload = parseRichClipboardPayload; exports.getClipboardData = getClipboardData; exports.readRichClipboard = readRichClipboard;
|
|
299
|
-
//# sourceMappingURL=chunk-
|
|
299
|
+
//# sourceMappingURL=chunk-CL37JGFW.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-LA7PEGU3.cjs","../src/serializers/html.ts","../src/serializers/rtf.ts","../src/serializers/clipboard.ts"],"names":[],"mappings":"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","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-LA7PEGU3.cjs","sourcesContent":[null,"import { sanitizeHtml } from \"./markdown\";\nimport { escapeHtml } from \"./plain-text\";\n\n/**\n * Elements whose content is never document text (scripts, styles, metadata,\n * embedded objects, form controls). They are removed together with their\n * content before sanitizing, so e.g. CSS rules don't show up as paragraphs.\n */\nconst DROP_WITH_CONTENT_REGEX =\n /<(script|style|template|noscript|iframe|object|embed|head|title|svg|math|textarea|select|button)\\b[^>]*>[\\s\\S]*?<\\/\\1\\s*>/gi;\n/** An unclosed `<script>` / `<style>` swallows the rest of the document. */\nconst UNCLOSED_RAW_TEXT_REGEX = /<(script|style)\\b[^>]*>[\\s\\S]*$/i;\nconst BODY_REGEX = /<body\\b[^>]*>([\\s\\S]*?)(?:<\\/body\\s*>|$)/i;\n/** Namespaced tags such as Word's `<o:p>`, which the sanitizer cannot parse. */\nconst NAMESPACED_TAG_REGEX = /<\\/?[a-zA-Z][\\w-]*:[\\w-]+\\b[^>]*>/g;\n\n/**\n * Sanitizes a full HTML document or fragment for insertion into the editor,\n * e.g. an imported `.html` file or HTML from another application.\n *\n * - Keeps only the `<body>` content when the input is a full document.\n * - Removes comments, doctype/XML declarations, CDATA, scripts, styles and\n * other non-text elements together with their content.\n * - Applies the same tag/attribute/URL allowlist as embedded HTML in markdown\n * (see `sanitizeHtml`), but strips disallowed tags instead of escaping them,\n * so wrappers such as `<section>` or `<font>` keep their text.\n *\n * It is string-based (no live DOM), so handlers such as `<img onerror>` never run.\n */\nexport function sanitizeImportedHtml(html: string): string {\n if (!html) return \"<p></p>\";\n let content = html.replace(/^/, \"\");\n content = content\n .replace(/<!--[\\s\\S]*?(?:-->|$)/g, \"\")\n .replace(/<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>|$)/g, \"\")\n .replace(/<![^>]*>/g, \"\")\n .replace(/<\\?[\\s\\S]*?(?:\\?>|$)/g, \"\");\n const body = BODY_REGEX.exec(content);\n if (body) content = body[1];\n content = content\n .replace(DROP_WITH_CONTENT_REGEX, \"\")\n .replace(UNCLOSED_RAW_TEXT_REGEX, \"\")\n .replace(NAMESPACED_TAG_REGEX, \"\");\n const sanitized = sanitizeHtml(content, { disallowedTags: \"strip\" }).trim();\n return sanitized || \"<p></p>\";\n}\n\n/**\n * Converts untrusted plain text to TipTap paragraph HTML, always escaping it.\n *\n * Unlike `plainTextToTipTapHtml`, text that already looks like `<p>…</p>` is\n * not passed through as HTML, which matters for imported files.\n */\nexport function plainTextToParagraphHtml(text: string): string {\n if (!text) return \"<p></p>\";\n return text\n .replace(/\\r\\n?/g, \"\\n\")\n .split(\"\\n\")\n .map((line) => `<p>${escapeHtml(line)}</p>`)\n .join(\"\");\n}\n","/**\n * Destinations whose content is metadata or binary data rather than document\n * text. A group starting with one of these control words (or with `\\*`) is\n * skipped entirely.\n */\nconst SKIPPED_DESTINATIONS = new Set([\n \"author\", \"bkmkend\", \"bkmkstart\", \"colortbl\", \"comment\", \"datastore\",\n \"doccomm\", \"falt\", \"fldinst\", \"filetbl\", \"fonttbl\", \"footer\", \"footerf\",\n \"footerl\", \"footerr\", \"footnote\", \"generator\", \"header\", \"headerf\",\n \"headerl\", \"headerr\", \"info\", \"keywords\", \"latentstyles\", \"listoverridetable\",\n \"listtable\", \"nonshppict\", \"object\", \"operator\", \"pict\", \"private\", \"revtbl\",\n \"rsidtbl\", \"shppict\", \"stylesheet\", \"subject\", \"themedata\", \"title\",\n \"xmlnstbl\", \"colorschememapping\",\n]);\n\n/** Control words that map to a literal character. */\nconst CONTROL_WORD_TEXT: Record<string, string> = {\n par: \"\\n\",\n sect: \"\\n\",\n page: \"\\n\",\n line: \"\\n\",\n row: \"\\n\",\n cell: \"\\t\",\n tab: \"\\t\",\n emdash: \"—\",\n endash: \"–\",\n emspace: \" \",\n enspace: \" \",\n bullet: \"•\",\n lquote: \"‘\",\n rquote: \"’\",\n ldblquote: \"“\",\n rdblquote: \"”\",\n};\n\n/** Windows-1252 code points in 0x80–0x9F that differ from Latin-1. */\nconst CP1252: Record<number, string> = {\n 0x80: \"€\", 0x82: \"‚\", 0x83: \"ƒ\", 0x84: \"„\", 0x85: \"…\",\n 0x86: \"†\", 0x87: \"‡\", 0x88: \"ˆ\", 0x89: \"‰\", 0x8a: \"Š\",\n 0x8b: \"‹\", 0x8c: \"Œ\", 0x8e: \"Ž\", 0x91: \"‘\", 0x92: \"’\",\n 0x93: \"“\", 0x94: \"”\", 0x95: \"•\", 0x96: \"–\", 0x97: \"—\",\n 0x98: \"˜\", 0x99: \"™\", 0x9a: \"š\", 0x9b: \"›\", 0x9c: \"œ\",\n 0x9e: \"ž\", 0x9f: \"Ÿ\",\n};\n\nconst CONTROL_WORD_REGEX = /\\\\([a-zA-Z]{1,32})(-?\\d{1,10})? ?/y;\n\n/**\n * Best-effort plain-text extraction from an RTF document.\n *\n * Handles paragraphs/line breaks/tabs, escaped braces and backslashes,\n * `\\'hh` hex escapes (decoded as Windows-1252), `\\uN` Unicode escapes with\n * their `\\ucN` fallback characters, common typographic control words, and\n * skips metadata destinations (font/colour tables, `\\info`, pictures, field\n * instructions, `\\*` destinations). Formatting (bold, lists, tables) is not\n * preserved. Input that does not start with `{\\rtf` is returned unchanged.\n */\nexport function rtfToPlainText(rtf: string): string {\n if (!rtf) return \"\";\n if (!/^\\s*\\{\\\\rtf/.test(rtf)) return rtf;\n\n let out = \"\";\n const stack: Array<{ skip: boolean; uc: number }> = [];\n let skip = false;\n // Number of fallback characters that follow a \\uN escape.\n let uc = 1;\n let pendingFallback = 0;\n // True right after `{`, so a destination control word can mark the group.\n let groupStart = false;\n\n const emit = (text: string) => {\n if (skip) return;\n if (pendingFallback > 0) {\n pendingFallback--;\n return;\n }\n out += text;\n };\n\n let i = 0;\n while (i < rtf.length) {\n const ch = rtf[i];\n if (ch === \"{\") {\n stack.push({ skip, uc });\n groupStart = true;\n i++;\n continue;\n }\n if (ch === \"}\") {\n const prev = stack.pop();\n if (prev) {\n skip = prev.skip;\n uc = prev.uc;\n }\n pendingFallback = 0;\n groupStart = false;\n i++;\n continue;\n }\n const atGroupStart = groupStart;\n groupStart = false;\n\n if (ch === \"\\\\\") {\n const next = rtf[i + 1];\n if (next === \"\\\\\" || next === \"{\" || next === \"}\") {\n emit(next);\n i += 2;\n continue;\n }\n if (next === \"*\") {\n // `{\\*\\dest …}`: an ignorable destination this reader doesn't know.\n skip = true;\n i += 2;\n continue;\n }\n if (next === \"'\") {\n const code = parseInt(rtf.slice(i + 2, i + 4), 16);\n if (!Number.isNaN(code)) emit(CP1252[code] ?? String.fromCharCode(code));\n i += 4;\n continue;\n }\n if (next === \"~\") {\n emit(\" \");\n i += 2;\n continue;\n }\n if (next === \"_\") {\n emit(\"‑\");\n i += 2;\n continue;\n }\n if (next === \"\\n\" || next === \"\\r\") {\n emit(\"\\n\");\n i += 2;\n continue;\n }\n CONTROL_WORD_REGEX.lastIndex = i;\n const match = CONTROL_WORD_REGEX.exec(rtf);\n if (!match) {\n // Other control symbols (`\\-`, `\\|`, `\\:`) produce no text.\n i += 2;\n continue;\n }\n i = CONTROL_WORD_REGEX.lastIndex;\n const word = match[1];\n const param = match[2] !== undefined ? parseInt(match[2], 10) : undefined;\n if (atGroupStart && SKIPPED_DESTINATIONS.has(word)) {\n skip = true;\n } else if (word === \"u\" && param !== undefined) {\n emit(String.fromCharCode(param < 0 ? param + 65536 : param));\n if (!skip) pendingFallback = uc;\n } else if (word === \"uc\" && param !== undefined) {\n uc = param;\n } else if (word === \"bin\" && param !== undefined) {\n // Raw binary data: skip it.\n i += param;\n } else if (CONTROL_WORD_TEXT[word] !== undefined) {\n emit(CONTROL_WORD_TEXT[word]);\n }\n continue;\n }\n // Raw newlines are formatting in RTF; only control words break lines.\n if (ch === \"\\n\" || ch === \"\\r\") {\n i++;\n continue;\n }\n emit(ch);\n i++;\n }\n\n return out.replace(/\\n+$/, \"\");\n}\n","import { Slice, type Schema } from \"@tiptap/pm/model\";\n\n/**\n * Custom clipboard MIME type for content copied from a @pixerate/editor\n * rich-text editor. It carries the ProseMirror slice as JSON so pasting back\n * into a compatible editor is lossless (no HTML round trip).\n */\nexport const PIXERATE_DOCUMENT_MIME = \"application/x.pixerate.document+json\";\n\n/** Payload written under {@link PIXERATE_DOCUMENT_MIME}. */\nexport interface RichClipboardPayload {\n version: 1;\n /** {@link getSchemaSignature} of the source editor's schema. */\n schema: string;\n /** `Slice.toJSON()` of the copied selection. */\n slice: { content?: unknown[]; openStart?: number; openEnd?: number } | null;\n}\n\nconst signatureCache = new WeakMap<Schema, string>();\n\n/** 32-bit FNV-1a, enough to tell schemas apart without a crypto dependency. */\nfunction fnv1a(text: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < text.length; i++) {\n hash ^= text.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/**\n * Returns a stable signature for a ProseMirror schema: node names, content\n * expressions, groups, attribute names and mark names. Two editors with the\n * same extensions produce the same signature, so a slice copied from one can\n * be deserialized in the other without losing or rejecting nodes.\n */\nexport function getSchemaSignature(schema: Schema): string {\n const cached = signatureCache.get(schema);\n if (cached) return cached;\n const parts: string[] = [];\n for (const [name, type] of Object.entries(schema.nodes)) {\n const spec = type.spec;\n parts.push(\n `n:${name}|${spec.content ?? \"\"}|${spec.group ?? \"\"}|${spec.marks ?? \"\"}|${Object.keys(type.spec.attrs ?? {}).sort().join(\",\")}`,\n );\n }\n for (const [name, type] of Object.entries(schema.marks)) {\n parts.push(`m:${name}|${Object.keys(type.spec.attrs ?? {}).sort().join(\",\")}`);\n }\n const signature = `pm1-${fnv1a(parts.join(\";\"))}`;\n signatureCache.set(schema, signature);\n return signature;\n}\n\n/** Serializes a slice into the {@link PIXERATE_DOCUMENT_MIME} payload string. */\nexport function serializeRichClipboardPayload(slice: Slice, schema: Schema): string {\n const payload: RichClipboardPayload = {\n version: 1,\n schema: getSchemaSignature(schema),\n slice: slice.toJSON(),\n };\n return JSON.stringify(payload);\n}\n\n/**\n * Parses a {@link PIXERATE_DOCUMENT_MIME} payload into a slice for `schema`.\n * Returns `null` (so callers fall back to HTML / plain text) when the payload\n * is malformed, from another payload version, or from an editor whose schema\n * signature differs.\n */\nexport function parseRichClipboardPayload(data: string | null | undefined, schema: Schema): Slice | null {\n if (!data) return null;\n try {\n const payload = JSON.parse(data) as Partial<RichClipboardPayload>;\n if (!payload || payload.version !== 1) return null;\n if (payload.schema !== getSchemaSignature(schema)) return null;\n if (!payload.slice) return null;\n return Slice.fromJSON(schema, payload.slice as any);\n } catch {\n return null;\n }\n}\n\n/** Minimal clipboard data surface (`DataTransfer`). */\nexport interface ClipboardDataLike {\n getData(format: string): string;\n setData(format: string, data: string): void;\n clearData?(format?: string): void;\n}\n\n/**\n * Returns the `clipboardData` of a native `ClipboardEvent` or of a React\n * synthetic clipboard event (which wraps the native event in `nativeEvent`).\n */\nexport function getClipboardData(event: unknown): ClipboardDataLike | null {\n if (!event || typeof event !== \"object\") return null;\n const native = \"nativeEvent\" in event ? (event as any).nativeEvent : event;\n return ((event as any).clipboardData ?? native?.clipboardData ?? null) as ClipboardDataLike | null;\n}\n\n/**\n * Reads a slice from a paste event's {@link PIXERATE_DOCUMENT_MIME} data.\n * Returns `null` when the data is missing or its schema does not match.\n */\nexport function readRichClipboard(\n event: unknown,\n schema: Schema,\n mimeType: string = PIXERATE_DOCUMENT_MIME,\n): Slice | null {\n const data = getClipboardData(event);\n if (!data) return null;\n let raw: string | undefined;\n try {\n raw = data.getData(mimeType);\n } catch {\n return null;\n }\n return parseRichClipboardPayload(raw, schema);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-CL37JGFW.cjs","../src/serializers/html.ts","../src/serializers/rtf.ts","../src/serializers/clipboard.ts"],"names":[],"mappings":"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","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-CL37JGFW.cjs","sourcesContent":[null,"import { sanitizeHtml } from \"./markdown\";\nimport { escapeHtml } from \"./plain-text\";\n\n/**\n * Elements whose content is never document text (scripts, styles, metadata,\n * embedded objects, form controls). They are removed together with their\n * content before sanitizing, so e.g. CSS rules don't show up as paragraphs.\n */\nconst DROP_WITH_CONTENT_REGEX =\n /<(script|style|template|noscript|iframe|object|embed|head|title|svg|math|textarea|select|button)\\b[^>]*>[\\s\\S]*?<\\/\\1\\s*>/gi;\n/** An unclosed `<script>` / `<style>` swallows the rest of the document. */\nconst UNCLOSED_RAW_TEXT_REGEX = /<(script|style)\\b[^>]*>[\\s\\S]*$/i;\nconst BODY_REGEX = /<body\\b[^>]*>([\\s\\S]*?)(?:<\\/body\\s*>|$)/i;\n/** Namespaced tags such as Word's `<o:p>`, which the sanitizer cannot parse. */\nconst NAMESPACED_TAG_REGEX = /<\\/?[a-zA-Z][\\w-]*:[\\w-]+\\b[^>]*>/g;\n\n/**\n * Sanitizes a full HTML document or fragment for insertion into the editor,\n * e.g. an imported `.html` file or HTML from another application.\n *\n * - Keeps only the `<body>` content when the input is a full document.\n * - Removes comments, doctype/XML declarations, CDATA, scripts, styles and\n * other non-text elements together with their content.\n * - Applies the same tag/attribute/URL allowlist as embedded HTML in markdown\n * (see `sanitizeHtml`), but strips disallowed tags instead of escaping them,\n * so wrappers such as `<section>` or `<font>` keep their text.\n *\n * It is string-based (no live DOM), so handlers such as `<img onerror>` never run.\n */\nexport function sanitizeImportedHtml(html: string): string {\n if (!html) return \"<p></p>\";\n let content = html.replace(/^/, \"\");\n content = content\n .replace(/<!--[\\s\\S]*?(?:-->|$)/g, \"\")\n .replace(/<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>|$)/g, \"\")\n .replace(/<![^>]*>/g, \"\")\n .replace(/<\\?[\\s\\S]*?(?:\\?>|$)/g, \"\");\n const body = BODY_REGEX.exec(content);\n if (body) content = body[1];\n content = content\n .replace(DROP_WITH_CONTENT_REGEX, \"\")\n .replace(UNCLOSED_RAW_TEXT_REGEX, \"\")\n .replace(NAMESPACED_TAG_REGEX, \"\");\n const sanitized = sanitizeHtml(content, { disallowedTags: \"strip\" }).trim();\n return sanitized || \"<p></p>\";\n}\n\n/**\n * Converts untrusted plain text to TipTap paragraph HTML, always escaping it.\n *\n * Unlike `plainTextToTipTapHtml`, text that already looks like `<p>…</p>` is\n * not passed through as HTML, which matters for imported files.\n */\nexport function plainTextToParagraphHtml(text: string): string {\n if (!text) return \"<p></p>\";\n return text\n .replace(/\\r\\n?/g, \"\\n\")\n .split(\"\\n\")\n .map((line) => `<p>${escapeHtml(line)}</p>`)\n .join(\"\");\n}\n","/**\n * Destinations whose content is metadata or binary data rather than document\n * text. A group starting with one of these control words (or with `\\*`) is\n * skipped entirely.\n */\nconst SKIPPED_DESTINATIONS = new Set([\n \"author\", \"bkmkend\", \"bkmkstart\", \"colortbl\", \"comment\", \"datastore\",\n \"doccomm\", \"falt\", \"fldinst\", \"filetbl\", \"fonttbl\", \"footer\", \"footerf\",\n \"footerl\", \"footerr\", \"footnote\", \"generator\", \"header\", \"headerf\",\n \"headerl\", \"headerr\", \"info\", \"keywords\", \"latentstyles\", \"listoverridetable\",\n \"listtable\", \"nonshppict\", \"object\", \"operator\", \"pict\", \"private\", \"revtbl\",\n \"rsidtbl\", \"shppict\", \"stylesheet\", \"subject\", \"themedata\", \"title\",\n \"xmlnstbl\", \"colorschememapping\",\n]);\n\n/** Control words that map to a literal character. */\nconst CONTROL_WORD_TEXT: Record<string, string> = {\n par: \"\\n\",\n sect: \"\\n\",\n page: \"\\n\",\n line: \"\\n\",\n row: \"\\n\",\n cell: \"\\t\",\n tab: \"\\t\",\n emdash: \"—\",\n endash: \"–\",\n emspace: \" \",\n enspace: \" \",\n bullet: \"•\",\n lquote: \"‘\",\n rquote: \"’\",\n ldblquote: \"“\",\n rdblquote: \"”\",\n};\n\n/** Windows-1252 code points in 0x80–0x9F that differ from Latin-1. */\nconst CP1252: Record<number, string> = {\n 0x80: \"€\", 0x82: \"‚\", 0x83: \"ƒ\", 0x84: \"„\", 0x85: \"…\",\n 0x86: \"†\", 0x87: \"‡\", 0x88: \"ˆ\", 0x89: \"‰\", 0x8a: \"Š\",\n 0x8b: \"‹\", 0x8c: \"Œ\", 0x8e: \"Ž\", 0x91: \"‘\", 0x92: \"’\",\n 0x93: \"“\", 0x94: \"”\", 0x95: \"•\", 0x96: \"–\", 0x97: \"—\",\n 0x98: \"˜\", 0x99: \"™\", 0x9a: \"š\", 0x9b: \"›\", 0x9c: \"œ\",\n 0x9e: \"ž\", 0x9f: \"Ÿ\",\n};\n\nconst CONTROL_WORD_REGEX = /\\\\([a-zA-Z]{1,32})(-?\\d{1,10})? ?/y;\n\n/**\n * Best-effort plain-text extraction from an RTF document.\n *\n * Handles paragraphs/line breaks/tabs, escaped braces and backslashes,\n * `\\'hh` hex escapes (decoded as Windows-1252), `\\uN` Unicode escapes with\n * their `\\ucN` fallback characters, common typographic control words, and\n * skips metadata destinations (font/colour tables, `\\info`, pictures, field\n * instructions, `\\*` destinations). Formatting (bold, lists, tables) is not\n * preserved. Input that does not start with `{\\rtf` is returned unchanged.\n */\nexport function rtfToPlainText(rtf: string): string {\n if (!rtf) return \"\";\n if (!/^\\s*\\{\\\\rtf/.test(rtf)) return rtf;\n\n let out = \"\";\n const stack: Array<{ skip: boolean; uc: number }> = [];\n let skip = false;\n // Number of fallback characters that follow a \\uN escape.\n let uc = 1;\n let pendingFallback = 0;\n // True right after `{`, so a destination control word can mark the group.\n let groupStart = false;\n\n const emit = (text: string) => {\n if (skip) return;\n if (pendingFallback > 0) {\n pendingFallback--;\n return;\n }\n out += text;\n };\n\n let i = 0;\n while (i < rtf.length) {\n const ch = rtf[i];\n if (ch === \"{\") {\n stack.push({ skip, uc });\n groupStart = true;\n i++;\n continue;\n }\n if (ch === \"}\") {\n const prev = stack.pop();\n if (prev) {\n skip = prev.skip;\n uc = prev.uc;\n }\n pendingFallback = 0;\n groupStart = false;\n i++;\n continue;\n }\n const atGroupStart = groupStart;\n groupStart = false;\n\n if (ch === \"\\\\\") {\n const next = rtf[i + 1];\n if (next === \"\\\\\" || next === \"{\" || next === \"}\") {\n emit(next);\n i += 2;\n continue;\n }\n if (next === \"*\") {\n // `{\\*\\dest …}`: an ignorable destination this reader doesn't know.\n skip = true;\n i += 2;\n continue;\n }\n if (next === \"'\") {\n const code = parseInt(rtf.slice(i + 2, i + 4), 16);\n if (!Number.isNaN(code)) emit(CP1252[code] ?? String.fromCharCode(code));\n i += 4;\n continue;\n }\n if (next === \"~\") {\n emit(\" \");\n i += 2;\n continue;\n }\n if (next === \"_\") {\n emit(\"‑\");\n i += 2;\n continue;\n }\n if (next === \"\\n\" || next === \"\\r\") {\n emit(\"\\n\");\n i += 2;\n continue;\n }\n CONTROL_WORD_REGEX.lastIndex = i;\n const match = CONTROL_WORD_REGEX.exec(rtf);\n if (!match) {\n // Other control symbols (`\\-`, `\\|`, `\\:`) produce no text.\n i += 2;\n continue;\n }\n i = CONTROL_WORD_REGEX.lastIndex;\n const word = match[1];\n const param = match[2] !== undefined ? parseInt(match[2], 10) : undefined;\n if (atGroupStart && SKIPPED_DESTINATIONS.has(word)) {\n skip = true;\n } else if (word === \"u\" && param !== undefined) {\n emit(String.fromCharCode(param < 0 ? param + 65536 : param));\n if (!skip) pendingFallback = uc;\n } else if (word === \"uc\" && param !== undefined) {\n uc = param;\n } else if (word === \"bin\" && param !== undefined) {\n // Raw binary data: skip it.\n i += param;\n } else if (CONTROL_WORD_TEXT[word] !== undefined) {\n emit(CONTROL_WORD_TEXT[word]);\n }\n continue;\n }\n // Raw newlines are formatting in RTF; only control words break lines.\n if (ch === \"\\n\" || ch === \"\\r\") {\n i++;\n continue;\n }\n emit(ch);\n i++;\n }\n\n return out.replace(/\\n+$/, \"\");\n}\n","import { Slice, type Schema } from \"@tiptap/pm/model\";\n\n/**\n * Custom clipboard MIME type for content copied from a @pixerate/editor\n * rich-text editor. It carries the ProseMirror slice as JSON so pasting back\n * into a compatible editor is lossless (no HTML round trip).\n */\nexport const PIXERATE_DOCUMENT_MIME = \"application/x.pixerate.document+json\";\n\n/** Payload written under {@link PIXERATE_DOCUMENT_MIME}. */\nexport interface RichClipboardPayload {\n version: 1;\n /** {@link getSchemaSignature} of the source editor's schema. */\n schema: string;\n /** `Slice.toJSON()` of the copied selection. */\n slice: { content?: unknown[]; openStart?: number; openEnd?: number } | null;\n}\n\nconst signatureCache = new WeakMap<Schema, string>();\n\n/** 32-bit FNV-1a, enough to tell schemas apart without a crypto dependency. */\nfunction fnv1a(text: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < text.length; i++) {\n hash ^= text.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/**\n * Returns a stable signature for a ProseMirror schema: node names, content\n * expressions, groups, attribute names and mark names. Two editors with the\n * same extensions produce the same signature, so a slice copied from one can\n * be deserialized in the other without losing or rejecting nodes.\n */\nexport function getSchemaSignature(schema: Schema): string {\n const cached = signatureCache.get(schema);\n if (cached) return cached;\n const parts: string[] = [];\n for (const [name, type] of Object.entries(schema.nodes)) {\n const spec = type.spec;\n parts.push(\n `n:${name}|${spec.content ?? \"\"}|${spec.group ?? \"\"}|${spec.marks ?? \"\"}|${Object.keys(type.spec.attrs ?? {}).sort().join(\",\")}`,\n );\n }\n for (const [name, type] of Object.entries(schema.marks)) {\n parts.push(`m:${name}|${Object.keys(type.spec.attrs ?? {}).sort().join(\",\")}`);\n }\n const signature = `pm1-${fnv1a(parts.join(\";\"))}`;\n signatureCache.set(schema, signature);\n return signature;\n}\n\n/** Serializes a slice into the {@link PIXERATE_DOCUMENT_MIME} payload string. */\nexport function serializeRichClipboardPayload(slice: Slice, schema: Schema): string {\n const payload: RichClipboardPayload = {\n version: 1,\n schema: getSchemaSignature(schema),\n slice: slice.toJSON(),\n };\n return JSON.stringify(payload);\n}\n\n/**\n * Parses a {@link PIXERATE_DOCUMENT_MIME} payload into a slice for `schema`.\n * Returns `null` (so callers fall back to HTML / plain text) when the payload\n * is malformed, from another payload version, or from an editor whose schema\n * signature differs.\n */\nexport function parseRichClipboardPayload(data: string | null | undefined, schema: Schema): Slice | null {\n if (!data) return null;\n try {\n const payload = JSON.parse(data) as Partial<RichClipboardPayload>;\n if (!payload || payload.version !== 1) return null;\n if (payload.schema !== getSchemaSignature(schema)) return null;\n if (!payload.slice) return null;\n return Slice.fromJSON(schema, payload.slice as any);\n } catch {\n return null;\n }\n}\n\n/** Minimal clipboard data surface (`DataTransfer`). */\nexport interface ClipboardDataLike {\n getData(format: string): string;\n setData(format: string, data: string): void;\n clearData?(format?: string): void;\n}\n\n/**\n * Returns the `clipboardData` of a native `ClipboardEvent` or of a React\n * synthetic clipboard event (which wraps the native event in `nativeEvent`).\n */\nexport function getClipboardData(event: unknown): ClipboardDataLike | null {\n if (!event || typeof event !== \"object\") return null;\n const native = \"nativeEvent\" in event ? (event as any).nativeEvent : event;\n return ((event as any).clipboardData ?? native?.clipboardData ?? null) as ClipboardDataLike | null;\n}\n\n/**\n * Reads a slice from a paste event's {@link PIXERATE_DOCUMENT_MIME} data.\n * Returns `null` when the data is missing or its schema does not match.\n */\nexport function readRichClipboard(\n event: unknown,\n schema: Schema,\n mimeType: string = PIXERATE_DOCUMENT_MIME,\n): Slice | null {\n const data = getClipboardData(event);\n if (!data) return null;\n let raw: string | undefined;\n try {\n raw = data.getData(mimeType);\n } catch {\n return null;\n }\n return parseRichClipboardPayload(raw, schema);\n}\n"]}
|