@researai/deepscientist 1.5.15 → 1.5.17

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 (202) hide show
  1. package/README.md +385 -104
  2. package/bin/ds.js +1241 -110
  3. package/docs/en/00_QUICK_START.md +100 -19
  4. package/docs/en/01_SETTINGS_REFERENCE.md +34 -1
  5. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  6. package/docs/en/05_TUI_GUIDE.md +6 -0
  7. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  8. package/docs/en/09_DOCTOR.md +25 -8
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  10. package/docs/en/15_CODEX_PROVIDER_SETUP.md +37 -11
  11. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  12. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  13. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  14. package/docs/en/21_LOCAL_MODEL_BACKENDS_GUIDE.md +283 -0
  15. package/docs/en/91_DEVELOPMENT.md +237 -0
  16. package/docs/en/README.md +24 -2
  17. package/docs/zh/00_QUICK_START.md +89 -19
  18. package/docs/zh/01_SETTINGS_REFERENCE.md +34 -1
  19. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  20. package/docs/zh/05_TUI_GUIDE.md +6 -0
  21. package/docs/zh/09_DOCTOR.md +26 -9
  22. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +63 -13
  23. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +37 -11
  24. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  25. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  26. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  27. package/docs/zh/21_LOCAL_MODEL_BACKENDS_GUIDE.md +281 -0
  28. package/docs/zh/README.md +24 -2
  29. package/install.sh +46 -4
  30. package/package.json +2 -1
  31. package/pyproject.toml +1 -1
  32. package/src/deepscientist/__init__.py +1 -1
  33. package/src/deepscientist/acp/envelope.py +6 -0
  34. package/src/deepscientist/artifact/service.py +647 -22
  35. package/src/deepscientist/bash_exec/service.py +234 -9
  36. package/src/deepscientist/bridges/connectors.py +8 -2
  37. package/src/deepscientist/cli.py +115 -19
  38. package/src/deepscientist/codex_cli_compat.py +367 -22
  39. package/src/deepscientist/config/models.py +2 -1
  40. package/src/deepscientist/config/service.py +183 -13
  41. package/src/deepscientist/daemon/api/handlers.py +255 -31
  42. package/src/deepscientist/daemon/api/router.py +9 -0
  43. package/src/deepscientist/daemon/app.py +1146 -105
  44. package/src/deepscientist/diagnostics/__init__.py +6 -0
  45. package/src/deepscientist/diagnostics/runner_failures.py +130 -0
  46. package/src/deepscientist/doctor.py +207 -3
  47. package/src/deepscientist/gitops/__init__.py +10 -1
  48. package/src/deepscientist/gitops/diff.py +129 -0
  49. package/src/deepscientist/gitops/service.py +4 -1
  50. package/src/deepscientist/mcp/server.py +39 -0
  51. package/src/deepscientist/prompts/builder.py +275 -34
  52. package/src/deepscientist/quest/layout.py +15 -2
  53. package/src/deepscientist/quest/service.py +707 -55
  54. package/src/deepscientist/quest/stage_views.py +6 -1
  55. package/src/deepscientist/runners/codex.py +143 -43
  56. package/src/deepscientist/shared.py +19 -0
  57. package/src/deepscientist/skills/__init__.py +2 -2
  58. package/src/deepscientist/skills/installer.py +196 -5
  59. package/src/deepscientist/skills/registry.py +66 -0
  60. package/src/prompts/connectors/qq.md +18 -8
  61. package/src/prompts/connectors/weixin.md +16 -6
  62. package/src/prompts/contracts/shared_interaction.md +14 -2
  63. package/src/prompts/system.md +23 -5
  64. package/src/prompts/system_copilot.md +56 -0
  65. package/src/skills/analysis-campaign/SKILL.md +1 -0
  66. package/src/skills/baseline/SKILL.md +8 -0
  67. package/src/skills/decision/SKILL.md +8 -0
  68. package/src/skills/experiment/SKILL.md +8 -0
  69. package/src/skills/figure-polish/SKILL.md +1 -0
  70. package/src/skills/finalize/SKILL.md +1 -0
  71. package/src/skills/idea/SKILL.md +1 -0
  72. package/src/skills/intake-audit/SKILL.md +8 -0
  73. package/src/skills/mentor/SKILL.md +217 -0
  74. package/src/skills/mentor/references/correction-rules.md +210 -0
  75. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  76. package/src/skills/mentor/references/persona-profile.md +138 -0
  77. package/src/skills/mentor/references/taste-profile.md +128 -0
  78. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  79. package/src/skills/mentor/references/work-profile.md +289 -0
  80. package/src/skills/mentor/references/workflow-profile.md +240 -0
  81. package/src/skills/optimize/SKILL.md +1 -0
  82. package/src/skills/rebuttal/SKILL.md +1 -0
  83. package/src/skills/review/SKILL.md +1 -0
  84. package/src/skills/scout/SKILL.md +8 -0
  85. package/src/skills/write/SKILL.md +1 -0
  86. package/src/tui/dist/app/AppContainer.js +19 -11
  87. package/src/tui/dist/index.js +4 -1
  88. package/src/tui/dist/lib/api.js +33 -3
  89. package/src/tui/package.json +1 -1
  90. package/src/ui/dist/assets/AiManusChatView-Bv-Z8YpU.js +204 -0
  91. package/src/ui/dist/assets/AnalysisPlugin-BCKAfjba.js +1 -0
  92. package/src/ui/dist/assets/CliPlugin-BCKcpc35.js +109 -0
  93. package/src/ui/dist/assets/CodeEditorPlugin-DbOfSJ8K.js +2 -0
  94. package/src/ui/dist/assets/CodeViewerPlugin-CbaFRrUU.js +270 -0
  95. package/src/ui/dist/assets/DocViewerPlugin-DAjLVeQD.js +7 -0
  96. package/src/ui/dist/assets/GitCommitViewerPlugin-CIUqbUDO.js +1 -0
  97. package/src/ui/dist/assets/GitDiffViewerPlugin-CQACjoAA.js +6 -0
  98. package/src/ui/dist/assets/GitSnapshotViewer-0r4nLPke.js +30 -0
  99. package/src/ui/dist/assets/ImageViewerPlugin-nBOmI2v_.js +26 -0
  100. package/src/ui/dist/assets/LabCopilotPanel-BHxOxF4z.js +14 -0
  101. package/src/ui/dist/assets/LabPlugin-BKoZGs95.js +22 -0
  102. package/src/ui/dist/assets/LatexPlugin-ZwtV8pIp.js +25 -0
  103. package/src/ui/dist/assets/MarkdownViewerPlugin-DKqVfKyW.js +128 -0
  104. package/src/ui/dist/assets/MarketplacePlugin-BwxStZ9D.js +13 -0
  105. package/src/ui/dist/assets/NotebookEditor-BEQhaQbt.js +81 -0
  106. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  107. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  108. package/src/ui/dist/assets/NotebookEditor-DB9N_T9q.js +361 -0
  109. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  110. package/src/ui/dist/assets/PdfLoader-eWBONbQP.js +16 -0
  111. package/src/ui/dist/assets/PdfMarkdownPlugin-D22YOZL3.js +1 -0
  112. package/src/ui/dist/assets/PdfViewerPlugin-c-RK9DLM.js +17 -0
  113. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  114. package/src/ui/dist/assets/SearchPlugin-CxF9ytAx.js +16 -0
  115. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  116. package/src/ui/dist/assets/TextViewerPlugin-C5xqeeUH.js +54 -0
  117. package/src/ui/dist/assets/VNCViewer-BoLGLnHz.js +11 -0
  118. package/src/ui/dist/assets/bot-DREQOxzP.js +6 -0
  119. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  120. package/src/ui/dist/assets/chevron-up-C9Qpx4DE.js +6 -0
  121. package/src/ui/dist/assets/code-WlFHE7z_.js +6 -0
  122. package/src/ui/dist/assets/file-content-BZMz3RYp.js +1 -0
  123. package/src/ui/dist/assets/file-diff-panel-CQhw0jS2.js +1 -0
  124. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  125. package/src/ui/dist/assets/file-socket-CfQPKQKj.js +1 -0
  126. package/src/ui/dist/assets/git-commit-horizontal-DxZ8DCZh.js +6 -0
  127. package/src/ui/dist/assets/image-Bgl4VIyx.js +6 -0
  128. package/src/ui/dist/assets/index-BpV6lusQ.css +33 -0
  129. package/src/ui/dist/assets/index-CBNVuWcP.js +2496 -0
  130. package/src/ui/dist/assets/index-CwNu1aH4.js +11 -0
  131. package/src/ui/dist/assets/index-DrUnlf6K.js +1 -0
  132. package/src/ui/dist/assets/index-NW-h8VzN.js +1 -0
  133. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  134. package/src/ui/dist/assets/pdf-effect-queue-J8OnM0jE.js +6 -0
  135. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  136. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  137. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  138. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  139. package/src/ui/dist/assets/popover-CLc0pPP8.js +1 -0
  140. package/src/ui/dist/assets/project-sync-C9IdzdZW.js +1 -0
  141. package/src/ui/dist/assets/select-Cs2PmzwL.js +11 -0
  142. package/src/ui/dist/assets/sigma-ClKcHAXm.js +6 -0
  143. package/src/ui/dist/assets/trash-DwpbFr3w.js +11 -0
  144. package/src/ui/dist/assets/useCliAccess-NQ8m0Let.js +1 -0
  145. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  146. package/src/ui/dist/assets/wrap-text-BC-Hltpd.js +11 -0
  147. package/src/ui/dist/assets/zoom-out-E_gaeAxL.js +11 -0
  148. package/src/ui/dist/index.html +5 -2
  149. package/src/ui/dist/assets/AiManusChatView-DDjbFnbt.js +0 -26597
  150. package/src/ui/dist/assets/AnalysisPlugin-Yb5IdmaU.js +0 -123
  151. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +0 -31037
  152. package/src/ui/dist/assets/CodeEditorPlugin-C4D2TIkU.js +0 -427
  153. package/src/ui/dist/assets/CodeViewerPlugin-BVoNZIvC.js +0 -905
  154. package/src/ui/dist/assets/DocViewerPlugin-CLChbllo.js +0 -278
  155. package/src/ui/dist/assets/GitDiffViewerPlugin-C4xeFyFQ.js +0 -2661
  156. package/src/ui/dist/assets/ImageViewerPlugin-OiMUAcLi.js +0 -500
  157. package/src/ui/dist/assets/LabCopilotPanel-BjD2ThQF.js +0 -4104
  158. package/src/ui/dist/assets/LabPlugin-DQPg-NrB.js +0 -2677
  159. package/src/ui/dist/assets/LatexPlugin-CI05XAV9.js +0 -1792
  160. package/src/ui/dist/assets/MarkdownViewerPlugin-DpeBLYZf.js +0 -308
  161. package/src/ui/dist/assets/MarketplacePlugin-DolE58Q2.js +0 -413
  162. package/src/ui/dist/assets/NotebookEditor-7Qm2rSWD.js +0 -4214
  163. package/src/ui/dist/assets/NotebookEditor-C1kWaxKi.js +0 -84873
  164. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  165. package/src/ui/dist/assets/PdfLoader-BfOHw8Zw.js +0 -25468
  166. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  167. package/src/ui/dist/assets/PdfMarkdownPlugin-BulDREv1.js +0 -409
  168. package/src/ui/dist/assets/PdfViewerPlugin-C-daaOaL.js +0 -3095
  169. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  170. package/src/ui/dist/assets/SearchPlugin-CjpaiJ3A.js +0 -741
  171. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  172. package/src/ui/dist/assets/TextViewerPlugin-BxIyqPQC.js +0 -472
  173. package/src/ui/dist/assets/VNCViewer-HAg9mF7M.js +0 -18821
  174. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  175. package/src/ui/dist/assets/bot-0DYntytV.js +0 -21
  176. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  177. package/src/ui/dist/assets/code-B20Slj_w.js +0 -17
  178. package/src/ui/dist/assets/file-content-DT24KFma.js +0 -377
  179. package/src/ui/dist/assets/file-diff-panel-DK13YPql.js +0 -92
  180. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  181. package/src/ui/dist/assets/file-socket-B4T2o4nR.js +0 -58
  182. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  183. package/src/ui/dist/assets/image-DSeR_sDS.js +0 -18
  184. package/src/ui/dist/assets/index-BrFje2Uk.js +0 -120
  185. package/src/ui/dist/assets/index-BwRJaoTl.js +0 -25
  186. package/src/ui/dist/assets/index-D_E4281X.js +0 -221322
  187. package/src/ui/dist/assets/index-DnYB3xb1.js +0 -159
  188. package/src/ui/dist/assets/index-G7AcWcMu.css +0 -12594
  189. package/src/ui/dist/assets/monaco-LExaAN3Y.js +0 -623
  190. package/src/ui/dist/assets/pdf-effect-queue-BJk5okWJ.js +0 -47
  191. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  192. package/src/ui/dist/assets/popover-D3Gg_FoV.js +0 -476
  193. package/src/ui/dist/assets/project-sync-C_ygLlVU.js +0 -297
  194. package/src/ui/dist/assets/select-CpAK6uWm.js +0 -1690
  195. package/src/ui/dist/assets/sigma-DEccaSgk.js +0 -22
  196. package/src/ui/dist/assets/square-check-big-uUfyVsbD.js +0 -17
  197. package/src/ui/dist/assets/trash-CXvwwSe8.js +0 -32
  198. package/src/ui/dist/assets/useCliAccess-Bnop4mgR.js +0 -957
  199. package/src/ui/dist/assets/useFileDiffOverlay-B8eUAX0I.js +0 -53
  200. package/src/ui/dist/assets/wrap-text-9vbOBpkW.js +0 -35
  201. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  202. package/src/ui/dist/assets/zoom-out-BgVMmOW4.js +0 -34
