@fiduswriter/editor 0.1.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 (525) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +22 -0
  3. package/dist/bibliography/tools.js +53 -0
  4. package/dist/bibliography/tools.js.map +1 -0
  5. package/dist/citations/index.js +339 -0
  6. package/dist/citations/index.js.map +1 -0
  7. package/dist/citations/render.js +47 -0
  8. package/dist/citations/render.js.map +1 -0
  9. package/dist/clipboard/copy/index.js +2 -0
  10. package/dist/clipboard/copy/index.js.map +1 -0
  11. package/dist/clipboard/copy/schema.js +54 -0
  12. package/dist/clipboard/copy/schema.js.map +1 -0
  13. package/dist/clipboard/copy/serializers.js +132 -0
  14. package/dist/clipboard/copy/serializers.js.map +1 -0
  15. package/dist/clipboard/paste/fidus_writer.js +45 -0
  16. package/dist/clipboard/paste/fidus_writer.js.map +1 -0
  17. package/dist/clipboard/paste/general.js +76 -0
  18. package/dist/clipboard/paste/general.js.map +1 -0
  19. package/dist/clipboard/paste/google_docs.js +24 -0
  20. package/dist/clipboard/paste/google_docs.js.map +1 -0
  21. package/dist/clipboard/paste/html.js +68 -0
  22. package/dist/clipboard/paste/html.js.map +1 -0
  23. package/dist/clipboard/paste/index.js +3 -0
  24. package/dist/clipboard/paste/index.js.map +1 -0
  25. package/dist/clipboard/paste/libreoffice_writer.js +27 -0
  26. package/dist/clipboard/paste/libreoffice_writer.js.map +1 -0
  27. package/dist/clipboard/paste/microsoft_word.js +79 -0
  28. package/dist/clipboard/paste/microsoft_word.js.map +1 -0
  29. package/dist/clipboard/paste/text.js +41 -0
  30. package/dist/clipboard/paste/text.js.map +1 -0
  31. package/dist/collab/chat.js +154 -0
  32. package/dist/collab/chat.js.map +1 -0
  33. package/dist/collab/colors.js +62 -0
  34. package/dist/collab/colors.js.map +1 -0
  35. package/dist/collab/doc.js +1090 -0
  36. package/dist/collab/doc.js.map +1 -0
  37. package/dist/collab/index.js +75 -0
  38. package/dist/collab/index.js.map +1 -0
  39. package/dist/collab/merge/changeset.js +90 -0
  40. package/dist/collab/merge/changeset.js.map +1 -0
  41. package/dist/collab/merge/editor.js +708 -0
  42. package/dist/collab/merge/editor.js.map +1 -0
  43. package/dist/collab/merge/footnotes.js +149 -0
  44. package/dist/collab/merge/footnotes.js.map +1 -0
  45. package/dist/collab/merge/index.js +270 -0
  46. package/dist/collab/merge/index.js.map +1 -0
  47. package/dist/collab/merge/recreate_transform.js +259 -0
  48. package/dist/collab/merge/recreate_transform.js.map +1 -0
  49. package/dist/collab/merge/schema.js +117 -0
  50. package/dist/collab/merge/schema.js.map +1 -0
  51. package/dist/collab/merge/state_plugins/action.js +138 -0
  52. package/dist/collab/merge/state_plugins/action.js.map +1 -0
  53. package/dist/collab/merge/state_plugins/clipboard.js +32 -0
  54. package/dist/collab/merge/state_plugins/clipboard.js.map +1 -0
  55. package/dist/collab/merge/state_plugins/diff.js +541 -0
  56. package/dist/collab/merge/state_plugins/diff.js.map +1 -0
  57. package/dist/collab/merge/state_plugins/index.js +3 -0
  58. package/dist/collab/merge/state_plugins/index.js.map +1 -0
  59. package/dist/collab/merge/tools.js +175 -0
  60. package/dist/collab/merge/tools.js.map +1 -0
  61. package/dist/collab/templates.js +10 -0
  62. package/dist/collab/templates.js.map +1 -0
  63. package/dist/comments/comment.js +17 -0
  64. package/dist/comments/comment.js.map +1 -0
  65. package/dist/comments/editors/answer.js +43 -0
  66. package/dist/comments/editors/answer.js.map +1 -0
  67. package/dist/comments/editors/comment.js +278 -0
  68. package/dist/comments/editors/comment.js.map +1 -0
  69. package/dist/comments/editors/index.js +4 -0
  70. package/dist/comments/editors/index.js.map +1 -0
  71. package/dist/comments/editors/notify.js +13 -0
  72. package/dist/comments/editors/notify.js.map +1 -0
  73. package/dist/comments/editors/schema.js +60 -0
  74. package/dist/comments/editors/schema.js.map +1 -0
  75. package/dist/comments/index.js +11 -0
  76. package/dist/comments/index.js.map +1 -0
  77. package/dist/comments/interactions.js +379 -0
  78. package/dist/comments/interactions.js.map +1 -0
  79. package/dist/comments/store.js +438 -0
  80. package/dist/comments/store.js.map +1 -0
  81. package/dist/contacts/add_dialog.js +88 -0
  82. package/dist/contacts/add_dialog.js.map +1 -0
  83. package/dist/contacts/templates.js +23 -0
  84. package/dist/contacts/templates.js.map +1 -0
  85. package/dist/copyright_dialog/index.js +148 -0
  86. package/dist/copyright_dialog/index.js.map +1 -0
  87. package/dist/copyright_dialog/templates.js +48 -0
  88. package/dist/copyright_dialog/templates.js.map +1 -0
  89. package/dist/databases/bibliography.js +162 -0
  90. package/dist/databases/bibliography.js.map +1 -0
  91. package/dist/databases/images.js +260 -0
  92. package/dist/databases/images.js.map +1 -0
  93. package/dist/databases/index.js +55 -0
  94. package/dist/databases/index.js.map +1 -0
  95. package/dist/dialogs/citation.js +321 -0
  96. package/dist/dialogs/citation.js.map +1 -0
  97. package/dist/dialogs/code_block.js +192 -0
  98. package/dist/dialogs/code_block.js.map +1 -0
  99. package/dist/dialogs/contributor.js +132 -0
  100. package/dist/dialogs/contributor.js.map +1 -0
  101. package/dist/dialogs/figure.js +378 -0
  102. package/dist/dialogs/figure.js.map +1 -0
  103. package/dist/dialogs/index.js +11 -0
  104. package/dist/dialogs/index.js.map +1 -0
  105. package/dist/dialogs/language.js +47 -0
  106. package/dist/dialogs/language.js.map +1 -0
  107. package/dist/dialogs/link.js +243 -0
  108. package/dist/dialogs/link.js.map +1 -0
  109. package/dist/dialogs/math.js +121 -0
  110. package/dist/dialogs/math.js.map +1 -0
  111. package/dist/dialogs/ordered_list.js +67 -0
  112. package/dist/dialogs/ordered_list.js.map +1 -0
  113. package/dist/dialogs/revision.js +36 -0
  114. package/dist/dialogs/revision.js.map +1 -0
  115. package/dist/dialogs/subsup.js +109 -0
  116. package/dist/dialogs/subsup.js.map +1 -0
  117. package/dist/dialogs/table.js +217 -0
  118. package/dist/dialogs/table.js.map +1 -0
  119. package/dist/dialogs/templates.js +366 -0
  120. package/dist/dialogs/templates.js.map +1 -0
  121. package/dist/document_template/exporter.js +83 -0
  122. package/dist/document_template/exporter.js.map +1 -0
  123. package/dist/document_template/index.js +327 -0
  124. package/dist/document_template/index.js.map +1 -0
  125. package/dist/documents/access_rights/index.js +532 -0
  126. package/dist/documents/access_rights/index.js.map +1 -0
  127. package/dist/documents/access_rights/templates.js +150 -0
  128. package/dist/documents/access_rights/templates.js.map +1 -0
  129. package/dist/e2ee/encryptor.js +201 -0
  130. package/dist/e2ee/encryptor.js.map +1 -0
  131. package/dist/e2ee/key-manager.js +171 -0
  132. package/dist/e2ee/key-manager.js.map +1 -0
  133. package/dist/e2ee/passphrase-crypto.js +365 -0
  134. package/dist/e2ee/passphrase-crypto.js.map +1 -0
  135. package/dist/e2ee/passphrase-dialog.js +548 -0
  136. package/dist/e2ee/passphrase-dialog.js.map +1 -0
  137. package/dist/e2ee/passphrase-manager.js +350 -0
  138. package/dist/e2ee/passphrase-manager.js.map +1 -0
  139. package/dist/e2ee/password-dialog.js +552 -0
  140. package/dist/e2ee/password-dialog.js.map +1 -0
  141. package/dist/e2ee/snapshot-manager.js +238 -0
  142. package/dist/e2ee/snapshot-manager.js.map +1 -0
  143. package/dist/exporter/native/copy.js +29 -0
  144. package/dist/exporter/native/copy.js.map +1 -0
  145. package/dist/exporter/native/file.js +19 -0
  146. package/dist/exporter/native/file.js.map +1 -0
  147. package/dist/exporter/native/index.js +4 -0
  148. package/dist/exporter/native/index.js.map +1 -0
  149. package/dist/exporter/native/revision.js +54 -0
  150. package/dist/exporter/native/revision.js.map +1 -0
  151. package/dist/exporter/native/zip.js +2 -0
  152. package/dist/exporter/native/zip.js.map +1 -0
  153. package/dist/footnotes/editor.js +180 -0
  154. package/dist/footnotes/editor.js.map +1 -0
  155. package/dist/footnotes/index.js +15 -0
  156. package/dist/footnotes/index.js.map +1 -0
  157. package/dist/footnotes/layout.js +125 -0
  158. package/dist/footnotes/layout.js.map +1 -0
  159. package/dist/footnotes/state_plugins/access_rights.js +25 -0
  160. package/dist/footnotes/state_plugins/access_rights.js.map +1 -0
  161. package/dist/footnotes/state_plugins/index.js +2 -0
  162. package/dist/footnotes/state_plugins/index.js.map +1 -0
  163. package/dist/images/edit_dialog/edit_dialog/index.js +186 -0
  164. package/dist/images/edit_dialog/edit_dialog/index.js.map +1 -0
  165. package/dist/images/edit_dialog/edit_dialog/model.js +152 -0
  166. package/dist/images/edit_dialog/edit_dialog/model.js.map +1 -0
  167. package/dist/images/edit_dialog/edit_dialog/templates.js +33 -0
  168. package/dist/images/edit_dialog/edit_dialog/templates.js.map +1 -0
  169. package/dist/images/edit_dialog/model.js +6 -0
  170. package/dist/images/edit_dialog/model.js.map +1 -0
  171. package/dist/images/selection_dialog/index.js +149 -0
  172. package/dist/images/selection_dialog/index.js.map +1 -0
  173. package/dist/index.js +1002 -0
  174. package/dist/index.js.map +1 -0
  175. package/dist/keymap.js +73 -0
  176. package/dist/keymap.js.map +1 -0
  177. package/dist/marginboxes/index.js +674 -0
  178. package/dist/marginboxes/index.js.map +1 -0
  179. package/dist/marginboxes/templates.js +447 -0
  180. package/dist/marginboxes/templates.js.map +1 -0
  181. package/dist/menus/code_block/index.js +2 -0
  182. package/dist/menus/code_block/index.js.map +1 -0
  183. package/dist/menus/code_block/model.js +49 -0
  184. package/dist/menus/code_block/model.js.map +1 -0
  185. package/dist/menus/figure/index.js +3 -0
  186. package/dist/menus/figure/index.js.map +1 -0
  187. package/dist/menus/figure/model.js +80 -0
  188. package/dist/menus/figure/model.js.map +1 -0
  189. package/dist/menus/figure/utils.js +6 -0
  190. package/dist/menus/figure/utils.js.map +1 -0
  191. package/dist/menus/headerbar/index.js +3 -0
  192. package/dist/menus/headerbar/index.js.map +1 -0
  193. package/dist/menus/headerbar/model.js +743 -0
  194. package/dist/menus/headerbar/model.js.map +1 -0
  195. package/dist/menus/headerbar/view.js +579 -0
  196. package/dist/menus/headerbar/view.js.map +1 -0
  197. package/dist/menus/image/index.js +2 -0
  198. package/dist/menus/image/index.js.map +1 -0
  199. package/dist/menus/image/model.js +32 -0
  200. package/dist/menus/image/model.js.map +1 -0
  201. package/dist/menus/index.js +10 -0
  202. package/dist/menus/index.js.map +1 -0
  203. package/dist/menus/navigator/index.js +2 -0
  204. package/dist/menus/navigator/index.js.map +1 -0
  205. package/dist/menus/navigator/model.js +29 -0
  206. package/dist/menus/navigator/model.js.map +1 -0
  207. package/dist/menus/ordered_list/index.js +2 -0
  208. package/dist/menus/ordered_list/index.js.map +1 -0
  209. package/dist/menus/ordered_list/model.js +17 -0
  210. package/dist/menus/ordered_list/model.js.map +1 -0
  211. package/dist/menus/selection/index.js +3 -0
  212. package/dist/menus/selection/index.js.map +1 -0
  213. package/dist/menus/selection/model.js +108 -0
  214. package/dist/menus/selection/model.js.map +1 -0
  215. package/dist/menus/selection/view.js +125 -0
  216. package/dist/menus/selection/view.js.map +1 -0
  217. package/dist/menus/table/index.js +2 -0
  218. package/dist/menus/table/index.js.map +1 -0
  219. package/dist/menus/table/model.js +319 -0
  220. package/dist/menus/table/model.js.map +1 -0
  221. package/dist/menus/toolbar/index.js +3 -0
  222. package/dist/menus/toolbar/index.js.map +1 -0
  223. package/dist/menus/toolbar/model.js +747 -0
  224. package/dist/menus/toolbar/model.js.map +1 -0
  225. package/dist/menus/toolbar/view.js +314 -0
  226. package/dist/menus/toolbar/view.js.map +1 -0
  227. package/dist/navigator/index.js +435 -0
  228. package/dist/navigator/index.js.map +1 -0
  229. package/dist/no_collab_save/index.js +169 -0
  230. package/dist/no_collab_save/index.js.map +1 -0
  231. package/dist/plugins/citation_dialog/index.js +2 -0
  232. package/dist/plugins/citation_dialog/index.js.map +1 -0
  233. package/dist/plugins/editor/index.js +2 -0
  234. package/dist/plugins/editor/index.js.map +1 -0
  235. package/dist/state_plugins/access_rights.js +30 -0
  236. package/dist/state_plugins/access_rights.js.map +1 -0
  237. package/dist/state_plugins/citation_render.js +78 -0
  238. package/dist/state_plugins/citation_render.js.map +1 -0
  239. package/dist/state_plugins/clipboard.js +217 -0
  240. package/dist/state_plugins/clipboard.js.map +1 -0
  241. package/dist/state_plugins/code_block.js +165 -0
  242. package/dist/state_plugins/code_block.js.map +1 -0
  243. package/dist/state_plugins/collab_carets.js +115 -0
  244. package/dist/state_plugins/collab_carets.js.map +1 -0
  245. package/dist/state_plugins/comments.js +197 -0
  246. package/dist/state_plugins/comments.js.map +1 -0
  247. package/dist/state_plugins/contributor_input/add_button.js +101 -0
  248. package/dist/state_plugins/contributor_input/add_button.js.map +1 -0
  249. package/dist/state_plugins/contributor_input/dropup.js +273 -0
  250. package/dist/state_plugins/contributor_input/dropup.js.map +1 -0
  251. package/dist/state_plugins/contributor_input/helpers.js +23 -0
  252. package/dist/state_plugins/contributor_input/helpers.js.map +1 -0
  253. package/dist/state_plugins/contributor_input/index.js +3 -0
  254. package/dist/state_plugins/contributor_input/index.js.map +1 -0
  255. package/dist/state_plugins/contributor_input/node_view.js +76 -0
  256. package/dist/state_plugins/contributor_input/node_view.js.map +1 -0
  257. package/dist/state_plugins/contributor_input/plugin.js +236 -0
  258. package/dist/state_plugins/contributor_input/plugin.js.map +1 -0
  259. package/dist/state_plugins/document_template.js +213 -0
  260. package/dist/state_plugins/document_template.js.map +1 -0
  261. package/dist/state_plugins/figure.js +90 -0
  262. package/dist/state_plugins/figure.js.map +1 -0
  263. package/dist/state_plugins/footnote_markers.js +187 -0
  264. package/dist/state_plugins/footnote_markers.js.map +1 -0
  265. package/dist/state_plugins/headerbar.js +10 -0
  266. package/dist/state_plugins/headerbar.js.map +1 -0
  267. package/dist/state_plugins/index.js +27 -0
  268. package/dist/state_plugins/index.js.map +1 -0
  269. package/dist/state_plugins/inline_math.js +719 -0
  270. package/dist/state_plugins/inline_math.js.map +1 -0
  271. package/dist/state_plugins/inline_reference/dropup.js +182 -0
  272. package/dist/state_plugins/inline_reference/dropup.js.map +1 -0
  273. package/dist/state_plugins/inline_reference/index.js +2 -0
  274. package/dist/state_plugins/inline_reference/index.js.map +1 -0
  275. package/dist/state_plugins/inline_reference/plugin.js +1134 -0
  276. package/dist/state_plugins/inline_reference/plugin.js.map +1 -0
  277. package/dist/state_plugins/jump_hidden_nodes.js +47 -0
  278. package/dist/state_plugins/jump_hidden_nodes.js.map +1 -0
  279. package/dist/state_plugins/links.js +622 -0
  280. package/dist/state_plugins/links.js.map +1 -0
  281. package/dist/state_plugins/marginboxes.js +13 -0
  282. package/dist/state_plugins/marginboxes.js.map +1 -0
  283. package/dist/state_plugins/ordered_list_menu.js +72 -0
  284. package/dist/state_plugins/ordered_list_menu.js.map +1 -0
  285. package/dist/state_plugins/placeholders.js +98 -0
  286. package/dist/state_plugins/placeholders.js.map +1 -0
  287. package/dist/state_plugins/search.js +212 -0
  288. package/dist/state_plugins/search.js.map +1 -0
  289. package/dist/state_plugins/selection_menu.js +10 -0
  290. package/dist/state_plugins/selection_menu.js.map +1 -0
  291. package/dist/state_plugins/settings.js +191 -0
  292. package/dist/state_plugins/settings.js.map +1 -0
  293. package/dist/state_plugins/table.js +104 -0
  294. package/dist/state_plugins/table.js.map +1 -0
  295. package/dist/state_plugins/tag_input/helpers.js +55 -0
  296. package/dist/state_plugins/tag_input/helpers.js.map +1 -0
  297. package/dist/state_plugins/tag_input/index.js +3 -0
  298. package/dist/state_plugins/tag_input/index.js.map +1 -0
  299. package/dist/state_plugins/tag_input/node_view.js +77 -0
  300. package/dist/state_plugins/tag_input/node_view.js.map +1 -0
  301. package/dist/state_plugins/tag_input/plugin.js +199 -0
  302. package/dist/state_plugins/tag_input/plugin.js.map +1 -0
  303. package/dist/state_plugins/tag_input/tag_editor.js +159 -0
  304. package/dist/state_plugins/tag_input/tag_editor.js.map +1 -0
  305. package/dist/state_plugins/tag_input/tag_editor_plugins.js +61 -0
  306. package/dist/state_plugins/tag_input/tag_editor_plugins.js.map +1 -0
  307. package/dist/state_plugins/toc_render.js +142 -0
  308. package/dist/state_plugins/toc_render.js.map +1 -0
  309. package/dist/state_plugins/toolbar.js +10 -0
  310. package/dist/state_plugins/toolbar.js.map +1 -0
  311. package/dist/state_plugins/track/find_selected_changes.js +82 -0
  312. package/dist/state_plugins/track/find_selected_changes.js.map +1 -0
  313. package/dist/state_plugins/track/helpers.js +67 -0
  314. package/dist/state_plugins/track/helpers.js.map +1 -0
  315. package/dist/state_plugins/track/index.js +3 -0
  316. package/dist/state_plugins/track/index.js.map +1 -0
  317. package/dist/state_plugins/track/plugin.js +114 -0
  318. package/dist/state_plugins/track/plugin.js.map +1 -0
  319. package/dist/tools/index.js +4 -0
  320. package/dist/tools/index.js.map +1 -0
  321. package/dist/tools/key_bindings/index.js +20 -0
  322. package/dist/tools/key_bindings/index.js.map +1 -0
  323. package/dist/tools/key_bindings/templates.js +68 -0
  324. package/dist/tools/key_bindings/templates.js.map +1 -0
  325. package/dist/tools/search_replace/index.js +275 -0
  326. package/dist/tools/search_replace/index.js.map +1 -0
  327. package/dist/tools/search_replace/templates.js +18 -0
  328. package/dist/tools/search_replace/templates.js.map +1 -0
  329. package/dist/tools/word_count/index.js +40 -0
  330. package/dist/tools/word_count/index.js.map +1 -0
  331. package/dist/tools/word_count/templates.js +27 -0
  332. package/dist/tools/word_count/templates.js.map +1 -0
  333. package/dist/track/accept.js +66 -0
  334. package/dist/track/accept.js.map +1 -0
  335. package/dist/track/accept_all.js +51 -0
  336. package/dist/track/accept_all.js.map +1 -0
  337. package/dist/track/accept_all_no_insertions.js +63 -0
  338. package/dist/track/accept_all_no_insertions.js.map +1 -0
  339. package/dist/track/amend_transaction.js +390 -0
  340. package/dist/track/amend_transaction.js.map +1 -0
  341. package/dist/track/delete.js +62 -0
  342. package/dist/track/delete.js.map +1 -0
  343. package/dist/track/index.js +6 -0
  344. package/dist/track/index.js.map +1 -0
  345. package/dist/track/mod.js +86 -0
  346. package/dist/track/mod.js.map +1 -0
  347. package/dist/track/reject.js +67 -0
  348. package/dist/track/reject.js.map +1 -0
  349. package/dist/track/reject_all.js +56 -0
  350. package/dist/track/reject_all.js.map +1 -0
  351. package/package.json +80 -0
  352. package/src/bibliography/tools.js +58 -0
  353. package/src/citations/index.js +443 -0
  354. package/src/citations/render.js +68 -0
  355. package/src/clipboard/copy/index.js +1 -0
  356. package/src/clipboard/copy/schema.js +62 -0
  357. package/src/clipboard/copy/serializers.js +179 -0
  358. package/src/clipboard/paste/fidus_writer.js +55 -0
  359. package/src/clipboard/paste/general.js +80 -0
  360. package/src/clipboard/paste/google_docs.js +26 -0
  361. package/src/clipboard/paste/html.js +76 -0
  362. package/src/clipboard/paste/index.js +2 -0
  363. package/src/clipboard/paste/libreoffice_writer.js +32 -0
  364. package/src/clipboard/paste/microsoft_word.js +87 -0
  365. package/src/clipboard/paste/text.js +58 -0
  366. package/src/collab/chat.js +195 -0
  367. package/src/collab/colors.js +67 -0
  368. package/src/collab/doc.js +1436 -0
  369. package/src/collab/index.js +81 -0
  370. package/src/collab/merge/changeset.js +103 -0
  371. package/src/collab/merge/editor.js +1065 -0
  372. package/src/collab/merge/footnotes.js +175 -0
  373. package/src/collab/merge/index.js +401 -0
  374. package/src/collab/merge/recreate_transform.js +326 -0
  375. package/src/collab/merge/schema.js +135 -0
  376. package/src/collab/merge/state_plugins/action.js +171 -0
  377. package/src/collab/merge/state_plugins/clipboard.js +32 -0
  378. package/src/collab/merge/state_plugins/diff.js +780 -0
  379. package/src/collab/merge/state_plugins/index.js +2 -0
  380. package/src/collab/merge/tools.js +217 -0
  381. package/src/collab/templates.js +10 -0
  382. package/src/comments/comment.js +29 -0
  383. package/src/comments/editors/answer.js +55 -0
  384. package/src/comments/editors/comment.js +318 -0
  385. package/src/comments/editors/index.js +4 -0
  386. package/src/comments/editors/notify.js +13 -0
  387. package/src/comments/editors/schema.js +65 -0
  388. package/src/comments/index.js +11 -0
  389. package/src/comments/interactions.js +465 -0
  390. package/src/comments/store.js +513 -0
  391. package/src/contacts/add_dialog.js +110 -0
  392. package/src/contacts/templates.js +28 -0
  393. package/src/copyright_dialog/index.js +179 -0
  394. package/src/copyright_dialog/templates.js +56 -0
  395. package/src/databases/bibliography.js +179 -0
  396. package/src/databases/images.js +320 -0
  397. package/src/databases/index.js +69 -0
  398. package/src/dialogs/citation.js +428 -0
  399. package/src/dialogs/code_block.js +242 -0
  400. package/src/dialogs/contributor.js +173 -0
  401. package/src/dialogs/figure.js +479 -0
  402. package/src/dialogs/index.js +10 -0
  403. package/src/dialogs/language.js +58 -0
  404. package/src/dialogs/link.js +358 -0
  405. package/src/dialogs/math.js +148 -0
  406. package/src/dialogs/ordered_list.js +84 -0
  407. package/src/dialogs/revision.js +43 -0
  408. package/src/dialogs/subsup.js +116 -0
  409. package/src/dialogs/table.js +258 -0
  410. package/src/dialogs/templates.js +426 -0
  411. package/src/document_template/exporter.js +97 -0
  412. package/src/document_template/index.js +483 -0
  413. package/src/documents/access_rights/index.js +693 -0
  414. package/src/documents/access_rights/templates.js +178 -0
  415. package/src/e2ee/encryptor.js +228 -0
  416. package/src/e2ee/key-manager.js +202 -0
  417. package/src/e2ee/passphrase-crypto.js +606 -0
  418. package/src/e2ee/passphrase-dialog.js +655 -0
  419. package/src/e2ee/passphrase-manager.js +502 -0
  420. package/src/e2ee/password-dialog.js +652 -0
  421. package/src/e2ee/snapshot-manager.js +305 -0
  422. package/src/exporter/native/copy.js +54 -0
  423. package/src/exporter/native/file.js +37 -0
  424. package/src/exporter/native/index.js +3 -0
  425. package/src/exporter/native/revision.js +86 -0
  426. package/src/exporter/native/zip.js +1 -0
  427. package/src/footnotes/editor.js +227 -0
  428. package/src/footnotes/index.js +16 -0
  429. package/src/footnotes/layout.js +190 -0
  430. package/src/footnotes/state_plugins/access_rights.js +39 -0
  431. package/src/footnotes/state_plugins/index.js +1 -0
  432. package/src/images/edit_dialog/edit_dialog/index.js +222 -0
  433. package/src/images/edit_dialog/edit_dialog/model.js +183 -0
  434. package/src/images/edit_dialog/edit_dialog/templates.js +41 -0
  435. package/src/images/edit_dialog/model.js +5 -0
  436. package/src/images/selection_dialog/index.js +176 -0
  437. package/src/index.js +1252 -0
  438. package/src/keymap.js +102 -0
  439. package/src/marginboxes/index.js +946 -0
  440. package/src/marginboxes/templates.js +573 -0
  441. package/src/menus/code_block/index.js +1 -0
  442. package/src/menus/code_block/model.js +52 -0
  443. package/src/menus/figure/index.js +2 -0
  444. package/src/menus/figure/model.js +91 -0
  445. package/src/menus/figure/utils.js +5 -0
  446. package/src/menus/headerbar/index.js +2 -0
  447. package/src/menus/headerbar/model.js +1077 -0
  448. package/src/menus/headerbar/view.js +675 -0
  449. package/src/menus/image/index.js +1 -0
  450. package/src/menus/image/model.js +32 -0
  451. package/src/menus/index.js +9 -0
  452. package/src/menus/navigator/index.js +1 -0
  453. package/src/menus/navigator/model.js +28 -0
  454. package/src/menus/ordered_list/index.js +1 -0
  455. package/src/menus/ordered_list/model.js +19 -0
  456. package/src/menus/selection/index.js +2 -0
  457. package/src/menus/selection/model.js +134 -0
  458. package/src/menus/selection/view.js +154 -0
  459. package/src/menus/table/index.js +1 -0
  460. package/src/menus/table/model.js +414 -0
  461. package/src/menus/toolbar/index.js +2 -0
  462. package/src/menus/toolbar/model.js +920 -0
  463. package/src/menus/toolbar/view.js +354 -0
  464. package/src/navigator/index.js +515 -0
  465. package/src/no_collab_save/index.js +209 -0
  466. package/src/plugins/citation_dialog/index.js +1 -0
  467. package/src/plugins/editor/index.js +1 -0
  468. package/src/state_plugins/access_rights.js +47 -0
  469. package/src/state_plugins/citation_render.js +88 -0
  470. package/src/state_plugins/clipboard.js +289 -0
  471. package/src/state_plugins/code_block.js +201 -0
  472. package/src/state_plugins/collab_carets.js +152 -0
  473. package/src/state_plugins/comments.js +259 -0
  474. package/src/state_plugins/contributor_input/add_button.js +134 -0
  475. package/src/state_plugins/contributor_input/dropup.js +318 -0
  476. package/src/state_plugins/contributor_input/helpers.js +24 -0
  477. package/src/state_plugins/contributor_input/index.js +2 -0
  478. package/src/state_plugins/contributor_input/node_view.js +86 -0
  479. package/src/state_plugins/contributor_input/plugin.js +318 -0
  480. package/src/state_plugins/document_template.js +315 -0
  481. package/src/state_plugins/figure.js +108 -0
  482. package/src/state_plugins/footnote_markers.js +238 -0
  483. package/src/state_plugins/headerbar.js +12 -0
  484. package/src/state_plugins/index.js +73 -0
  485. package/src/state_plugins/inline_math.js +918 -0
  486. package/src/state_plugins/inline_reference/dropup.js +208 -0
  487. package/src/state_plugins/inline_reference/index.js +5 -0
  488. package/src/state_plugins/inline_reference/plugin.js +1429 -0
  489. package/src/state_plugins/jump_hidden_nodes.js +58 -0
  490. package/src/state_plugins/links.js +830 -0
  491. package/src/state_plugins/marginboxes.js +14 -0
  492. package/src/state_plugins/ordered_list_menu.js +84 -0
  493. package/src/state_plugins/placeholders.js +123 -0
  494. package/src/state_plugins/search.js +251 -0
  495. package/src/state_plugins/selection_menu.js +12 -0
  496. package/src/state_plugins/settings.js +223 -0
  497. package/src/state_plugins/table.js +127 -0
  498. package/src/state_plugins/tag_input/helpers.js +69 -0
  499. package/src/state_plugins/tag_input/index.js +2 -0
  500. package/src/state_plugins/tag_input/node_view.js +85 -0
  501. package/src/state_plugins/tag_input/plugin.js +278 -0
  502. package/src/state_plugins/tag_input/tag_editor.js +202 -0
  503. package/src/state_plugins/tag_input/tag_editor_plugins.js +74 -0
  504. package/src/state_plugins/toc_render.js +160 -0
  505. package/src/state_plugins/toolbar.js +12 -0
  506. package/src/state_plugins/track/find_selected_changes.js +107 -0
  507. package/src/state_plugins/track/helpers.js +108 -0
  508. package/src/state_plugins/track/index.js +6 -0
  509. package/src/state_plugins/track/plugin.js +140 -0
  510. package/src/tools/index.js +3 -0
  511. package/src/tools/key_bindings/index.js +21 -0
  512. package/src/tools/key_bindings/templates.js +68 -0
  513. package/src/tools/search_replace/index.js +421 -0
  514. package/src/tools/search_replace/templates.js +19 -0
  515. package/src/tools/word_count/index.js +59 -0
  516. package/src/tools/word_count/templates.js +34 -0
  517. package/src/track/accept.js +102 -0
  518. package/src/track/accept_all.js +103 -0
  519. package/src/track/accept_all_no_insertions.js +102 -0
  520. package/src/track/amend_transaction.js +562 -0
  521. package/src/track/delete.js +92 -0
  522. package/src/track/index.js +8 -0
  523. package/src/track/mod.js +116 -0
  524. package/src/track/reject.js +116 -0
  525. package/src/track/reject_all.js +121 -0
