@pixerate/editor 0.23.0 → 0.24.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.
Files changed (155) hide show
  1. package/README.md +96 -1
  2. package/THIRD_PARTY_NOTICES.md +51 -0
  3. package/dist/agent/index.cjs +9 -8
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +4 -3
  6. package/dist/agent/index.d.ts +4 -3
  7. package/dist/agent/index.js +3 -2
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/canvas/index.cjs +1210 -17
  10. package/dist/canvas/index.cjs.map +1 -1
  11. package/dist/canvas/index.d.cts +318 -1
  12. package/dist/canvas/index.d.ts +318 -1
  13. package/dist/canvas/index.js +1199 -6
  14. package/dist/canvas/index.js.map +1 -1
  15. package/dist/{chunk-QBJ22YZR.js → chunk-3CN2WD3X.js} +1354 -206
  16. package/dist/chunk-3CN2WD3X.js.map +1 -0
  17. package/dist/{chunk-JJDLPEWL.js → chunk-3LV6RRPH.js} +2 -2
  18. package/dist/{chunk-MJZBNWG7.js → chunk-5S3Z4PPF.js} +78 -557
  19. package/dist/chunk-5S3Z4PPF.js.map +1 -0
  20. package/dist/chunk-664LQYFJ.js +115 -0
  21. package/dist/chunk-664LQYFJ.js.map +1 -0
  22. package/dist/{chunk-AA4ZEVCC.cjs → chunk-D27YFC46.cjs} +101 -580
  23. package/dist/chunk-D27YFC46.cjs.map +1 -0
  24. package/dist/chunk-D6FH2YAX.cjs +131 -0
  25. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  26. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  27. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  28. package/dist/chunk-FODNLX5N.js +749 -0
  29. package/dist/chunk-FODNLX5N.js.map +1 -0
  30. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  31. package/dist/chunk-FPENXAEJ.js.map +1 -0
  32. package/dist/chunk-HLIMTC6N.js +1 -0
  33. package/dist/chunk-HLIMTC6N.js.map +1 -0
  34. package/dist/{chunk-QGHGJ56O.js → chunk-ITBSKI26.js} +1483 -19
  35. package/dist/chunk-ITBSKI26.js.map +1 -0
  36. package/dist/{chunk-XDMXXC4Z.cjs → chunk-MA2F5KHF.cjs} +350 -227
  37. package/dist/chunk-MA2F5KHF.cjs.map +1 -0
  38. package/dist/{chunk-RIKBJLS2.cjs → chunk-MCSJFWAO.cjs} +1356 -208
  39. package/dist/chunk-MCSJFWAO.cjs.map +1 -0
  40. package/dist/chunk-MKA3AHLV.cjs +256 -0
  41. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  42. package/dist/chunk-OLZJXL4R.cjs +749 -0
  43. package/dist/chunk-OLZJXL4R.cjs.map +1 -0
  44. package/dist/chunk-OXSV64JX.js +689 -0
  45. package/dist/chunk-OXSV64JX.js.map +1 -0
  46. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  47. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  48. package/dist/chunk-P6ODPZ4W.js +256 -0
  49. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  50. package/dist/chunk-PGNZKFQS.js +685 -0
  51. package/dist/chunk-PGNZKFQS.js.map +1 -0
  52. package/dist/chunk-PL4JB3BF.cjs +1 -0
  53. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  54. package/dist/{chunk-GVSMRPHB.cjs → chunk-SBCZ6RN5.cjs} +1568 -104
  55. package/dist/chunk-SBCZ6RN5.cjs.map +1 -0
  56. package/dist/{chunk-CL37JGFW.cjs → chunk-SML64VJG.cjs} +4 -4
  57. package/dist/{chunk-CL37JGFW.cjs.map → chunk-SML64VJG.cjs.map} +1 -1
  58. package/dist/chunk-TNNJXF46.cjs +115 -0
  59. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  60. package/dist/chunk-U5HZSFCS.js +131 -0
  61. package/dist/chunk-U5HZSFCS.js.map +1 -0
  62. package/dist/{chunk-K3R5NG2D.js → chunk-U5KWZU4X.js} +251 -128
  63. package/dist/chunk-U5KWZU4X.js.map +1 -0
  64. package/dist/chunk-XNORY7M6.cjs +685 -0
  65. package/dist/chunk-XNORY7M6.cjs.map +1 -0
  66. package/dist/commands/index.d.cts +5 -353
  67. package/dist/commands/index.d.ts +5 -353
  68. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  69. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  70. package/dist/compare/index.cjs +15 -0
  71. package/dist/compare/index.cjs.map +1 -0
  72. package/dist/compare/index.d.cts +58 -0
  73. package/dist/compare/index.d.ts +58 -0
  74. package/dist/compare/index.js +15 -0
  75. package/dist/compare/index.js.map +1 -0
  76. package/dist/{controller-pePCJLT1.d.cts → controller-BhLLAoi8.d.cts} +45 -4
  77. package/dist/{controller-pePCJLT1.d.ts → controller-BhLLAoi8.d.ts} +45 -4
  78. package/dist/documents-BQjzJNe2.d.ts +76 -0
  79. package/dist/documents-KRTPhvst.d.cts +76 -0
  80. package/dist/extensions/index.cjs +53 -5
  81. package/dist/extensions/index.cjs.map +1 -1
  82. package/dist/extensions/index.d.cts +7 -3
  83. package/dist/extensions/index.d.ts +7 -3
  84. package/dist/extensions/index.js +52 -4
  85. package/dist/generation/index.cjs +1373 -0
  86. package/dist/generation/index.cjs.map +1 -0
  87. package/dist/generation/index.d.cts +554 -0
  88. package/dist/generation/index.d.ts +554 -0
  89. package/dist/generation/index.js +1373 -0
  90. package/dist/generation/index.js.map +1 -0
  91. package/dist/grammar/index.cjs +48 -3
  92. package/dist/grammar/index.cjs.map +1 -1
  93. package/dist/grammar/index.d.cts +322 -100
  94. package/dist/grammar/index.d.ts +322 -100
  95. package/dist/grammar/index.js +54 -9
  96. package/dist/image-editor/index.cjs +1831 -30
  97. package/dist/image-editor/index.cjs.map +1 -1
  98. package/dist/image-editor/index.d.cts +590 -3
  99. package/dist/image-editor/index.d.ts +590 -3
  100. package/dist/image-editor/index.js +1807 -6
  101. package/dist/image-editor/index.js.map +1 -1
  102. package/dist/{index-CCr_z9kF.d.cts → index-BhJBSm2d.d.cts} +370 -4
  103. package/dist/{index-Ck6ajwXh.d.ts → index-fJrlkJCd.d.ts} +370 -4
  104. package/dist/index.cjs +162 -25
  105. package/dist/index.cjs.map +1 -1
  106. package/dist/index.d.cts +14 -8
  107. package/dist/index.d.ts +14 -8
  108. package/dist/index.js +174 -37
  109. package/dist/index.js.map +1 -1
  110. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  111. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  112. package/dist/persistence/index.cjs +676 -0
  113. package/dist/persistence/index.cjs.map +1 -0
  114. package/dist/persistence/index.d.cts +347 -0
  115. package/dist/persistence/index.d.ts +347 -0
  116. package/dist/persistence/index.js +676 -0
  117. package/dist/persistence/index.js.map +1 -0
  118. package/dist/{registry-D5-R90p0.d.cts → registry-BDe-vlv3.d.ts} +2 -40
  119. package/dist/registry-D32LpIc4.d.cts +354 -0
  120. package/dist/{registry-D5-R90p0.d.ts → registry-DnzpgY27.d.cts} +2 -40
  121. package/dist/registry-evKTP4ls.d.ts +354 -0
  122. package/dist/schema-DYDGdYL9.d.cts +41 -0
  123. package/dist/schema-DYDGdYL9.d.ts +41 -0
  124. package/dist/serializers/index.cjs +4 -3
  125. package/dist/serializers/index.cjs.map +1 -1
  126. package/dist/serializers/index.d.cts +2 -1
  127. package/dist/serializers/index.d.ts +2 -1
  128. package/dist/serializers/index.js +3 -2
  129. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  130. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  131. package/dist/spreadsheet/index.cjs +37 -3
  132. package/dist/spreadsheet/index.cjs.map +1 -1
  133. package/dist/spreadsheet/index.d.cts +188 -17
  134. package/dist/spreadsheet/index.d.ts +188 -17
  135. package/dist/spreadsheet/index.js +43 -9
  136. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  137. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  138. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-BK5sVf4_.d.cts} +18 -136
  139. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-CDjCrwKe.d.ts} +18 -136
  140. package/package.json +16 -1
  141. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  142. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  143. package/dist/chunk-AXV2ZW34.cjs +0 -118
  144. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  145. package/dist/chunk-B2ZRLN4S.js +0 -118
  146. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  147. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  148. package/dist/chunk-K3R5NG2D.js.map +0 -1
  149. package/dist/chunk-MJZBNWG7.js.map +0 -1
  150. package/dist/chunk-OYXSKQFR.js.map +0 -1
  151. package/dist/chunk-QBJ22YZR.js.map +0 -1
  152. package/dist/chunk-QGHGJ56O.js.map +0 -1
  153. package/dist/chunk-RIKBJLS2.cjs.map +0 -1
  154. package/dist/chunk-XDMXXC4Z.cjs.map +0 -1
  155. /package/dist/{chunk-JJDLPEWL.js.map → chunk-3LV6RRPH.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-D27YFC46.cjs","../src/extensions/comment-store.ts","../src/extensions/track-changes.ts"],"names":[],"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;AA+IA;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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","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-D27YFC46.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/**\n * Whole-block operation of a `blockChange` record: the block was inserted\n * (rejecting removes it) or deleted (accepting removes it). Produced by\n * `compareDocuments`; absent for type/attribute/wrapping changes.\n */\nexport type TrackedBlockOperation = \"insert\" | \"delete\";\n\n/** Value of the `blockChange` node attribute. */\nexport interface TrackedBlockChangeAttributes extends TrackedChangeAttributes {\n previous: TrackedBlockSnapshot;\n operation?: TrackedBlockOperation;\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 /**\n * Block changes (`type: \"block\"`): the first block's previous and current\n * state, and the whole-block operation for inserted/deleted blocks.\n */\n block?: {\n previous: TrackedBlockSnapshot;\n current: TrackedBlockSnapshot;\n operation?: TrackedBlockOperation;\n };\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 /** Extra class of wholly inserted blocks (`blockChange.operation: \"insert\"`, from comparisons). */\n blockInsertedClass: string;\n /** Extra class of wholly deleted blocks (`blockChange.operation: \"delete\"`, from comparisons). */\n blockDeletedClass: 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 const blockCounts = new Map<string, number>();\n doc.descendants((node, pos) => {\n const block = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n if (block?.changeId && node.isTextblock) {\n blockCounts.set(block.changeId, (blockCounts.get(block.changeId) ?? 0) + 1);\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 = {\n previous: block.previous,\n current: blockSnapshot(doc, pos),\n ...(block.operation ? { operation: block.operation } : {}),\n };\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 const { previous, current, operation } = change.block!;\n if (operation) {\n const count = blockCounts.get(change.id) ?? 1;\n const verb = operation === \"insert\" ? \"Inserted\" : \"Deleted\";\n change.description =\n count > 1 ? `${verb} ${count} blocks` : `${verb} ${blockLabel(current.type, current.attrs)}`;\n } else {\n change.description = describeBlockChange(previous, current);\n }\n break;\n }\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\n/**\n * Removes the blocks of whole-block operations that the decision drops\n * (accepted deletions, rejected insertions). Containers whose textblocks are\n * all removed (list items, lists, quotes, table rows) are removed with them.\n */\nfunction removeBlocks(tr: Transform, ids: ReadonlySet<string>, decision: \"accept\" | \"reject\"): void {\n const removed: TrackedBlockOperation = decision === \"accept\" ? \"delete\" : \"insert\";\n const memo = new Map<ProsemirrorNode, boolean>();\n const removable = (node: ProsemirrorNode): boolean => {\n const cached = memo.get(node);\n if (cached !== undefined) return cached;\n let result: boolean;\n if (node.isTextblock) {\n const record = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n result = !!record?.changeId && record.operation === removed && ids.has(record.changeId);\n } else if (node.isLeaf || node.childCount === 0) {\n result = false;\n } else {\n result = true;\n for (let i = 0; i < node.childCount && result; i++) result = removable(node.child(i));\n }\n memo.set(node, result);\n return result;\n };\n\n const doc = tr.doc;\n if (removable(doc)) {\n const filler = doc.type.contentMatch.defaultType?.createAndFill();\n if (filler) tr.replaceWith(0, doc.content.size, filler);\n return;\n }\n const ranges: { from: number; to: number }[] = [];\n const visit = (node: ProsemirrorNode, contentStart: number) => {\n node.forEach((child, offset) => {\n const from = contentStart + offset;\n if (removable(child)) {\n ranges.push({ from, to: from + child.nodeSize });\n } else if (!child.isTextblock && !child.isLeaf) {\n visit(child, from + 1);\n }\n });\n };\n visit(doc, 0);\n if (ranges.length === 0) return;\n const startIndex = tr.steps.length;\n ranges.reverse().forEach(({ from, to }) => {\n const mapping = tr.mapping.slice(startIndex);\n try {\n tr.delete(mapping.map(from, 1), mapping.map(to, -1));\n } catch {\n // The parent cannot lose this block (e.g. a list item's first paragraph): keep it.\n }\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 removeBlocks(tr, ids, decision);\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\" || record.operation) {\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 // Inserted/deleted blocks (from a comparison) keep their record.\n if (existing?.operation || node.attrs[BLOCK_CHANGE_ATTRIBUTE]?.operation) return;\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 blockInsertedClass:\n \"pixerate-block-inserted underline decoration-emerald-500 text-emerald-700 dark:text-emerald-400\",\n blockDeletedClass:\n \"pixerate-block-deleted line-through decoration-red-500 text-red-600 dark:text-red-400\",\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 const insertedClass = this.options?.blockInsertedClass ?? \"\";\n const deletedClass = this.options?.blockDeletedClass ?? \"\";\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 const operation = (value as TrackedBlockChangeAttributes).operation;\n const extra = operation === \"insert\" ? insertedClass : operation === \"delete\" ? deletedClass : \"\";\n const className = [blockClass, extra].filter(Boolean).join(\" \");\n return {\n \"data-block-change\": JSON.stringify(value),\n ...(operation ? { \"data-block-operation\": operation } : {}),\n ...(className ? { class: className } : {}),\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"]}
