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