@@ -0,0 +1,1090 @@
1
+ import { SchemaExport } from "@fiduswriter/document/schema/export";
2
+ import { Dialog, activateWait, deactivateWait, makeWorker } from "fwtoolkit";
3
+ import { receiveTransaction, sendableSteps } from "prosemirror-collab";
4
+ import { EditorState } from "prosemirror-state";
5
+ import { Step } from "prosemirror-transform";
6
+ import { E2EEEncryptor } from "../e2ee/encryptor";
7
+ import { E2EEKeyManager } from "../e2ee/key-manager";
8
+ import { enterPassphraseDialog } from "../e2ee/passphrase-dialog";
9
+ import { PassphraseManager } from "../e2ee/passphrase-manager";
10
+ import { enterPasswordDialog } from "../e2ee/password-dialog";
11
+ import { getSelectionUpdate, removeCollaboratorSelection, updateCollaboratorSelection } from "../state_plugins";
12
+ import { Merge } from "./merge";
13
+ export class ModCollabDoc {
14
+ constructor(mod) {
15
+ mod.doc = this;
16
+ this.mod = mod;
17
+ this.merge = new Merge(mod);
18
+ this.unconfirmedDiffs = {};
19
+ this.confirmStepsRequestCounter = 0;
20
+ this.awaitingDiffResponse = false;
21
+ this.receiving = false;
22
+ this.currentlyCheckingVersion = false;
23
+ this.footnoteRender = false; // If the offline user edited a footnote , it needs to be rendered properly to connected users too!
24
+ this.initialVersionConfirmed = false;
25
+ this.initialDocLoaded = false;
26
+ this.templateAdjustmentPending = false;
27
+ }
28
+ cancelCurrentlyCheckingVersion() {
29
+ this.currentlyCheckingVersion = false;
30
+ window.clearTimeout(this.enableCheckVersion);
31
+ }
32
+ checkVersion(offline = false) {
33
+ // Guard: WebSocket may not be initialized yet (e.g. during
34
+ // loadDocument before the WS connector is created).
35
+ if (!this.mod.editor.ws) {
36
+ return;
37
+ }
38
+ this.mod.editor.ws.send(() => {
39
+ if (this.currentlyCheckingVersion ||
40
+ this.mod.editor.docInfo.version === undefined) {
41
+ return;
42
+ }
43
+ this.currentlyCheckingVersion = true;
44
+ this.enableCheckVersion = window.setTimeout(() => {
45
+ this.currentlyCheckingVersion = false;
46
+ }, 1000);
47
+ if (this.mod.editor.ws.connected) {
48
+ this.disableDiffSending();
49
+ }
50
+ const msg = {
51
+ type: "check_version",
52
+ v: this.mod.editor.docInfo.version
53
+ };
54
+ if (offline) {
55
+ msg.offline = true;
56
+ }
57
+ return msg;
58
+ });
59
+ }
60
+ disableDiffSending() {
61
+ this.awaitingDiffResponse = true;
62
+ // If no answer has been received from the server within 2 seconds,
63
+ // check the version
64
+ this.sendNextDiffTimer = window.setTimeout(() => {
65
+ this.awaitingDiffResponse = false;
66
+ this.sendToCollaborators();
67
+ }, 8000);
68
+ }
69
+ enableDiffSending() {
70
+ window.clearTimeout(this.sendNextDiffTimer);
71
+ this.awaitingDiffResponse = false;
72
+ this.sendToCollaborators();
73
+ }
74
+ receiveDocument(data) {
75
+ this.cancelCurrentlyCheckingVersion();
76
+ if (this.mod.editor.docInfo.confirmedDoc &&
77
+ !this.mod.editor.e2ee?.encrypted) {
78
+ this.merge.adjustDocument(data);
79
+ }
80
+ else {
81
+ this.loadDocument(data);
82
+ }
83
+ }
84
+ finishInitialLoad() {
85
+ if (this.initialDocLoaded &&
86
+ this.initialVersionConfirmed &&
87
+ !this.templateAdjustmentPending) {
88
+ deactivateWait();
89
+ this.mod.editor.waitingForDocument = false;
90
+ }
91
+ }
92
+ confirmVersion(version) {
93
+ if (!this.mod.editor.docInfo.confirmedDoc) {
94
+ return;
95
+ }
96
+ if (!this.initialVersionConfirmed) {
97
+ this.initialVersionConfirmed = true;
98
+ if (version === this.mod.editor.docInfo.version) {
99
+ this.finishInitialLoad();
100
+ }
101
+ }
102
+ }
103
+ loadDocument({ doc, time, doc_info }) {
104
+ const isInitialLoad = !this.mod.editor.docInfo.confirmedDoc;
105
+ // Reset collaboration
106
+ this.unconfirmedDiffs = {};
107
+ if (this.awaitingDiffResponse) {
108
+ this.enableDiffSending();
109
+ }
110
+ this.mod.editor.clientTimeAdjustment = Date.now() - time;
111
+ const token = this.mod.editor.docInfo.token;
112
+ this.mod.editor.docInfo = doc_info;
113
+ this.mod.editor.docInfo.token = token;
114
+ // For guests, update user object with the token UUID (stable identity)
115
+ // and session_id (for display). Token UUID persists across reconnections.
116
+ // session_id may not be present when loading from REST (it comes via
117
+ // the session_info WebSocket message). Only update if available.
118
+ if (!this.mod.editor.user.is_authenticated &&
119
+ doc_info.session_id !== undefined) {
120
+ const sessionId = doc_info.session_id;
121
+ this.mod.editor.user = {
122
+ id: token, // stable — same token UUID even after reconnect
123
+ username: `guest${sessionId}`, // display name per session
124
+ name: `Guest ${sessionId}`,
125
+ is_authenticated: false
126
+ };
127
+ }
128
+ this.mod.editor.docInfo.token = token;
129
+ this.mod.editor.docInfo.version = doc.v;
130
+ this.mod.editor.docInfo.updated = new Date();
131
+ // Check if this is an E2EE document. If so, we need to decrypt
132
+ // the content before loading it into ProseMirror.
133
+ const isE2EE = doc_info.e2ee === true;
134
+ if (isE2EE) {
135
+ this._loadE2EEDocument(doc, doc_info, isInitialLoad);
136
+ }
137
+ else {
138
+ this.mod.editor.e2ee = { encrypted: false };
139
+ this._loadUnencryptedDocument(doc, isInitialLoad);
140
+ }
141
+ }
142
+ /**
143
+ * Load an E2EE document: try passphrase/DEK first, then fall back to
144
+ * per-document password prompt, decrypt, then load.
145
+ * @private
146
+ */
147
+ async _loadE2EEDocument(doc, doc_info, isInitialLoad) {
148
+ // Extract password from URL fragment if present (share link format:
149
+ // https://example.com/share/TOKEN/#target?password=PASSWORD). The fragment is never
150
+ // sent to the server, so this is safe.
151
+ const locationHash = window.location.hash;
152
+ let urlFragmentPassword = "";
153
+ if (locationHash && locationHash.includes("?")) {
154
+ urlFragmentPassword = decodeURIComponent(new URLSearchParams(locationHash.split("?")[1]).get("password") || "");
155
+ }
156
+ // Get the salt and iterations. These may come from the REST response
157
+ // (doc_info) or from the WebSocket session_info message (already
158
+ // stored in this.mod.editor.e2ee).
159
+ let salt, iterations;
160
+ if (this.mod.editor.e2ee) {
161
+ salt = this.mod.editor.e2ee.encryptionSalt;
162
+ iterations = this.mod.editor.e2ee.encryptionIterations;
163
+ }
164
+ if (doc_info.e2ee_salt) {
165
+ salt = doc_info.e2ee_salt;
166
+ }
167
+ if (doc_info.e2ee_iterations) {
168
+ iterations = doc_info.e2ee_iterations;
169
+ }
170
+ const docId = this.mod.editor.docInfo.id;
171
+ // Helper to save password for passphrase users after successful decryption
172
+ // (migrates per-document-password docs into the passphrase system)
173
+ const maybeSavePasswordForPassphrase = async (password) => {
174
+ if (!PassphraseManager.hasKeysInSession()) {
175
+ return;
176
+ }
177
+ try {
178
+ const existing = await PassphraseManager.getDocumentPassword(docId);
179
+ if (!existing) {
180
+ await PassphraseManager.saveDocumentPassword(docId, password, null, "user", true);
181
+ }
182
+ }
183
+ catch (_e) {
184
+ console.error("E2EE: Failed to save document password for passphrase:", _e);
185
+ }
186
+ };
187
+ // Helper to resolve a password to a key and attempt decryption
188
+ const tryPassword = async (password) => {
189
+ const key = await E2EEKeyManager.resolvePasswordToKey(password, salt
190
+ ? new Uint8Array(atob(salt)
191
+ .split("")
192
+ .map(c => c.charCodeAt(0)))
193
+ : null, iterations || 600000);
194
+ await this._decryptAndLoadDoc(doc, key, salt, iterations, isInitialLoad, urlFragmentPassword);
195
+ // Cache password and key for this session
196
+ E2EEKeyManager.storePasswordInSession(docId, password);
197
+ await E2EEKeyManager.storeKeyInSession(docId, key);
198
+ if (this.mod.editor.e2ee) {
199
+ this.mod.editor.e2ee.key = key;
200
+ this.mod.editor.e2ee.password = password;
201
+ }
202
+ // Migrate per-document-password docs to passphrase system
203
+ await maybeSavePasswordForPassphrase(password);
204
+ };
205
+ // --- Try passphrase path first ---
206
+ if (PassphraseManager.hasKeysInSession()) {
207
+ const password = await PassphraseManager.getDocumentPassword(docId);
208
+ if (password) {
209
+ try {
210
+ await tryPassword(password);
211
+ if (this.mod.editor.e2ee) {
212
+ this.mod.editor.e2ee.usesPassphrase = true;
213
+ }
214
+ return;
215
+ }
216
+ catch (_error) {
217
+ // Password didn't work — fall through
218
+ }
219
+ }
220
+ }
221
+ // If passphrase keys exist but are not in session, try to unlock
222
+ if (!PassphraseManager.hasKeysInSession()) {
223
+ const hasKeys = await PassphraseManager.hasEncryptionKeys();
224
+ if (hasKeys) {
225
+ let errorMessage = "";
226
+ let unlocked = false;
227
+ while (!unlocked) {
228
+ const result = await new Promise(resolve => {
229
+ enterPassphraseDialog(pwd => resolve({ action: "unlock", passphrase: pwd }), () => resolve({ action: "recover" }), { errorMessage });
230
+ });
231
+ if (result.action === "unlock" && result.passphrase) {
232
+ try {
233
+ await PassphraseManager.unlockWithPassphrase(result.passphrase);
234
+ const password = await PassphraseManager.getDocumentPassword(docId);
235
+ if (password) {
236
+ await tryPassword(password);
237
+ if (this.mod.editor.e2ee) {
238
+ this.mod.editor.e2ee.usesPassphrase = true;
239
+ }
240
+ unlocked = true;
241
+ return;
242
+ }
243
+ }
244
+ catch (_e) {
245
+ errorMessage = gettext("Incorrect passphrase. Please try again.");
246
+ }
247
+ }
248
+ else if (result?.action === "recover") {
249
+ // Recovery flow
250
+ const { recoverWithKeyDialog } = await import("../e2ee/passphrase-dialog.js");
251
+ const recoverResult = await new Promise(resolve => {
252
+ recoverWithKeyDialog(resolve);
253
+ });
254
+ if (recoverResult) {
255
+ try {
256
+ const { newRecoveryKey } = await PassphraseManager.recoverWithRecoveryKey(recoverResult.recoveryKey, recoverResult.newPassphrase);
257
+ const { showRecoveryKeyDialog } = await import("../e2ee/passphrase-dialog.js");
258
+ await new Promise(resolve => {
259
+ showRecoveryKeyDialog(newRecoveryKey, resolve);
260
+ });
261
+ // After recovery, try to get document password again
262
+ const password = await PassphraseManager.getDocumentPassword(docId);
263
+ if (password) {
264
+ await tryPassword(password);
265
+ if (this.mod.editor.e2ee) {
266
+ this.mod.editor.e2ee.usesPassphrase = true;
267
+ }
268
+ return;
269
+ }
270
+ }
271
+ catch (e) {
272
+ console.error("E2EE: Recovery failed:", e);
273
+ const errorDialog = new Dialog({
274
+ title: gettext("Recovery Failed"),
275
+ id: "e2ee-recovery-failed",
276
+ body: gettext("The recovery key you entered is incorrect, or the recovery process failed. Please try again."),
277
+ buttons: [
278
+ {
279
+ text: gettext("Retry"),
280
+ classes: "fw-dark",
281
+ click: () => {
282
+ errorDialog.close();
283
+ this._loadE2EEDocument(doc, doc_info, isInitialLoad, urlFragmentPassword);
284
+ }
285
+ },
286
+ {
287
+ text: gettext("Cancel"),
288
+ classes: "fw-light",
289
+ click: () => errorDialog.close()
290
+ }
291
+ ],
292
+ canClose: false
293
+ });
294
+ errorDialog.open();
295
+ return;
296
+ }
297
+ }
298
+ // If recovery was cancelled, continue the loop to show
299
+ // the passphrase dialog again.
300
+ continue;
301
+ }
302
+ else {
303
+ // User cancelled the passphrase dialog — break out of
304
+ // the loop and fall through to other password sources.
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ }
310
+ // --- Try sessionStorage password ---
311
+ const sessionPassword = E2EEKeyManager.getPasswordFromSession(docId);
312
+ if (sessionPassword) {
313
+ try {
314
+ await tryPassword(sessionPassword);
315
+ return;
316
+ }
317
+ catch (_error) {
318
+ E2EEKeyManager.clearPasswordFromSession(docId);
319
+ E2EEKeyManager.clearKeyFromSession(docId);
320
+ }
321
+ }
322
+ // --- Try URL fragment password ---
323
+ if (urlFragmentPassword) {
324
+ try {
325
+ await tryPassword(urlFragmentPassword);
326
+ return;
327
+ }
328
+ catch (_error) {
329
+ // URL password didn't work — fall through to prompt
330
+ }
331
+ }
332
+ // --- Try existing key (e.g. from _createE2EEDocument) ---
333
+ const existingKey = this.mod.editor.e2ee?.key;
334
+ if (existingKey) {
335
+ try {
336
+ await this._decryptAndLoadDoc(doc, existingKey, salt, iterations, isInitialLoad, urlFragmentPassword);
337
+ }
338
+ catch (error) {
339
+ console.error("E2EE: Decryption failed with existing key", error);
340
+ if (this.mod.editor.e2ee) {
341
+ this.mod.editor.e2ee.key = null;
342
+ }
343
+ await this._promptPasswordAndDecrypt(doc, doc_info, salt, iterations, isInitialLoad, urlFragmentPassword);
344
+ }
345
+ return;
346
+ }
347
+ // No key available — prompt for the password
348
+ await this._promptPasswordAndDecrypt(doc, doc_info, salt, iterations, isInitialLoad, urlFragmentPassword);
349
+ }
350
+ /**
351
+ * Prompt for a password, derive the key, and decrypt/load the document.
352
+ * @private
353
+ */
354
+ async _promptPasswordAndDecrypt(doc, doc_info, salt, iterations, isInitialLoad, urlFragmentPassword) {
355
+ // Decode the salt from Base64 to Uint8Array
356
+ let saltBytes = null;
357
+ if (salt) {
358
+ const binary = atob(salt);
359
+ saltBytes = new Uint8Array(binary.length);
360
+ for (let i = 0; i < binary.length; i++) {
361
+ saltBytes[i] = binary.charCodeAt(i);
362
+ }
363
+ }
364
+ const goBack = () => {
365
+ const folderPath = this.mod.editor.docInfo.path.slice(0, this.mod.editor.docInfo.path.lastIndexOf("/"));
366
+ if (this.mod.editor.app && this.mod.editor.app.goTo) {
367
+ if (!folderPath.length) {
368
+ this.mod.editor.app.goTo("/");
369
+ }
370
+ else {
371
+ this.mod.editor.app.goTo(`/documents${folderPath}/`);
372
+ }
373
+ }
374
+ else {
375
+ window.location.href = "/";
376
+ }
377
+ };
378
+ await enterPasswordDialog(async (password) => {
379
+ try {
380
+ const key = await E2EEKeyManager.resolvePasswordToKey(password, saltBytes, iterations || 600000);
381
+ await this._decryptAndLoadDoc(doc, key, salt, iterations, isInitialLoad, urlFragmentPassword);
382
+ // Cache password and key for this session
383
+ E2EEKeyManager.storePasswordInSession(this.mod.editor.docInfo.id, password);
384
+ await E2EEKeyManager.storeKeyInSession(this.mod.editor.docInfo.id, key);
385
+ if (this.mod.editor.e2ee) {
386
+ this.mod.editor.e2ee.key = key;
387
+ this.mod.editor.e2ee.password = password;
388
+ }
389
+ // Migrate per-document-password docs to passphrase system
390
+ if (PassphraseManager.hasKeysInSession()) {
391
+ try {
392
+ const existing = await PassphraseManager.getDocumentPassword(this.mod.editor.docInfo.id);
393
+ if (!existing) {
394
+ await PassphraseManager.saveDocumentPassword(this.mod.editor.docInfo.id, password, null, "user", true);
395
+ }
396
+ }
397
+ catch (_e) {
398
+ console.error("E2EE: Failed to save document password for passphrase:", _e);
399
+ }
400
+ }
401
+ }
402
+ catch (_error) {
403
+ console.error("E2EE DECRYPT ERROR:", _error);
404
+ const errorDialog = new Dialog({
405
+ title: gettext("Decryption Failed"),
406
+ id: "e2ee-decryption-failed",
407
+ body: gettext("That password didn't work. Please try again."),
408
+ buttons: [
409
+ {
410
+ text: gettext("Retry"),
411
+ classes: "fw-dark",
412
+ click: () => {
413
+ this._promptPasswordAndDecrypt(doc, doc_info, salt, iterations, isInitialLoad, "");
414
+ }
415
+ },
416
+ {
417
+ text: gettext("Cancel"),
418
+ classes: "fw-light",
419
+ click: () => {
420
+ errorDialog.close();
421
+ goBack();
422
+ }
423
+ }
424
+ ],
425
+ canClose: false
426
+ });
427
+ errorDialog.open();
428
+ }
429
+ }, urlFragmentPassword, goBack);
430
+ }
431
+ /**
432
+ * Decrypt document content/comments/bibliography with the given key
433
+ * and load into ProseMirror.
434
+ *
435
+ * For a newly created E2EE document, no encrypted snapshot has been
436
+ * saved yet, so the content/comments/bibliography are still plaintext
437
+ * JSON objects (the template content). We detect this by checking
438
+ * whether the value is a string (encrypted = Base64 ciphertext) or
439
+ * an object (plaintext).
440
+ *
441
+ * @private
442
+ */
443
+ async _decryptAndLoadDoc(doc, key, salt, iterations, isInitialLoad, urlFragmentPassword) {
444
+ let decryptedContent = doc.content;
445
+ if (typeof doc.content === "string") {
446
+ decryptedContent = await E2EEEncryptor.decryptObject(doc.content, key);
447
+ }
448
+ let decryptedComments = doc.comments;
449
+ if (typeof doc.comments === "string") {
450
+ decryptedComments = await E2EEEncryptor.decryptObject(doc.comments, key);
451
+ }
452
+ let decryptedBibliography = doc.bibliography;
453
+ if (typeof doc.bibliography === "string") {
454
+ decryptedBibliography = await E2EEEncryptor.decryptObject(doc.bibliography, key);
455
+ }
456
+ // Store the E2EE state on the editor
457
+ this.mod.editor.e2ee = {
458
+ encrypted: true,
459
+ encryptionSalt: salt,
460
+ encryptionIterations: iterations || 600000,
461
+ key: key,
462
+ snapshotManager: this.mod.editor.e2ee?.snapshotManager || null
463
+ };
464
+ // Initialize snapshot manager now that we have the key
465
+ if (!this.mod.editor.e2ee.snapshotManager) {
466
+ const { E2EESnapshotManager } = await import("../e2ee/snapshot-manager");
467
+ this.mod.editor.e2ee.snapshotManager = new E2EESnapshotManager(this.mod.editor);
468
+ }
469
+ this.mod.editor.e2ee.snapshotManager.setKey(key);
470
+ // Cache the key in sessionStorage so the user doesn't have to
471
+ // re-enter the password when reopening the document this session.
472
+ try {
473
+ await E2EEKeyManager.storeKeyInSession(this.mod.editor.docInfo.id, key);
474
+ }
475
+ catch (_e) {
476
+ // If the key is non-extractable, we can't store it.
477
+ }
478
+ // Remove the password from the URL fragment to avoid
479
+ // leaving it in the address bar or browser history.
480
+ if (urlFragmentPassword && window.history.replaceState) {
481
+ const hash = window.location.hash; // "#title?password=fish&targetting=true"
482
+ const [anchor, queryString] = hash.split("?"); // ["#title", "password=fish&targetting=true"]
483
+ const params = new URLSearchParams(queryString || "");
484
+ params.delete("password"); // Removes "password" if it exists
485
+ const newQueryString = params.toString();
486
+ const newHash = newQueryString
487
+ ? `${anchor}?${newQueryString}`
488
+ : anchor;
489
+ window.history.replaceState(null, "", newHash);
490
+ }
491
+ // Decrypt image copyright metadata for E2EE documents.
492
+ // The server stores it as an opaque encrypted string; we decrypt
493
+ // it here so the image DB receives plaintext objects.
494
+ let decryptedImages = doc.images;
495
+ if (doc.images) {
496
+ decryptedImages = {};
497
+ const { E2EEEncryptor } = await import("../e2ee/encryptor");
498
+ for (const [id, image] of Object.entries(doc.images)) {
499
+ decryptedImages[id] = image;
500
+ if (typeof image.copyright === "string") {
501
+ try {
502
+ decryptedImages[id].copyright =
503
+ await E2EEEncryptor.decryptObject(image.copyright, key);
504
+ }
505
+ catch (_e) {
506
+ // If decryption fails, leave as-is (legacy plaintext)
507
+ }
508
+ }
509
+ }
510
+ }
511
+ // Now load the decrypted document using the standard path
512
+ const decryptedDoc = {
513
+ ...doc,
514
+ content: decryptedContent,
515
+ comments: decryptedComments,
516
+ bibliography: decryptedBibliography,
517
+ images: decryptedImages
518
+ };
519
+ this._loadUnencryptedDocument(decryptedDoc, isInitialLoad);
520
+ // Cache the decrypted title in sessionStorage so the document overview
521
+ // can display the real title without prompting for the password again.
522
+ let title = "";
523
+ this.mod.editor.view.state.doc.firstChild.forEach(child => {
524
+ if (!child.marks.find(mark => mark.type.name === "deletion")) {
525
+ title += child.textContent;
526
+ }
527
+ });
528
+ sessionStorage.setItem(`e2ee_title_${this.mod.editor.docInfo.id}`, title);
529
+ // For newly created E2EE documents, the initial content is still
530
+ // plaintext (it came from the template). We need to send an initial
531
+ // encrypted snapshot so the server stores encrypted content.
532
+ // We detect a new document by checking if the content was plaintext
533
+ // (not a Base64 string) and this is the initial load.
534
+ const isNewE2EEDocument = isInitialLoad && typeof doc.content !== "string";
535
+ if (isNewE2EEDocument && this.mod.editor.e2ee.snapshotManager) {
536
+ this.mod.editor.e2ee.snapshotManager.sendInitialSnapshot(decryptedContent, decryptedComments, decryptedBibliography, this.mod.editor.docInfo.version);
537
+ }
538
+ // Now that the key is available and the document is loaded, open the
539
+ // WebSocket connection. This ensures encrypted catch-up diffs (ep)
540
+ // from the server can be decrypted as soon as they arrive.
541
+ if (this.mod.editor.ws && !this.mod.editor.ws.connected) {
542
+ this.mod.editor.startWebSocket();
543
+ }
544
+ else if (!this.mod.editor.ws) {
545
+ // Non-collaborative mode: mark version as confirmed so the editor
546
+ // finishes loading and periodic saves can begin.
547
+ this.mod.editor.mod.collab.doc.confirmVersion(this.mod.editor.docInfo.version);
548
+ }
549
+ }
550
+ /**
551
+ * Load an unencrypted (or already decrypted) document into ProseMirror.
552
+ * This is the original loadDocument logic, extracted for reuse.
553
+ * @private
554
+ */
555
+ _loadUnencryptedDocument(doc, isInitialLoad) {
556
+ // Remember location hash to scroll there subsequently.
557
+ const [locationHash, _queryString] = window.location.hash.split("?");
558
+ this.mod.editor.mod.db.bibDB.setDB(doc.bibliography);
559
+ this.mod.editor.mod.db.imageDB.setDB(doc.images);
560
+ const stateDoc = this.mod.editor.schema.nodeFromJSON(doc.content);
561
+ const plugins = this.mod.editor.statePlugins.map(plugin => {
562
+ if (plugin[1]) {
563
+ return plugin[0](plugin[1](doc));
564
+ }
565
+ else {
566
+ return plugin[0]();
567
+ }
568
+ });
569
+ const stateConfig = {
570
+ schema: this.mod.editor.schema,
571
+ doc: stateDoc,
572
+ plugins
573
+ };
574
+ // Set document in prosemirror
575
+ this.mod.editor.view.setProps({ state: EditorState.create(stateConfig) });
576
+ this.mod.editor.view.setProps({ nodeViews: {} }); // Needed to initialize nodeViews in plugins
577
+ // Set initial confirmed doc
578
+ this.mod.editor.docInfo.confirmedDoc = this.mod.editor.view.state.doc;
579
+ // Render footnotes based on main doc
580
+ this.mod.editor.mod.footnotes.fnEditor.renderAllFootnotes();
581
+ // Setup comment handling
582
+ this.mod.editor.mod.comments.store.reset();
583
+ this.mod.editor.mod.comments.store.loadComments(doc.comments);
584
+ this.mod.editor.mod.marginboxes.view(this.mod.editor.view);
585
+ if (locationHash && locationHash.length) {
586
+ this.mod.editor.scrollIdIntoView(locationHash.slice(1));
587
+ }
588
+ // Update the header bar to reflect the loaded document's title.
589
+ // This is needed because setStyles() (which calls headerView.update())
590
+ // runs before loadDocument(), so the header still shows "Untitled"
591
+ // from the empty initial document.
592
+ if (this.mod.editor.menu.headerView) {
593
+ this.mod.editor.menu.headerView.update();
594
+ }
595
+ if (isInitialLoad) {
596
+ this.initialDocLoaded = true;
597
+ }
598
+ else {
599
+ deactivateWait();
600
+ this.mod.editor.waitingForDocument = false;
601
+ }
602
+ if (doc.template) {
603
+ // We received the template. That means we are the first user present with write access.
604
+ // We will adjust the document to the template if necessary.
605
+ // For E2EE documents, the content has already been decrypted
606
+ // by this point, so template adjustment works normally.
607
+ if (isInitialLoad) {
608
+ this.templateAdjustmentPending = true;
609
+ }
610
+ activateWait(true, gettext("Updating document. Please wait..."));
611
+ const activateWaitTimer = setTimeout(() => {
612
+ activateWait(true, gettext("It's taking a bit longer than usual, but it should be ready soon. Please wait..."));
613
+ }, 60000);
614
+ const adjustWorker = makeWorker(staticUrl("js/adjust_doc_to_template_worker.js"));
615
+ adjustWorker.onmessage = message => {
616
+ if (message.data.type === "result") {
617
+ if (message.data.steps.length) {
618
+ const tr = this.mod.editor.view.state.tr;
619
+ message.data.steps.forEach(step => tr.step(Step.fromJSON(this.mod.editor.schema, step)));
620
+ tr.setMeta("remote", true);
621
+ this.mod.editor.view.dispatch(tr);
622
+ }
623
+ // clearing timer for updating message since operation is completed
624
+ clearTimeout(activateWaitTimer);
625
+ if (isInitialLoad) {
626
+ this.templateAdjustmentPending = false;
627
+ this.finishInitialLoad();
628
+ }
629
+ else {
630
+ deactivateWait();
631
+ }
632
+ this.setDocSettings();
633
+ }
634
+ };
635
+ adjustWorker.onerror = error => console.error(error);
636
+ const schemaExporter = new SchemaExport();
637
+ adjustWorker.postMessage({
638
+ schemaSpec: JSON.parse(schemaExporter.init()),
639
+ doc: doc.content,
640
+ template: doc.template.content,
641
+ documentStyleSlugs: this.mod.editor.mod.documentTemplate.documentStyles.map(style => style.slug)
642
+ });
643
+ }
644
+ else {
645
+ this.setDocSettings();
646
+ if (isInitialLoad) {
647
+ this.finishInitialLoad();
648
+ }
649
+ }
650
+ }
651
+ setDocSettings() {
652
+ // Set part specific settings
653
+ this.mod.editor.mod.documentTemplate.addDocPartSettings();
654
+ this.mod.editor.mod.documentTemplate.addCitationStylesMenuEntries();
655
+ }
656
+ sendToCollaborators() {
657
+ // Guard: WebSocket may not be initialized yet (e.g. during
658
+ // loadDocument before the WS connector is created).
659
+ if (!this.mod.editor.ws) {
660
+ return;
661
+ }
662
+ // For E2EE documents, we need to encrypt diffs before sending.
663
+ // Since encryption is async, we use a separate code path.
664
+ const isE2EE = this.mod.editor.e2ee &&
665
+ this.mod.editor.e2ee.encrypted &&
666
+ this.mod.editor.e2ee.key;
667
+ if (isE2EE) {
668
+ this._sendE2EEDiff();
669
+ return;
670
+ }
671
+ // Handle either doc change and comment updates OR caret update. Priority
672
+ // for doc change/comment update.
673
+ this.mod.editor.ws.send(() => {
674
+ if (this.awaitingDiffResponse ||
675
+ this.mod.editor.waitingForDocument ||
676
+ this.receiving) {
677
+ return false;
678
+ }
679
+ else if (sendableSteps(this.mod.editor.view.state) ||
680
+ this.mod.editor.mod.comments.store.unsentEvents().length ||
681
+ this.mod.editor.mod.db.bibDB.unsentEvents().length ||
682
+ this.mod.editor.mod.db.imageDB.unsentEvents().length) {
683
+ this.disableDiffSending();
684
+ const stepsToSend = sendableSteps(this.mod.editor.view.state), fnStepsToSend = sendableSteps(this.mod.editor.mod.footnotes.fnEditor.view.state), commentUpdates = this.mod.editor.mod.comments.store.unsentEvents(), bibliographyUpdates = this.mod.editor.mod.db.bibDB.unsentEvents(), imageUpdates = this.mod.editor.mod.db.imageDB.unsentEvents();
685
+ if (!stepsToSend &&
686
+ !fnStepsToSend &&
687
+ !commentUpdates.length &&
688
+ !bibliographyUpdates.length &&
689
+ !imageUpdates.length) {
690
+ // no diff. abandon operation
691
+ return;
692
+ }
693
+ const rid = this.confirmStepsRequestCounter++, unconfirmedDiff = {
694
+ type: "diff",
695
+ v: this.mod.editor.docInfo.version,
696
+ rid
697
+ };
698
+ unconfirmedDiff["cid"] = this.mod.editor.client_id;
699
+ if (stepsToSend) {
700
+ unconfirmedDiff["ds"] = stepsToSend.steps.map(s => s.toJSON());
701
+ // In case the title changed, we also add a title field to
702
+ // update the title field instantly - important for the
703
+ // document overview page.
704
+ let newTitle = "";
705
+ this.mod.editor.view.state.doc.firstChild.forEach(child => {
706
+ if (!child.marks.find(mark => mark.type.name === "deletion")) {
707
+ newTitle += child.textContent;
708
+ }
709
+ });
710
+ newTitle = newTitle.slice(0, 255);
711
+ let oldTitle = "";
712
+ this.mod.editor.docInfo.confirmedDoc.firstChild.forEach(child => {
713
+ if (!child.marks.find(mark => mark.type.name === "deletion")) {
714
+ oldTitle += child.textContent;
715
+ }
716
+ });
717
+ oldTitle = oldTitle.slice(0, 255);
718
+ if (newTitle !== oldTitle) {
719
+ unconfirmedDiff["ti"] = newTitle;
720
+ }
721
+ }
722
+ if (fnStepsToSend) {
723
+ // We add the client ID to every single step
724
+ unconfirmedDiff["fs"] = fnStepsToSend.steps.map(s => s.toJSON());
725
+ }
726
+ if (this.footnoteRender) {
727
+ unconfirmedDiff["footnoterender"] = true;
728
+ this.footnoteRender = false;
729
+ }
730
+ if (commentUpdates.length) {
731
+ unconfirmedDiff["cu"] = commentUpdates;
732
+ }
733
+ if (bibliographyUpdates.length) {
734
+ unconfirmedDiff["bu"] = bibliographyUpdates;
735
+ }
736
+ if (imageUpdates.length) {
737
+ unconfirmedDiff["iu"] = imageUpdates;
738
+ }
739
+ this.unconfirmedDiffs[rid] = Object.assign({ doc: this.mod.editor.view.state.doc }, unconfirmedDiff);
740
+ return unconfirmedDiff;
741
+ }
742
+ else if (this.mod.editor.currentView?.state &&
743
+ getSelectionUpdate(this.mod.editor.currentView.state)) {
744
+ const currentView = this.mod.editor.currentView;
745
+ if (this.lastSelectionUpdateState === currentView.state) {
746
+ // Selection update has been sent for this state already. Skip
747
+ return false;
748
+ }
749
+ this.lastSelectionUpdateState = currentView.state;
750
+ // Create a new caret as the current user
751
+ const selectionUpdate = getSelectionUpdate(currentView.state);
752
+ return {
753
+ type: "selection_change",
754
+ id: this.mod.editor.user.id,
755
+ v: this.mod.editor.docInfo.version,
756
+ session_id: this.mod.editor.docInfo.session_id,
757
+ anchor: selectionUpdate.anchor,
758
+ head: selectionUpdate.head,
759
+ // Whether the selection is in the footnote or the main editor
760
+ editor: currentView === this.mod.editor.view
761
+ ? "main"
762
+ : "footnotes"
763
+ };
764
+ }
765
+ else {
766
+ return false;
767
+ }
768
+ });
769
+ }
770
+ /**
771
+ * Send an encrypted diff for an E2EE document.
772
+ *
773
+ * For E2EE documents, the diff payload (steps, comments, bibliography,
774
+ * image updates) is encrypted as a single blob before sending. The
775
+ * server relays the encrypted diff to other clients without being able
776
+ * to read the content.
777
+ *
778
+ * The unencrypted diff is stored locally in `unconfirmedDiffs` for
779
+ * confirmation/rejection handling. Only the wire format is encrypted.
780
+ *
781
+ * @private
782
+ */
783
+ async _sendE2EEDiff() {
784
+ // Check if there's anything to send
785
+ if (this.awaitingDiffResponse ||
786
+ this.mod.editor.waitingForDocument ||
787
+ this.receiving) {
788
+ // No diff to send, but check for selection update.
789
+ // Guard: the editor view may not be initialized yet
790
+ // (e.g. during loadDocument before ProseMirror is set up).
791
+ if (this.mod.editor.view) {
792
+ this._sendE2EESelectionChange();
793
+ }
794
+ return;
795
+ }
796
+ if (!sendableSteps(this.mod.editor.view.state) &&
797
+ !this.mod.editor.mod.comments.store.unsentEvents().length &&
798
+ !this.mod.editor.mod.db.bibDB.unsentEvents().length &&
799
+ !this.mod.editor.mod.db.imageDB.unsentEvents().length) {
800
+ // No diff, check for selection update
801
+ if (this.mod.editor.view) {
802
+ this._sendE2EESelectionChange();
803
+ }
804
+ return;
805
+ }
806
+ this.disableDiffSending();
807
+ const stepsToSend = sendableSteps(this.mod.editor.view.state), fnStepsToSend = sendableSteps(this.mod.editor.mod.footnotes.fnEditor.view.state), commentUpdates = this.mod.editor.mod.comments.store.unsentEvents(), bibliographyUpdates = this.mod.editor.mod.db.bibDB.unsentEvents(), imageUpdates = this.mod.editor.mod.db.imageDB.unsentEvents();
808
+ if (!stepsToSend &&
809
+ !fnStepsToSend &&
810
+ !commentUpdates.length &&
811
+ !bibliographyUpdates.length &&
812
+ !imageUpdates.length) {
813
+ // no diff. abandon operation
814
+ this.enableDiffSending();
815
+ return;
816
+ }
817
+ const rid = this.confirmStepsRequestCounter++, unconfirmedDiff = {
818
+ type: "diff",
819
+ v: this.mod.editor.docInfo.version,
820
+ rid
821
+ };
822
+ unconfirmedDiff["cid"] = this.mod.editor.client_id;
823
+ // Collect the payload fields that need encryption
824
+ const encryptedPayload = {};
825
+ if (stepsToSend) {
826
+ encryptedPayload["ds"] = stepsToSend.steps.map(s => s.toJSON());
827
+ // Track title changes for the document overview
828
+ let newTitle = "";
829
+ this.mod.editor.view.state.doc.firstChild.forEach(child => {
830
+ if (!child.marks.find(mark => mark.type.name === "deletion")) {
831
+ newTitle += child.textContent;
832
+ }
833
+ });
834
+ newTitle = newTitle.slice(0, 255);
835
+ let oldTitle = "";
836
+ this.mod.editor.docInfo.confirmedDoc.firstChild.forEach(child => {
837
+ if (!child.marks.find(mark => mark.type.name === "deletion")) {
838
+ oldTitle += child.textContent;
839
+ }
840
+ });
841
+ oldTitle = oldTitle.slice(0, 255);
842
+ if (newTitle !== oldTitle) {
843
+ // For E2EE documents, the title is encrypted too.
844
+ // We don't send "ti" in plaintext; it goes in the
845
+ // encrypted payload.
846
+ encryptedPayload["ti"] = newTitle;
847
+ }
848
+ }
849
+ if (fnStepsToSend) {
850
+ encryptedPayload["fs"] = fnStepsToSend.steps.map(s => s.toJSON());
851
+ }
852
+ if (this.footnoteRender) {
853
+ unconfirmedDiff["footnoterender"] = true;
854
+ this.footnoteRender = false;
855
+ }
856
+ if (commentUpdates.length) {
857
+ encryptedPayload["cu"] = commentUpdates;
858
+ }
859
+ if (bibliographyUpdates.length) {
860
+ encryptedPayload["bu"] = bibliographyUpdates;
861
+ }
862
+ if (imageUpdates.length) {
863
+ encryptedPayload["iu"] = imageUpdates;
864
+ }
865
+ // Store the unencrypted diff locally for confirmation handling.
866
+ // We keep the plaintext steps so confirmDiff can apply them.
867
+ this.unconfirmedDiffs[rid] = Object.assign({ doc: this.mod.editor.view.state.doc }, unconfirmedDiff, encryptedPayload);
868
+ try {
869
+ // Encrypt the payload fields as a single blob
870
+ const key = this.mod.editor.e2ee.key;
871
+ const ep = await E2EEEncryptor.encryptObject(encryptedPayload, key);
872
+ // Build the wire-format diff: metadata in plaintext,
873
+ // payload encrypted
874
+ const wireDiff = {
875
+ type: "diff",
876
+ v: unconfirmedDiff.v,
877
+ rid: unconfirmedDiff.rid,
878
+ cid: unconfirmedDiff.cid,
879
+ ep, // encrypted payload
880
+ e2ee_salt: this.mod.editor.e2ee.encryptionSalt
881
+ };
882
+ if (unconfirmedDiff.footnoterender) {
883
+ wireDiff.footnoterender = true;
884
+ }
885
+ // Send the encrypted diff
886
+ this.mod.editor.ws.send(() => wireDiff);
887
+ }
888
+ catch (error) {
889
+ console.error("E2EE: Failed to encrypt diff", error);
890
+ // Re-enable diff sending so we can try again
891
+ this.enableDiffSending();
892
+ }
893
+ }
894
+ /**
895
+ * Send a selection change for an E2EE document.
896
+ * Selection changes are not encrypted (they only contain cursor position).
897
+ *
898
+ * @private
899
+ */
900
+ _sendE2EESelectionChange() {
901
+ if (!this.mod.editor.currentView ||
902
+ !this.mod.editor.currentView.state) {
903
+ return;
904
+ }
905
+ if (!getSelectionUpdate(this.mod.editor.currentView.state)) {
906
+ return;
907
+ }
908
+ const currentView = this.mod.editor.currentView;
909
+ if (this.lastSelectionUpdateState === currentView.state) {
910
+ return;
911
+ }
912
+ this.lastSelectionUpdateState = currentView.state;
913
+ const selectionUpdate = getSelectionUpdate(currentView.state);
914
+ this.mod.editor.ws.send(() => ({
915
+ type: "selection_change",
916
+ id: this.mod.editor.user.id,
917
+ v: this.mod.editor.docInfo.version,
918
+ session_id: this.mod.editor.docInfo.session_id,
919
+ anchor: selectionUpdate.anchor,
920
+ head: selectionUpdate.head,
921
+ editor: currentView === this.mod.editor.view ? "main" : "footnotes"
922
+ }));
923
+ }
924
+ receiveSelectionChange(data) {
925
+ const participant = this.mod.participants.find(par => par.id === data.id);
926
+ let tr, fnTr;
927
+ if (!participant) {
928
+ // participant is still unknown to us. Ignore
929
+ return;
930
+ }
931
+ if (data.editor === "footnotes") {
932
+ fnTr = updateCollaboratorSelection(this.mod.editor.mod.footnotes.fnEditor.view.state, participant, data);
933
+ tr = removeCollaboratorSelection(this.mod.editor.view.state, data);
934
+ }
935
+ else {
936
+ tr = updateCollaboratorSelection(this.mod.editor.view.state, participant, data);
937
+ fnTr = removeCollaboratorSelection(this.mod.editor.mod.footnotes.fnEditor.view.state, data);
938
+ }
939
+ if (tr) {
940
+ this.mod.editor.view.dispatch(tr);
941
+ }
942
+ if (fnTr) {
943
+ this.mod.editor.mod.footnotes.fnEditor.view.dispatch(fnTr);
944
+ }
945
+ }
946
+ receiveDiff(data, serverFix = false) {
947
+ // Check if this is an encrypted diff for an E2EE document.
948
+ // Encrypted diffs have an "ep" (encrypted payload) field instead
949
+ // of plaintext ds/fs/cu/bu/iu fields. We decrypt the payload
950
+ // first, then process the diff normally.
951
+ if (data["ep"] && this.mod.editor.e2ee && this.mod.editor.e2ee.key) {
952
+ this._receiveE2EEDiff(data, serverFix);
953
+ return;
954
+ }
955
+ this._processDiff(data, serverFix);
956
+ }
957
+ /**
958
+ * Process a diff (unencrypted or already decrypted).
959
+ * @private
960
+ */
961
+ _processDiff(data, serverFix = false) {
962
+ this.mod.editor.docInfo.version++;
963
+ if (data["bu"]) {
964
+ // bibliography updates
965
+ this.mod.editor.mod.db.bibDB.receive(data["bu"]);
966
+ }
967
+ if (data["iu"]) {
968
+ // images updates
969
+ this.mod.editor.mod.db.imageDB.receive(data["iu"]);
970
+ }
971
+ if (data["cu"]) {
972
+ // comment updates
973
+ this.mod.editor.mod.comments.store.receive(data["cu"]);
974
+ }
975
+ if (data["ds"]) {
976
+ // document steps
977
+ this.applyDiffs(data["ds"], data["cid"]);
978
+ }
979
+ if (data["fs"]) {
980
+ // footnote steps
981
+ this.mod.editor.mod.footnotes.fnEditor.applyDiffs(data["fs"], data["cid"]);
982
+ }
983
+ if (data["footnoterender"]) {
984
+ // re-render footnotes properly
985
+ this.mod.editor.mod.footnotes.fnEditor.renderAllFootnotes();
986
+ }
987
+ if (serverFix) {
988
+ // Diff is a fix created by server due to missing diffs.
989
+ if ("reject_request_id" in data) {
990
+ delete this.unconfirmedDiffs[data.reject_request_id];
991
+ }
992
+ this.cancelCurrentlyCheckingVersion();
993
+ // There may be unsent local changes. Send them now after .5 seconds,
994
+ // in case collaborators want to send something first.
995
+ this.enableDiffSending();
996
+ window.setTimeout(() => this.sendToCollaborators(), 500);
997
+ }
998
+ }
999
+ /**
1000
+ * Receive and decrypt an E2EE diff from another client.
1001
+ *
1002
+ * For E2EE documents, diffs arrive with an "ep" (encrypted payload)
1003
+ * field containing the encrypted steps, comments, bibliography, and
1004
+ * image updates. This method decrypts the payload and then processes
1005
+ * the diff using the standard _processDiff path.
1006
+ *
1007
+ * @param {Object} data - The diff message with "ep" field
1008
+ * @param {boolean} serverFix - Whether this is a server-generated fix
1009
+ * @private
1010
+ */
1011
+ async _receiveE2EEDiff(data, serverFix = false) {
1012
+ try {
1013
+ const key = this.mod.editor.e2ee.key;
1014
+ const decryptedPayload = await E2EEEncryptor.decryptObject(data["ep"], key);
1015
+ // Merge the decrypted payload with the metadata fields
1016
+ // that were sent in plaintext (cid, rid, footnoterender)
1017
+ const mergedData = {
1018
+ cid: data["cid"],
1019
+ rid: data["rid"],
1020
+ footnoterender: data["footnoterender"],
1021
+ reject_request_id: data["reject_request_id"],
1022
+ ...decryptedPayload
1023
+ };
1024
+ // Process the decrypted diff normally
1025
+ this._processDiff(mergedData, serverFix);
1026
+ }
1027
+ catch (error) {
1028
+ console.error("E2EE: Failed to decrypt incoming diff", error);
1029
+ // If decryption fails, we still need to increment the version
1030
+ // to stay in sync with the server, but we skip applying the
1031
+ // diff content.
1032
+ this.mod.editor.docInfo.version++;
1033
+ }
1034
+ }
1035
+ setConfirmedDoc(tr, stepsLength) {
1036
+ // Find the latest version of the doc without any unconfirmed local changes
1037
+ const rebased = tr.getMeta("rebased"), docNumber = rebased + stepsLength;
1038
+ this.mod.editor.docInfo.confirmedDoc =
1039
+ docNumber === tr.docs.length ? tr.doc : tr.docs[docNumber];
1040
+ }
1041
+ confirmDiff(request_id) {
1042
+ const unconfirmedDiffs = this.unconfirmedDiffs[request_id];
1043
+ if (!unconfirmedDiffs) {
1044
+ return;
1045
+ }
1046
+ this.mod.editor.docInfo.version++;
1047
+ const sentSteps = unconfirmedDiffs["ds"]; // document steps
1048
+ if (sentSteps) {
1049
+ const ourIds = sentSteps.map(_step => this.mod.editor.client_id);
1050
+ const tr = receiveTransaction(this.mod.editor.view.state, sentSteps, ourIds);
1051
+ this.mod.editor.view.dispatch(tr);
1052
+ this.mod.editor.docInfo.confirmedDoc = unconfirmedDiffs["doc"];
1053
+ }
1054
+ const sentFnSteps = unconfirmedDiffs["fs"]; // footnote steps
1055
+ if (sentFnSteps) {
1056
+ const fnTr = receiveTransaction(this.mod.editor.mod.footnotes.fnEditor.view.state, sentFnSteps, sentFnSteps.map(_step => this.mod.editor.client_id));
1057
+ this.mod.editor.mod.footnotes.fnEditor.view.dispatch(fnTr);
1058
+ }
1059
+ const sentComments = unconfirmedDiffs["cu"]; // comment updates
1060
+ if (sentComments) {
1061
+ this.mod.editor.mod.comments.store.eventsSent(sentComments);
1062
+ }
1063
+ const sentBibliographyUpdates = unconfirmedDiffs["bu"]; // bibliography updates
1064
+ if (sentBibliographyUpdates) {
1065
+ this.mod.editor.mod.db.bibDB.eventsSent(sentBibliographyUpdates);
1066
+ }
1067
+ const sentImageUpdates = unconfirmedDiffs["iu"]; // image updates
1068
+ if (sentImageUpdates) {
1069
+ this.mod.editor.mod.db.imageDB.eventsSent(sentImageUpdates);
1070
+ }
1071
+ delete this.unconfirmedDiffs[request_id];
1072
+ this.enableDiffSending();
1073
+ }
1074
+ rejectDiff(request_id) {
1075
+ delete this.unconfirmedDiffs[request_id];
1076
+ this.enableDiffSending();
1077
+ }
1078
+ applyDiffs(diffs, cid) {
1079
+ this.receiving = true;
1080
+ const steps = diffs.map(j => Step.fromJSON(this.mod.editor.schema, j));
1081
+ const clientIds = diffs.map(_ => cid);
1082
+ const tr = receiveTransaction(this.mod.editor.view.state, steps, clientIds);
1083
+ tr.setMeta("remote", true);
1084
+ this.mod.editor.view.dispatch(tr);
1085
+ this.setConfirmedDoc(tr, steps.length);
1086
+ this.receiving = false;
1087
+ this.sendToCollaborators();
1088
+ }
1089
+ }
1090
+ //# sourceMappingURL=doc.js.map