@@ -1,4214 +0,0 @@
1
- import { w as createLucideIcon, j as jsxRuntimeExports, aO as Button, m as ChevronDown, d as Check, b as cn, r as reactExports, bx as ArrowLeft, c6 as ArrowRight, c7 as Separator, c8 as ArrowUp, c9 as Trash2, u as useI18n, o as useToast, V as getFileContent, L as LoaderCircle, e as Copy, C as CircleAlert, f as useFileTreeStore, a7 as getQuestMarkdownContextFromFileId, ca as rewriteQuestMarkdownForDisplay, cb as rewriteQuestMarkdownForSave, cc as uploadQuestDocumentAsset, U as updateFileContent } from './index-D_E4281X.js';
2
- import { P as PluginKey, a as Plugin, S as Slice, F as Fragment, T as TextSelection, A as AllSelection, N as NodeSelection, E as Extension, m as me, l as le, u as useCurrentEditor, w, b as P, f as findParentNodeClosestToPos, g as getEditorMarkdown, s as setEditorMarkdown, q, c as uploadNotebookAsset, r as resolveNotebookAssetUrl, d as defaultExtensions, J, U, I, e as T, B, O, h as S, K as K$1, G, i as ce, j as isSupportedNotebookAsset } from './NotebookEditor-C1kWaxKi.js';
3
- import { a4 as BIT8, ai as BIT30, ab as min, $ as max, y as unexpectedCase, n as methodUnimplemented, aj as keys, u as every, H as setIfUndefined, D as create } from './function-B5QZkkHC.js';
4
- import { t as toBase64, e as encodeAny, i as isBrowser, d as doc, s as snapshot, D as Doc, a as applyUpdateV2, f as findIndexSS, b as iterateDeletedStructs, c as typeListToArraySnapshot, S as Snapshot, U as UndoManager, g as createSnapshot, h as isDeleted, Y as YXmlElement, j as YXmlText, k as YText, o as oneOf, C as ContentString, l as ContentFormat, m as createDeleteSet, I as Item, n as createRelativePositionFromTypeIndex, R as RelativePosition, p as findRootTypeKey, q as createID, r as createAbsolutePositionFromRelativePosition, u as isParentOf, v as ContentType, w as encodeStateVector, x as applyUpdate, y as mergeUpdates } from './yjs-DncrqiZ8.js';
5
- import { P as ProjectSyncClient } from './project-sync-C_ygLlVU.js';
6
- import { S as SquareCheckBig } from './square-check-big-uUfyVsbD.js';
7
- import { C as Code } from './code-B20Slj_w.js';
8
- import { I as Image } from './image-DSeR_sDS.js';
9
- import { P as Popover, a as PopoverTrigger, b as PopoverContent } from './popover-D3Gg_FoV.js';
10
- import { T as Trash, A as ArrowDown } from './trash-CXvwwSe8.js';
11
- import { S as Sigma } from './sigma-DEccaSgk.js';
12
- import { u as useFileDiffOverlay } from './useFileDiffOverlay-B8eUAX0I.js';
13
- import { F as FileDiffPanel } from './file-diff-panel-DK13YPql.js';
14
-
15
- /**
16
- * @license lucide-react v0.511.0 - ISC
17
- *
18
- * This source code is licensed under the ISC license.
19
- * See the LICENSE file in the root directory of this source tree.
20
- */
21
-
22
-
23
- const __iconNode$f = [
24
- [
25
- "path",
26
- { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8", key: "mg9rjx" }
27
- ]
28
- ];
29
- const Bold = createLucideIcon("bold", __iconNode$f);
30
-
31
- /**
32
- * @license lucide-react v0.511.0 - ISC
33
- *
34
- * This source code is licensed under the ISC license.
35
- * See the LICENSE file in the root directory of this source tree.
36
- */
37
-
38
-
39
- const __iconNode$e = [
40
- ["path", { d: "M4 12h8", key: "17cfdx" }],
41
- ["path", { d: "M4 18V6", key: "1rz3zl" }],
42
- ["path", { d: "M12 18V6", key: "zqpxq5" }],
43
- ["path", { d: "m17 12 3-2v8", key: "1hhhft" }]
44
- ];
45
- const Heading1 = createLucideIcon("heading-1", __iconNode$e);
46
-
47
- /**
48
- * @license lucide-react v0.511.0 - ISC
49
- *
50
- * This source code is licensed under the ISC license.
51
- * See the LICENSE file in the root directory of this source tree.
52
- */
53
-
54
-
55
- const __iconNode$d = [
56
- ["path", { d: "M4 12h8", key: "17cfdx" }],
57
- ["path", { d: "M4 18V6", key: "1rz3zl" }],
58
- ["path", { d: "M12 18V6", key: "zqpxq5" }],
59
- ["path", { d: "M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1", key: "9jr5yi" }]
60
- ];
61
- const Heading2 = createLucideIcon("heading-2", __iconNode$d);
62
-
63
- /**
64
- * @license lucide-react v0.511.0 - ISC
65
- *
66
- * This source code is licensed under the ISC license.
67
- * See the LICENSE file in the root directory of this source tree.
68
- */
69
-
70
-
71
- const __iconNode$c = [
72
- ["path", { d: "M4 12h8", key: "17cfdx" }],
73
- ["path", { d: "M4 18V6", key: "1rz3zl" }],
74
- ["path", { d: "M12 18V6", key: "zqpxq5" }],
75
- ["path", { d: "M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2", key: "68ncm8" }],
76
- ["path", { d: "M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2", key: "1ejuhz" }]
77
- ];
78
- const Heading3 = createLucideIcon("heading-3", __iconNode$c);
79
-
80
- /**
81
- * @license lucide-react v0.511.0 - ISC
82
- *
83
- * This source code is licensed under the ISC license.
84
- * See the LICENSE file in the root directory of this source tree.
85
- */
86
-
87
-
88
- const __iconNode$b = [
89
- ["line", { x1: "19", x2: "10", y1: "4", y2: "4", key: "15jd3p" }],
90
- ["line", { x1: "14", x2: "5", y1: "20", y2: "20", key: "bu0au3" }],
91
- ["line", { x1: "15", x2: "9", y1: "4", y2: "20", key: "uljnxc" }]
92
- ];
93
- const Italic = createLucideIcon("italic", __iconNode$b);
94
-
95
- /**
96
- * @license lucide-react v0.511.0 - ISC
97
- *
98
- * This source code is licensed under the ISC license.
99
- * See the LICENSE file in the root directory of this source tree.
100
- */
101
-
102
-
103
- const __iconNode$a = [
104
- ["path", { d: "M10 12h11", key: "6m4ad9" }],
105
- ["path", { d: "M10 18h11", key: "11hvi2" }],
106
- ["path", { d: "M10 6h11", key: "c7qv1k" }],
107
- ["path", { d: "M4 10h2", key: "16xx2s" }],
108
- ["path", { d: "M4 6h1v4", key: "cnovpq" }],
109
- ["path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1", key: "m9a95d" }]
110
- ];
111
- const ListOrdered = createLucideIcon("list-ordered", __iconNode$a);
112
-
113
- /**
114
- * @license lucide-react v0.511.0 - ISC
115
- *
116
- * This source code is licensed under the ISC license.
117
- * See the LICENSE file in the root directory of this source tree.
118
- */
119
-
120
-
121
- const __iconNode$9 = [
122
- ["path", { d: "M3 12h.01", key: "nlz23k" }],
123
- ["path", { d: "M3 18h.01", key: "1tta3j" }],
124
- ["path", { d: "M3 6h.01", key: "1rqtza" }],
125
- ["path", { d: "M8 12h13", key: "1za7za" }],
126
- ["path", { d: "M8 18h13", key: "1lx6n3" }],
127
- ["path", { d: "M8 6h13", key: "ik3vkj" }]
128
- ];
129
- const List = createLucideIcon("list", __iconNode$9);
130
-
131
- /**
132
- * @license lucide-react v0.511.0 - ISC
133
- *
134
- * This source code is licensed under the ISC license.
135
- * See the LICENSE file in the root directory of this source tree.
136
- */
137
-
138
-
139
- const __iconNode$8 = [
140
- ["path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", key: "1lielz" }],
141
- ["path", { d: "M12 7v6", key: "lw1j43" }],
142
- ["path", { d: "M9 10h6", key: "9gxzsh" }]
143
- ];
144
- const MessageSquarePlus = createLucideIcon("message-square-plus", __iconNode$8);
145
-
146
- /**
147
- * @license lucide-react v0.511.0 - ISC
148
- *
149
- * This source code is licensed under the ISC license.
150
- * See the LICENSE file in the root directory of this source tree.
151
- */
152
-
153
-
154
- const __iconNode$7 = [["path", { d: "M5 12h14", key: "1ays0h" }]];
155
- const Minus = createLucideIcon("minus", __iconNode$7);
156
-
157
- /**
158
- * @license lucide-react v0.511.0 - ISC
159
- *
160
- * This source code is licensed under the ISC license.
161
- * See the LICENSE file in the root directory of this source tree.
162
- */
163
-
164
-
165
- const __iconNode$6 = [
166
- ["path", { d: "M16 4H9a3 3 0 0 0-2.83 4", key: "43sutm" }],
167
- ["path", { d: "M14 12a4 4 0 0 1 0 8H6", key: "nlfj13" }],
168
- ["line", { x1: "4", x2: "20", y1: "12", y2: "12", key: "1e0a9i" }]
169
- ];
170
- const Strikethrough = createLucideIcon("strikethrough", __iconNode$6);
171
-
172
- /**
173
- * @license lucide-react v0.511.0 - ISC
174
- *
175
- * This source code is licensed under the ISC license.
176
- * See the LICENSE file in the root directory of this source tree.
177
- */
178
-
179
-
180
- const __iconNode$5 = [
181
- [
182
- "path",
183
- {
184
- d: "M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18",
185
- key: "gugj83"
186
- }
187
- ]
188
- ];
189
- const Table2 = createLucideIcon("table-2", __iconNode$5);
190
-
191
- /**
192
- * @license lucide-react v0.511.0 - ISC
193
- *
194
- * This source code is licensed under the ISC license.
195
- * See the LICENSE file in the root directory of this source tree.
196
- */
197
-
198
-
199
- const __iconNode$4 = [
200
- ["path", { d: "M17 6H3", key: "16j9eg" }],
201
- ["path", { d: "M21 12H8", key: "scolzb" }],
202
- ["path", { d: "M21 18H8", key: "1wfozv" }],
203
- ["path", { d: "M3 12v6", key: "fv4c87" }]
204
- ];
205
- const TextQuote = createLucideIcon("text-quote", __iconNode$4);
206
-
207
- /**
208
- * @license lucide-react v0.511.0 - ISC
209
- *
210
- * This source code is licensed under the ISC license.
211
- * See the LICENSE file in the root directory of this source tree.
212
- */
213
-
214
-
215
- const __iconNode$3 = [
216
- ["path", { d: "M15 18H3", key: "olowqp" }],
217
- ["path", { d: "M17 6H3", key: "16j9eg" }],
218
- ["path", { d: "M21 12H3", key: "2avoz0" }]
219
- ];
220
- const Text = createLucideIcon("text", __iconNode$3);
221
-
222
- /**
223
- * @license lucide-react v0.511.0 - ISC
224
- *
225
- * This source code is licensed under the ISC license.
226
- * See the LICENSE file in the root directory of this source tree.
227
- */
228
-
229
-
230
- const __iconNode$2 = [
231
- [
232
- "path",
233
- {
234
- d: "M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z",
235
- key: "pff0z6"
236
- }
237
- ]
238
- ];
239
- const Twitter = createLucideIcon("twitter", __iconNode$2);
240
-
241
- /**
242
- * @license lucide-react v0.511.0 - ISC
243
- *
244
- * This source code is licensed under the ISC license.
245
- * See the LICENSE file in the root directory of this source tree.
246
- */
247
-
248
-
249
- const __iconNode$1 = [
250
- ["path", { d: "M6 4v6a6 6 0 0 0 12 0V4", key: "9kb039" }],
251
- ["line", { x1: "4", x2: "20", y1: "20", y2: "20", key: "nun2al" }]
252
- ];
253
- const Underline = createLucideIcon("underline", __iconNode$1);
254
-
255
- /**
256
- * @license lucide-react v0.511.0 - ISC
257
- *
258
- * This source code is licensed under the ISC license.
259
- * See the LICENSE file in the root directory of this source tree.
260
- */
261
-
262
-
263
- const __iconNode = [
264
- [
265
- "path",
266
- {
267
- d: "M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17",
268
- key: "1q2vi4"
269
- }
270
- ],
271
- ["path", { d: "m10 15 5-3-5-3z", key: "1jp15x" }]
272
- ];
273
- const Youtube = createLucideIcon("youtube", __iconNode);
274
-
275
- /* global requestIdleCallback, requestAnimationFrame, cancelIdleCallback, cancelAnimationFrame */
276
-
277
-
278
- /**
279
- * @typedef {Object} TimeoutObject
280
- * @property {function} TimeoutObject.destroy
281
- */
282
-
283
- /**
284
- * @param {function(number):void} clearFunction
285
- */
286
- const createTimeoutClass = clearFunction => class TT {
287
- /**
288
- * @param {number} timeoutId
289
- */
290
- constructor (timeoutId) {
291
- this._ = timeoutId;
292
- }
293
-
294
- destroy () {
295
- clearFunction(this._);
296
- }
297
- };
298
-
299
- const Timeout = createTimeoutClass(clearTimeout);
300
-
301
- /**
302
- * @param {number} timeout
303
- * @param {function} callback
304
- * @return {TimeoutObject}
305
- */
306
- const timeout = (timeout, callback) => new Timeout(setTimeout(callback, timeout));
307
-
308
- /**
309
- * Mutual exclude for JavaScript.
310
- *
311
- * @module mutex
312
- */
313
-
314
- /**
315
- * @callback mutex
316
- * @param {function():void} cb Only executed when this mutex is not in the current stack
317
- * @param {function():void} [elseCb] Executed when this mutex is in the current stack
318
- */
319
-
320
- /**
321
- * Creates a mutual exclude function with the following property:
322
- *
323
- * ```js
324
- * const mutex = createMutex()
325
- * mutex(() => {
326
- * // This function is immediately executed
327
- * mutex(() => {
328
- * // This function is not executed, as the mutex is already active.
329
- * })
330
- * })
331
- * ```
332
- *
333
- * @return {mutex} A mutual exclude function
334
- * @public
335
- */
336
- const createMutex = () => {
337
- let token = true;
338
- return (f, g) => {
339
- if (token) {
340
- token = false;
341
- try {
342
- f();
343
- } finally {
344
- token = true;
345
- }
346
- } else if (g !== undefined) {
347
- g();
348
- }
349
- }
350
- };
351
-
352
- /**
353
- * Efficient diffs.
354
- *
355
- * @module diff
356
- */
357
-
358
-
359
- /**
360
- * A SimpleDiff describes a change on a String.
361
- *
362
- * ```js
363
- * console.log(a) // the old value
364
- * console.log(b) // the updated value
365
- * // Apply changes of diff (pseudocode)
366
- * a.remove(diff.index, diff.remove) // Remove `diff.remove` characters
367
- * a.insert(diff.index, diff.insert) // Insert `diff.insert`
368
- * a === b // values match
369
- * ```
370
- *
371
- * @template {string|Array<any>} T
372
- * @typedef {Object} SimpleDiff
373
- * @property {Number} index The index where changes were applied
374
- * @property {Number} remove The number of characters to delete starting
375
- * at `index`.
376
- * @property {T} insert The new text to insert at `index` after applying
377
- */
378
-
379
- const highSurrogateRegex = /[\uD800-\uDBFF]/;
380
- const lowSurrogateRegex = /[\uDC00-\uDFFF]/;
381
-
382
- /**
383
- * Create a diff between two strings. This diff implementation is highly
384
- * efficient, but not very sophisticated.
385
- *
386
- * @function
387
- *
388
- * @param {string} a The old version of the string
389
- * @param {string} b The updated version of the string
390
- * @return {SimpleDiff<string>} The diff description.
391
- */
392
- const simpleDiffString = (a, b) => {
393
- let left = 0; // number of same characters counting from left
394
- let right = 0; // number of same characters counting from right
395
- while (left < a.length && left < b.length && a[left] === b[left]) {
396
- left++;
397
- }
398
- // If the last same character is a high surrogate, we need to rollback to the previous character
399
- if (left > 0 && highSurrogateRegex.test(a[left - 1])) left--;
400
- while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) {
401
- right++;
402
- }
403
- // If the last same character is a low surrogate, we need to rollback to the previous character
404
- if (right > 0 && lowSurrogateRegex.test(a[a.length - right])) right--;
405
- return {
406
- index: left,
407
- remove: a.length - left - right,
408
- insert: b.slice(left, b.length - right)
409
- }
410
- };
411
-
412
- /**
413
- * @todo Remove in favor of simpleDiffString
414
- * @deprecated
415
- */
416
- const simpleDiff = simpleDiffString;
417
-
418
- /**
419
- * The unique prosemirror plugin key for syncPlugin
420
- *
421
- * @public
422
- */
423
- const ySyncPluginKey = new PluginKey('y-sync');
424
-
425
- /**
426
- * The unique prosemirror plugin key for undoPlugin
427
- *
428
- * @public
429
- * @type {PluginKey<import('./undo-plugin').UndoPluginState>}
430
- */
431
- const yUndoPluginKey = new PluginKey('y-undo');
432
-
433
- /**
434
- * The unique prosemirror plugin key for cursorPlugin
435
- *
436
- * @public
437
- */
438
- new PluginKey('yjs-cursor');
439
-
440
- /**
441
- * @module sha256
442
- * Spec: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf
443
- * Resources:
444
- * - https://web.archive.org/web/20150315061807/http://csrc.nist.gov/groups/STM/cavp/documents/shs/sha256-384-512.pdf
445
- */
446
-
447
-
448
- /**
449
- * @param {number} w - a 32bit uint
450
- * @param {number} shift
451
- */
452
- const rotr = (w, shift) => (w >>> shift) | (w << (32 - shift));
453
-
454
- /**
455
- * Helper for SHA-224 & SHA-256. See 4.1.2.
456
- * @param {number} x
457
- */
458
- const sum0to256 = x => rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22);
459
-
460
- /**
461
- * Helper for SHA-224 & SHA-256. See 4.1.2.
462
- * @param {number} x
463
- */
464
- const sum1to256 = x => rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25);
465
-
466
- /**
467
- * Helper for SHA-224 & SHA-256. See 4.1.2.
468
- * @param {number} x
469
- */
470
- const sigma0to256 = x => rotr(x, 7) ^ rotr(x, 18) ^ x >>> 3;
471
-
472
- /**
473
- * Helper for SHA-224 & SHA-256. See 4.1.2.
474
- * @param {number} x
475
- */
476
- const sigma1to256 = x => rotr(x, 17) ^ rotr(x, 19) ^ x >>> 10;
477
-
478
- // @todo don't init these variables globally
479
-
480
- /**
481
- * See 4.2.2: Constant for sha256 & sha224
482
- * These words represent the first thirty-two bits of the fractional parts of
483
- * the cube roots of the first sixty-four prime numbers. In hex, these constant words are (from left to
484
- * right)
485
- */
486
- const K = new Uint32Array([
487
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
488
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
489
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
490
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
491
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
492
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
493
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
494
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
495
- ]);
496
-
497
- /**
498
- * See 5.3.3. Initial hash value.
499
- *
500
- * These words were obtained by taking the first thirty-two bits of the fractional parts of the
501
- * square roots of the first eight prime numbers.
502
- *
503
- * @todo shouldn't be a global variable
504
- */
505
- const HINIT = new Uint32Array([
506
- 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
507
- ]);
508
-
509
- // time to beat: (large value < 4.35s)
510
-
511
- class Hasher {
512
- constructor () {
513
- const buf = new ArrayBuffer(64 + 64 * 4);
514
- // Init working variables using a single arraybuffer
515
- this._H = new Uint32Array(buf, 0, 8);
516
- this._H.set(HINIT);
517
- // "Message schedule" - a working variable
518
- this._W = new Uint32Array(buf, 64, 64);
519
- }
520
-
521
- _updateHash () {
522
- const H = this._H;
523
- const W = this._W;
524
- for (let t = 16; t < 64; t++) {
525
- W[t] = sigma1to256(W[t - 2]) + W[t - 7] + sigma0to256(W[t - 15]) + W[t - 16];
526
- }
527
- let a = H[0];
528
- let b = H[1];
529
- let c = H[2];
530
- let d = H[3];
531
- let e = H[4];
532
- let f = H[5];
533
- let g = H[6];
534
- let h = H[7];
535
- for (let tt = 0, T1, T2; tt < 64; tt++) {
536
- T1 = (h + sum1to256(e) + ((e & f) ^ (~e & g)) + K[tt] + W[tt]) >>> 0;
537
- T2 = (sum0to256(a) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
538
- h = g;
539
- g = f;
540
- f = e;
541
- e = (d + T1) >>> 0;
542
- d = c;
543
- c = b;
544
- b = a;
545
- a = (T1 + T2) >>> 0;
546
- }
547
- H[0] += a;
548
- H[1] += b;
549
- H[2] += c;
550
- H[3] += d;
551
- H[4] += e;
552
- H[5] += f;
553
- H[6] += g;
554
- H[7] += h;
555
- }
556
-
557
- /**
558
- * Returns a 32-byte hash.
559
- *
560
- * @param {Uint8Array} data
561
- */
562
- digest (data) {
563
- let i = 0;
564
- for (; i + 56 <= data.length;) {
565
- // write data in big endianess
566
- let j = 0;
567
- for (; j < 16 && i + 3 < data.length; j++) {
568
- this._W[j] = data[i++] << 24 | data[i++] << 16 | data[i++] << 8 | data[i++];
569
- }
570
- if (i % 64 !== 0) { // there is still room to write partial content and the ending bit.
571
- this._W.fill(0, j, 16);
572
- while (i < data.length) {
573
- this._W[j] |= data[i] << ((3 - (i % 4)) * 8);
574
- i++;
575
- }
576
- this._W[j] |= BIT8 << ((3 - (i % 4)) * 8);
577
- }
578
- this._updateHash();
579
- }
580
- // same check as earlier - the ending bit has been written
581
- const isPaddedWith1 = i % 64 !== 0;
582
- this._W.fill(0, 0, 16);
583
- let j = 0;
584
- for (; i < data.length; j++) {
585
- for (let ci = 3; ci >= 0 && i < data.length; ci--) {
586
- this._W[j] |= data[i++] << (ci * 8);
587
- }
588
- }
589
- // Write padding of the message. See 5.1.2.
590
- if (!isPaddedWith1) {
591
- this._W[j - (i % 4 === 0 ? 0 : 1)] |= BIT8 << ((3 - (i % 4)) * 8);
592
- }
593
- // write length of message (size in bits) as 64 bit uint
594
- // @todo test that this works correctly
595
- this._W[14] = data.byteLength / BIT30; // same as data.byteLength >>> 30 - but works on floats
596
- this._W[15] = data.byteLength * 8;
597
- this._updateHash();
598
- // correct H endianness to use big endiannes and return a Uint8Array
599
- const dv = new Uint8Array(32);
600
- for (let i = 0; i < this._H.length; i++) {
601
- for (let ci = 0; ci < 4; ci++) {
602
- dv[i * 4 + ci] = this._H[i] >>> (3 - ci) * 8;
603
- }
604
- }
605
- return dv
606
- }
607
- }
608
-
609
- /**
610
- * Returns a 32-byte hash.
611
- *
612
- * @param {Uint8Array} data
613
- */
614
- const digest = data => new Hasher().digest(data);
615
-
616
- /**
617
- * Custom function to transform sha256 hash to N byte
618
- *
619
- * @param {Uint8Array} digest
620
- */
621
- const _convolute = digest => {
622
- const N = 6;
623
- for (let i = N; i < digest.length; i++) {
624
- digest[i % N] = digest[i % N] ^ digest[i];
625
- }
626
- return digest.slice(0, N)
627
- };
628
-
629
- /**
630
- * @param {any} json
631
- */
632
- const hashOfJSON = (json) => toBase64(_convolute(digest(encodeAny(json))));
633
-
634
- /**
635
- * @module bindings/prosemirror
636
- */
637
-
638
-
639
- /**
640
- * @param {Y.Item} item
641
- * @param {Y.Snapshot} [snapshot]
642
- */
643
- const isVisible = (item, snapshot) =>
644
- snapshot === undefined
645
- ? !item.deleted
646
- : (snapshot.sv.has(item.id.client) && /** @type {number} */
647
- (snapshot.sv.get(item.id.client)) > item.id.clock &&
648
- !isDeleted(snapshot.ds, item.id));
649
-
650
- /**
651
- * Either a node if type is YXmlElement or an Array of text nodes if YXmlText
652
- * @typedef {Map<Y.AbstractType<any>, PModel.Node | Array<PModel.Node>>} ProsemirrorMapping
653
- */
654
-
655
- /**
656
- * @typedef {Object} ColorDef
657
- * @property {string} ColorDef.light
658
- * @property {string} ColorDef.dark
659
- */
660
-
661
- /**
662
- * @typedef {Object} YSyncOpts
663
- * @property {Array<ColorDef>} [YSyncOpts.colors]
664
- * @property {Map<string,ColorDef>} [YSyncOpts.colorMapping]
665
- * @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData]
666
- * @property {ProsemirrorMapping} [YSyncOpts.mapping]
667
- * @property {function} [YSyncOpts.onFirstRender] Fired when the content from Yjs is initially rendered to ProseMirror
668
- */
669
-
670
- /**
671
- * @type {Array<ColorDef>}
672
- */
673
- const defaultColors = [{ light: '#ecd44433', dark: '#ecd444' }];
674
-
675
- /**
676
- * @param {Map<string,ColorDef>} colorMapping
677
- * @param {Array<ColorDef>} colors
678
- * @param {string} user
679
- * @return {ColorDef}
680
- */
681
- const getUserColor = (colorMapping, colors, user) => {
682
- // @todo do not hit the same color twice if possible
683
- if (!colorMapping.has(user)) {
684
- if (colorMapping.size < colors.length) {
685
- const usedColors = create();
686
- colorMapping.forEach((color) => usedColors.add(color));
687
- colors = colors.filter((color) => !usedColors.has(color));
688
- }
689
- colorMapping.set(user, oneOf(colors));
690
- }
691
- return /** @type {ColorDef} */ (colorMapping.get(user))
692
- };
693
-
694
- /**
695
- * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
696
- *
697
- * This plugin also keeps references to the type and the shared document so other plugins can access it.
698
- * @param {Y.XmlFragment} yXmlFragment
699
- * @param {YSyncOpts} opts
700
- * @return {any} Returns a prosemirror plugin that binds to this type
701
- */
702
- const ySyncPlugin = (yXmlFragment, {
703
- colors = defaultColors,
704
- colorMapping = new Map(),
705
- permanentUserData = null,
706
- onFirstRender = () => {},
707
- mapping
708
- } = {}) => {
709
- let initialContentChanged = false;
710
- const binding = new ProsemirrorBinding(yXmlFragment, mapping);
711
- const plugin = new Plugin({
712
- props: {
713
- editable: (state) => {
714
- const syncState = ySyncPluginKey.getState(state);
715
- return syncState.snapshot == null && syncState.prevSnapshot == null
716
- }
717
- },
718
- key: ySyncPluginKey,
719
- state: {
720
- /**
721
- * @returns {any}
722
- */
723
- init: (_initargs, _state) => {
724
- return {
725
- type: yXmlFragment,
726
- doc: yXmlFragment.doc,
727
- binding,
728
- snapshot: null,
729
- prevSnapshot: null,
730
- isChangeOrigin: false,
731
- isUndoRedoOperation: false,
732
- addToHistory: true,
733
- colors,
734
- colorMapping,
735
- permanentUserData
736
- }
737
- },
738
- apply: (tr, pluginState) => {
739
- const change = tr.getMeta(ySyncPluginKey);
740
- if (change !== undefined) {
741
- pluginState = Object.assign({}, pluginState);
742
- for (const key in change) {
743
- pluginState[key] = change[key];
744
- }
745
- }
746
- pluginState.addToHistory = tr.getMeta('addToHistory') !== false;
747
- // always set isChangeOrigin. If undefined, this is not change origin.
748
- pluginState.isChangeOrigin = change !== undefined &&
749
- !!change.isChangeOrigin;
750
- pluginState.isUndoRedoOperation = change !== undefined && !!change.isChangeOrigin && !!change.isUndoRedoOperation;
751
- if (binding.prosemirrorView !== null) {
752
- if (
753
- change !== undefined &&
754
- (change.snapshot != null || change.prevSnapshot != null)
755
- ) {
756
- // snapshot changed, rerender next
757
- timeout(0, () => {
758
- if (binding.prosemirrorView == null) {
759
- return
760
- }
761
- if (change.restore == null) {
762
- binding._renderSnapshot(
763
- change.snapshot,
764
- change.prevSnapshot,
765
- pluginState
766
- );
767
- } else {
768
- binding._renderSnapshot(
769
- change.snapshot,
770
- change.snapshot,
771
- pluginState
772
- );
773
- // reset to current prosemirror state
774
- delete pluginState.restore;
775
- delete pluginState.snapshot;
776
- delete pluginState.prevSnapshot;
777
- binding.mux(() => {
778
- binding._prosemirrorChanged(
779
- binding.prosemirrorView.state.doc
780
- );
781
- });
782
- }
783
- });
784
- }
785
- }
786
- return pluginState
787
- }
788
- },
789
- view: (view) => {
790
- binding.initView(view);
791
- if (mapping == null) {
792
- // force rerender to update the bindings mapping
793
- binding._forceRerender();
794
- }
795
- onFirstRender();
796
- return {
797
- update: () => {
798
- const pluginState = plugin.getState(view.state);
799
- if (
800
- pluginState.snapshot == null && pluginState.prevSnapshot == null
801
- ) {
802
- if (
803
- // If the content doesn't change initially, we don't render anything to Yjs
804
- // If the content was cleared by a user action, we want to catch the change and
805
- // represent it in Yjs
806
- initialContentChanged ||
807
- view.state.doc.content.findDiffStart(
808
- view.state.doc.type.createAndFill().content
809
- ) !== null
810
- ) {
811
- initialContentChanged = true;
812
- if (
813
- pluginState.addToHistory === false &&
814
- !pluginState.isChangeOrigin
815
- ) {
816
- const yUndoPluginState = yUndoPluginKey.getState(view.state);
817
- /**
818
- * @type {Y.UndoManager}
819
- */
820
- const um = yUndoPluginState && yUndoPluginState.undoManager;
821
- if (um) {
822
- um.stopCapturing();
823
- }
824
- }
825
- binding.mux(() => {
826
- /** @type {Y.Doc} */ (pluginState.doc).transact((tr) => {
827
- tr.meta.set('addToHistory', pluginState.addToHistory);
828
- binding._prosemirrorChanged(view.state.doc);
829
- }, ySyncPluginKey);
830
- });
831
- }
832
- }
833
- },
834
- destroy: () => {
835
- binding.destroy();
836
- }
837
- }
838
- }
839
- });
840
- return plugin
841
- };
842
-
843
- /**
844
- * @param {import('prosemirror-state').Transaction} tr
845
- * @param {ReturnType<typeof getRelativeSelection>} relSel
846
- * @param {ProsemirrorBinding} binding
847
- */
848
- const restoreRelativeSelection = (tr, relSel, binding) => {
849
- if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
850
- if (relSel.type === 'all') {
851
- tr.setSelection(new AllSelection(tr.doc));
852
- } else if (relSel.type === 'node') {
853
- const anchor = relativePositionToAbsolutePosition(
854
- binding.doc,
855
- binding.type,
856
- relSel.anchor,
857
- binding.mapping
858
- );
859
- tr.setSelection(NodeSelection.create(tr.doc, anchor));
860
- } else {
861
- const anchor = relativePositionToAbsolutePosition(
862
- binding.doc,
863
- binding.type,
864
- relSel.anchor,
865
- binding.mapping
866
- );
867
- const head = relativePositionToAbsolutePosition(
868
- binding.doc,
869
- binding.type,
870
- relSel.head,
871
- binding.mapping
872
- );
873
- if (anchor !== null && head !== null) {
874
- const sel = TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head));
875
- tr.setSelection(sel);
876
- }
877
- }
878
- }
879
- };
880
-
881
- /**
882
- * @param {ProsemirrorBinding} pmbinding
883
- * @param {import('prosemirror-state').EditorState} state
884
- */
885
- const getRelativeSelection = (pmbinding, state) => ({
886
- type: /** @type {any} */ (state.selection).jsonID,
887
- anchor: absolutePositionToRelativePosition(
888
- state.selection.anchor,
889
- pmbinding.type,
890
- pmbinding.mapping
891
- ),
892
- head: absolutePositionToRelativePosition(
893
- state.selection.head,
894
- pmbinding.type,
895
- pmbinding.mapping
896
- )
897
- });
898
-
899
- /**
900
- * Binding for prosemirror.
901
- *
902
- * @protected
903
- */
904
- class ProsemirrorBinding {
905
- /**
906
- * @param {Y.XmlFragment} yXmlFragment The bind source
907
- * @param {ProsemirrorMapping} mapping
908
- */
909
- constructor (yXmlFragment, mapping = new Map()) {
910
- this.type = yXmlFragment;
911
- /**
912
- * this will be set once the view is created
913
- * @type {any}
914
- */
915
- this.prosemirrorView = null;
916
- this.mux = createMutex();
917
- this.mapping = mapping;
918
- /**
919
- * Is overlapping mark - i.e. mark does not exclude itself.
920
- *
921
- * @type {Map<import('prosemirror-model').MarkType, boolean>}
922
- */
923
- this.isOMark = new Map();
924
- this._observeFunction = this._typeChanged.bind(this);
925
- /**
926
- * @type {Y.Doc}
927
- */
928
- // @ts-ignore
929
- this.doc = yXmlFragment.doc;
930
- /**
931
- * current selection as relative positions in the Yjs model
932
- */
933
- this.beforeTransactionSelection = null;
934
- this.beforeAllTransactions = () => {
935
- if (this.beforeTransactionSelection === null && this.prosemirrorView != null) {
936
- this.beforeTransactionSelection = getRelativeSelection(
937
- this,
938
- this.prosemirrorView.state
939
- );
940
- }
941
- };
942
- this.afterAllTransactions = () => {
943
- this.beforeTransactionSelection = null;
944
- };
945
- this._domSelectionInView = null;
946
- }
947
-
948
- /**
949
- * Create a transaction for changing the prosemirror state.
950
- *
951
- * @returns
952
- */
953
- get _tr () {
954
- return this.prosemirrorView.state.tr.setMeta('addToHistory', false)
955
- }
956
-
957
- _isLocalCursorInView () {
958
- if (!this.prosemirrorView.hasFocus()) return false
959
- if (isBrowser && this._domSelectionInView === null) {
960
- // Calculate the domSelectionInView and clear by next tick after all events are finished
961
- timeout(0, () => {
962
- this._domSelectionInView = null;
963
- });
964
- this._domSelectionInView = this._isDomSelectionInView();
965
- }
966
- return this._domSelectionInView
967
- }
968
-
969
- _isDomSelectionInView () {
970
- const selection = this.prosemirrorView._root.getSelection();
971
-
972
- if (selection == null || selection.anchorNode == null) return false
973
-
974
- const range = this.prosemirrorView._root.createRange();
975
- range.setStart(selection.anchorNode, selection.anchorOffset);
976
- range.setEnd(selection.focusNode, selection.focusOffset);
977
-
978
- // This is a workaround for an edgecase where getBoundingClientRect will
979
- // return zero values if the selection is collapsed at the start of a newline
980
- // see reference here: https://stackoverflow.com/a/59780954
981
- const rects = range.getClientRects();
982
- if (rects.length === 0) {
983
- // probably buggy newline behavior, explicitly select the node contents
984
- if (range.startContainer && range.collapsed) {
985
- range.selectNodeContents(range.startContainer);
986
- }
987
- }
988
-
989
- const bounding = range.getBoundingClientRect();
990
- const documentElement = doc.documentElement;
991
-
992
- return bounding.bottom >= 0 && bounding.right >= 0 &&
993
- bounding.left <=
994
- (window.innerWidth || documentElement.clientWidth || 0) &&
995
- bounding.top <= (window.innerHeight || documentElement.clientHeight || 0)
996
- }
997
-
998
- /**
999
- * @param {Y.Snapshot} snapshot
1000
- * @param {Y.Snapshot} prevSnapshot
1001
- */
1002
- renderSnapshot (snapshot, prevSnapshot) {
1003
- if (!prevSnapshot) {
1004
- prevSnapshot = createSnapshot(createDeleteSet(), new Map());
1005
- }
1006
- this.prosemirrorView.dispatch(
1007
- this._tr.setMeta(ySyncPluginKey, { snapshot, prevSnapshot })
1008
- );
1009
- }
1010
-
1011
- unrenderSnapshot () {
1012
- this.mapping.clear();
1013
- this.mux(() => {
1014
- const fragmentContent = this.type.toArray().map((t) =>
1015
- createNodeFromYElement(
1016
- /** @type {Y.XmlElement} */ (t),
1017
- this.prosemirrorView.state.schema,
1018
- this
1019
- )
1020
- ).filter((n) => n !== null);
1021
- // @ts-ignore
1022
- const tr = this._tr.replace(
1023
- 0,
1024
- this.prosemirrorView.state.doc.content.size,
1025
- new Slice(Fragment.from(fragmentContent), 0, 0)
1026
- );
1027
- tr.setMeta(ySyncPluginKey, { snapshot: null, prevSnapshot: null });
1028
- this.prosemirrorView.dispatch(tr);
1029
- });
1030
- }
1031
-
1032
- _forceRerender () {
1033
- this.mapping.clear();
1034
- this.mux(() => {
1035
- // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs
1036
- // transaction. Then the "before selection" doesn't exist. In this case, we need to create a
1037
- // relative position before replacing content. Fixes #126
1038
- const sel = this.beforeTransactionSelection !== null ? null : this.prosemirrorView.state.selection;
1039
- const fragmentContent = this.type.toArray().map((t) =>
1040
- createNodeFromYElement(
1041
- /** @type {Y.XmlElement} */ (t),
1042
- this.prosemirrorView.state.schema,
1043
- this
1044
- )
1045
- ).filter((n) => n !== null);
1046
- // @ts-ignore
1047
- const tr = this._tr.replace(
1048
- 0,
1049
- this.prosemirrorView.state.doc.content.size,
1050
- new Slice(Fragment.from(fragmentContent), 0, 0)
1051
- );
1052
- if (sel) {
1053
- /**
1054
- * If the Prosemirror document we just created from this.type is
1055
- * smaller than the previous document, the selection might be
1056
- * out of bound, which would make Prosemirror throw an error.
1057
- */
1058
- const clampedAnchor = min(max(sel.anchor, 0), tr.doc.content.size);
1059
- const clampedHead = min(max(sel.head, 0), tr.doc.content.size);
1060
-
1061
- tr.setSelection(TextSelection.create(tr.doc, clampedAnchor, clampedHead));
1062
- }
1063
- this.prosemirrorView.dispatch(
1064
- tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })
1065
- );
1066
- });
1067
- }
1068
-
1069
- /**
1070
- * @param {Y.Snapshot|Uint8Array} snapshot
1071
- * @param {Y.Snapshot|Uint8Array} prevSnapshot
1072
- * @param {Object} pluginState
1073
- */
1074
- _renderSnapshot (snapshot$1, prevSnapshot, pluginState) {
1075
- /**
1076
- * The document that contains the full history of this document.
1077
- * @type {Y.Doc}
1078
- */
1079
- let historyDoc = this.doc;
1080
- let historyType = this.type;
1081
- if (!snapshot$1) {
1082
- snapshot$1 = snapshot(this.doc);
1083
- }
1084
- if (snapshot$1 instanceof Uint8Array || prevSnapshot instanceof Uint8Array) {
1085
- if (!(snapshot$1 instanceof Uint8Array) || !(prevSnapshot instanceof Uint8Array)) {
1086
- // expected both snapshots to be v2 updates
1087
- unexpectedCase();
1088
- }
1089
- historyDoc = new Doc({ gc: false });
1090
- applyUpdateV2(historyDoc, prevSnapshot);
1091
- prevSnapshot = snapshot(historyDoc);
1092
- applyUpdateV2(historyDoc, snapshot$1);
1093
- snapshot$1 = snapshot(historyDoc);
1094
- if (historyType._item === null) {
1095
- /**
1096
- * If is a root type, we need to find the root key in the initial document
1097
- * and use it to get the history type.
1098
- */
1099
- const rootKey = Array.from(this.doc.share.keys()).find(
1100
- (key) => this.doc.share.get(key) === this.type
1101
- );
1102
- historyType = historyDoc.getXmlFragment(rootKey);
1103
- } else {
1104
- /**
1105
- * If it is a sub type, we use the item id to find the history type.
1106
- */
1107
- const historyStructs =
1108
- historyDoc.store.clients.get(historyType._item.id.client) ?? [];
1109
- const itemIndex = findIndexSS(
1110
- historyStructs,
1111
- historyType._item.id.clock
1112
- );
1113
- const item = /** @type {Y.Item} */ (historyStructs[itemIndex]);
1114
- const content = /** @type {Y.ContentType} */ (item.content);
1115
- historyType = /** @type {Y.XmlFragment} */ (content.type);
1116
- }
1117
- }
1118
- // clear mapping because we are going to rerender
1119
- this.mapping.clear();
1120
- this.mux(() => {
1121
- historyDoc.transact((transaction) => {
1122
- // before rendering, we are going to sanitize ops and split deleted ops
1123
- // if they were deleted by seperate users.
1124
- /**
1125
- * @type {Y.PermanentUserData}
1126
- */
1127
- const pud = pluginState.permanentUserData;
1128
- if (pud) {
1129
- pud.dss.forEach((ds) => {
1130
- iterateDeletedStructs(transaction, ds, (_item) => {});
1131
- });
1132
- }
1133
- /**
1134
- * @param {'removed'|'added'} type
1135
- * @param {Y.ID} id
1136
- */
1137
- const computeYChange = (type, id) => {
1138
- const user = type === 'added'
1139
- ? pud.getUserByClientId(id.client)
1140
- : pud.getUserByDeletedId(id);
1141
- return {
1142
- user,
1143
- type,
1144
- color: getUserColor(
1145
- pluginState.colorMapping,
1146
- pluginState.colors,
1147
- user
1148
- )
1149
- }
1150
- };
1151
- // Create document fragment and render
1152
- const fragmentContent = typeListToArraySnapshot(
1153
- historyType,
1154
- new Snapshot(prevSnapshot.ds, snapshot$1.sv)
1155
- ).map((t) => {
1156
- if (
1157
- !t._item.deleted || isVisible(t._item, snapshot$1) ||
1158
- isVisible(t._item, prevSnapshot)
1159
- ) {
1160
- return createNodeFromYElement(
1161
- t,
1162
- this.prosemirrorView.state.schema,
1163
- { mapping: new Map(), isOMark: new Map() },
1164
- snapshot$1,
1165
- prevSnapshot,
1166
- computeYChange
1167
- )
1168
- } else {
1169
- // No need to render elements that are not visible by either snapshot.
1170
- // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot.
1171
- return null
1172
- }
1173
- }).filter((n) => n !== null);
1174
- // @ts-ignore
1175
- const tr = this._tr.replace(
1176
- 0,
1177
- this.prosemirrorView.state.doc.content.size,
1178
- new Slice(Fragment.from(fragmentContent), 0, 0)
1179
- );
1180
- this.prosemirrorView.dispatch(
1181
- tr.setMeta(ySyncPluginKey, { isChangeOrigin: true })
1182
- );
1183
- }, ySyncPluginKey);
1184
- });
1185
- }
1186
-
1187
- /**
1188
- * @param {Array<Y.YEvent<any>>} events
1189
- * @param {Y.Transaction} transaction
1190
- */
1191
- _typeChanged (events, transaction) {
1192
- if (this.prosemirrorView == null) return
1193
- const syncState = ySyncPluginKey.getState(this.prosemirrorView.state);
1194
- if (
1195
- events.length === 0 || syncState.snapshot != null ||
1196
- syncState.prevSnapshot != null
1197
- ) {
1198
- // drop out if snapshot is active
1199
- this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot);
1200
- return
1201
- }
1202
- this.mux(() => {
1203
- /**
1204
- * @param {any} _
1205
- * @param {Y.AbstractType<any>} type
1206
- */
1207
- const delType = (_, type) => this.mapping.delete(type);
1208
- iterateDeletedStructs(
1209
- transaction,
1210
- transaction.deleteSet,
1211
- (struct) => {
1212
- if (struct.constructor === Item) {
1213
- const type = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type;
1214
- type && this.mapping.delete(type);
1215
- }
1216
- }
1217
- );
1218
- transaction.changed.forEach(delType);
1219
- transaction.changedParentTypes.forEach(delType);
1220
- const fragmentContent = this.type.toArray().map((t) =>
1221
- createNodeIfNotExists(
1222
- /** @type {Y.XmlElement | Y.XmlHook} */ (t),
1223
- this.prosemirrorView.state.schema,
1224
- this
1225
- )
1226
- ).filter((n) => n !== null);
1227
- // @ts-ignore
1228
- let tr = this._tr.replace(
1229
- 0,
1230
- this.prosemirrorView.state.doc.content.size,
1231
- new Slice(Fragment.from(fragmentContent), 0, 0)
1232
- );
1233
- restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
1234
- tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof UndoManager });
1235
- if (
1236
- this.beforeTransactionSelection !== null && this._isLocalCursorInView()
1237
- ) {
1238
- tr.scrollIntoView();
1239
- }
1240
- this.prosemirrorView.dispatch(tr);
1241
- });
1242
- }
1243
-
1244
- /**
1245
- * @param {import('prosemirror-model').Node} doc
1246
- */
1247
- _prosemirrorChanged (doc) {
1248
- this.doc.transact(() => {
1249
- updateYFragment(this.doc, this.type, doc, this);
1250
- this.beforeTransactionSelection = getRelativeSelection(
1251
- this,
1252
- this.prosemirrorView.state
1253
- );
1254
- }, ySyncPluginKey);
1255
- }
1256
-
1257
- /**
1258
- * View is ready to listen to changes. Register observers.
1259
- * @param {any} prosemirrorView
1260
- */
1261
- initView (prosemirrorView) {
1262
- if (this.prosemirrorView != null) this.destroy();
1263
- this.prosemirrorView = prosemirrorView;
1264
- this.doc.on('beforeAllTransactions', this.beforeAllTransactions);
1265
- this.doc.on('afterAllTransactions', this.afterAllTransactions);
1266
- this.type.observeDeep(this._observeFunction);
1267
- }
1268
-
1269
- destroy () {
1270
- if (this.prosemirrorView == null) return
1271
- this.prosemirrorView = null;
1272
- this.type.unobserveDeep(this._observeFunction);
1273
- this.doc.off('beforeAllTransactions', this.beforeAllTransactions);
1274
- this.doc.off('afterAllTransactions', this.afterAllTransactions);
1275
- }
1276
- }
1277
-
1278
- /**
1279
- * @private
1280
- * @param {Y.XmlElement | Y.XmlHook} el
1281
- * @param {PModel.Schema} schema
1282
- * @param {BindingMetadata} meta
1283
- * @param {Y.Snapshot} [snapshot]
1284
- * @param {Y.Snapshot} [prevSnapshot]
1285
- * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
1286
- * @return {PModel.Node | null}
1287
- */
1288
- const createNodeIfNotExists = (
1289
- el,
1290
- schema,
1291
- meta,
1292
- snapshot,
1293
- prevSnapshot,
1294
- computeYChange
1295
- ) => {
1296
- const node = /** @type {PModel.Node} */ (meta.mapping.get(el));
1297
- if (node === undefined) {
1298
- if (el instanceof YXmlElement) {
1299
- return createNodeFromYElement(
1300
- el,
1301
- schema,
1302
- meta,
1303
- snapshot,
1304
- prevSnapshot,
1305
- computeYChange
1306
- )
1307
- } else {
1308
- throw methodUnimplemented() // we are currently not handling hooks
1309
- }
1310
- }
1311
- return node
1312
- };
1313
-
1314
- /**
1315
- * @private
1316
- * @param {Y.XmlElement} el
1317
- * @param {any} schema
1318
- * @param {BindingMetadata} meta
1319
- * @param {Y.Snapshot} [snapshot]
1320
- * @param {Y.Snapshot} [prevSnapshot]
1321
- * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
1322
- * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
1323
- */
1324
- const createNodeFromYElement = (
1325
- el,
1326
- schema,
1327
- meta,
1328
- snapshot,
1329
- prevSnapshot,
1330
- computeYChange
1331
- ) => {
1332
- const children = [];
1333
- /**
1334
- * @param {Y.XmlElement | Y.XmlText} type
1335
- */
1336
- const createChildren = (type) => {
1337
- if (type instanceof YXmlElement) {
1338
- const n = createNodeIfNotExists(
1339
- type,
1340
- schema,
1341
- meta,
1342
- snapshot,
1343
- prevSnapshot,
1344
- computeYChange
1345
- );
1346
- if (n !== null) {
1347
- children.push(n);
1348
- }
1349
- } else {
1350
- // If the next ytext exists and was created by us, move the content to the current ytext.
1351
- // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each
1352
- // other.
1353
- const nextytext = /** @type {Y.ContentType} */ (type._item.right?.content)?.type;
1354
- if (nextytext instanceof YText && !nextytext._item.deleted && nextytext._item.id.client === nextytext.doc.clientID) {
1355
- type.applyDelta([
1356
- { retain: type.length },
1357
- ...nextytext.toDelta()
1358
- ]);
1359
- nextytext.doc.transact(tr => {
1360
- nextytext._item.delete(tr);
1361
- });
1362
- }
1363
- // now create the prosemirror text nodes
1364
- const ns = createTextNodesFromYText(
1365
- type,
1366
- schema,
1367
- meta,
1368
- snapshot,
1369
- prevSnapshot,
1370
- computeYChange
1371
- );
1372
- if (ns !== null) {
1373
- ns.forEach((textchild) => {
1374
- if (textchild !== null) {
1375
- children.push(textchild);
1376
- }
1377
- });
1378
- }
1379
- }
1380
- };
1381
- if (snapshot === undefined || prevSnapshot === undefined) {
1382
- el.toArray().forEach(createChildren);
1383
- } else {
1384
- typeListToArraySnapshot(el, new Snapshot(prevSnapshot.ds, snapshot.sv))
1385
- .forEach(createChildren);
1386
- }
1387
- try {
1388
- const attrs = el.getAttributes(snapshot);
1389
- if (snapshot !== undefined) {
1390
- if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) {
1391
- attrs.ychange = computeYChange
1392
- ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id)
1393
- : { type: 'removed' };
1394
- } else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) {
1395
- attrs.ychange = computeYChange
1396
- ? computeYChange('added', /** @type {Y.Item} */ (el._item).id)
1397
- : { type: 'added' };
1398
- }
1399
- }
1400
- const node = schema.node(el.nodeName, attrs, children);
1401
- meta.mapping.set(el, node);
1402
- return node
1403
- } catch (e) {
1404
- // an error occured while creating the node. This is probably a result of a concurrent action.
1405
- /** @type {Y.Doc} */ (el.doc).transact((transaction) => {
1406
- /** @type {Y.Item} */ (el._item).delete(transaction);
1407
- }, ySyncPluginKey);
1408
- meta.mapping.delete(el);
1409
- return null
1410
- }
1411
- };
1412
-
1413
- /**
1414
- * @private
1415
- * @param {Y.XmlText} text
1416
- * @param {import('prosemirror-model').Schema} schema
1417
- * @param {BindingMetadata} _meta
1418
- * @param {Y.Snapshot} [snapshot]
1419
- * @param {Y.Snapshot} [prevSnapshot]
1420
- * @param {function('removed' | 'added', Y.ID):any} [computeYChange]
1421
- * @return {Array<PModel.Node>|null}
1422
- */
1423
- const createTextNodesFromYText = (
1424
- text,
1425
- schema,
1426
- _meta,
1427
- snapshot,
1428
- prevSnapshot,
1429
- computeYChange
1430
- ) => {
1431
- const nodes = [];
1432
- const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange);
1433
- try {
1434
- for (let i = 0; i < deltas.length; i++) {
1435
- const delta = deltas[i];
1436
- nodes.push(schema.text(delta.insert, attributesToMarks(delta.attributes, schema)));
1437
- }
1438
- } catch (e) {
1439
- // an error occured while creating the node. This is probably a result of a concurrent action.
1440
- /** @type {Y.Doc} */ (text.doc).transact((transaction) => {
1441
- /** @type {Y.Item} */ (text._item).delete(transaction);
1442
- }, ySyncPluginKey);
1443
- return null
1444
- }
1445
- // @ts-ignore
1446
- return nodes
1447
- };
1448
-
1449
- /**
1450
- * @private
1451
- * @param {Array<any>} nodes prosemirror node
1452
- * @param {BindingMetadata} meta
1453
- * @return {Y.XmlText}
1454
- */
1455
- const createTypeFromTextNodes = (nodes, meta) => {
1456
- const type = new YXmlText();
1457
- const delta = nodes.map((node) => ({
1458
- // @ts-ignore
1459
- insert: node.text,
1460
- attributes: marksToAttributes(node.marks, meta)
1461
- }));
1462
- type.applyDelta(delta);
1463
- meta.mapping.set(type, nodes);
1464
- return type
1465
- };
1466
-
1467
- /**
1468
- * @private
1469
- * @param {any} node prosemirror node
1470
- * @param {BindingMetadata} meta
1471
- * @return {Y.XmlElement}
1472
- */
1473
- const createTypeFromElementNode = (node, meta) => {
1474
- const type = new YXmlElement(node.type.name);
1475
- for (const key in node.attrs) {
1476
- const val = node.attrs[key];
1477
- if (val !== null && key !== 'ychange') {
1478
- type.setAttribute(key, val);
1479
- }
1480
- }
1481
- type.insert(
1482
- 0,
1483
- normalizePNodeContent(node).map((n) =>
1484
- createTypeFromTextOrElementNode(n, meta)
1485
- )
1486
- );
1487
- meta.mapping.set(type, node);
1488
- return type
1489
- };
1490
-
1491
- /**
1492
- * @private
1493
- * @param {PModel.Node|Array<PModel.Node>} node prosemirror text node
1494
- * @param {BindingMetadata} meta
1495
- * @return {Y.XmlElement|Y.XmlText}
1496
- */
1497
- const createTypeFromTextOrElementNode = (node, meta) =>
1498
- node instanceof Array
1499
- ? createTypeFromTextNodes(node, meta)
1500
- : createTypeFromElementNode(node, meta);
1501
-
1502
- /**
1503
- * @param {any} val
1504
- */
1505
- const isObject = (val) => typeof val === 'object' && val !== null;
1506
-
1507
- /**
1508
- * @param {any} pattrs
1509
- * @param {any} yattrs
1510
- */
1511
- const equalAttrs = (pattrs, yattrs) => {
1512
- const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null);
1513
- let eq =
1514
- keys.length ===
1515
- (yattrs == null ? 0 : Object.keys(yattrs).filter((key) => yattrs[key] !== null).length);
1516
- for (let i = 0; i < keys.length && eq; i++) {
1517
- const key = keys[i];
1518
- const l = pattrs[key];
1519
- const r = yattrs[key];
1520
- eq = key === 'ychange' || l === r ||
1521
- (isObject(l) && isObject(r) && equalAttrs(l, r));
1522
- }
1523
- return eq
1524
- };
1525
-
1526
- /**
1527
- * @typedef {Array<Array<PModel.Node>|PModel.Node>} NormalizedPNodeContent
1528
- */
1529
-
1530
- /**
1531
- * @param {any} pnode
1532
- * @return {NormalizedPNodeContent}
1533
- */
1534
- const normalizePNodeContent = (pnode) => {
1535
- const c = pnode.content.content;
1536
- const res = [];
1537
- for (let i = 0; i < c.length; i++) {
1538
- const n = c[i];
1539
- if (n.isText) {
1540
- const textNodes = [];
1541
- for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) {
1542
- textNodes.push(tnode);
1543
- }
1544
- i--;
1545
- res.push(textNodes);
1546
- } else {
1547
- res.push(n);
1548
- }
1549
- }
1550
- return res
1551
- };
1552
-
1553
- /**
1554
- * @param {Y.XmlText} ytext
1555
- * @param {Array<any>} ptexts
1556
- */
1557
- const equalYTextPText = (ytext, ptexts) => {
1558
- const delta = ytext.toDelta();
1559
- return delta.length === ptexts.length &&
1560
- delta.every(/** @type {(d:any,i:number) => boolean} */ (d, i) =>
1561
- d.insert === /** @type {any} */ (ptexts[i]).text &&
1562
- keys(d.attributes || {}).length === ptexts[i].marks.length &&
1563
- every(d.attributes, (attr, yattrname) => {
1564
- const markname = yattr2markname(yattrname);
1565
- const pmarks = ptexts[i].marks;
1566
- return equalAttrs(attr, pmarks.find(/** @param {any} mark */ mark => mark.type.name === markname)?.attrs)
1567
- })
1568
- )
1569
- };
1570
-
1571
- /**
1572
- * @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype
1573
- * @param {any|Array<any>} pnode
1574
- */
1575
- const equalYTypePNode = (ytype, pnode) => {
1576
- if (
1577
- ytype instanceof YXmlElement && !(pnode instanceof Array) &&
1578
- matchNodeName(ytype, pnode)
1579
- ) {
1580
- const normalizedContent = normalizePNodeContent(pnode);
1581
- return ytype._length === normalizedContent.length &&
1582
- equalAttrs(ytype.getAttributes(), pnode.attrs) &&
1583
- ytype.toArray().every((ychild, i) =>
1584
- equalYTypePNode(ychild, normalizedContent[i])
1585
- )
1586
- }
1587
- return ytype instanceof YXmlText && pnode instanceof Array &&
1588
- equalYTextPText(ytype, pnode)
1589
- };
1590
-
1591
- /**
1592
- * @param {PModel.Node | Array<PModel.Node> | undefined} mapped
1593
- * @param {PModel.Node | Array<PModel.Node>} pcontent
1594
- */
1595
- const mappedIdentity = (mapped, pcontent) =>
1596
- mapped === pcontent ||
1597
- (mapped instanceof Array && pcontent instanceof Array &&
1598
- mapped.length === pcontent.length && mapped.every((a, i) =>
1599
- pcontent[i] === a
1600
- ));
1601
-
1602
- /**
1603
- * @param {Y.XmlElement} ytype
1604
- * @param {PModel.Node} pnode
1605
- * @param {BindingMetadata} meta
1606
- * @return {{ foundMappedChild: boolean, equalityFactor: number }}
1607
- */
1608
- const computeChildEqualityFactor = (ytype, pnode, meta) => {
1609
- const yChildren = ytype.toArray();
1610
- const pChildren = normalizePNodeContent(pnode);
1611
- const pChildCnt = pChildren.length;
1612
- const yChildCnt = yChildren.length;
1613
- const minCnt = min(yChildCnt, pChildCnt);
1614
- let left = 0;
1615
- let right = 0;
1616
- let foundMappedChild = false;
1617
- for (; left < minCnt; left++) {
1618
- const leftY = yChildren[left];
1619
- const leftP = pChildren[left];
1620
- if (mappedIdentity(meta.mapping.get(leftY), leftP)) {
1621
- foundMappedChild = true; // definite (good) match!
1622
- } else if (!equalYTypePNode(leftY, leftP)) {
1623
- break
1624
- }
1625
- }
1626
- for (; left + right < minCnt; right++) {
1627
- const rightY = yChildren[yChildCnt - right - 1];
1628
- const rightP = pChildren[pChildCnt - right - 1];
1629
- if (mappedIdentity(meta.mapping.get(rightY), rightP)) {
1630
- foundMappedChild = true;
1631
- } else if (!equalYTypePNode(rightY, rightP)) {
1632
- break
1633
- }
1634
- }
1635
- return {
1636
- equalityFactor: left + right,
1637
- foundMappedChild
1638
- }
1639
- };
1640
-
1641
- /**
1642
- * @param {Y.Text} ytext
1643
- */
1644
- const ytextTrans = (ytext) => {
1645
- let str = '';
1646
- /**
1647
- * @type {Y.Item|null}
1648
- */
1649
- let n = ytext._start;
1650
- const nAttrs = {};
1651
- while (n !== null) {
1652
- if (!n.deleted) {
1653
- if (n.countable && n.content instanceof ContentString) {
1654
- str += n.content.str;
1655
- } else if (n.content instanceof ContentFormat) {
1656
- nAttrs[n.content.key] = null;
1657
- }
1658
- }
1659
- n = n.right;
1660
- }
1661
- return {
1662
- str,
1663
- nAttrs
1664
- }
1665
- };
1666
-
1667
- /**
1668
- * @todo test this more
1669
- *
1670
- * @param {Y.Text} ytext
1671
- * @param {Array<any>} ptexts
1672
- * @param {BindingMetadata} meta
1673
- */
1674
- const updateYText = (ytext, ptexts, meta) => {
1675
- meta.mapping.set(ytext, ptexts);
1676
- const { nAttrs, str } = ytextTrans(ytext);
1677
- const content = ptexts.map((p) => ({
1678
- insert: /** @type {any} */ (p).text,
1679
- attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks, meta))
1680
- }));
1681
- const { insert, remove, index } = simpleDiff(
1682
- str,
1683
- content.map((c) => c.insert).join('')
1684
- );
1685
- ytext.delete(index, remove);
1686
- ytext.insert(index, insert);
1687
- ytext.applyDelta(
1688
- content.map((c) => ({ retain: c.insert.length, attributes: c.attributes }))
1689
- );
1690
- };
1691
-
1692
- const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/;
1693
- /**
1694
- * @param {string} attrName
1695
- */
1696
- const yattr2markname = attrName => hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName;
1697
-
1698
- /**
1699
- * @todo move this to markstoattributes
1700
- *
1701
- * @param {Object<string, any>} attrs
1702
- * @param {import('prosemirror-model').Schema} schema
1703
- */
1704
- const attributesToMarks = (attrs, schema) => {
1705
- /**
1706
- * @type {Array<import('prosemirror-model').Mark>}
1707
- */
1708
- const marks = [];
1709
- for (const markName in attrs) {
1710
- // remove hashes if necessary
1711
- marks.push(schema.mark(yattr2markname(markName), attrs[markName]));
1712
- }
1713
- return marks
1714
- };
1715
-
1716
- /**
1717
- * @param {Array<import('prosemirror-model').Mark>} marks
1718
- * @param {BindingMetadata} meta
1719
- */
1720
- const marksToAttributes = (marks, meta) => {
1721
- const pattrs = {};
1722
- marks.forEach((mark) => {
1723
- if (mark.type.name !== 'ychange') {
1724
- const isOverlapping = setIfUndefined(meta.isOMark, mark.type, () => !mark.type.excludes(mark.type));
1725
- pattrs[isOverlapping ? `${mark.type.name}--${hashOfJSON(mark.toJSON())}` : mark.type.name] = mark.attrs;
1726
- }
1727
- });
1728
- return pattrs
1729
- };
1730
-
1731
- /**
1732
- * Update a yDom node by syncing the current content of the prosemirror node.
1733
- *
1734
- * This is a y-prosemirror internal feature that you can use at your own risk.
1735
- *
1736
- * @private
1737
- * @unstable
1738
- *
1739
- * @param {{transact: Function}} y
1740
- * @param {Y.XmlFragment} yDomFragment
1741
- * @param {any} pNode
1742
- * @param {BindingMetadata} meta
1743
- */
1744
- const updateYFragment = (y, yDomFragment, pNode, meta) => {
1745
- if (
1746
- yDomFragment instanceof YXmlElement &&
1747
- yDomFragment.nodeName !== pNode.type.name
1748
- ) {
1749
- throw new Error('node name mismatch!')
1750
- }
1751
- meta.mapping.set(yDomFragment, pNode);
1752
- // update attributes
1753
- if (yDomFragment instanceof YXmlElement) {
1754
- const yDomAttrs = yDomFragment.getAttributes();
1755
- const pAttrs = pNode.attrs;
1756
- for (const key in pAttrs) {
1757
- if (pAttrs[key] !== null) {
1758
- if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
1759
- yDomFragment.setAttribute(key, pAttrs[key]);
1760
- }
1761
- } else {
1762
- yDomFragment.removeAttribute(key);
1763
- }
1764
- }
1765
- // remove all keys that are no longer in pAttrs
1766
- for (const key in yDomAttrs) {
1767
- if (pAttrs[key] === undefined) {
1768
- yDomFragment.removeAttribute(key);
1769
- }
1770
- }
1771
- }
1772
- // update children
1773
- const pChildren = normalizePNodeContent(pNode);
1774
- const pChildCnt = pChildren.length;
1775
- const yChildren = yDomFragment.toArray();
1776
- const yChildCnt = yChildren.length;
1777
- const minCnt = min(pChildCnt, yChildCnt);
1778
- let left = 0;
1779
- let right = 0;
1780
- // find number of matching elements from left
1781
- for (; left < minCnt; left++) {
1782
- const leftY = yChildren[left];
1783
- const leftP = pChildren[left];
1784
- if (!mappedIdentity(meta.mapping.get(leftY), leftP)) {
1785
- if (equalYTypePNode(leftY, leftP)) {
1786
- // update mapping
1787
- meta.mapping.set(leftY, leftP);
1788
- } else {
1789
- break
1790
- }
1791
- }
1792
- }
1793
- // find number of matching elements from right
1794
- for (; right + left < minCnt; right++) {
1795
- const rightY = yChildren[yChildCnt - right - 1];
1796
- const rightP = pChildren[pChildCnt - right - 1];
1797
- if (!mappedIdentity(meta.mapping.get(rightY), rightP)) {
1798
- if (equalYTypePNode(rightY, rightP)) {
1799
- // update mapping
1800
- meta.mapping.set(rightY, rightP);
1801
- } else {
1802
- break
1803
- }
1804
- }
1805
- }
1806
- y.transact(() => {
1807
- // try to compare and update
1808
- while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
1809
- const leftY = yChildren[left];
1810
- const leftP = pChildren[left];
1811
- const rightY = yChildren[yChildCnt - right - 1];
1812
- const rightP = pChildren[pChildCnt - right - 1];
1813
- if (leftY instanceof YXmlText && leftP instanceof Array) {
1814
- if (!equalYTextPText(leftY, leftP)) {
1815
- updateYText(leftY, leftP, meta);
1816
- }
1817
- left += 1;
1818
- } else {
1819
- let updateLeft = leftY instanceof YXmlElement &&
1820
- matchNodeName(leftY, leftP);
1821
- let updateRight = rightY instanceof YXmlElement &&
1822
- matchNodeName(rightY, rightP);
1823
- if (updateLeft && updateRight) {
1824
- // decide which which element to update
1825
- const equalityLeft = computeChildEqualityFactor(
1826
- /** @type {Y.XmlElement} */ (leftY),
1827
- /** @type {PModel.Node} */ (leftP),
1828
- meta
1829
- );
1830
- const equalityRight = computeChildEqualityFactor(
1831
- /** @type {Y.XmlElement} */ (rightY),
1832
- /** @type {PModel.Node} */ (rightP),
1833
- meta
1834
- );
1835
- if (
1836
- equalityLeft.foundMappedChild && !equalityRight.foundMappedChild
1837
- ) {
1838
- updateRight = false;
1839
- } else if (
1840
- !equalityLeft.foundMappedChild && equalityRight.foundMappedChild
1841
- ) {
1842
- updateLeft = false;
1843
- } else if (
1844
- equalityLeft.equalityFactor < equalityRight.equalityFactor
1845
- ) {
1846
- updateLeft = false;
1847
- } else {
1848
- updateRight = false;
1849
- }
1850
- }
1851
- if (updateLeft) {
1852
- updateYFragment(
1853
- y,
1854
- /** @type {Y.XmlFragment} */ (leftY),
1855
- /** @type {PModel.Node} */ (leftP),
1856
- meta
1857
- );
1858
- left += 1;
1859
- } else if (updateRight) {
1860
- updateYFragment(
1861
- y,
1862
- /** @type {Y.XmlFragment} */ (rightY),
1863
- /** @type {PModel.Node} */ (rightP),
1864
- meta
1865
- );
1866
- right += 1;
1867
- } else {
1868
- meta.mapping.delete(yDomFragment.get(left));
1869
- yDomFragment.delete(left, 1);
1870
- yDomFragment.insert(left, [
1871
- createTypeFromTextOrElementNode(leftP, meta)
1872
- ]);
1873
- left += 1;
1874
- }
1875
- }
1876
- }
1877
- const yDelLen = yChildCnt - left - right;
1878
- if (
1879
- yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof YXmlText
1880
- ) {
1881
- meta.mapping.delete(yChildren[0]);
1882
- // Edge case handling https://github.com/yjs/y-prosemirror/issues/108
1883
- // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object
1884
- yChildren[0].delete(0, yChildren[0].length);
1885
- } else if (yDelLen > 0) {
1886
- yDomFragment.slice(left, left + yDelLen).forEach(type => meta.mapping.delete(type));
1887
- yDomFragment.delete(left, yDelLen);
1888
- }
1889
- if (left + right < pChildCnt) {
1890
- const ins = [];
1891
- for (let i = left; i < pChildCnt - right; i++) {
1892
- ins.push(createTypeFromTextOrElementNode(pChildren[i], meta));
1893
- }
1894
- yDomFragment.insert(left, ins);
1895
- }
1896
- }, ySyncPluginKey);
1897
- };
1898
-
1899
- /**
1900
- * @function
1901
- * @param {Y.XmlElement} yElement
1902
- * @param {any} pNode Prosemirror Node
1903
- */
1904
- const matchNodeName = (yElement, pNode) =>
1905
- !(pNode instanceof Array) && yElement.nodeName === pNode.type.name;
1906
-
1907
- /**
1908
- * Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model).
1909
- *
1910
- * @param {number} pos
1911
- * @param {Y.XmlFragment} type
1912
- * @param {ProsemirrorMapping} mapping
1913
- * @return {any} relative position
1914
- */
1915
- const absolutePositionToRelativePosition = (pos, type, mapping) => {
1916
- if (pos === 0) {
1917
- // if the type is later populated, we want to retain the 0 position (hence assoc=-1)
1918
- return createRelativePositionFromTypeIndex(type, 0, type.length === 0 ? -1 : 0)
1919
- }
1920
- /**
1921
- * @type {any}
1922
- */
1923
- let n = type._first === null ? null : /** @type {Y.ContentType} */ (type._first.content).type;
1924
- while (n !== null && type !== n) {
1925
- if (n instanceof YXmlText) {
1926
- if (n._length >= pos) {
1927
- return createRelativePositionFromTypeIndex(n, pos, type.length === 0 ? -1 : 0)
1928
- } else {
1929
- pos -= n._length;
1930
- }
1931
- if (n._item !== null && n._item.next !== null) {
1932
- n = /** @type {Y.ContentType} */ (n._item.next.content).type;
1933
- } else {
1934
- do {
1935
- n = n._item === null ? null : n._item.parent;
1936
- pos--;
1937
- } while (n !== type && n !== null && n._item !== null && n._item.next === null)
1938
- if (n !== null && n !== type) {
1939
- // @ts-gnore we know that n.next !== null because of above loop conditition
1940
- n = n._item === null ? null : /** @type {Y.ContentType} */ (/** @type Y.Item */ (n._item.next).content).type;
1941
- }
1942
- }
1943
- } else {
1944
- const pNodeSize = /** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize;
1945
- if (n._first !== null && pos < pNodeSize) {
1946
- n = /** @type {Y.ContentType} */ (n._first.content).type;
1947
- pos--;
1948
- } else {
1949
- if (pos === 1 && n._length === 0 && pNodeSize > 1) {
1950
- // edge case, should end in this paragraph
1951
- return new RelativePosition(n._item === null ? null : n._item.id, n._item === null ? findRootTypeKey(n) : null, null)
1952
- }
1953
- pos -= pNodeSize;
1954
- if (n._item !== null && n._item.next !== null) {
1955
- n = /** @type {Y.ContentType} */ (n._item.next.content).type;
1956
- } else {
1957
- if (pos === 0) {
1958
- // set to end of n.parent
1959
- n = n._item === null ? n : n._item.parent;
1960
- return new RelativePosition(n._item === null ? null : n._item.id, n._item === null ? findRootTypeKey(n) : null, null)
1961
- }
1962
- do {
1963
- n = /** @type {Y.Item} */ (n._item).parent;
1964
- pos--;
1965
- } while (n !== type && /** @type {Y.Item} */ (n._item).next === null)
1966
- // if n is null at this point, we have an unexpected case
1967
- if (n !== type) {
1968
- // We know that n._item.next is defined because of above loop condition
1969
- n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type;
1970
- }
1971
- }
1972
- }
1973
- }
1974
- if (n === null) {
1975
- throw unexpectedCase()
1976
- }
1977
- if (pos === 0 && n.constructor !== YXmlText && n !== type) { // TODO: set to <= 0
1978
- return createRelativePosition(n._item.parent, n._item)
1979
- }
1980
- }
1981
- return createRelativePositionFromTypeIndex(type, type._length, type.length === 0 ? -1 : 0)
1982
- };
1983
-
1984
- const createRelativePosition = (type, item) => {
1985
- let typeid = null;
1986
- let tname = null;
1987
- if (type._item === null) {
1988
- tname = findRootTypeKey(type);
1989
- } else {
1990
- typeid = createID(type._item.id.client, type._item.id.clock);
1991
- }
1992
- return new RelativePosition(typeid, tname, item.id)
1993
- };
1994
-
1995
- /**
1996
- * @param {Y.Doc} y
1997
- * @param {Y.XmlFragment} documentType Top level type that is bound to pView
1998
- * @param {any} relPos Encoded Yjs based relative position
1999
- * @param {ProsemirrorMapping} mapping
2000
- * @return {null|number}
2001
- */
2002
- const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) => {
2003
- const decodedPos = createAbsolutePositionFromRelativePosition(relPos, y);
2004
- if (decodedPos === null || (decodedPos.type !== documentType && !isParentOf(documentType, decodedPos.type._item))) {
2005
- return null
2006
- }
2007
- let type = decodedPos.type;
2008
- let pos = 0;
2009
- if (type.constructor === YXmlText) {
2010
- pos = decodedPos.index;
2011
- } else if (type._item === null || !type._item.deleted) {
2012
- let n = type._first;
2013
- let i = 0;
2014
- while (i < type._length && i < decodedPos.index && n !== null) {
2015
- if (!n.deleted) {
2016
- const t = /** @type {Y.ContentType} */ (n.content).type;
2017
- i++;
2018
- if (t instanceof YXmlText) {
2019
- pos += t._length;
2020
- } else {
2021
- pos += /** @type {any} */ (mapping.get(t)).nodeSize;
2022
- }
2023
- }
2024
- n = /** @type {Y.Item} */ (n.right);
2025
- }
2026
- pos += 1; // increase because we go out of n
2027
- }
2028
- while (type !== documentType && type._item !== null) {
2029
- // @ts-ignore
2030
- const parent = type._item.parent;
2031
- // @ts-ignore
2032
- if (parent._item === null || !parent._item.deleted) {
2033
- pos += 1; // the start tag
2034
- let n = /** @type {Y.AbstractType} */ (parent)._first;
2035
- // now iterate until we found type
2036
- while (n !== null) {
2037
- const contentType = /** @type {Y.ContentType} */ (n.content).type;
2038
- if (contentType === type) {
2039
- break
2040
- }
2041
- if (!n.deleted) {
2042
- if (contentType instanceof YXmlText) {
2043
- pos += contentType._length;
2044
- } else {
2045
- pos += /** @type {any} */ (mapping.get(contentType)).nodeSize;
2046
- }
2047
- }
2048
- n = n.right;
2049
- }
2050
- }
2051
- type = /** @type {Y.AbstractType} */ (parent);
2052
- }
2053
- return pos - 1 // we don't count the most outer tag, because it is a fragment
2054
- };
2055
-
2056
- /**
2057
- * @deprecated Use `yXmlFragmentToProseMirrorRootNode` instead
2058
- *
2059
- * Utility method to convert a Y.Doc to Prosemirror compatible JSON.
2060
- *
2061
- * @param {Y.XmlFragment} xmlFragment The fragment, which must be part of a Y.Doc.
2062
- * @return {Record<string, any>}
2063
- */
2064
- function yXmlFragmentToProsemirrorJSON (xmlFragment) {
2065
- const items = xmlFragment.toArray();
2066
-
2067
- /**
2068
- * @param {Y.AbstractType} item
2069
- */
2070
- const serialize = item => {
2071
- /**
2072
- * @type {Object} NodeObject
2073
- * @property {string} NodeObject.type
2074
- * @property {Record<string, string>=} NodeObject.attrs
2075
- * @property {Array<NodeObject>=} NodeObject.content
2076
- */
2077
- let response;
2078
-
2079
- // TODO: Must be a better way to detect text nodes than this
2080
- if (item instanceof YXmlText) {
2081
- const delta = item.toDelta();
2082
- response = delta.map(/** @param {any} d */ (d) => {
2083
- const text = {
2084
- type: 'text',
2085
- text: d.insert
2086
- };
2087
- if (d.attributes) {
2088
- text.marks = Object.keys(d.attributes).map((type_) => {
2089
- const attrs = d.attributes[type_];
2090
- const type = yattr2markname(type_);
2091
- const mark = {
2092
- type
2093
- };
2094
- if (Object.keys(attrs)) {
2095
- mark.attrs = attrs;
2096
- }
2097
- return mark
2098
- });
2099
- }
2100
- return text
2101
- });
2102
- } else if (item instanceof YXmlElement) {
2103
- response = {
2104
- type: item.nodeName
2105
- };
2106
-
2107
- const attrs = item.getAttributes();
2108
- if (Object.keys(attrs).length) {
2109
- response.attrs = attrs;
2110
- }
2111
-
2112
- const children = item.toArray();
2113
- if (children.length) {
2114
- response.content = children.map(serialize).flat();
2115
- }
2116
- } else {
2117
- // expected either Y.XmlElement or Y.XmlText
2118
- unexpectedCase();
2119
- }
2120
-
2121
- return response
2122
- };
2123
-
2124
- return {
2125
- type: 'doc',
2126
- content: items.map(serialize)
2127
- }
2128
- }
2129
-
2130
- /**
2131
- * @typedef {Object} UndoPluginState
2132
- * @property {import('yjs').UndoManager} undoManager
2133
- * @property {ReturnType<typeof getRelativeSelection> | null} prevSel
2134
- * @property {boolean} hasUndoOps
2135
- * @property {boolean} hasRedoOps
2136
- */
2137
-
2138
- /**
2139
- * Undo the last user action
2140
- *
2141
- * @param {import('prosemirror-state').EditorState} state
2142
- * @return {boolean} whether a change was undone
2143
- */
2144
- const undo = state => yUndoPluginKey.getState(state)?.undoManager?.undo() != null;
2145
-
2146
- /**
2147
- * Redo the last user action
2148
- *
2149
- * @param {import('prosemirror-state').EditorState} state
2150
- * @return {boolean} whether a change was undone
2151
- */
2152
- const redo = state => yUndoPluginKey.getState(state)?.undoManager?.redo() != null;
2153
-
2154
- const defaultProtectedNodes = new Set(['paragraph']);
2155
-
2156
- /**
2157
- * @param {import('yjs').Item} item
2158
- * @param {Set<string>} protectedNodes
2159
- * @returns {boolean}
2160
- */
2161
- const defaultDeleteFilter = (item, protectedNodes) => !(item instanceof Item) ||
2162
- !(item.content instanceof ContentType) ||
2163
- !(item.content.type instanceof YText ||
2164
- (item.content.type instanceof YXmlElement && protectedNodes.has(item.content.type.nodeName))) ||
2165
- item.content.type._length === 0;
2166
-
2167
- /**
2168
- * @param {object} [options]
2169
- * @param {Set<string>} [options.protectedNodes]
2170
- * @param {any[]} [options.trackedOrigins]
2171
- * @param {import('yjs').UndoManager | null} [options.undoManager]
2172
- */
2173
- const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins = [], undoManager = null } = {}) => new Plugin({
2174
- key: yUndoPluginKey,
2175
- state: {
2176
- init: (initargs, state) => {
2177
- // TODO: check if plugin order matches and fix
2178
- const ystate = ySyncPluginKey.getState(state);
2179
- const _undoManager = undoManager || new UndoManager(ystate.type, {
2180
- trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)),
2181
- deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes),
2182
- captureTransaction: tr => tr.meta.get('addToHistory') !== false
2183
- });
2184
- return {
2185
- undoManager: _undoManager,
2186
- prevSel: null,
2187
- hasUndoOps: _undoManager.undoStack.length > 0,
2188
- hasRedoOps: _undoManager.redoStack.length > 0
2189
- }
2190
- },
2191
- apply: (tr, val, oldState, state) => {
2192
- const binding = ySyncPluginKey.getState(state).binding;
2193
- const undoManager = val.undoManager;
2194
- const hasUndoOps = undoManager.undoStack.length > 0;
2195
- const hasRedoOps = undoManager.redoStack.length > 0;
2196
- if (binding) {
2197
- return {
2198
- undoManager,
2199
- prevSel: getRelativeSelection(binding, oldState),
2200
- hasUndoOps,
2201
- hasRedoOps
2202
- }
2203
- } else {
2204
- if (hasUndoOps !== val.hasUndoOps || hasRedoOps !== val.hasRedoOps) {
2205
- return Object.assign({}, val, {
2206
- hasUndoOps: undoManager.undoStack.length > 0,
2207
- hasRedoOps: undoManager.redoStack.length > 0
2208
- })
2209
- } else { // nothing changed
2210
- return val
2211
- }
2212
- }
2213
- }
2214
- },
2215
- view: view => {
2216
- const ystate = ySyncPluginKey.getState(view.state);
2217
- const undoManager = yUndoPluginKey.getState(view.state).undoManager;
2218
- undoManager.on('stack-item-added', ({ stackItem }) => {
2219
- const binding = ystate.binding;
2220
- if (binding) {
2221
- stackItem.meta.set(binding, yUndoPluginKey.getState(view.state).prevSel);
2222
- }
2223
- });
2224
- undoManager.on('stack-item-popped', ({ stackItem }) => {
2225
- const binding = ystate.binding;
2226
- if (binding) {
2227
- binding.beforeTransactionSelection = stackItem.meta.get(binding) || binding.beforeTransactionSelection;
2228
- }
2229
- });
2230
- return {
2231
- destroy: () => {
2232
- undoManager.destroy();
2233
- }
2234
- }
2235
- }
2236
- });
2237
-
2238
- /**
2239
- * This extension allows you to collaborate with others in real-time.
2240
- * @see https://tiptap.dev/api/extensions/collaboration
2241
- */
2242
- const Collaboration = Extension.create({
2243
- name: 'collaboration',
2244
- priority: 1000,
2245
- addOptions() {
2246
- return {
2247
- document: null,
2248
- field: 'default',
2249
- fragment: null,
2250
- };
2251
- },
2252
- addStorage() {
2253
- return {
2254
- isDisabled: false,
2255
- };
2256
- },
2257
- onCreate() {
2258
- if (this.editor.extensionManager.extensions.find(extension => extension.name === 'history')) {
2259
- console.warn('[tiptap warn]: "@tiptap/extension-collaboration" comes with its own history support and is not compatible with "@tiptap/extension-history".');
2260
- }
2261
- },
2262
- addCommands() {
2263
- return {
2264
- undo: () => ({ tr, state, dispatch }) => {
2265
- tr.setMeta('preventDispatch', true);
2266
- const undoManager = yUndoPluginKey.getState(state).undoManager;
2267
- if (undoManager.undoStack.length === 0) {
2268
- return false;
2269
- }
2270
- if (!dispatch) {
2271
- return true;
2272
- }
2273
- return undo(state);
2274
- },
2275
- redo: () => ({ tr, state, dispatch }) => {
2276
- tr.setMeta('preventDispatch', true);
2277
- const undoManager = yUndoPluginKey.getState(state).undoManager;
2278
- if (undoManager.redoStack.length === 0) {
2279
- return false;
2280
- }
2281
- if (!dispatch) {
2282
- return true;
2283
- }
2284
- return redo(state);
2285
- },
2286
- };
2287
- },
2288
- addKeyboardShortcuts() {
2289
- return {
2290
- 'Mod-z': () => this.editor.commands.undo(),
2291
- 'Mod-y': () => this.editor.commands.redo(),
2292
- 'Shift-Mod-z': () => this.editor.commands.redo(),
2293
- };
2294
- },
2295
- addProseMirrorPlugins() {
2296
- var _a;
2297
- const fragment = this.options.fragment
2298
- ? this.options.fragment
2299
- : this.options.document.getXmlFragment(this.options.field);
2300
- // Quick fix until there is an official implementation (thanks to @hamflx).
2301
- // See https://github.com/yjs/y-prosemirror/issues/114 and https://github.com/yjs/y-prosemirror/issues/102
2302
- const yUndoPluginInstance = yUndoPlugin(this.options.yUndoOptions);
2303
- const originalUndoPluginView = yUndoPluginInstance.spec.view;
2304
- yUndoPluginInstance.spec.view = (view) => {
2305
- const { undoManager } = yUndoPluginKey.getState(view.state);
2306
- if (undoManager.restore) {
2307
- undoManager.restore();
2308
- undoManager.restore = () => {
2309
- // noop
2310
- };
2311
- }
2312
- const viewRet = originalUndoPluginView ? originalUndoPluginView(view) : undefined;
2313
- return {
2314
- destroy: () => {
2315
- const hasUndoManSelf = undoManager.trackedOrigins.has(undoManager);
2316
- // eslint-disable-next-line no-underscore-dangle
2317
- const observers = undoManager._observers;
2318
- undoManager.restore = () => {
2319
- if (hasUndoManSelf) {
2320
- undoManager.trackedOrigins.add(undoManager);
2321
- }
2322
- undoManager.doc.on('afterTransaction', undoManager.afterTransactionHandler);
2323
- // eslint-disable-next-line no-underscore-dangle
2324
- undoManager._observers = observers;
2325
- };
2326
- if (viewRet === null || viewRet === void 0 ? void 0 : viewRet.destroy) {
2327
- viewRet.destroy();
2328
- }
2329
- },
2330
- };
2331
- };
2332
- const ySyncPluginOptions = {
2333
- ...this.options.ySyncOptions,
2334
- onFirstRender: this.options.onFirstRender,
2335
- };
2336
- const ySyncPluginInstance = ySyncPlugin(fragment, ySyncPluginOptions);
2337
- if (this.editor.options.enableContentCheck) {
2338
- (_a = fragment.doc) === null || _a === void 0 ? void 0 : _a.on('beforeTransaction', () => {
2339
- try {
2340
- const jsonContent = (yXmlFragmentToProsemirrorJSON(fragment));
2341
- if (jsonContent.content.length === 0) {
2342
- return;
2343
- }
2344
- this.editor.schema.nodeFromJSON(jsonContent).check();
2345
- }
2346
- catch (error) {
2347
- this.editor.emit('contentError', {
2348
- error: error,
2349
- editor: this.editor,
2350
- disableCollaboration: () => {
2351
- var _a;
2352
- (_a = fragment.doc) === null || _a === void 0 ? void 0 : _a.destroy();
2353
- this.storage.isDisabled = true;
2354
- },
2355
- });
2356
- // If the content is invalid, return false to prevent the transaction from being applied
2357
- return false;
2358
- }
2359
- });
2360
- }
2361
- return [
2362
- ySyncPluginInstance,
2363
- yUndoPluginInstance,
2364
- // Only add the filterInvalidContent plugin if content checking is enabled
2365
- this.editor.options.enableContentCheck
2366
- && new Plugin({
2367
- key: new PluginKey('filterInvalidContent'),
2368
- filterTransaction: () => {
2369
- var _a;
2370
- // When collaboration is disabled, prevent any sync transactions from being applied
2371
- if (this.storage.isDisabled) {
2372
- // Destroy the Yjs document to prevent any further sync transactions
2373
- (_a = fragment.doc) === null || _a === void 0 ? void 0 : _a.destroy();
2374
- return true;
2375
- }
2376
- return true;
2377
- },
2378
- }),
2379
- ].filter(Boolean);
2380
- },
2381
- });
2382
-
2383
- const MARKDOWN_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
2384
- const MARKDOWN_MIME_TYPES = /* @__PURE__ */ new Set(["text/markdown", "text/x-markdown"]);
2385
- function getExtension(name) {
2386
- if (!name) return null;
2387
- const lastDot = name.lastIndexOf(".");
2388
- if (lastDot < 0) return null;
2389
- return name.slice(lastDot).toLowerCase();
2390
- }
2391
- function isMarkdownFileName(name, mimeType) {
2392
- if (mimeType && MARKDOWN_MIME_TYPES.has(mimeType)) return true;
2393
- const ext = getExtension(name);
2394
- return ext ? MARKDOWN_EXTENSIONS.has(ext) : false;
2395
- }
2396
- function inferNotebookDocKind(context) {
2397
- const override = context.customData?.docKind;
2398
- if (override === "markdown" || override === "notebook") return override;
2399
- if (context.type === "notebook") return "notebook";
2400
- const customData = context.customData;
2401
- const mimeType = typeof context.mimeType === "string" ? context.mimeType : typeof customData?.mimeType === "string" ? customData.mimeType : typeof customData?.fileMeta?.mimeType === "string" ? customData.fileMeta.mimeType : void 0;
2402
- if (isMarkdownFileName(context.resourceName, mimeType)) return "markdown";
2403
- return "notebook";
2404
- }
2405
-
2406
- const REMOTE_ORIGIN = "ds-remote";
2407
- async function createNovelYjsProvider(options) {
2408
- const { projectId, notebookId, readonly = false, onStatus, onReset } = options;
2409
- const sync = new ProjectSyncClient(projectId, {
2410
- authMode: "user",
2411
- docKind: "notebook"
2412
- });
2413
- const ydoc = new Doc();
2414
- let disposed = false;
2415
- let flushTimer = null;
2416
- let idleTimer = null;
2417
- let pending = [];
2418
- const setStatus = (status) => {
2419
- if (!onStatus) return;
2420
- onStatus(status);
2421
- if (status === "saved") {
2422
- if (idleTimer != null) window.clearTimeout(idleTimer);
2423
- idleTimer = window.setTimeout(() => onStatus("idle"), 1500);
2424
- }
2425
- };
2426
- const scheduleFlush = () => {
2427
- if (flushTimer != null) window.clearTimeout(flushTimer);
2428
- flushTimer = window.setTimeout(() => void flush(), 250);
2429
- };
2430
- const flush = async () => {
2431
- if (disposed || readonly || pending.length === 0) return;
2432
- const updates = pending;
2433
- pending = [];
2434
- try {
2435
- setStatus("saving");
2436
- const merged = updates.length === 1 ? updates[0] : mergeUpdates(updates);
2437
- await sync.pushDocUpdate(notebookId, merged);
2438
- setStatus("saved");
2439
- } catch (error) {
2440
- console.error("[NovelYjsProvider] Failed to push update:", error);
2441
- setStatus("error");
2442
- }
2443
- };
2444
- await sync.connect();
2445
- const diff = await sync.loadDoc(notebookId, encodeStateVector(ydoc));
2446
- if (diff?.missing) {
2447
- applyUpdate(ydoc, diff.missing, REMOTE_ORIGIN);
2448
- }
2449
- const offRemote = sync.onDocUpdate((msg) => {
2450
- if (msg.docId !== notebookId) return;
2451
- applyUpdate(ydoc, msg.update, REMOTE_ORIGIN);
2452
- });
2453
- const offReset = sync.onDocReset((msg) => {
2454
- if (msg.docId !== notebookId) return;
2455
- onReset?.({ timestamp: msg.timestamp, reason: msg.reason, actorUserId: msg.actorUserId ?? null });
2456
- });
2457
- const onLocalUpdate = (update, origin) => {
2458
- if (origin === REMOTE_ORIGIN) return;
2459
- if (readonly) return;
2460
- pending.push(update);
2461
- scheduleFlush();
2462
- };
2463
- ydoc.on("update", onLocalUpdate);
2464
- const dispose = () => {
2465
- disposed = true;
2466
- if (flushTimer != null) window.clearTimeout(flushTimer);
2467
- if (idleTimer != null) window.clearTimeout(idleTimer);
2468
- ydoc.off("update", onLocalUpdate);
2469
- offRemote?.();
2470
- offReset?.();
2471
- sync.disconnect();
2472
- };
2473
- return { ydoc, dispose };
2474
- }
2475
-
2476
- function buildSuggestionItems(onImageUpload) {
2477
- return [
2478
- {
2479
- title: "Send Feedback",
2480
- description: "Let us know how we can improve.",
2481
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquarePlus, { size: 18 }),
2482
- command: ({ editor, range }) => {
2483
- editor.chain().focus().deleteRange(range).run();
2484
- window.open("/feedback", "_blank");
2485
- }
2486
- },
2487
- {
2488
- title: "Text",
2489
- description: "Just start typing with plain text.",
2490
- searchTerms: ["p", "paragraph"],
2491
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { size: 18 }),
2492
- command: ({ editor, range }) => {
2493
- editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
2494
- }
2495
- },
2496
- {
2497
- title: "To-do List",
2498
- description: "Track tasks with a to-do list.",
2499
- searchTerms: ["todo", "task", "list", "check", "checkbox"],
2500
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(SquareCheckBig, { size: 18 }),
2501
- command: ({ editor, range }) => {
2502
- editor.chain().focus().deleteRange(range).toggleTaskList().run();
2503
- }
2504
- },
2505
- {
2506
- title: "Heading 1",
2507
- description: "Big section heading.",
2508
- searchTerms: ["title", "big", "large"],
2509
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Heading1, { size: 18 }),
2510
- command: ({ editor, range }) => {
2511
- editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
2512
- }
2513
- },
2514
- {
2515
- title: "Heading 2",
2516
- description: "Medium section heading.",
2517
- searchTerms: ["subtitle", "medium"],
2518
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Heading2, { size: 18 }),
2519
- command: ({ editor, range }) => {
2520
- editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
2521
- }
2522
- },
2523
- {
2524
- title: "Heading 3",
2525
- description: "Small section heading.",
2526
- searchTerms: ["subtitle", "small"],
2527
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Heading3, { size: 18 }),
2528
- command: ({ editor, range }) => {
2529
- editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
2530
- }
2531
- },
2532
- {
2533
- title: "Bullet List",
2534
- description: "Create a simple bullet list.",
2535
- searchTerms: ["unordered", "point"],
2536
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(List, { size: 18 }),
2537
- command: ({ editor, range }) => {
2538
- editor.chain().focus().deleteRange(range).toggleBulletList().run();
2539
- }
2540
- },
2541
- {
2542
- title: "Numbered List",
2543
- description: "Create a list with numbering.",
2544
- searchTerms: ["ordered"],
2545
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(ListOrdered, { size: 18 }),
2546
- command: ({ editor, range }) => {
2547
- editor.chain().focus().deleteRange(range).toggleOrderedList().run();
2548
- }
2549
- },
2550
- {
2551
- title: "Quote",
2552
- description: "Capture a quote.",
2553
- searchTerms: ["blockquote"],
2554
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(TextQuote, { size: 18 }),
2555
- command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run()
2556
- },
2557
- {
2558
- title: "Code",
2559
- description: "Capture a code snippet.",
2560
- searchTerms: ["codeblock"],
2561
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Code, { size: 18 }),
2562
- command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run()
2563
- },
2564
- {
2565
- title: "Table",
2566
- description: "Insert a table with rows and columns.",
2567
- searchTerms: ["table", "grid", "spreadsheet", "data"],
2568
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Table2, { size: 18 }),
2569
- command: ({ editor, range }) => {
2570
- editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
2571
- }
2572
- },
2573
- {
2574
- title: "Image",
2575
- description: "Upload an image from your computer.",
2576
- searchTerms: ["photo", "picture", "media"],
2577
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Image, { size: 18 }),
2578
- command: ({ editor, range }) => {
2579
- editor.chain().focus().deleteRange(range).run();
2580
- const input = document.createElement("input");
2581
- input.type = "file";
2582
- input.accept = "image/*";
2583
- input.onchange = async () => {
2584
- if (input.files?.length) {
2585
- const file = input.files[0];
2586
- await onImageUpload(file, editor);
2587
- }
2588
- };
2589
- input.click();
2590
- }
2591
- },
2592
- {
2593
- title: "Youtube",
2594
- description: "Embed a Youtube video.",
2595
- searchTerms: ["video", "youtube", "embed"],
2596
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Youtube, { size: 18 }),
2597
- command: ({ editor, range }) => {
2598
- const videoLink = prompt("Please enter Youtube Video Link");
2599
- if (!videoLink) return;
2600
- const ytregex = new RegExp(
2601
- /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/
2602
- );
2603
- if (ytregex.test(videoLink)) {
2604
- editor.chain().focus().deleteRange(range).setYoutubeVideo({
2605
- src: videoLink
2606
- }).run();
2607
- } else {
2608
- alert("Please enter a correct Youtube Video Link");
2609
- }
2610
- }
2611
- },
2612
- {
2613
- title: "Twitter",
2614
- description: "Embed a Tweet.",
2615
- searchTerms: ["twitter", "embed"],
2616
- icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Twitter, { size: 18 }),
2617
- command: ({ editor, range }) => {
2618
- const tweetLink = prompt("Please enter Twitter Link");
2619
- if (!tweetLink) return;
2620
- const tweetRegex = new RegExp(
2621
- /^https?:\/\/(www\.)?x\.com\/([a-zA-Z0-9_]{1,15})(\/status\/(\d+))?(\/\S*)?$/
2622
- );
2623
- if (tweetRegex.test(tweetLink)) {
2624
- editor.chain().focus().deleteRange(range).setTweet({
2625
- src: tweetLink
2626
- }).run();
2627
- } else {
2628
- alert("Please enter a correct Twitter Link");
2629
- }
2630
- }
2631
- }
2632
- ];
2633
- }
2634
- function createSlashCommand(items) {
2635
- return me.configure({
2636
- suggestion: {
2637
- items: () => items,
2638
- render: le
2639
- }
2640
- });
2641
- }
2642
-
2643
- const items = [
2644
- {
2645
- name: "Text",
2646
- icon: Text,
2647
- command: (editor) => editor.chain().focus().clearNodes().run(),
2648
- isActive: (editor) => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList")
2649
- },
2650
- {
2651
- name: "Heading 1",
2652
- icon: Heading1,
2653
- command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
2654
- isActive: (editor) => editor.isActive("heading", { level: 1 })
2655
- },
2656
- {
2657
- name: "Heading 2",
2658
- icon: Heading2,
2659
- command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
2660
- isActive: (editor) => editor.isActive("heading", { level: 2 })
2661
- },
2662
- {
2663
- name: "Heading 3",
2664
- icon: Heading3,
2665
- command: (editor) => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
2666
- isActive: (editor) => editor.isActive("heading", { level: 3 })
2667
- },
2668
- {
2669
- name: "To-do List",
2670
- icon: SquareCheckBig,
2671
- command: (editor) => editor.chain().focus().clearNodes().toggleTaskList().run(),
2672
- isActive: (editor) => editor.isActive("taskItem")
2673
- },
2674
- {
2675
- name: "Bullet List",
2676
- icon: List,
2677
- command: (editor) => editor.chain().focus().clearNodes().toggleBulletList().run(),
2678
- isActive: (editor) => editor.isActive("bulletList")
2679
- },
2680
- {
2681
- name: "Numbered List",
2682
- icon: ListOrdered,
2683
- command: (editor) => editor.chain().focus().clearNodes().toggleOrderedList().run(),
2684
- isActive: (editor) => editor.isActive("orderedList")
2685
- },
2686
- {
2687
- name: "Quote",
2688
- icon: TextQuote,
2689
- command: (editor) => editor.chain().focus().clearNodes().toggleBlockquote().run(),
2690
- isActive: (editor) => editor.isActive("blockquote")
2691
- },
2692
- {
2693
- name: "Code",
2694
- icon: Code,
2695
- command: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
2696
- isActive: (editor) => editor.isActive("codeBlock")
2697
- }
2698
- ];
2699
- const NodeSelector = ({ open, onOpenChange }) => {
2700
- const { editor } = useCurrentEditor();
2701
- if (!editor) return null;
2702
- const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? {
2703
- name: "Multiple"
2704
- };
2705
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(Popover, { open, onOpenChange, children: [
2706
- /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
2707
- Button,
2708
- {
2709
- size: "sm",
2710
- variant: "ghost",
2711
- className: "gap-2 rounded-none border-none hover:bg-accent focus:ring-0",
2712
- children: [
2713
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "whitespace-nowrap text-sm", children: activeItem.name }),
2714
- /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "h-4 w-4" })
2715
- ]
2716
- }
2717
- ) }),
2718
- /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverContent, { sideOffset: 5, align: "start", className: "w-48 p-1", children: items.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
2719
- w,
2720
- {
2721
- onSelect: (ed) => {
2722
- item.command(ed);
2723
- onOpenChange(false);
2724
- },
2725
- className: "flex cursor-pointer items-center justify-between rounded-sm px-2 py-1 text-sm hover:bg-accent",
2726
- children: [
2727
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center space-x-2", children: [
2728
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded-sm border p-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(item.icon, { className: "h-3 w-3" }) }),
2729
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: item.name })
2730
- ] }),
2731
- activeItem.name === item.name && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "h-4 w-4" })
2732
- ]
2733
- },
2734
- item.name
2735
- )) })
2736
- ] });
2737
- };
2738
-
2739
- const TextButtons = () => {
2740
- const { editor } = useCurrentEditor();
2741
- if (!editor) return null;
2742
- const items = [
2743
- {
2744
- name: "bold",
2745
- isActive: (ed) => ed.isActive("bold"),
2746
- command: (ed) => ed.chain().focus().toggleBold().run(),
2747
- icon: Bold
2748
- },
2749
- {
2750
- name: "italic",
2751
- isActive: (ed) => ed.isActive("italic"),
2752
- command: (ed) => ed.chain().focus().toggleItalic().run(),
2753
- icon: Italic
2754
- },
2755
- {
2756
- name: "underline",
2757
- isActive: (ed) => ed.isActive("underline"),
2758
- command: (ed) => ed.chain().focus().toggleUnderline().run(),
2759
- icon: Underline
2760
- },
2761
- {
2762
- name: "strike",
2763
- isActive: (ed) => ed.isActive("strike"),
2764
- command: (ed) => ed.chain().focus().toggleStrike().run(),
2765
- icon: Strikethrough
2766
- },
2767
- {
2768
- name: "code",
2769
- isActive: (ed) => ed.isActive("code"),
2770
- command: (ed) => ed.chain().focus().toggleCode().run(),
2771
- icon: Code
2772
- }
2773
- ];
2774
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex", children: items.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx(
2775
- w,
2776
- {
2777
- onSelect: (ed) => {
2778
- item.command(ed);
2779
- },
2780
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2781
- Button,
2782
- {
2783
- size: "sm",
2784
- className: "rounded-none",
2785
- variant: "ghost",
2786
- type: "button",
2787
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
2788
- item.icon,
2789
- {
2790
- className: cn("h-4 w-4", {
2791
- "text-blue-500": item.isActive(editor)
2792
- })
2793
- }
2794
- )
2795
- }
2796
- )
2797
- },
2798
- item.name
2799
- )) });
2800
- };
2801
-
2802
- function isValidUrl(url) {
2803
- try {
2804
- new URL(url);
2805
- return true;
2806
- } catch (_e) {
2807
- return false;
2808
- }
2809
- }
2810
- function getUrlFromString(str) {
2811
- if (isValidUrl(str)) return str;
2812
- try {
2813
- if (str.includes(".") && !str.includes(" ")) {
2814
- return new URL(`https://${str}`).toString();
2815
- }
2816
- } catch (_e) {
2817
- return null;
2818
- }
2819
- return null;
2820
- }
2821
- const LinkSelector = ({ open, onOpenChange }) => {
2822
- const inputRef = reactExports.useRef(null);
2823
- const { editor } = useCurrentEditor();
2824
- reactExports.useEffect(() => {
2825
- if (open) {
2826
- inputRef.current?.focus();
2827
- }
2828
- }, [open]);
2829
- if (!editor) return null;
2830
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(Popover, { open, onOpenChange, children: [
2831
- /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
2832
- Button,
2833
- {
2834
- size: "sm",
2835
- variant: "ghost",
2836
- className: "gap-2 rounded-none border-none",
2837
- children: [
2838
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-base", children: "↗" }),
2839
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2840
- "p",
2841
- {
2842
- className: cn("underline decoration-stone-400 underline-offset-4", {
2843
- "text-blue-500": editor.isActive("link")
2844
- }),
2845
- children: "Link"
2846
- }
2847
- )
2848
- ]
2849
- }
2850
- ) }),
2851
- /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverContent, { align: "start", className: "w-60 p-0", sideOffset: 10, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
2852
- "form",
2853
- {
2854
- onSubmit: (e) => {
2855
- const target = e.currentTarget;
2856
- e.preventDefault();
2857
- const input = target[0];
2858
- const url = getUrlFromString(input.value);
2859
- if (url) {
2860
- editor.chain().focus().setLink({ href: url }).run();
2861
- onOpenChange(false);
2862
- }
2863
- },
2864
- className: "flex p-1",
2865
- children: [
2866
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2867
- "input",
2868
- {
2869
- ref: inputRef,
2870
- type: "text",
2871
- placeholder: "Paste a link",
2872
- className: "flex-1 bg-background p-1 text-sm outline-none",
2873
- defaultValue: editor.getAttributes("link").href || ""
2874
- }
2875
- ),
2876
- editor.getAttributes("link").href ? /* @__PURE__ */ jsxRuntimeExports.jsx(
2877
- Button,
2878
- {
2879
- size: "icon",
2880
- variant: "outline",
2881
- type: "button",
2882
- className: "flex h-8 items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800",
2883
- onClick: () => {
2884
- editor.chain().focus().unsetLink().run();
2885
- if (inputRef.current) {
2886
- inputRef.current.value = "";
2887
- }
2888
- onOpenChange(false);
2889
- },
2890
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash, { className: "h-4 w-4" })
2891
- }
2892
- ) : /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { size: "icon", type: "submit", className: "h-8", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "h-4 w-4" }) })
2893
- ]
2894
- }
2895
- ) })
2896
- ] });
2897
- };
2898
-
2899
- const TEXT_COLORS = [
2900
- {
2901
- name: "Default",
2902
- color: "var(--novel-black)"
2903
- },
2904
- {
2905
- name: "Purple",
2906
- color: "#9333EA"
2907
- },
2908
- {
2909
- name: "Red",
2910
- color: "#E00000"
2911
- },
2912
- {
2913
- name: "Yellow",
2914
- color: "#EAB308"
2915
- },
2916
- {
2917
- name: "Blue",
2918
- color: "#2563EB"
2919
- },
2920
- {
2921
- name: "Green",
2922
- color: "#008A00"
2923
- },
2924
- {
2925
- name: "Orange",
2926
- color: "#FFA500"
2927
- },
2928
- {
2929
- name: "Pink",
2930
- color: "#BA4081"
2931
- },
2932
- {
2933
- name: "Gray",
2934
- color: "#A8A29E"
2935
- }
2936
- ];
2937
- const HIGHLIGHT_COLORS = [
2938
- {
2939
- name: "Default",
2940
- color: "var(--novel-highlight-default)"
2941
- },
2942
- {
2943
- name: "Purple",
2944
- color: "var(--novel-highlight-purple)"
2945
- },
2946
- {
2947
- name: "Red",
2948
- color: "var(--novel-highlight-red)"
2949
- },
2950
- {
2951
- name: "Yellow",
2952
- color: "var(--novel-highlight-yellow)"
2953
- },
2954
- {
2955
- name: "Blue",
2956
- color: "var(--novel-highlight-blue)"
2957
- },
2958
- {
2959
- name: "Green",
2960
- color: "var(--novel-highlight-green)"
2961
- },
2962
- {
2963
- name: "Orange",
2964
- color: "var(--novel-highlight-orange)"
2965
- },
2966
- {
2967
- name: "Pink",
2968
- color: "var(--novel-highlight-pink)"
2969
- },
2970
- {
2971
- name: "Gray",
2972
- color: "var(--novel-highlight-gray)"
2973
- }
2974
- ];
2975
- const ColorSelector = ({ open, onOpenChange }) => {
2976
- const { editor } = useCurrentEditor();
2977
- if (!editor) return null;
2978
- const activeColorItem = TEXT_COLORS.find(
2979
- ({ color }) => editor.isActive("textStyle", { color })
2980
- );
2981
- const activeHighlightItem = HIGHLIGHT_COLORS.find(
2982
- ({ color }) => editor.isActive("highlight", { color })
2983
- );
2984
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(Popover, { open, onOpenChange, children: [
2985
- /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { size: "sm", className: "gap-2 rounded-none", variant: "ghost", children: [
2986
- /* @__PURE__ */ jsxRuntimeExports.jsx(
2987
- "span",
2988
- {
2989
- className: "rounded-sm px-1",
2990
- style: {
2991
- color: activeColorItem?.color,
2992
- backgroundColor: activeHighlightItem?.color
2993
- },
2994
- children: "A"
2995
- }
2996
- ),
2997
- /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { className: "h-4 w-4" })
2998
- ] }) }),
2999
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
3000
- PopoverContent,
3001
- {
3002
- sideOffset: 5,
3003
- className: "my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl",
3004
- align: "start",
3005
- children: [
3006
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [
3007
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "my-1 px-2 text-sm font-semibold text-muted-foreground", children: "Color" }),
3008
- TEXT_COLORS.map(({ name, color }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3009
- w,
3010
- {
3011
- onSelect: () => {
3012
- editor.commands.unsetColor();
3013
- name !== "Default" && editor.chain().focus().setColor(color || "").run();
3014
- onOpenChange(false);
3015
- },
3016
- className: "flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-accent",
3017
- children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3018
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3019
- "div",
3020
- {
3021
- className: "rounded-sm border px-2 py-px font-medium",
3022
- style: { color },
3023
- children: "A"
3024
- }
3025
- ),
3026
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: name })
3027
- ] })
3028
- },
3029
- name
3030
- ))
3031
- ] }),
3032
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
3033
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "my-1 px-2 text-sm font-semibold text-muted-foreground", children: "Background" }),
3034
- HIGHLIGHT_COLORS.map(({ name, color }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
3035
- w,
3036
- {
3037
- onSelect: () => {
3038
- editor.commands.unsetHighlight();
3039
- name !== "Default" && editor.chain().focus().setHighlight({ color }).run();
3040
- onOpenChange(false);
3041
- },
3042
- className: "flex cursor-pointer items-center justify-between px-2 py-1 text-sm hover:bg-accent",
3043
- children: [
3044
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3045
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3046
- "div",
3047
- {
3048
- className: "rounded-sm border px-2 py-px font-medium",
3049
- style: { backgroundColor: color },
3050
- children: "A"
3051
- }
3052
- ),
3053
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: name })
3054
- ] }),
3055
- editor.isActive("highlight", { color }) && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "h-4 w-4" })
3056
- ]
3057
- },
3058
- name
3059
- ))
3060
- ] })
3061
- ]
3062
- }
3063
- )
3064
- ] });
3065
- };
3066
-
3067
- const MathSelector = () => {
3068
- const { editor } = useCurrentEditor();
3069
- if (!editor) return null;
3070
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3071
- Button,
3072
- {
3073
- variant: "ghost",
3074
- size: "sm",
3075
- className: "rounded-none w-12",
3076
- onClick: () => {
3077
- if (editor.isActive("math")) {
3078
- editor.chain().focus().unsetLatex().run();
3079
- } else {
3080
- const { from, to } = editor.state.selection;
3081
- const latex = editor.state.doc.textBetween(from, to);
3082
- if (!latex) return;
3083
- editor.chain().focus().setLatex({ latex }).run();
3084
- }
3085
- },
3086
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(
3087
- Sigma,
3088
- {
3089
- className: cn("size-4", { "text-blue-500": editor.isActive("math") }),
3090
- strokeWidth: 2.3
3091
- }
3092
- )
3093
- }
3094
- );
3095
- };
3096
-
3097
- const TABLE_BUBBLE_MENU_KEY = "notebook-table-bubble-menu";
3098
- const TableActionButton = ({
3099
- label,
3100
- onClick,
3101
- disabled,
3102
- icon: Icon,
3103
- tone = "default"
3104
- }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
3105
- Button,
3106
- {
3107
- type: "button",
3108
- size: "icon",
3109
- variant: "ghost",
3110
- disabled,
3111
- onClick,
3112
- title: label,
3113
- className: cn(
3114
- "h-8 w-8",
3115
- tone === "destructive" && "text-destructive hover:text-destructive"
3116
- ),
3117
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { className: "h-4 w-4" })
3118
- }
3119
- );
3120
- const TableControls = ({ className }) => {
3121
- const { editor } = useCurrentEditor();
3122
- if (!editor) return null;
3123
- const can = editor.can();
3124
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
3125
- "div",
3126
- {
3127
- className: cn(
3128
- "flex flex-wrap items-center gap-1 rounded-md border border-muted bg-background p-1 shadow-md",
3129
- className
3130
- ),
3131
- children: [
3132
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3133
- TableActionButton,
3134
- {
3135
- label: "Add column before",
3136
- icon: ArrowLeft,
3137
- onClick: () => editor.chain().focus().addColumnBefore().run(),
3138
- disabled: !can.addColumnBefore()
3139
- }
3140
- ),
3141
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3142
- TableActionButton,
3143
- {
3144
- label: "Add column after",
3145
- icon: ArrowRight,
3146
- onClick: () => editor.chain().focus().addColumnAfter().run(),
3147
- disabled: !can.addColumnAfter()
3148
- }
3149
- ),
3150
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical", className: "h-5" }),
3151
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3152
- TableActionButton,
3153
- {
3154
- label: "Add row above",
3155
- icon: ArrowUp,
3156
- onClick: () => editor.chain().focus().addRowBefore().run(),
3157
- disabled: !can.addRowBefore()
3158
- }
3159
- ),
3160
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3161
- TableActionButton,
3162
- {
3163
- label: "Add row below",
3164
- icon: ArrowDown,
3165
- onClick: () => editor.chain().focus().addRowAfter().run(),
3166
- disabled: !can.addRowAfter()
3167
- }
3168
- ),
3169
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical", className: "h-5" }),
3170
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3171
- TableActionButton,
3172
- {
3173
- label: "Toggle header row",
3174
- icon: Heading1,
3175
- onClick: () => editor.chain().focus().toggleHeaderRow().run(),
3176
- disabled: !can.toggleHeaderRow()
3177
- }
3178
- ),
3179
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical", className: "h-5" }),
3180
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3181
- TableActionButton,
3182
- {
3183
- label: "Delete column",
3184
- icon: Minus,
3185
- onClick: () => editor.chain().focus().deleteColumn().run(),
3186
- disabled: !can.deleteColumn()
3187
- }
3188
- ),
3189
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3190
- TableActionButton,
3191
- {
3192
- label: "Delete row",
3193
- icon: Minus,
3194
- onClick: () => editor.chain().focus().deleteRow().run(),
3195
- disabled: !can.deleteRow()
3196
- }
3197
- ),
3198
- /* @__PURE__ */ jsxRuntimeExports.jsx(
3199
- TableActionButton,
3200
- {
3201
- label: "Delete table",
3202
- icon: Trash2,
3203
- onClick: () => editor.chain().focus().deleteTable().run(),
3204
- disabled: !can.deleteTable(),
3205
- tone: "destructive"
3206
- }
3207
- )
3208
- ]
3209
- }
3210
- );
3211
- };
3212
- function getActiveTableElement(editor) {
3213
- const { $from } = editor.state.selection;
3214
- const tableNode = findParentNodeClosestToPos(
3215
- $from,
3216
- (node) => node.type.name === "table"
3217
- );
3218
- if (!tableNode) return null;
3219
- const dom = editor.view.nodeDOM(tableNode.pos);
3220
- return dom instanceof HTMLElement ? dom : null;
3221
- }
3222
- const TableBubbleMenu = ({ className }) => {
3223
- const { editor } = useCurrentEditor();
3224
- if (!editor || editor.isDestroyed) return null;
3225
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3226
- P,
3227
- {
3228
- pluginKey: TABLE_BUBBLE_MENU_KEY,
3229
- shouldShow: ({ editor: current }) => current.isEditable && current.isActive("table"),
3230
- tippyOptions: {
3231
- placement: "bottom",
3232
- moveTransition: "transform 0.15s ease-out"
3233
- },
3234
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(TableControls, { className })
3235
- }
3236
- );
3237
- };
3238
- const TableToolbar = ({ className }) => {
3239
- const { editor } = useCurrentEditor();
3240
- const [position, setPosition] = reactExports.useState(null);
3241
- const updatePosition = reactExports.useCallback(() => {
3242
- if (!editor || editor.isDestroyed || !editor.isEditable) {
3243
- setPosition(null);
3244
- return;
3245
- }
3246
- if (!editor.isActive("table")) {
3247
- setPosition(null);
3248
- return;
3249
- }
3250
- const table = getActiveTableElement(editor);
3251
- if (!table) {
3252
- setPosition(null);
3253
- return;
3254
- }
3255
- const container = editor.view.dom.parentElement || editor.view.dom;
3256
- const containerRect = container.getBoundingClientRect();
3257
- const tableRect = table.getBoundingClientRect();
3258
- const top = tableRect.top - containerRect.top + container.scrollTop - 12;
3259
- const left = tableRect.left - containerRect.left + container.scrollLeft;
3260
- setPosition({ top, left, width: tableRect.width });
3261
- }, [editor]);
3262
- const scheduleUpdate = reactExports.useMemo(() => {
3263
- let frame = 0;
3264
- const handler = () => {
3265
- if (frame) return;
3266
- frame = window.requestAnimationFrame(() => {
3267
- frame = 0;
3268
- updatePosition();
3269
- });
3270
- };
3271
- return handler;
3272
- }, [updatePosition]);
3273
- reactExports.useEffect(() => {
3274
- if (!editor || editor.isDestroyed) return;
3275
- const container = editor.view.dom.parentElement || editor.view.dom;
3276
- updatePosition();
3277
- editor.on("selectionUpdate", scheduleUpdate);
3278
- editor.on("transaction", scheduleUpdate);
3279
- editor.on("focus", scheduleUpdate);
3280
- editor.on("blur", scheduleUpdate);
3281
- container.addEventListener("scroll", scheduleUpdate, { passive: true });
3282
- window.addEventListener("resize", scheduleUpdate);
3283
- return () => {
3284
- editor.off("selectionUpdate", scheduleUpdate);
3285
- editor.off("transaction", scheduleUpdate);
3286
- editor.off("focus", scheduleUpdate);
3287
- editor.off("blur", scheduleUpdate);
3288
- container.removeEventListener("scroll", scheduleUpdate);
3289
- window.removeEventListener("resize", scheduleUpdate);
3290
- };
3291
- }, [editor, scheduleUpdate, updatePosition]);
3292
- if (!editor || editor.isDestroyed || !position) return null;
3293
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3294
- "div",
3295
- {
3296
- className: cn(
3297
- "absolute z-30",
3298
- className
3299
- ),
3300
- style: {
3301
- top: position.top,
3302
- left: position.left,
3303
- width: position.width
3304
- },
3305
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(TableControls, { className: "w-full justify-start" })
3306
- }
3307
- );
3308
- };
3309
-
3310
- const EditorBubbleMenu = () => {
3311
- const { editor } = useCurrentEditor();
3312
- const [openNode, setOpenNode] = reactExports.useState(false);
3313
- const [openColor, setOpenColor] = reactExports.useState(false);
3314
- const [openLink, setOpenLink] = reactExports.useState(false);
3315
- if (!editor) return null;
3316
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3317
- P,
3318
- {
3319
- tippyOptions: {
3320
- placement: "top",
3321
- onHidden: () => {
3322
- setOpenNode(false);
3323
- setOpenColor(false);
3324
- setOpenLink(false);
3325
- }
3326
- },
3327
- className: "flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl",
3328
- children: /* @__PURE__ */ jsxRuntimeExports.jsxs(reactExports.Fragment, { children: [
3329
- /* @__PURE__ */ jsxRuntimeExports.jsx(NodeSelector, { open: openNode, onOpenChange: setOpenNode }),
3330
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical" }),
3331
- /* @__PURE__ */ jsxRuntimeExports.jsx(LinkSelector, { open: openLink, onOpenChange: setOpenLink }),
3332
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical" }),
3333
- /* @__PURE__ */ jsxRuntimeExports.jsx(MathSelector, {}),
3334
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical" }),
3335
- /* @__PURE__ */ jsxRuntimeExports.jsx(TextButtons, {}),
3336
- /* @__PURE__ */ jsxRuntimeExports.jsx(Separator, { orientation: "vertical" }),
3337
- /* @__PURE__ */ jsxRuntimeExports.jsx(ColorSelector, { open: openColor, onOpenChange: setOpenColor })
3338
- ] })
3339
- }
3340
- );
3341
- };
3342
-
3343
- function SkeletonLine({
3344
- width,
3345
- className = ""
3346
- }) {
3347
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
3348
- "div",
3349
- {
3350
- className: `h-4 bg-muted rounded animate-pulse ${className}`,
3351
- style: { width }
3352
- }
3353
- );
3354
- }
3355
- function SkeletonBlock() {
3356
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
3357
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "85%" }),
3358
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "70%" }),
3359
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "90%" })
3360
- ] });
3361
- }
3362
- function EditorLoading({ message = "Loading..." }) {
3363
- const { t } = useI18n("notebook");
3364
- const resolvedMessage = message === "Loading..." ? t("loading") : message;
3365
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "editor-loading h-full flex flex-col bg-background", children: [
3366
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b", children: [
3367
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-32 h-7 bg-muted rounded animate-pulse" }) }),
3368
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
3369
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 bg-muted rounded animate-pulse" }),
3370
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 bg-muted rounded animate-pulse" }),
3371
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 bg-muted rounded animate-pulse" })
3372
- ] })
3373
- ] }),
3374
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-[900px] mx-auto px-16 py-12 space-y-8", children: [
3375
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-10 bg-muted rounded animate-pulse w-2/3" }),
3376
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
3377
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonBlock, {}),
3378
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonBlock, {}),
3379
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pt-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-6 bg-muted rounded animate-pulse w-1/2" }) }),
3380
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonBlock, {}),
3381
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-muted/50 rounded-md p-4 space-y-2", children: [
3382
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "60%", className: "h-3" }),
3383
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "80%", className: "h-3" }),
3384
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "45%", className: "h-3" }),
3385
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonLine, { width: "70%", className: "h-3" })
3386
- ] }),
3387
- /* @__PURE__ */ jsxRuntimeExports.jsx(SkeletonBlock, {})
3388
- ] })
3389
- ] }) }),
3390
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-3 bg-background/80 px-6 py-4 rounded-lg shadow-lg backdrop-blur-sm", children: [
3391
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-8 h-8", children: [
3392
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 border-2 border-muted rounded-full" }),
3393
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 border-2 border-primary border-t-transparent rounded-full animate-spin" })
3394
- ] }),
3395
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-muted-foreground", children: resolvedMessage })
3396
- ] }) })
3397
- ] });
3398
- }
3399
-
3400
- function SaveStatusIndicator({ status }) {
3401
- const { t } = useI18n("notebook");
3402
- switch (status) {
3403
- case "saving":
3404
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
3405
- /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-3 h-3 animate-spin" }),
3406
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("saving") })
3407
- ] });
3408
- case "saved":
3409
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-green-600", children: [
3410
- /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "w-3 h-3" }),
3411
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("saved") })
3412
- ] });
3413
- case "error":
3414
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-destructive", children: [
3415
- /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "w-3 h-3" }),
3416
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("save_failed") })
3417
- ] });
3418
- default:
3419
- return null;
3420
- }
3421
- }
3422
- function NotebookToolbar({
3423
- notebookId,
3424
- autoSaveStatus,
3425
- getMarkdown,
3426
- allowCopy = true
3427
- }) {
3428
- const [isCopying, setIsCopying] = reactExports.useState(false);
3429
- const { toast } = useToast();
3430
- const { t } = useI18n("notebook");
3431
- const canCopy = allowCopy && (Boolean(notebookId) || Boolean(getMarkdown));
3432
- const resolveMarkdownContent = reactExports.useCallback(async () => {
3433
- if (getMarkdown) {
3434
- const content = await Promise.resolve(getMarkdown());
3435
- if (typeof content === "string") return content;
3436
- }
3437
- if (!notebookId) return "";
3438
- return await getFileContent(notebookId);
3439
- }, [getMarkdown, notebookId]);
3440
- const handleCopy = reactExports.useCallback(async () => {
3441
- if (!canCopy) return;
3442
- setIsCopying(true);
3443
- try {
3444
- const markdown = await resolveMarkdownContent();
3445
- if (navigator?.clipboard?.writeText) {
3446
- await navigator.clipboard.writeText(markdown);
3447
- } else {
3448
- const textarea = document.createElement("textarea");
3449
- textarea.value = markdown;
3450
- textarea.style.position = "fixed";
3451
- textarea.style.opacity = "0";
3452
- document.body.appendChild(textarea);
3453
- textarea.focus();
3454
- textarea.select();
3455
- document.execCommand("copy");
3456
- textarea.remove();
3457
- }
3458
- toast({
3459
- title: t("copy_success_title"),
3460
- description: t("copy_success_desc"),
3461
- variant: "success"
3462
- });
3463
- } catch (error) {
3464
- console.error("[NotebookToolbar] Copy failed:", error);
3465
- toast({
3466
- title: t("copy_failed_title"),
3467
- description: t("try_again"),
3468
- variant: "destructive"
3469
- });
3470
- } finally {
3471
- setIsCopying(false);
3472
- }
3473
- }, [canCopy, resolveMarkdownContent, t, toast]);
3474
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "notebook-toolbar flex items-center justify-between px-4 py-2 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60", children: [
3475
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SaveStatusIndicator, { status: autoSaveStatus }) }),
3476
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-1", children: canCopy ? /* @__PURE__ */ jsxRuntimeExports.jsx(
3477
- "button",
3478
- {
3479
- type: "button",
3480
- className: "flex items-center gap-1.5 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors",
3481
- onClick: handleCopy,
3482
- title: t("copy"),
3483
- children: isCopying ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { className: "w-4 h-4" })
3484
- }
3485
- ) : null })
3486
- ] });
3487
- }
3488
-
3489
- const DEFAULT_DOC = {
3490
- type: "doc",
3491
- content: [{ type: "paragraph" }]
3492
- };
3493
- const YJS_FIELD = "prosemirror";
3494
- function isEditorDocEmpty(editor) {
3495
- const doc = editor.state.doc;
3496
- if (doc.childCount !== 1) return false;
3497
- const first = doc.firstChild;
3498
- if (!first) return true;
3499
- if (first.type.name !== "paragraph") return false;
3500
- return first.content.size === 0 && doc.textContent.trim().length === 0;
3501
- }
3502
- function NotebookEditor({
3503
- context,
3504
- setDirty,
3505
- setTitle,
3506
- className,
3507
- onMarkdownChange
3508
- }) {
3509
- const notebookId = context.resourceId;
3510
- const notebookName = context.resourceName || "Untitled Notebook";
3511
- const inlineMarkdown = typeof context.customData?.inlineMarkdown === "string" ? context.customData.inlineMarkdown : null;
3512
- const isInline = inlineMarkdown !== null;
3513
- const isReadonly = context.customData?.readonly === true || isInline;
3514
- const fallbackProjectId = useFileTreeStore((state) => state.projectId);
3515
- const projectId = typeof context.customData?.projectId === "string" ? context.customData.projectId : fallbackProjectId || void 0;
3516
- const filePath = context.resourcePath ?? void 0;
3517
- const docKind = inferNotebookDocKind(context);
3518
- const isMarkdownDoc = docKind === "markdown";
3519
- const markdownAssetContext = reactExports.useMemo(
3520
- () => isMarkdownDoc ? getQuestMarkdownContextFromFileId(notebookId) : null,
3521
- [isMarkdownDoc, notebookId]
3522
- );
3523
- const providerRef = reactExports.useRef(null);
3524
- const projectIdRef = reactExports.useRef(projectId);
3525
- const saveTimeoutRef = reactExports.useRef(null);
3526
- const markdownChangeTimeoutRef = reactExports.useRef(null);
3527
- const baselineMarkdownRef = reactExports.useRef(null);
3528
- const activeNotebookIdRef = reactExports.useRef(null);
3529
- const currentNotebookIdRef = reactExports.useRef(null);
3530
- const suppressMarkdownUpdateRef = reactExports.useRef(false);
3531
- const [editor, setEditor] = reactExports.useState(null);
3532
- const [ydoc, setYdoc] = reactExports.useState(null);
3533
- const [initialMarkdown, setInitialMarkdown] = reactExports.useState(null);
3534
- const baselineNotebookMarkdownRef = reactExports.useRef(null);
3535
- const [loadedNotebookId, setLoadedNotebookId] = reactExports.useState(null);
3536
- const [isInitializing, setIsInitializing] = reactExports.useState(true);
3537
- const [error, setError] = reactExports.useState(null);
3538
- const [autoSaveStatus, setAutoSaveStatus] = reactExports.useState("idle");
3539
- const [resetNonce, setResetNonce] = reactExports.useState(0);
3540
- const { diffEvent, clearDiff } = useFileDiffOverlay({
3541
- fileId: notebookId ?? void 0,
3542
- filePath,
3543
- projectId
3544
- });
3545
- const getCurrentMarkdown = reactExports.useCallback(() => {
3546
- if (editor) return getEditorMarkdown(editor);
3547
- if (typeof initialMarkdown === "string") return initialMarkdown;
3548
- if (typeof baselineNotebookMarkdownRef.current === "string") {
3549
- return baselineNotebookMarkdownRef.current;
3550
- }
3551
- return "";
3552
- }, [editor, initialMarkdown]);
3553
- const toDisplayMarkdown = reactExports.useCallback(
3554
- (markdown) => isMarkdownDoc ? rewriteQuestMarkdownForDisplay(markdown, markdownAssetContext) : markdown,
3555
- [isMarkdownDoc, markdownAssetContext]
3556
- );
3557
- const toStoredMarkdown = reactExports.useCallback(
3558
- (markdown) => isMarkdownDoc ? rewriteQuestMarkdownForSave(markdown, markdownAssetContext) : markdown,
3559
- [isMarkdownDoc, markdownAssetContext]
3560
- );
3561
- const reloadMarkdownFromServer = reactExports.useCallback(
3562
- async (force) => {
3563
- if (!editor || !notebookId || !isMarkdownDoc) return;
3564
- const currentMarkdown = getEditorMarkdown(editor);
3565
- const baseline = baselineMarkdownRef.current ?? currentMarkdown;
3566
- if (!force && currentMarkdown !== baseline) return;
3567
- try {
3568
- const latest = toDisplayMarkdown(await getFileContent(notebookId));
3569
- suppressMarkdownUpdateRef.current = true;
3570
- setEditorMarkdown(editor, latest);
3571
- baselineMarkdownRef.current = latest;
3572
- } catch (error2) {
3573
- console.warn("[NotebookEditor] Failed to refresh markdown", error2);
3574
- }
3575
- },
3576
- [editor, isMarkdownDoc, notebookId, toDisplayMarkdown]
3577
- );
3578
- const notebookIdValue = notebookId ?? null;
3579
- if (currentNotebookIdRef.current !== notebookIdValue) {
3580
- currentNotebookIdRef.current = notebookIdValue;
3581
- activeNotebookIdRef.current = null;
3582
- }
3583
- reactExports.useEffect(() => {
3584
- if (!diffEvent?.diff) return;
3585
- void reloadMarkdownFromServer(false);
3586
- }, [diffEvent, reloadMarkdownFromServer]);
3587
- reactExports.useEffect(() => {
3588
- if (!notebookId || isInline) return;
3589
- const handleReload = (event) => {
3590
- const detail = event.detail;
3591
- if (!detail) return;
3592
- if (detail.fileId && detail.fileId !== notebookId) return;
3593
- if (!detail.fileId && detail.filePath && filePath && detail.filePath !== filePath) {
3594
- return;
3595
- }
3596
- if (!detail.fileId && !detail.filePath) return;
3597
- void reloadMarkdownFromServer(Boolean(detail.force));
3598
- };
3599
- window.addEventListener("ds:file:reload", handleReload);
3600
- return () => window.removeEventListener("ds:file:reload", handleReload);
3601
- }, [filePath, isInline, notebookId, reloadMarkdownFromServer]);
3602
- const imageUploadFn = reactExports.useMemo(() => {
3603
- return q({
3604
- onUpload: async (file) => {
3605
- const currentProjectId = projectIdRef.current;
3606
- if (!currentProjectId) {
3607
- console.warn("[NotebookEditor] upload skipped: missing projectId", {
3608
- fileName: file.name,
3609
- fileType: file.type
3610
- });
3611
- throw new Error("Missing project ID for upload.");
3612
- }
3613
- console.debug("[NotebookEditor] upload start", {
3614
- fileName: file.name,
3615
- fileType: file.type,
3616
- fileSize: file.size
3617
- });
3618
- if (isMarkdownDoc && markdownAssetContext) {
3619
- const uploaded = await uploadQuestDocumentAsset(
3620
- currentProjectId,
3621
- markdownAssetContext.baseDocumentId,
3622
- file,
3623
- "image"
3624
- );
3625
- return String(resolveNotebookAssetUrl(uploaded.asset_url));
3626
- }
3627
- return uploadNotebookAsset(currentProjectId, file, "image");
3628
- },
3629
- validateFn: (file) => {
3630
- try {
3631
- if (!projectIdRef.current) {
3632
- console.warn("[NotebookEditor] validate failed: missing projectId", {
3633
- fileName: file.name,
3634
- fileType: file.type
3635
- });
3636
- return false;
3637
- }
3638
- if (!isSupportedNotebookAsset(file, "image")) {
3639
- console.warn("[NotebookEditor] validate failed: unsupported image", {
3640
- fileName: file.name,
3641
- fileType: file.type
3642
- });
3643
- return false;
3644
- }
3645
- if (file.size > 10 * 1024 * 1024) {
3646
- console.warn("[NotebookEditor] validate failed: size limit", {
3647
- fileName: file.name,
3648
- fileSize: file.size
3649
- });
3650
- return false;
3651
- }
3652
- return true;
3653
- } catch {
3654
- console.warn("[NotebookEditor] validate failed: unexpected error", {
3655
- fileName: file.name,
3656
- fileType: file.type
3657
- });
3658
- return false;
3659
- }
3660
- }
3661
- });
3662
- }, [isMarkdownDoc, markdownAssetContext]);
3663
- reactExports.useEffect(() => {
3664
- projectIdRef.current = projectId;
3665
- }, [projectId]);
3666
- const handleVideoFiles = reactExports.useCallback(
3667
- async (files) => {
3668
- const currentProjectId = projectIdRef.current;
3669
- if (!editor || !currentProjectId) return;
3670
- const videoFiles = files.filter(
3671
- (file) => file.type.startsWith("video/")
3672
- );
3673
- if (videoFiles.length === 0) return;
3674
- for (const file of videoFiles) {
3675
- try {
3676
- console.debug("[NotebookEditor] video upload start", {
3677
- fileName: file.name,
3678
- fileType: file.type,
3679
- fileSize: file.size
3680
- });
3681
- const src = await uploadNotebookAsset(currentProjectId, file, "video");
3682
- editor.chain().focus().insertContent({ type: "video", attrs: { src } }).run();
3683
- } catch (err) {
3684
- console.error("[NotebookEditor] Video upload failed:", err);
3685
- }
3686
- }
3687
- },
3688
- [editor]
3689
- );
3690
- reactExports.useEffect(() => {
3691
- setTitle(notebookName);
3692
- }, [notebookName, setTitle]);
3693
- reactExports.useEffect(() => {
3694
- setDirty(autoSaveStatus === "saving" || autoSaveStatus === "error");
3695
- }, [autoSaveStatus, setDirty]);
3696
- reactExports.useEffect(() => {
3697
- if (!editor || initialMarkdown === null) return;
3698
- if (!notebookId) return;
3699
- if (activeNotebookIdRef.current !== notebookId) return;
3700
- if (!initialMarkdown.trim()) return;
3701
- if (!isEditorDocEmpty(editor)) return;
3702
- suppressMarkdownUpdateRef.current = true;
3703
- setEditorMarkdown(editor, initialMarkdown);
3704
- if (isMarkdownDoc) {
3705
- baselineMarkdownRef.current = initialMarkdown;
3706
- } else {
3707
- baselineNotebookMarkdownRef.current = initialMarkdown;
3708
- }
3709
- }, [editor, initialMarkdown, isMarkdownDoc, notebookId]);
3710
- reactExports.useEffect(() => {
3711
- if (!onMarkdownChange) return;
3712
- if (!isMarkdownDoc) return;
3713
- if (initialMarkdown === null) return;
3714
- onMarkdownChange(initialMarkdown);
3715
- }, [initialMarkdown, isMarkdownDoc, onMarkdownChange]);
3716
- const handleImageUpload = reactExports.useCallback(
3717
- async (file, editorInstance) => {
3718
- const currentProjectId = projectIdRef.current;
3719
- if (!currentProjectId) return;
3720
- try {
3721
- console.debug("[NotebookEditor] image upload start", {
3722
- fileName: file.name,
3723
- fileType: file.type,
3724
- fileSize: file.size
3725
- });
3726
- const src = isMarkdownDoc && markdownAssetContext ? String(
3727
- resolveNotebookAssetUrl(
3728
- (await uploadQuestDocumentAsset(
3729
- currentProjectId,
3730
- markdownAssetContext.baseDocumentId,
3731
- file,
3732
- "image"
3733
- )).asset_url
3734
- )
3735
- ) : await uploadNotebookAsset(currentProjectId, file, "image");
3736
- editorInstance.chain().focus().setImage({ src }).run();
3737
- } catch (err) {
3738
- console.error("[NotebookEditor] Image upload failed:", err);
3739
- }
3740
- },
3741
- [isMarkdownDoc, markdownAssetContext]
3742
- );
3743
- const suggestionItems = reactExports.useMemo(
3744
- () => buildSuggestionItems(handleImageUpload),
3745
- [handleImageUpload]
3746
- );
3747
- const slashCommand = reactExports.useMemo(
3748
- () => createSlashCommand(suggestionItems),
3749
- [suggestionItems]
3750
- );
3751
- const extensions = reactExports.useMemo(() => {
3752
- const list = [...defaultExtensions, slashCommand];
3753
- if (ydoc) {
3754
- list.unshift(
3755
- Collaboration.configure({ document: ydoc, field: YJS_FIELD })
3756
- );
3757
- }
3758
- return list;
3759
- }, [ydoc, slashCommand]);
3760
- reactExports.useEffect(() => {
3761
- if (isInline) {
3762
- setError(null);
3763
- setIsInitializing(false);
3764
- setAutoSaveStatus("idle");
3765
- setInitialMarkdown(toDisplayMarkdown(inlineMarkdown ?? ""));
3766
- setYdoc(null);
3767
- setLoadedNotebookId(notebookIdValue);
3768
- baselineMarkdownRef.current = toDisplayMarkdown(inlineMarkdown ?? "");
3769
- baselineNotebookMarkdownRef.current = inlineMarkdown ?? "";
3770
- suppressMarkdownUpdateRef.current = false;
3771
- activeNotebookIdRef.current = notebookIdValue;
3772
- return;
3773
- }
3774
- if (!notebookId) {
3775
- setError("No notebook ID provided");
3776
- setIsInitializing(false);
3777
- return;
3778
- }
3779
- setError(null);
3780
- setIsInitializing(true);
3781
- setAutoSaveStatus("idle");
3782
- setInitialMarkdown(null);
3783
- setYdoc(null);
3784
- setLoadedNotebookId(null);
3785
- baselineMarkdownRef.current = null;
3786
- baselineNotebookMarkdownRef.current = null;
3787
- suppressMarkdownUpdateRef.current = false;
3788
- activeNotebookIdRef.current = null;
3789
- let canceled = false;
3790
- const init = async () => {
3791
- try {
3792
- let markdown = null;
3793
- try {
3794
- markdown = await getFileContent(notebookId);
3795
- } catch (err) {
3796
- if (isMarkdownDoc) {
3797
- throw err;
3798
- }
3799
- console.warn(
3800
- "[NotebookEditor] Failed to load markdown for notebook:",
3801
- err
3802
- );
3803
- }
3804
- if (canceled) return;
3805
- const displayMarkdown = toDisplayMarkdown(markdown ?? "");
3806
- setInitialMarkdown(displayMarkdown);
3807
- baselineNotebookMarkdownRef.current = markdown ?? "";
3808
- if (isMarkdownDoc) {
3809
- baselineMarkdownRef.current = displayMarkdown;
3810
- activeNotebookIdRef.current = notebookId;
3811
- setLoadedNotebookId(notebookId);
3812
- setIsInitializing(false);
3813
- return;
3814
- }
3815
- if (!projectId) {
3816
- setError("No project ID provided");
3817
- setIsInitializing(false);
3818
- return;
3819
- }
3820
- const provider = await createNovelYjsProvider({
3821
- projectId,
3822
- notebookId,
3823
- readonly: isReadonly,
3824
- onStatus: setAutoSaveStatus,
3825
- onReset: () => {
3826
- setHistoryOpen(false);
3827
- setCollaboratorsOpen(false);
3828
- setResetNonce((v) => v + 1);
3829
- }
3830
- });
3831
- if (canceled) {
3832
- provider.dispose();
3833
- return;
3834
- }
3835
- providerRef.current = provider;
3836
- setYdoc(provider.ydoc);
3837
- activeNotebookIdRef.current = notebookId;
3838
- setLoadedNotebookId(notebookId);
3839
- setIsInitializing(false);
3840
- } catch (err) {
3841
- if (canceled) return;
3842
- console.error("[NotebookEditor] Initialization failed:", err);
3843
- setError(
3844
- err instanceof Error ? err.message : "Failed to initialize editor"
3845
- );
3846
- setIsInitializing(false);
3847
- }
3848
- };
3849
- init();
3850
- return () => {
3851
- canceled = true;
3852
- if (saveTimeoutRef.current) {
3853
- clearTimeout(saveTimeoutRef.current);
3854
- saveTimeoutRef.current = null;
3855
- }
3856
- if (markdownChangeTimeoutRef.current) {
3857
- clearTimeout(markdownChangeTimeoutRef.current);
3858
- markdownChangeTimeoutRef.current = null;
3859
- }
3860
- activeNotebookIdRef.current = null;
3861
- providerRef.current?.dispose();
3862
- providerRef.current = null;
3863
- setEditor(null);
3864
- };
3865
- }, [inlineMarkdown, isInline, notebookId, notebookIdValue, projectId, isMarkdownDoc, isReadonly, resetNonce, toDisplayMarkdown]);
3866
- const handleMarkdownUpdate = reactExports.useCallback(
3867
- (instance) => {
3868
- if (isReadonly || !isMarkdownDoc) return;
3869
- if (!notebookId) return;
3870
- if (activeNotebookIdRef.current !== notebookId) return;
3871
- if (suppressMarkdownUpdateRef.current) {
3872
- suppressMarkdownUpdateRef.current = false;
3873
- return;
3874
- }
3875
- if (onMarkdownChange) {
3876
- if (markdownChangeTimeoutRef.current) {
3877
- clearTimeout(markdownChangeTimeoutRef.current);
3878
- }
3879
- const previewNotebookId = notebookId;
3880
- markdownChangeTimeoutRef.current = setTimeout(() => {
3881
- if (activeNotebookIdRef.current !== previewNotebookId) {
3882
- return;
3883
- }
3884
- onMarkdownChange(getEditorMarkdown(instance));
3885
- }, 120);
3886
- }
3887
- if (saveTimeoutRef.current) {
3888
- clearTimeout(saveTimeoutRef.current);
3889
- }
3890
- setAutoSaveStatus("saving");
3891
- const saveNotebookId = notebookId;
3892
- saveTimeoutRef.current = setTimeout(async () => {
3893
- try {
3894
- if (activeNotebookIdRef.current !== saveNotebookId) {
3895
- return;
3896
- }
3897
- const markdown = getEditorMarkdown(instance);
3898
- const storedMarkdown = toStoredMarkdown(markdown);
3899
- if (baselineMarkdownRef.current === markdown) {
3900
- setAutoSaveStatus("saved");
3901
- setTimeout(() => setAutoSaveStatus("idle"), 1500);
3902
- return;
3903
- }
3904
- await updateFileContent(saveNotebookId, storedMarkdown);
3905
- baselineMarkdownRef.current = markdown;
3906
- setAutoSaveStatus("saved");
3907
- setTimeout(() => setAutoSaveStatus("idle"), 1500);
3908
- } catch (err) {
3909
- console.error("[NotebookEditor] Markdown save failed:", err);
3910
- setAutoSaveStatus("error");
3911
- }
3912
- }, 1500);
3913
- },
3914
- [isReadonly, isMarkdownDoc, notebookId, onMarkdownChange, toStoredMarkdown]
3915
- );
3916
- const handleNotebookMarkdownUpdate = reactExports.useCallback(
3917
- (instance) => {
3918
- if (isReadonly || isMarkdownDoc) return;
3919
- if (!notebookId) return;
3920
- if (activeNotebookIdRef.current !== notebookId) return;
3921
- if (suppressMarkdownUpdateRef.current) {
3922
- suppressMarkdownUpdateRef.current = false;
3923
- return;
3924
- }
3925
- if (saveTimeoutRef.current) {
3926
- clearTimeout(saveTimeoutRef.current);
3927
- }
3928
- setAutoSaveStatus("saving");
3929
- const saveNotebookId = notebookId;
3930
- saveTimeoutRef.current = setTimeout(async () => {
3931
- try {
3932
- if (activeNotebookIdRef.current !== saveNotebookId) {
3933
- return;
3934
- }
3935
- const markdown = getEditorMarkdown(instance);
3936
- if (baselineNotebookMarkdownRef.current === markdown) {
3937
- setAutoSaveStatus("saved");
3938
- setTimeout(() => setAutoSaveStatus("idle"), 1500);
3939
- return;
3940
- }
3941
- await updateFileContent(saveNotebookId, markdown);
3942
- baselineNotebookMarkdownRef.current = markdown;
3943
- setAutoSaveStatus("saved");
3944
- setTimeout(() => setAutoSaveStatus("idle"), 1500);
3945
- } catch (err) {
3946
- console.error("[NotebookEditor] Notebook markdown save failed:", err);
3947
- setAutoSaveStatus("error");
3948
- }
3949
- }, 1500);
3950
- },
3951
- [isReadonly, isMarkdownDoc, notebookId]
3952
- );
3953
- const isFileTransfer = reactExports.useCallback((dataTransfer) => {
3954
- if (!dataTransfer) return false;
3955
- if (dataTransfer.files && dataTransfer.files.length > 0) return true;
3956
- const items = dataTransfer.items ? Array.from(dataTransfer.items) : [];
3957
- return items.some((item) => item.kind === "file");
3958
- }, []);
3959
- const getDroppedFiles = reactExports.useCallback((dataTransfer) => {
3960
- if (!dataTransfer) return [];
3961
- if (!isFileTransfer(dataTransfer)) return [];
3962
- if (dataTransfer.files && dataTransfer.files.length > 0) {
3963
- return Array.from(dataTransfer.files);
3964
- }
3965
- const items = dataTransfer.items ? Array.from(dataTransfer.items) : [];
3966
- const files = [];
3967
- for (const item of items) {
3968
- if (item.kind === "file") {
3969
- const file = item.getAsFile();
3970
- if (file) files.push(file);
3971
- }
3972
- }
3973
- return files;
3974
- }, [isFileTransfer]);
3975
- const handleContainerDragOver = reactExports.useCallback(
3976
- (event) => {
3977
- if (editor?.view.dragging) return;
3978
- if (!isFileTransfer(event.dataTransfer)) return;
3979
- const files = getDroppedFiles(event.dataTransfer);
3980
- if (!files.length) return;
3981
- event.preventDefault();
3982
- event.dataTransfer.dropEffect = "copy";
3983
- console.debug("[NotebookEditor] dragover", {
3984
- fileCount: files.length,
3985
- fileTypes: files.map((file) => file.type || "unknown"),
3986
- dataTypes: Array.from(event.dataTransfer.types ?? []),
3987
- projectId: projectIdRef.current
3988
- });
3989
- },
3990
- [editor, getDroppedFiles, isFileTransfer]
3991
- );
3992
- const handleContainerDrop = reactExports.useCallback(
3993
- (event) => {
3994
- if (editor?.view.dragging) return;
3995
- if (!isFileTransfer(event.dataTransfer)) return;
3996
- const files = getDroppedFiles(event.dataTransfer);
3997
- console.debug("[NotebookEditor] container drop", {
3998
- fileCount: files.length,
3999
- fileTypes: files.map((file) => file.type || "unknown"),
4000
- dataTypes: Array.from(event.dataTransfer.types ?? []),
4001
- hasUploadFn: Boolean(imageUploadFn),
4002
- projectId: projectIdRef.current
4003
- });
4004
- if (!files.length) return;
4005
- event.preventDefault();
4006
- event.stopPropagation();
4007
- if (!editor) {
4008
- console.warn("[NotebookEditor] container drop ignored: editor missing");
4009
- return;
4010
- }
4011
- const hasVideo = files.some((file) => file.type.startsWith("video/"));
4012
- if (hasVideo) {
4013
- handleVideoFiles(files);
4014
- }
4015
- if (imageUploadFn) {
4016
- const handled = J(
4017
- editor.view,
4018
- event.nativeEvent,
4019
- false,
4020
- imageUploadFn
4021
- );
4022
- console.debug("[NotebookEditor] container drop handled", { handled });
4023
- } else {
4024
- console.warn("[NotebookEditor] container drop ignored: uploadFn missing");
4025
- }
4026
- },
4027
- [editor, getDroppedFiles, handleVideoFiles, imageUploadFn, isFileTransfer]
4028
- );
4029
- if (error) {
4030
- return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "notebook-editor-error h-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center space-y-4 p-8 max-w-md", children: [
4031
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-destructive text-lg font-medium", children: "Failed to load notebook" }),
4032
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-muted-foreground text-sm", children: error }),
4033
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-2 justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
4034
- "button",
4035
- {
4036
- className: "px-4 py-2 bg-primary text-primary-foreground rounded-md text-sm",
4037
- onClick: () => window.location.reload(),
4038
- children: "Retry"
4039
- }
4040
- ) })
4041
- ] }) });
4042
- }
4043
- const isNotebookReady = loadedNotebookId === notebookIdValue && !isInitializing;
4044
- if (!isNotebookReady) {
4045
- return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorLoading, { message: "Initializing editor..." });
4046
- }
4047
- if (!isMarkdownDoc && !ydoc) {
4048
- return /* @__PURE__ */ jsxRuntimeExports.jsx(EditorLoading, { message: "Loading document..." });
4049
- }
4050
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
4051
- "div",
4052
- {
4053
- className: `notebook-editor flex flex-col flex-1 min-h-0 ${className || ""}`,
4054
- children: [
4055
- /* @__PURE__ */ jsxRuntimeExports.jsx(
4056
- NotebookToolbar,
4057
- {
4058
- notebookId: !isInline && notebookId ? notebookId : "",
4059
- autoSaveStatus,
4060
- getMarkdown: getCurrentMarkdown
4061
- }
4062
- ),
4063
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden flex justify-center px-4 py-4 bg-muted/10", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "relative h-full w-[min(62.5vw,100%)] max-w-[70rem]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
4064
- "div",
4065
- {
4066
- className: "notebook-editor-container relative h-full overflow-hidden rounded-2xl border border-border bg-background shadow-soft-card",
4067
- "data-notebook-id": notebookId,
4068
- onDragOver: handleContainerDragOver,
4069
- onDrop: handleContainerDrop,
4070
- children: [
4071
- diffEvent?.diff ? /* @__PURE__ */ jsxRuntimeExports.jsx(
4072
- FileDiffPanel,
4073
- {
4074
- diff: diffEvent.diff,
4075
- changeType: diffEvent.changeType,
4076
- title: "AI change",
4077
- subtitle: notebookName,
4078
- onClose: clearDiff,
4079
- className: "absolute right-4 top-4 z-20 w-[min(420px,46vw)]"
4080
- }
4081
- ) : null,
4082
- /* @__PURE__ */ jsxRuntimeExports.jsx(U, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
4083
- I,
4084
- {
4085
- extensions,
4086
- className: "notebook-doc-editor relative h-full w-full overflow-y-auto",
4087
- editorProps: {
4088
- handleDOMEvents: {
4089
- keydown: (_view, event) => ce(event)
4090
- },
4091
- handlePaste: (view, event) => {
4092
- const files = Array.from(event.clipboardData?.files ?? []);
4093
- const hasVideo = files.some(
4094
- (file) => file.type.startsWith("video/")
4095
- );
4096
- if (hasVideo) {
4097
- event.preventDefault();
4098
- handleVideoFiles(files);
4099
- if (imageUploadFn) {
4100
- G(view, event, imageUploadFn);
4101
- }
4102
- return true;
4103
- }
4104
- if (imageUploadFn) {
4105
- return G(view, event, imageUploadFn);
4106
- }
4107
- return false;
4108
- },
4109
- handleDrop: (view, event, _slice, moved) => {
4110
- const isFileDrop = isFileTransfer(event.dataTransfer);
4111
- if (!isFileDrop) {
4112
- return false;
4113
- }
4114
- if (moved) {
4115
- console.debug("[NotebookEditor] drop ignored: internal move", {
4116
- moved,
4117
- projectId: projectIdRef.current
4118
- });
4119
- return false;
4120
- }
4121
- const files = Array.from(event.dataTransfer?.files ?? []);
4122
- console.debug("[NotebookEditor] drop", {
4123
- moved,
4124
- fileCount: files.length,
4125
- fileTypes: files.map((file) => file.type || "unknown"),
4126
- dataTypes: Array.from(event.dataTransfer?.types ?? []),
4127
- hasUploadFn: Boolean(imageUploadFn),
4128
- projectId: projectIdRef.current
4129
- });
4130
- const hasVideo = files.some(
4131
- (file) => file.type.startsWith("video/")
4132
- );
4133
- if (hasVideo) {
4134
- event.preventDefault();
4135
- handleVideoFiles(files);
4136
- if (imageUploadFn) {
4137
- const handled = J(view, event, moved, imageUploadFn);
4138
- console.debug("[NotebookEditor] drop handled (video+image)", { handled });
4139
- }
4140
- return true;
4141
- }
4142
- if (imageUploadFn) {
4143
- const handled = J(view, event, moved, imageUploadFn);
4144
- console.debug("[NotebookEditor] drop handled (image)", { handled });
4145
- return handled;
4146
- }
4147
- console.warn("[NotebookEditor] drop ignored: uploadFn missing");
4148
- return false;
4149
- },
4150
- attributes: {
4151
- class: "prose prose-lg dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full"
4152
- }
4153
- },
4154
- onCreate: ({ editor: instance }) => {
4155
- setEditor(instance);
4156
- if (isMarkdownDoc && initialMarkdown !== null) {
4157
- suppressMarkdownUpdateRef.current = true;
4158
- setEditorMarkdown(instance, initialMarkdown);
4159
- baselineMarkdownRef.current = initialMarkdown;
4160
- }
4161
- if (!isMarkdownDoc && ydoc) {
4162
- const fragment = ydoc.getXmlFragment(YJS_FIELD);
4163
- if (fragment.length === 0 && initialMarkdown !== null) {
4164
- suppressMarkdownUpdateRef.current = true;
4165
- setEditorMarkdown(instance, initialMarkdown);
4166
- baselineNotebookMarkdownRef.current = initialMarkdown;
4167
- } else if (fragment.length === 0 && !isReadonly) {
4168
- instance.commands.setContent(DEFAULT_DOC);
4169
- }
4170
- }
4171
- if (isReadonly) {
4172
- instance.setEditable(false);
4173
- }
4174
- },
4175
- onUpdate: ({ editor: instance }) => {
4176
- handleMarkdownUpdate(instance);
4177
- handleNotebookMarkdownUpdate(instance);
4178
- },
4179
- slotAfter: /* @__PURE__ */ jsxRuntimeExports.jsx(K$1, {}),
4180
- children: [
4181
- /* @__PURE__ */ jsxRuntimeExports.jsxs(T, { className: "z-50 h-auto max-h-[330px] overflow-y-auto rounded-md border border-muted bg-background px-1 py-2 text-foreground shadow-md transition-all", children: [
4182
- /* @__PURE__ */ jsxRuntimeExports.jsx(B, { className: "px-2 text-muted-foreground", children: "No results" }),
4183
- /* @__PURE__ */ jsxRuntimeExports.jsx(O, { children: suggestionItems.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
4184
- S,
4185
- {
4186
- value: item.title,
4187
- onCommand: (val) => item.command?.(val),
4188
- className: "flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm hover:bg-accent aria-selected:bg-accent",
4189
- children: [
4190
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-md border border-muted bg-background text-foreground", children: item.icon }),
4191
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
4192
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-medium", children: item.title }),
4193
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-muted-foreground", children: item.description })
4194
- ] })
4195
- ]
4196
- },
4197
- item.title
4198
- )) })
4199
- ] }),
4200
- /* @__PURE__ */ jsxRuntimeExports.jsx(TableToolbar, {}),
4201
- /* @__PURE__ */ jsxRuntimeExports.jsx(TableBubbleMenu, {}),
4202
- /* @__PURE__ */ jsxRuntimeExports.jsx(EditorBubbleMenu, {})
4203
- ]
4204
- }
4205
- ) })
4206
- ]
4207
- }
4208
- ) }) })
4209
- ]
4210
- }
4211
- );
4212
- }
4213
-
4214
- export { EditorLoading as E, NotebookEditor as N, NotebookToolbar as a };