@@ -0,0 +1,131 @@
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; }// src/persistence/migrate.ts
2
+ function hasLoadProblems(report) {
3
+ return report.problems.length > 0 || report.readOnly !== null;
4
+ }
5
+ function createLoadReport(partial = {}) {
6
+ return {
7
+ migratedFrom: null,
8
+ version: null,
9
+ problems: [],
10
+ readOnly: null,
11
+ unknownKeys: [],
12
+ ...partial
13
+ };
14
+ }
15
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16
+ var deepClone = (value) => typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
17
+ function toObject(json) {
18
+ let value = json;
19
+ if (typeof json === "string") {
20
+ try {
21
+ value = JSON.parse(json);
22
+ } catch (e) {
23
+ return void 0;
24
+ }
25
+ }
26
+ return isObject(value) ? deepClone(value) : void 0;
27
+ }
28
+ function migrateDocument(json, options) {
29
+ const versionKey = _nullishCoalesce(options.versionKey, () => ( "version"));
30
+ const target = options.currentVersion;
31
+ const original = toObject(json);
32
+ if (!original) {
33
+ return {
34
+ doc: { [versionKey]: target },
35
+ report: createLoadReport({
36
+ readOnly: { kind: "invalid", message: "document is not a JSON object" }
37
+ })
38
+ };
39
+ }
40
+ const raw = original[versionKey];
41
+ const found = typeof raw === "number" && Number.isInteger(raw) && raw >= 0 ? raw : null;
42
+ const from = _nullishCoalesce(_nullishCoalesce(found, () => ( options.missingVersion)), () => ( target));
43
+ const report = createLoadReport({ version: found });
44
+ if (from > target) {
45
+ report.readOnly = {
46
+ kind: "newerVersion",
47
+ version: from,
48
+ supported: target,
49
+ message: `document has version ${from}, newer than the supported ${target}`
50
+ };
51
+ return { doc: original, report };
52
+ }
53
+ if (from === target) {
54
+ original[versionKey] = target;
55
+ return { doc: original, report };
56
+ }
57
+ let doc = deepClone(original);
58
+ for (let step = from; step < target; step++) {
59
+ const fn = _optionalChain([options, 'access', _ => _.steps, 'optionalAccess', _2 => _2[step]]);
60
+ if (!fn) {
61
+ report.readOnly = {
62
+ kind: "migrationFailed",
63
+ from: step,
64
+ message: `no migration step from version ${step}`
65
+ };
66
+ return { doc: original, report };
67
+ }
68
+ try {
69
+ doc = _nullishCoalesce(fn(doc), () => ( doc));
70
+ } catch (error) {
71
+ report.readOnly = {
72
+ kind: "migrationFailed",
73
+ from: step,
74
+ message: `migration from version ${step} failed: ${error instanceof Error ? error.message : String(error)}`
75
+ };
76
+ return { doc: original, report };
77
+ }
78
+ doc[versionKey] = step + 1;
79
+ }
80
+ report.migratedFrom = from;
81
+ return { doc, report };
82
+ }
83
+ function loadSections(doc, sections, options = {}) {
84
+ const source = isObject(doc) ? doc : {};
85
+ const ignore = new Set(_nullishCoalesce(options.ignore, () => ( ["version"])));
86
+ const value = {};
87
+ const problems = [];
88
+ for (const key of Object.keys(sections)) {
89
+ const spec = sections[key];
90
+ const fallback = () => typeof spec.defaults === "function" ? spec.defaults() : spec.defaults;
91
+ if (!Object.prototype.hasOwnProperty.call(source, key)) {
92
+ value[key] = fallback();
93
+ continue;
94
+ }
95
+ try {
96
+ const parsed = spec.parse(source[key]);
97
+ if (parsed === void 0) throw new Error("invalid value");
98
+ value[key] = parsed;
99
+ } catch (error) {
100
+ problems.push({
101
+ section: key,
102
+ message: error instanceof Error ? error.message : String(error)
103
+ });
104
+ value[key] = fallback();
105
+ }
106
+ }
107
+ const unknown = {};
108
+ for (const key of Object.keys(source)) {
109
+ if (!(key in sections) && !ignore.has(key)) unknown[key] = source[key];
110
+ }
111
+ return { value, problems, unknown };
112
+ }
113
+ function loadDocument(json, sections, options) {
114
+ const { doc, report } = migrateDocument(json, options);
115
+ const ignore = _nullishCoalesce(options.ignore, () => ( [_nullishCoalesce(options.versionKey, () => ( "version"))]));
116
+ const result = loadSections(_optionalChain([report, 'access', _3 => _3.readOnly, 'optionalAccess', _4 => _4.kind]) === "invalid" ? {} : doc, sections, {
117
+ ignore
118
+ });
119
+ report.problems = result.problems;
120
+ report.unknownKeys = Object.keys(result.unknown);
121
+ return { ...result, report };
122
+ }
123
+
124
+
125
+
126
+
127
+
128
+
129
+
130
+ exports.hasLoadProblems = hasLoadProblems; exports.createLoadReport = createLoadReport; exports.migrateDocument = migrateDocument; exports.loadSections = loadSections; exports.loadDocument = loadDocument;
131
+ //# sourceMappingURL=chunk-D6FH2YAX.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-D6FH2YAX.cjs","../src/persistence/migrate.ts"],"names":[],"mappings":"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","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-D6FH2YAX.cjs","sourcesContent":[null,"// Portions ported from craft-libs-main/crates/settings/src/migrate.rs, src/document.rs and\n// src/error.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** A section whose stored value did not fit; its defaults were used instead. */\nexport interface SectionProblem {\n /** Top-level key of the section. */\n section: string;\n message: string;\n}\n\n/** Why a loaded document must not be written back. */\nexport type ReadOnlyReason =\n | { kind: \"newerVersion\"; version: number; supported: number; message: string }\n | { kind: \"migrationFailed\"; from: number; message: string }\n | { kind: \"invalid\"; message: string };\n\n/** What happened while loading a document. Nothing in here is fatal. */\nexport interface LoadReport {\n /** Set when the document was upgraded step by step from this older version. */\n migratedFrom: number | null;\n /** The version found in the input (`null` when it had none). */\n version: number | null;\n /** Sections replaced by their defaults (see `loadSections`). */\n problems: SectionProblem[];\n /**\n * Set when the document must not be saved over: written by a newer build,\n * a migration step failed, or the input was not an object. Show the values,\n * but never overwrite the stored document.\n */\n readOnly: ReadOnlyReason | null;\n /** Top-level keys this build does not know; kept in `unknown` so a save can write them back. */\n unknownKeys: string[];\n}\n\n/** True when something should be shown to the user. */\nexport function hasLoadProblems(report: LoadReport): boolean {\n return report.problems.length > 0 || report.readOnly !== null;\n}\n\nexport function createLoadReport(partial: Partial<LoadReport> = {}): LoadReport {\n return {\n migratedFrom: null,\n version: null,\n problems: [],\n readOnly: null,\n unknownKeys: [],\n ...partial,\n };\n}\n\nexport type JsonObject = Record<string, unknown>;\n\n/** One migration step: turns a document at version `from` into version `from + 1`. */\nexport type MigrationStep = (doc: JsonObject) => JsonObject | void;\n\nexport interface MigrateOptions {\n /** The version this build writes. */\n currentVersion: number;\n /** Steps keyed by the version they start from. */\n steps?: Record<number, MigrationStep>;\n /** Key holding the version (default `\"version\"`). */\n versionKey?: string;\n /**\n * Version assumed when the key is missing (default: `currentVersion`, i.e. a\n * hand-written document is taken as current and the key is added).\n */\n missingVersion?: number;\n}\n\nexport interface MigrateResult {\n /** The migrated document (a deep copy; the input is never modified). */\n doc: JsonObject;\n report: LoadReport;\n}\n\nconst isObject = (value: unknown): value is JsonObject =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst deepClone = <T>(value: T): T =>\n typeof structuredClone === \"function\"\n ? structuredClone(value)\n : (JSON.parse(JSON.stringify(value)) as T);\n\n/** Parses a JSON string, or deep-copies an object. `undefined` when it is not an object. */\nfunction toObject(json: unknown): JsonObject | undefined {\n let value = json;\n if (typeof json === \"string\") {\n try {\n value = JSON.parse(json);\n } catch {\n return undefined;\n }\n }\n return isObject(value) ? deepClone(value) : undefined;\n}\n\n/**\n * Brings a stored document up to `currentVersion` by running one step per\n * version (`steps[v]` turns version `v` into `v + 1`).\n *\n * - Missing version: taken as `missingVersion` (default current); the key is added.\n * - Newer than `currentVersion`: left alone and reported `readOnly` (never overwrite\n * a file written by a newer build).\n * - A missing or throwing step: the document is returned as it was before\n * migrating and reported `readOnly`.\n * - Keys the steps do not touch are preserved.\n */\nexport function migrateDocument(json: unknown, options: MigrateOptions): MigrateResult {\n const versionKey = options.versionKey ?? \"version\";\n const target = options.currentVersion;\n const original = toObject(json);\n if (!original) {\n return {\n doc: { [versionKey]: target },\n report: createLoadReport({\n readOnly: { kind: \"invalid\", message: \"document is not a JSON object\" },\n }),\n };\n }\n const raw = original[versionKey];\n const found = typeof raw === \"number\" && Number.isInteger(raw) && raw >= 0 ? raw : null;\n const from = found ?? options.missingVersion ?? target;\n const report = createLoadReport({ version: found });\n\n if (from > target) {\n report.readOnly = {\n kind: \"newerVersion\",\n version: from,\n supported: target,\n message: `document has version ${from}, newer than the supported ${target}`,\n };\n return { doc: original, report };\n }\n if (from === target) {\n original[versionKey] = target;\n return { doc: original, report };\n }\n\n let doc = deepClone(original);\n for (let step = from; step < target; step++) {\n const fn = options.steps?.[step];\n if (!fn) {\n report.readOnly = {\n kind: \"migrationFailed\",\n from: step,\n message: `no migration step from version ${step}`,\n };\n return { doc: original, report };\n }\n try {\n doc = fn(doc) ?? doc;\n } catch (error) {\n report.readOnly = {\n kind: \"migrationFailed\",\n from: step,\n message: `migration from version ${step} failed: ${\n error instanceof Error ? error.message : String(error)\n }`,\n };\n // Never hand back half-migrated values.\n return { doc: original, report };\n }\n doc[versionKey] = step + 1;\n }\n report.migratedFrom = from;\n return { doc, report };\n}\n\n/** How one top-level section is read. */\nexport interface SectionSpec<V> {\n /** Validates / converts the stored value; throw (or return `undefined`) to reject it. */\n parse: (value: unknown) => V | undefined;\n /** Used when the section is missing or rejected. */\n defaults: V | (() => V);\n}\n\nexport type SectionSpecs<T> = { [K in keyof T]: SectionSpec<T[K]> };\n\nexport interface LoadSectionsOptions {\n /** Keys that are neither sections nor unknown (default `[\"version\"]`). */\n ignore?: string[];\n}\n\nexport interface LoadSectionsResult<T> {\n value: T;\n /** Sections that were present but rejected (missing ones are not problems). */\n problems: SectionProblem[];\n /** Top-level keys not described by `sections`; write them back to keep them. */\n unknown: JsonObject;\n}\n\n/**\n * Reads `doc` one top-level section at a time: a section that does not parse\n * is reported and replaced by its defaults, every other section is kept. This\n * keeps one bad value (e.g. a renamed enum) from resetting the whole document.\n */\nexport function loadSections<T>(\n doc: unknown,\n sections: SectionSpecs<T>,\n options: LoadSectionsOptions = {},\n): LoadSectionsResult<T> {\n const source = isObject(doc) ? doc : {};\n const ignore = new Set(options.ignore ?? [\"version\"]);\n const value = {} as T;\n const problems: SectionProblem[] = [];\n for (const key of Object.keys(sections) as (keyof T & string)[]) {\n const spec = sections[key];\n const fallback = () =>\n typeof spec.defaults === \"function\"\n ? (spec.defaults as () => T[typeof key])()\n : spec.defaults;\n if (!Object.prototype.hasOwnProperty.call(source, key)) {\n value[key] = fallback();\n continue;\n }\n try {\n const parsed = spec.parse(source[key]);\n if (parsed === undefined) throw new Error(\"invalid value\");\n value[key] = parsed;\n } catch (error) {\n problems.push({\n section: key,\n message: error instanceof Error ? error.message : String(error),\n });\n value[key] = fallback();\n }\n }\n const unknown: JsonObject = {};\n for (const key of Object.keys(source)) {\n if (!(key in sections) && !ignore.has(key)) unknown[key] = source[key];\n }\n return { value, problems, unknown };\n}\n\nexport interface LoadDocumentOptions extends MigrateOptions, LoadSectionsOptions {}\n\nexport interface LoadDocumentResult<T> extends LoadSectionsResult<T> {\n report: LoadReport;\n}\n\n/**\n * `migrateDocument` followed by `loadSections`, with one `LoadReport`\n * (`problems` and `unknownKeys` filled in).\n */\nexport function loadDocument<T>(\n json: unknown,\n sections: SectionSpecs<T>,\n options: LoadDocumentOptions,\n): LoadDocumentResult<T> {\n const { doc, report } = migrateDocument(json, options);\n const ignore = options.ignore ?? [options.versionKey ?? \"version\"];\n const result = loadSections(report.readOnly?.kind === \"invalid\" ? {} : doc, sections, {\n ignore,\n });\n report.problems = result.problems;\n report.unknownKeys = Object.keys(result.unknown);\n return { ...result, report };\n}\n"]}