@ecsplico/editor 2.1.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (692) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +39 -156
  3. package/dist/adapters/comments/drizzle.d.ts +1 -1
  4. package/dist/adapters/comments/{client.js → rest-model.js} +3 -3
  5. package/dist/adapters/comments/rest-model.js.map +1 -0
  6. package/dist/adapters/comments/types.d.ts +2 -2
  7. package/dist/adapters/document-state.d.ts +23 -0
  8. package/dist/adapters/document-state.js +66 -0
  9. package/dist/adapters/document-state.js.map +1 -0
  10. package/dist/adapters/documents.js +14 -11
  11. package/dist/adapters/documents.js.map +1 -1
  12. package/dist/adapters/drizzle.d.ts +2 -2
  13. package/dist/adapters/drizzle.js +97 -88
  14. package/dist/adapters/drizzle.js.map +1 -1
  15. package/dist/adapters/forge.d.ts +12 -7
  16. package/dist/adapters/forge.js +38 -45
  17. package/dist/adapters/forge.js.map +1 -1
  18. package/dist/adapters/http.d.ts +20 -0
  19. package/dist/adapters/http.js +68 -0
  20. package/dist/adapters/http.js.map +1 -0
  21. package/dist/adapters/index.d.ts +5 -5
  22. package/dist/adapters/json.d.ts +3 -3
  23. package/dist/adapters/json.js +58 -49
  24. package/dist/adapters/json.js.map +1 -1
  25. package/dist/adapters/memory.d.ts +21 -0
  26. package/dist/adapters/memory.js +53 -0
  27. package/dist/adapters/memory.js.map +1 -0
  28. package/dist/adapters/nitro.d.ts +23 -20
  29. package/dist/adapters/nitro.js +58 -163
  30. package/dist/adapters/nitro.js.map +1 -1
  31. package/dist/adapters/react.d.ts +55 -0
  32. package/dist/adapters/react.js +209 -0
  33. package/dist/adapters/react.js.map +1 -0
  34. package/dist/adapters/shared.d.ts +15 -21
  35. package/dist/adapters/shared.js +10 -8
  36. package/dist/adapters/shared.js.map +1 -1
  37. package/dist/adapters/vue.d.ts +15 -30
  38. package/dist/adapters/vue.js +36 -87
  39. package/dist/adapters/vue.js.map +1 -1
  40. package/dist/config/index.d.ts +105 -0
  41. package/dist/config/index.js +95 -0
  42. package/dist/config/index.js.map +1 -0
  43. package/dist/core/abilities.js +1 -0
  44. package/dist/core/abilities.js.map +1 -1
  45. package/dist/{adapters/routes.js → core/api-routes.js} +5 -3
  46. package/dist/core/api-routes.js.map +1 -0
  47. package/dist/core/backend-capability.d.ts +7 -0
  48. package/dist/core/backend-capability.js +17 -0
  49. package/dist/core/backend-capability.js.map +1 -0
  50. package/dist/core/backend-types.d.ts +245 -0
  51. package/dist/core/backend-types.js +17 -0
  52. package/dist/core/backend-types.js.map +1 -0
  53. package/dist/core/backend.d.ts +108 -0
  54. package/dist/core/backend.js +14 -0
  55. package/dist/core/backend.js.map +1 -0
  56. package/dist/core/block-convert.d.ts +7 -5
  57. package/dist/core/block-convert.js +28 -5
  58. package/dist/core/block-convert.js.map +1 -1
  59. package/dist/core/command-types.d.ts +89 -0
  60. package/dist/core/commands.d.ts +40 -0
  61. package/dist/core/commands.js +151 -0
  62. package/dist/core/commands.js.map +1 -0
  63. package/dist/core/context.d.ts +17 -20
  64. package/dist/core/context.js +24 -21
  65. package/dist/core/context.js.map +1 -1
  66. package/dist/core/core-feature.d.ts +22 -7
  67. package/dist/core/core-feature.js +51 -18
  68. package/dist/core/core-feature.js.map +1 -1
  69. package/dist/core/define-editor.d.ts +4 -3
  70. package/dist/core/define-editor.js +14 -7
  71. package/dist/core/define-editor.js.map +1 -1
  72. package/dist/core/document-markdown.js +12 -3
  73. package/dist/core/document-markdown.js.map +1 -1
  74. package/dist/core/editor-ui.d.ts +8 -1
  75. package/dist/core/editor-ui.js +9 -8
  76. package/dist/core/editor-ui.js.map +1 -1
  77. package/dist/core/extension-points.d.ts +3 -15
  78. package/dist/core/feature-context.d.ts +8 -7
  79. package/dist/core/feature-context.js +12 -11
  80. package/dist/core/feature-context.js.map +1 -1
  81. package/dist/core/feature.d.ts +35 -19
  82. package/dist/core/feature.js.map +1 -1
  83. package/dist/core/fences.d.ts +2 -1
  84. package/dist/core/fences.js +3 -2
  85. package/dist/core/fences.js.map +1 -1
  86. package/dist/core/fetch-error.d.ts +1 -1
  87. package/dist/core/fetch-error.js +3 -1
  88. package/dist/core/fetch-error.js.map +1 -1
  89. package/dist/core/hash.d.ts +5 -0
  90. package/dist/core/hash.js +11 -0
  91. package/dist/core/hash.js.map +1 -0
  92. package/dist/core/http-backend.d.ts +11 -0
  93. package/dist/core/http-backend.js +195 -0
  94. package/dist/core/http-backend.js.map +1 -0
  95. package/dist/core/index.d.ts +14 -9
  96. package/dist/core/index.js +12 -9
  97. package/dist/core/palette.d.ts +48 -0
  98. package/dist/core/palette.js +95 -0
  99. package/dist/core/palette.js.map +1 -0
  100. package/dist/core/schema/abilities.d.ts +2 -1
  101. package/dist/core/schema/abilities.js +5 -1
  102. package/dist/core/schema/abilities.js.map +1 -1
  103. package/dist/core/schema/index.d.ts +11 -5
  104. package/dist/core/schema/index.js +13 -8
  105. package/dist/core/schema/index.js.map +1 -1
  106. package/dist/core/schema/mark-flags.d.ts +8 -1
  107. package/dist/core/schema/of-features.d.ts +13 -0
  108. package/dist/core/schema/of-features.js +14 -0
  109. package/dist/core/schema/of-features.js.map +1 -0
  110. package/dist/core/slash.d.ts +8 -6
  111. package/dist/core/slash.js +24 -16
  112. package/dist/core/slash.js.map +1 -1
  113. package/dist/core/surface-session.js +13 -5
  114. package/dist/core/surface-session.js.map +1 -1
  115. package/dist/core/toolbar-items.d.ts +10 -4
  116. package/dist/core/toolbar-items.js +269 -195
  117. package/dist/core/toolbar-items.js.map +1 -1
  118. package/dist/core/util.js +11 -0
  119. package/dist/core/util.js.map +1 -0
  120. package/dist/document/DocumentSurface.d.ts +1 -1
  121. package/dist/features/ai/agent-op-review.d.ts +1 -1
  122. package/dist/features/ai/agent-op-review.js +1 -1
  123. package/dist/features/ai/agent-ops.js +1 -1
  124. package/dist/features/ai/ai-apply.d.ts +2 -2
  125. package/dist/features/ai/ai-apply.js +1 -1
  126. package/dist/features/ai/ai-review.js +1 -1
  127. package/dist/features/ai/ai-sse.d.ts +6 -6
  128. package/dist/features/ai/ai-sse.js +20 -63
  129. package/dist/features/ai/ai-sse.js.map +1 -1
  130. package/dist/features/ai/assistant.d.ts +8 -7
  131. package/dist/features/ai/assistant.js +3 -3
  132. package/dist/features/ai/assistant.js.map +1 -1
  133. package/dist/features/ai/block-actions.d.ts +3 -2
  134. package/dist/features/ai/block-actions.js +3 -2
  135. package/dist/features/ai/block-actions.js.map +1 -1
  136. package/dist/features/ai/components/AiSelectionMenu.js +1 -1
  137. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  138. package/dist/features/ai/components/DocumentChatOps.vue.d.ts +1 -1
  139. package/dist/features/ai/components/SettingsModelProfiles.js +8 -8
  140. package/dist/features/ai/components/SettingsModelProfiles.js.map +1 -1
  141. package/dist/features/ai/document-chat-model.d.ts +8 -7
  142. package/dist/features/ai/document-chat-model.js +5 -5
  143. package/dist/features/ai/document-chat-model.js.map +1 -1
  144. package/dist/features/ai/document-chat.js +1 -1
  145. package/dist/features/ai/feature.js +27 -14
  146. package/dist/features/ai/feature.js.map +1 -1
  147. package/dist/features/ai/ghost-autocomplete.d.ts +7 -6
  148. package/dist/features/ai/ghost-autocomplete.js +6 -9
  149. package/dist/features/ai/ghost-autocomplete.js.map +1 -1
  150. package/dist/features/ai/index.d.ts +4 -3
  151. package/dist/features/ai/index.js +10 -10
  152. package/dist/features/ai/model-profiles.d.ts +5 -4
  153. package/dist/features/ai/model-profiles.js +2 -2
  154. package/dist/features/ai/model-profiles.js.map +1 -1
  155. package/dist/features/ai/protocol/block-lines.js +1 -1
  156. package/dist/features/ai/protocol/block-lines.js.map +1 -1
  157. package/dist/features/ai/protocol/chat.js +4 -1
  158. package/dist/features/ai/protocol/chat.js.map +1 -1
  159. package/dist/features/ai/protocol/index.d.ts +3 -2
  160. package/dist/features/ai/protocol/models.d.ts +2 -16
  161. package/dist/features/ai/protocol/models.js.map +1 -1
  162. package/dist/features/ai/rate-limit.d.ts +1 -1
  163. package/dist/features/ai/rate-limit.js +3 -1
  164. package/dist/features/ai/rate-limit.js.map +1 -1
  165. package/dist/features/ai/react/AiSelectionMenu.js +2 -2
  166. package/dist/features/ai/react/AiSidebar.js +4 -4
  167. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  168. package/dist/features/ai/react/DocumentChatOps.d.ts +1 -1
  169. package/dist/features/ai/react/DocumentChatOps.js +1 -1
  170. package/dist/features/ai/react/SettingsModelProfiles.js +8 -10
  171. package/dist/features/ai/react/SettingsModelProfiles.js.map +1 -1
  172. package/dist/features/ai/react/use-ai-assistant.d.ts +1 -1
  173. package/dist/features/ai/react/use-ai-assistant.js +3 -3
  174. package/dist/features/ai/react/use-ai-assistant.js.map +1 -1
  175. package/dist/features/ai/react/use-document-chat.js +2 -2
  176. package/dist/features/ai/react/use-document-chat.js.map +1 -1
  177. package/dist/features/ai/react/use-model-profiles.d.ts +2 -1
  178. package/dist/features/ai/selection-flow.d.ts +1 -1
  179. package/dist/features/ai/selection-flow.js +1 -1
  180. package/dist/features/ai/sidebar-flow.d.ts +1 -1
  181. package/dist/features/ai/sidebar-flow.js +1 -1
  182. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  183. package/dist/features/ai/use-ai-assistant.js +3 -3
  184. package/dist/features/ai/use-ai-assistant.js.map +1 -1
  185. package/dist/features/ai/use-document-chat.js +2 -2
  186. package/dist/features/ai/use-document-chat.js.map +1 -1
  187. package/dist/features/ai/use-model-profiles.d.ts +2 -1
  188. package/dist/features/authors/attribution.d.ts +1 -1
  189. package/dist/features/authors/feature.js +12 -11
  190. package/dist/features/authors/feature.js.map +1 -1
  191. package/dist/features/block-ui/block-actions.d.ts +1 -1
  192. package/dist/features/block-ui/block-actions.js +1 -1
  193. package/dist/features/block-ui/block-actions.js.map +1 -1
  194. package/dist/features/block-ui/components/BlockUi.js +1 -1
  195. package/dist/features/block-ui/feature.js +1 -0
  196. package/dist/features/block-ui/feature.js.map +1 -1
  197. package/dist/features/block-ui/react/BlockUi.js +1 -1
  198. package/dist/features/collab/anchor-resolver.js +1 -1
  199. package/dist/features/collab/feature.js +2 -0
  200. package/dist/features/collab/feature.js.map +1 -1
  201. package/dist/features/comments/components/CommentThreadView.vue.d.ts +2 -1
  202. package/dist/features/comments/feature.js +12 -11
  203. package/dist/features/comments/feature.js.map +1 -1
  204. package/dist/features/comments/index.d.ts +3 -2
  205. package/dist/features/comments/index.js +1 -1
  206. package/dist/features/comments/margin-layout.d.ts +3 -2
  207. package/dist/features/comments/model.d.ts +3 -67
  208. package/dist/features/comments/model.js +2 -2
  209. package/dist/features/comments/model.js.map +1 -1
  210. package/dist/features/comments/react/CommentThreadView.d.ts +3 -2
  211. package/dist/features/comments/react/use-comments.d.ts +2 -1
  212. package/dist/features/comments/react.d.ts +1 -1
  213. package/dist/features/comments/schema.d.ts +2 -52
  214. package/dist/features/comments/schema.js.map +1 -1
  215. package/dist/features/comments/snapshot-equality.js.map +1 -1
  216. package/dist/features/comments/use-comments.d.ts +3 -2
  217. package/dist/features/comments/vue.d.ts +1 -1
  218. package/dist/features/composites/accepted-children.d.ts +1 -0
  219. package/dist/features/composites/accepted-children.js +16 -1
  220. package/dist/features/composites/accepted-children.js.map +1 -1
  221. package/dist/features/composites/ai-brief.js +31 -0
  222. package/dist/features/composites/ai-brief.js.map +1 -1
  223. package/dist/features/composites/bibliography-command.d.ts +11 -0
  224. package/dist/features/composites/bibliography-form.js +90 -0
  225. package/dist/features/composites/bibliography-form.js.map +1 -0
  226. package/dist/features/composites/bibliography.d.ts +33 -0
  227. package/dist/features/composites/bibliography.js +120 -0
  228. package/dist/features/composites/bibliography.js.map +1 -0
  229. package/dist/features/composites/components/BibliographyEditor.js +261 -0
  230. package/dist/features/composites/components/BibliographyEditor.js.map +1 -0
  231. package/dist/features/composites/components/CompositeChrome.js +75 -26
  232. package/dist/features/composites/components/CompositeChrome.js.map +1 -1
  233. package/dist/features/composites/components/CompositeDetails.js +46 -11
  234. package/dist/features/composites/components/CompositeDetails.js.map +1 -1
  235. package/dist/features/composites/components/CompositeDetails.vue.d.ts +2 -1
  236. package/dist/features/composites/components/CompositeFieldEditor.js +37 -4
  237. package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
  238. package/dist/features/composites/components/CompositeInspector.js +17 -5
  239. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  240. package/dist/features/composites/components/CompositeTable.js +13 -4
  241. package/dist/features/composites/components/CompositeTable.js.map +1 -1
  242. package/dist/features/composites/components/CompositeTagsEditor.js +1 -1
  243. package/dist/features/composites/components/CompositeView.js +11 -1
  244. package/dist/features/composites/components/CompositeView.js.map +1 -1
  245. package/dist/features/composites/components/DerivationBadge.js +103 -0
  246. package/dist/features/composites/components/DerivationBadge.js.map +1 -0
  247. package/dist/features/composites/components/ReferenceFieldSelect.js +60 -0
  248. package/dist/features/composites/components/ReferenceFieldSelect.js.map +1 -0
  249. package/dist/features/composites/components/ReferenceView.js +122 -0
  250. package/dist/features/composites/components/ReferenceView.js.map +1 -0
  251. package/dist/features/composites/components/ReferenceView.vue.d.ts +96 -0
  252. package/dist/features/composites/components/SettingsEditingTab.js +1 -1
  253. package/dist/features/composites/convert.js +2 -2
  254. package/dist/features/composites/convert.js.map +1 -1
  255. package/dist/features/composites/derivation.d.ts +61 -0
  256. package/dist/features/composites/derivation.js +98 -0
  257. package/dist/features/composites/derivation.js.map +1 -0
  258. package/dist/features/composites/derived-chrome.js +69 -0
  259. package/dist/features/composites/derived-chrome.js.map +1 -0
  260. package/dist/features/composites/feature.js +159 -109
  261. package/dist/features/composites/feature.js.map +1 -1
  262. package/dist/features/composites/fences.d.ts +4 -3
  263. package/dist/features/composites/fences.js +51 -7
  264. package/dist/features/composites/fences.js.map +1 -1
  265. package/dist/features/composites/index.d.ts +12 -3
  266. package/dist/features/composites/index.js +15 -6
  267. package/dist/features/composites/inspector-model.d.ts +4 -1
  268. package/dist/features/composites/inspector-model.js +8 -4
  269. package/dist/features/composites/inspector-model.js.map +1 -1
  270. package/dist/features/composites/kinds.d.ts +8 -1
  271. package/dist/features/composites/kinds.js +145 -6
  272. package/dist/features/composites/kinds.js.map +1 -1
  273. package/dist/features/composites/legacy.d.ts +4 -1
  274. package/dist/features/composites/legacy.js +56 -1
  275. package/dist/features/composites/legacy.js.map +1 -1
  276. package/dist/features/composites/meta-display.js +40 -1
  277. package/dist/features/composites/meta-display.js.map +1 -1
  278. package/dist/features/composites/meta-keys.d.ts +7 -0
  279. package/dist/features/composites/meta-keys.js +21 -0
  280. package/dist/features/composites/meta-keys.js.map +1 -0
  281. package/dist/features/composites/node-views.d.ts +6 -3
  282. package/dist/features/composites/node-views.js +7 -3
  283. package/dist/features/composites/node-views.js.map +1 -1
  284. package/dist/features/composites/nodes.d.ts +6 -1
  285. package/dist/features/composites/nodes.js +33 -7
  286. package/dist/features/composites/nodes.js.map +1 -1
  287. package/dist/features/composites/react/BibliographyEditor.js +229 -0
  288. package/dist/features/composites/react/BibliographyEditor.js.map +1 -0
  289. package/dist/features/composites/react/CompositeChrome.js +65 -17
  290. package/dist/features/composites/react/CompositeChrome.js.map +1 -1
  291. package/dist/features/composites/react/CompositeDetails.d.ts +2 -1
  292. package/dist/features/composites/react/CompositeDetails.js +45 -6
  293. package/dist/features/composites/react/CompositeDetails.js.map +1 -1
  294. package/dist/features/composites/react/CompositeFieldEditor.d.ts +1 -1
  295. package/dist/features/composites/react/CompositeFieldEditor.js +24 -1
  296. package/dist/features/composites/react/CompositeFieldEditor.js.map +1 -1
  297. package/dist/features/composites/react/CompositeInspector.js +11 -5
  298. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  299. package/dist/features/composites/react/CompositeStatusMenu.js +1 -1
  300. package/dist/features/composites/react/CompositeSummaryEditor.js +1 -1
  301. package/dist/features/composites/react/CompositeTable.js +16 -2
  302. package/dist/features/composites/react/CompositeTable.js.map +1 -1
  303. package/dist/features/composites/react/CompositeTagsEditor.js +2 -2
  304. package/dist/features/composites/react/CompositeView.js +6 -1
  305. package/dist/features/composites/react/CompositeView.js.map +1 -1
  306. package/dist/features/composites/react/DerivationBadge.js +116 -0
  307. package/dist/features/composites/react/DerivationBadge.js.map +1 -0
  308. package/dist/features/composites/react/ReferenceFieldSelect.js +52 -0
  309. package/dist/features/composites/react/ReferenceFieldSelect.js.map +1 -0
  310. package/dist/features/composites/react/ReferenceView.d.ts +11 -0
  311. package/dist/features/composites/react/ReferenceView.js +115 -0
  312. package/dist/features/composites/react/ReferenceView.js.map +1 -0
  313. package/dist/features/composites/react/SettingsEditingTab.js +2 -2
  314. package/dist/features/composites/react.d.ts +2 -1
  315. package/dist/features/composites/react.js +7 -3
  316. package/dist/features/composites/react.js.map +1 -1
  317. package/dist/features/composites/reference-ai.js +16 -0
  318. package/dist/features/composites/reference-ai.js.map +1 -0
  319. package/dist/features/composites/reference-label.d.ts +19 -0
  320. package/dist/features/composites/reference-label.js +58 -0
  321. package/dist/features/composites/reference-label.js.map +1 -0
  322. package/dist/features/composites/reference-suggestion.d.ts +10 -0
  323. package/dist/features/composites/reference-suggestion.js +41 -0
  324. package/dist/features/composites/reference-suggestion.js.map +1 -0
  325. package/dist/features/composites/reference-text.d.ts +18 -0
  326. package/dist/features/composites/reference-text.js +89 -0
  327. package/dist/features/composites/reference-text.js.map +1 -0
  328. package/dist/features/composites/reference.d.ts +44 -0
  329. package/dist/features/composites/reference.js +148 -0
  330. package/dist/features/composites/reference.js.map +1 -0
  331. package/dist/features/composites/reveal-source.d.ts +32 -0
  332. package/dist/features/composites/reveal-source.js +89 -0
  333. package/dist/features/composites/reveal-source.js.map +1 -0
  334. package/dist/features/composites/rows.d.ts +21 -2
  335. package/dist/features/composites/rows.js +63 -9
  336. package/dist/features/composites/rows.js.map +1 -1
  337. package/dist/features/composites/schema.d.ts +9 -3
  338. package/dist/features/composites/schema.js +11 -4
  339. package/dist/features/composites/schema.js.map +1 -1
  340. package/dist/features/composites/table-model.js +12 -21
  341. package/dist/features/composites/table-model.js.map +1 -1
  342. package/dist/features/composites/vue.d.ts +3 -2
  343. package/dist/features/composites/vue.js +8 -4
  344. package/dist/features/composites/vue.js.map +1 -1
  345. package/dist/features/media/assets.d.ts +2 -0
  346. package/dist/features/media/assets.js +3 -2
  347. package/dist/features/media/assets.js.map +1 -1
  348. package/dist/features/media/components/FileView.js +13 -4
  349. package/dist/features/media/components/FileView.js.map +1 -1
  350. package/dist/features/media/feature.d.ts +3 -3
  351. package/dist/features/media/feature.js +21 -28
  352. package/dist/features/media/feature.js.map +1 -1
  353. package/dist/features/media/file-import.js +2 -2
  354. package/dist/features/media/file-import.js.map +1 -1
  355. package/dist/features/media/index.d.ts +2 -2
  356. package/dist/features/media/index.js +2 -2
  357. package/dist/features/media/react/FileView.js +19 -2
  358. package/dist/features/media/react/FileView.js.map +1 -1
  359. package/dist/features/media/schema.d.ts +8 -0
  360. package/dist/features/media/schema.js +126 -11
  361. package/dist/features/media/schema.js.map +1 -1
  362. package/dist/features/media/upload.d.ts +16 -14
  363. package/dist/features/media/upload.js +13 -19
  364. package/dist/features/media/upload.js.map +1 -1
  365. package/dist/features/mentions/index.d.ts +4 -3
  366. package/dist/features/mentions/index.js +4 -1
  367. package/dist/features/mentions/index.js.map +1 -1
  368. package/dist/features/mentions/mention-text.d.ts +1 -1
  369. package/dist/features/mentions/mention-text.js.map +1 -1
  370. package/dist/features/mentions/mention.d.ts +0 -6
  371. package/dist/features/mentions/mention.js.map +1 -1
  372. package/dist/features/mentions/metadata-defaults.d.ts +2 -2
  373. package/dist/features/mentions/metadata-defaults.js +3 -9
  374. package/dist/features/mentions/metadata-defaults.js.map +1 -1
  375. package/dist/features/mentions/schema.d.ts +2 -1
  376. package/dist/features/mentions/schema.js.map +1 -1
  377. package/dist/features/tables/feature.d.ts +2 -1
  378. package/dist/features/tables/feature.js +90 -70
  379. package/dist/features/tables/feature.js.map +1 -1
  380. package/dist/features/themes/components/SettingsAppearanceTab.js +6 -9
  381. package/dist/features/themes/components/SettingsAppearanceTab.js.map +1 -1
  382. package/dist/features/themes/feature.js +2 -0
  383. package/dist/features/themes/feature.js.map +1 -1
  384. package/dist/features/themes/index.d.ts +2 -1
  385. package/dist/features/themes/react/SettingsAppearanceTab.js +7 -10
  386. package/dist/features/themes/react/SettingsAppearanceTab.js.map +1 -1
  387. package/dist/features/themes/schema.d.ts +2 -6
  388. package/dist/features/themes/schema.js.map +1 -1
  389. package/dist/features/themes/theme-runtime.d.ts +1 -1
  390. package/dist/features/themes/theme-runtime.js +4 -2
  391. package/dist/features/themes/theme-runtime.js.map +1 -1
  392. package/dist/features/transcript/TranscriptSegmentView.js +48 -28
  393. package/dist/features/transcript/TranscriptSegmentView.js.map +1 -1
  394. package/dist/features/transcript/feature.js +21 -19
  395. package/dist/features/transcript/feature.js.map +1 -1
  396. package/dist/features/transcript/react/TranscriptSegmentView.js +55 -36
  397. package/dist/features/transcript/react/TranscriptSegmentView.js.map +1 -1
  398. package/dist/features/transcript/segment-node.d.ts +2 -0
  399. package/dist/features/transcript/segment-node.js +35 -7
  400. package/dist/features/transcript/segment-node.js.map +1 -1
  401. package/dist/i18n/de/composites.js +91 -1
  402. package/dist/i18n/de/composites.js.map +1 -1
  403. package/dist/i18n/de/integrations.js +49 -8
  404. package/dist/i18n/de/integrations.js.map +1 -1
  405. package/dist/i18n/de/media.js +0 -1
  406. package/dist/i18n/de/media.js.map +1 -1
  407. package/dist/i18n/de/palette.js +22 -0
  408. package/dist/i18n/de/palette.js.map +1 -0
  409. package/dist/i18n/de/tables.js +0 -1
  410. package/dist/i18n/de/tables.js.map +1 -1
  411. package/dist/i18n/de/transcript.js +2 -1
  412. package/dist/i18n/de/transcript.js.map +1 -1
  413. package/dist/i18n/de/tree.js +3 -0
  414. package/dist/i18n/de/tree.js.map +1 -1
  415. package/dist/i18n/de.d.ts +157 -11
  416. package/dist/i18n/de.js +3 -1
  417. package/dist/i18n/de.js.map +1 -1
  418. package/dist/i18n/en/composites.js +89 -1
  419. package/dist/i18n/en/composites.js.map +1 -1
  420. package/dist/i18n/en/integrations.js +49 -8
  421. package/dist/i18n/en/integrations.js.map +1 -1
  422. package/dist/i18n/en/media.js +0 -1
  423. package/dist/i18n/en/media.js.map +1 -1
  424. package/dist/i18n/en/palette.js +22 -0
  425. package/dist/i18n/en/palette.js.map +1 -0
  426. package/dist/i18n/en/tables.js +0 -1
  427. package/dist/i18n/en/tables.js.map +1 -1
  428. package/dist/i18n/en/transcript.js +2 -1
  429. package/dist/i18n/en/transcript.js.map +1 -1
  430. package/dist/i18n/en/tree.js +3 -0
  431. package/dist/i18n/en/tree.js.map +1 -1
  432. package/dist/i18n/en.js +3 -1
  433. package/dist/i18n/en.js.map +1 -1
  434. package/dist/index.d.ts +4 -3
  435. package/dist/integrations/composite-ai/bibliography-fetch.d.ts +31 -0
  436. package/dist/integrations/composite-ai/bibliography-fetch.js +159 -0
  437. package/dist/integrations/composite-ai/bibliography-fetch.js.map +1 -0
  438. package/dist/integrations/composite-ai/composite-fill.d.ts +30 -8
  439. package/dist/integrations/composite-ai/composite-fill.js +97 -56
  440. package/dist/integrations/composite-ai/composite-fill.js.map +1 -1
  441. package/dist/integrations/composite-ai/composite-summary.d.ts +4 -12
  442. package/dist/integrations/composite-ai/composite-summary.js +7 -26
  443. package/dist/integrations/composite-ai/composite-summary.js.map +1 -1
  444. package/dist/integrations/composite-ai/derive.d.ts +29 -0
  445. package/dist/integrations/composite-ai/derive.js +173 -0
  446. package/dist/integrations/composite-ai/derive.js.map +1 -0
  447. package/dist/integrations/composite-ai/index.d.ts +11 -7
  448. package/dist/integrations/composite-ai/index.js +72 -14
  449. package/dist/integrations/composite-ai/index.js.map +1 -1
  450. package/dist/integrations/composite-ai/rendition.d.ts +45 -0
  451. package/dist/integrations/composite-ai/rendition.js +314 -0
  452. package/dist/integrations/composite-ai/rendition.js.map +1 -0
  453. package/dist/integrations/composite-media/index.d.ts +7 -5
  454. package/dist/integrations/composite-media/index.js +86 -13
  455. package/dist/integrations/composite-media/index.js.map +1 -1
  456. package/dist/integrations/transcript-ai/index.d.ts +1 -1
  457. package/dist/integrations/transcript-ai/index.js +5 -2
  458. package/dist/integrations/transcript-ai/index.js.map +1 -1
  459. package/dist/integrations/transcript-ai/protocol.d.ts +2 -12
  460. package/dist/integrations/transcript-ai/protocol.js +0 -2
  461. package/dist/integrations/transcript-ai/protocol.js.map +1 -1
  462. package/dist/integrations/transcript-ai/transcript-cleanup.d.ts +5 -5
  463. package/dist/integrations/transcript-ai/transcript-cleanup.js +7 -10
  464. package/dist/integrations/transcript-ai/transcript-cleanup.js.map +1 -1
  465. package/dist/markdown/MarkdownSurface.vue.d.ts +6 -5
  466. package/dist/markdown/MarkdownSurface2.js +3 -6
  467. package/dist/markdown/MarkdownSurface2.js.map +1 -1
  468. package/dist/markdown/document.js +1 -1
  469. package/dist/markdown/source-mode.d.ts +1 -1
  470. package/dist/nuxt/module.d.ts +1402 -54
  471. package/dist/nuxt/module.js +6 -49
  472. package/dist/nuxt/module.js.map +1 -1
  473. package/dist/nuxt/runtime/install-context.d.ts +7 -3
  474. package/dist/nuxt/runtime/install-context.js +14 -8
  475. package/dist/nuxt/runtime/install-context.js.map +1 -1
  476. package/dist/nuxt/runtime/plugins/editor-context-nuxt-ui.client.ts +2 -2
  477. package/dist/nuxt/runtime/server/plugins/editor-server.js +20 -12
  478. package/dist/nuxt/runtime/server/plugins/editor-server.js.map +1 -1
  479. package/dist/presets/compose.d.ts +94 -0
  480. package/dist/presets/compose.js +164 -0
  481. package/dist/presets/compose.js.map +1 -0
  482. package/dist/react/AdvancedEditor.d.ts +15 -15
  483. package/dist/react/AdvancedEditor.js +15 -13
  484. package/dist/react/AdvancedEditor.js.map +1 -1
  485. package/dist/react/EditorCommandPalette.d.ts +13 -0
  486. package/dist/react/EditorCommandPalette.js +118 -0
  487. package/dist/react/EditorCommandPalette.js.map +1 -0
  488. package/dist/react/EditorSettingsModal.d.ts +5 -5
  489. package/dist/react/EditorSettingsModal.js +7 -4
  490. package/dist/react/EditorSettingsModal.js.map +1 -1
  491. package/dist/react/EditorSettingsPanel.d.ts +7 -7
  492. package/dist/react/EditorSettingsPanel.js +8 -5
  493. package/dist/react/EditorSettingsPanel.js.map +1 -1
  494. package/dist/react/EditorSurface.d.ts +7 -7
  495. package/dist/react/EditorSurface.js +10 -6
  496. package/dist/react/EditorSurface.js.map +1 -1
  497. package/dist/react/EditorToolbar.d.ts +4 -6
  498. package/dist/react/EditorToolbar.js +29 -37
  499. package/dist/react/EditorToolbar.js.map +1 -1
  500. package/dist/react/SimpleEditor.d.ts +22 -23
  501. package/dist/react/SimpleEditor.js +21 -7
  502. package/dist/react/SimpleEditor.js.map +1 -1
  503. package/dist/react/advanced.d.ts +4 -3
  504. package/dist/react/advanced.js +2 -2
  505. package/dist/react/compose.d.ts +20 -0
  506. package/dist/react/compose.js +49 -0
  507. package/dist/react/compose.js.map +1 -0
  508. package/dist/react/composed-surface.d.ts +39 -0
  509. package/dist/react/composed-surface.js +71 -0
  510. package/dist/react/composed-surface.js.map +1 -0
  511. package/dist/react/context.js +6 -2
  512. package/dist/react/context.js.map +1 -1
  513. package/dist/react/index.d.ts +2 -1
  514. package/dist/react/index.js +2 -1
  515. package/dist/react/key-labels.js +15 -0
  516. package/dist/react/key-labels.js.map +1 -0
  517. package/dist/react/settings.d.ts +1 -1
  518. package/dist/react/simple.d.ts +2 -2
  519. package/dist/react/ui/command.js +36 -1
  520. package/dist/react/ui/command.js.map +1 -1
  521. package/dist/schema/advanced.d.ts +6 -6
  522. package/dist/schema/advanced.js +26 -29
  523. package/dist/schema/advanced.js.map +1 -1
  524. package/dist/server/ai.d.ts +5 -16
  525. package/dist/server/ai.js.map +1 -1
  526. package/dist/server/auth.d.ts +9 -0
  527. package/dist/server/collab-state.d.ts +1 -1
  528. package/dist/server/collab-state.js.map +1 -1
  529. package/dist/server/collab.d.ts +13 -13
  530. package/dist/server/collab.js +34 -18
  531. package/dist/server/collab.js.map +1 -1
  532. package/dist/server/crossref.js +78 -0
  533. package/dist/server/crossref.js.map +1 -0
  534. package/dist/server/errors.d.ts +1 -1
  535. package/dist/server/errors.js +1 -1
  536. package/dist/server/errors.js.map +1 -1
  537. package/dist/server/extract.js +308 -0
  538. package/dist/server/extract.js.map +1 -0
  539. package/dist/{nuxt/runtime/server/utils/editor-route.js → server/h3.js} +8 -16
  540. package/dist/server/h3.js.map +1 -0
  541. package/dist/server/handlers/ai.d.ts +30 -9
  542. package/dist/server/handlers/ai.js +170 -36
  543. package/dist/server/handlers/ai.js.map +1 -1
  544. package/dist/server/handlers/context.d.ts +12 -5
  545. package/dist/server/handlers/context.js +10 -4
  546. package/dist/server/handlers/context.js.map +1 -1
  547. package/dist/server/handlers/editor.d.ts +19 -13
  548. package/dist/server/handlers/editor.js +49 -38
  549. package/dist/server/handlers/editor.js.map +1 -1
  550. package/dist/server/handlers/index.d.ts +4 -4
  551. package/dist/server/handlers/index.js +4 -4
  552. package/dist/server/host.d.ts +16 -25
  553. package/dist/server/host.js +58 -41
  554. package/dist/server/host.js.map +1 -1
  555. package/dist/server/index.d.ts +6 -4
  556. package/dist/server/index.js +3 -2
  557. package/dist/server/pdf.js +97 -0
  558. package/dist/server/pdf.js.map +1 -0
  559. package/dist/server/{adapter.d.ts → ports.d.ts} +81 -30
  560. package/dist/server/ports.js +29 -0
  561. package/dist/server/ports.js.map +1 -0
  562. package/dist/server/routes.js +144 -0
  563. package/dist/server/routes.js.map +1 -0
  564. package/dist/settings/index.d.ts +1 -1
  565. package/dist/settings/index.js.map +1 -1
  566. package/dist/styles/composites.css +25 -0
  567. package/dist/tree/DocumentTree.js +56 -29
  568. package/dist/tree/DocumentTree.js.map +1 -1
  569. package/dist/tree/react/DocumentTree.js +23 -0
  570. package/dist/tree/react/DocumentTree.js.map +1 -1
  571. package/dist/tree/tree-host.js +2 -2
  572. package/dist/tree/tree-items.js +1 -1
  573. package/dist/vue/AdvancedEditor.js +30 -9
  574. package/dist/vue/AdvancedEditor.js.map +1 -1
  575. package/dist/vue/AdvancedEditor.vue.d.ts +28 -8
  576. package/dist/vue/SimpleEditor.js +62 -16
  577. package/dist/vue/SimpleEditor.js.map +1 -1
  578. package/dist/vue/SimpleEditor.vue.d.ts +37 -7
  579. package/dist/vue/advanced.d.ts +3 -2
  580. package/dist/vue/advanced.js +2 -2
  581. package/dist/vue/components/EditorCommandPalette.js +91 -0
  582. package/dist/vue/components/EditorCommandPalette.js.map +1 -0
  583. package/dist/vue/components/EditorCommandPalette.vue.d.ts +15 -0
  584. package/dist/vue/components/EditorSettingsModal.js +10 -6
  585. package/dist/vue/components/EditorSettingsModal.js.map +1 -1
  586. package/dist/vue/components/EditorSettingsModal.vue.d.ts +4 -4
  587. package/dist/vue/components/EditorSettingsPanel.js +10 -6
  588. package/dist/vue/components/EditorSettingsPanel.js.map +1 -1
  589. package/dist/vue/components/EditorSettingsPanel.vue.d.ts +6 -6
  590. package/dist/vue/components/EditorSurface.js +11 -6
  591. package/dist/vue/components/EditorSurface.js.map +1 -1
  592. package/dist/vue/components/EditorSurface.vue.d.ts +7 -7
  593. package/dist/vue/components/EditorToolbar.js +41 -49
  594. package/dist/vue/components/EditorToolbar.js.map +1 -1
  595. package/dist/vue/components/EditorToolbar.vue.d.ts +2 -4
  596. package/dist/vue/compose.d.ts +17 -0
  597. package/dist/vue/compose.js +51 -0
  598. package/dist/vue/compose.js.map +1 -0
  599. package/dist/vue/context-key.d.ts +6 -0
  600. package/dist/vue/context-key.js +6 -0
  601. package/dist/vue/context-key.js.map +1 -0
  602. package/dist/vue/context.d.ts +8 -6
  603. package/dist/vue/context.js +6 -5
  604. package/dist/vue/context.js.map +1 -1
  605. package/dist/vue/index.d.ts +10 -8
  606. package/dist/vue/index.js +10 -8
  607. package/dist/vue/kit/index.d.ts +2 -2
  608. package/dist/vue/kit/nuxt-ui.js +23 -0
  609. package/dist/vue/kit/nuxt-ui.js.map +1 -1
  610. package/dist/vue/kit/types.d.ts +40 -1
  611. package/package.json +30 -16
  612. package/dist/adapters/comments/client.d.ts +0 -64
  613. package/dist/adapters/comments/client.js.map +0 -1
  614. package/dist/adapters/routes.js.map +0 -1
  615. package/dist/core/toolbar.d.ts +0 -74
  616. package/dist/core/toolbar.js +0 -45
  617. package/dist/core/toolbar.js.map +0 -1
  618. package/dist/integrations/composite-ai/document-transcribe.d.ts +0 -14
  619. package/dist/integrations/composite-ai/document-transcribe.js +0 -89
  620. package/dist/integrations/composite-ai/document-transcribe.js.map +0 -1
  621. package/dist/nuxt/runtime/server/api/ai/chat.post.d.ts +0 -5
  622. package/dist/nuxt/runtime/server/api/ai/chat.post.js +0 -8
  623. package/dist/nuxt/runtime/server/api/ai/chat.post.js.map +0 -1
  624. package/dist/nuxt/runtime/server/api/ai/complete.post.d.ts +0 -5
  625. package/dist/nuxt/runtime/server/api/ai/complete.post.js +0 -8
  626. package/dist/nuxt/runtime/server/api/ai/complete.post.js.map +0 -1
  627. package/dist/nuxt/runtime/server/api/ai/generate.post.d.ts +0 -5
  628. package/dist/nuxt/runtime/server/api/ai/generate.post.js +0 -8
  629. package/dist/nuxt/runtime/server/api/ai/generate.post.js.map +0 -1
  630. package/dist/nuxt/runtime/server/api/ai/models.get.d.ts +0 -5
  631. package/dist/nuxt/runtime/server/api/ai/models.get.js +0 -8
  632. package/dist/nuxt/runtime/server/api/ai/models.get.js.map +0 -1
  633. package/dist/nuxt/runtime/server/api/ai/transcribe.post.d.ts +0 -5
  634. package/dist/nuxt/runtime/server/api/ai/transcribe.post.js +0 -8
  635. package/dist/nuxt/runtime/server/api/ai/transcribe.post.js.map +0 -1
  636. package/dist/nuxt/runtime/server/api/ai/transcript-cleanup.post.d.ts +0 -5
  637. package/dist/nuxt/runtime/server/api/ai/transcript-cleanup.post.js +0 -8
  638. package/dist/nuxt/runtime/server/api/ai/transcript-cleanup.post.js.map +0 -1
  639. package/dist/nuxt/runtime/server/api/editor/assets/[id]/[name].get.d.ts +0 -5
  640. package/dist/nuxt/runtime/server/api/editor/assets/[id]/[name].get.js +0 -8
  641. package/dist/nuxt/runtime/server/api/editor/assets/[id]/[name].get.js.map +0 -1
  642. package/dist/nuxt/runtime/server/api/editor/assets/[id].post.d.ts +0 -5
  643. package/dist/nuxt/runtime/server/api/editor/assets/[id].post.js +0 -8
  644. package/dist/nuxt/runtime/server/api/editor/assets/[id].post.js.map +0 -1
  645. package/dist/nuxt/runtime/server/api/editor/settings.get.d.ts +0 -5
  646. package/dist/nuxt/runtime/server/api/editor/settings.get.js +0 -8
  647. package/dist/nuxt/runtime/server/api/editor/settings.get.js.map +0 -1
  648. package/dist/nuxt/runtime/server/api/editor/settings.put.d.ts +0 -5
  649. package/dist/nuxt/runtime/server/api/editor/settings.put.js +0 -8
  650. package/dist/nuxt/runtime/server/api/editor/settings.put.js.map +0 -1
  651. package/dist/nuxt/runtime/server/api/editor/tags.get.d.ts +0 -5
  652. package/dist/nuxt/runtime/server/api/editor/tags.get.js +0 -8
  653. package/dist/nuxt/runtime/server/api/editor/tags.get.js.map +0 -1
  654. package/dist/nuxt/runtime/server/api/editor/themes/[id].delete.d.ts +0 -5
  655. package/dist/nuxt/runtime/server/api/editor/themes/[id].delete.js +0 -8
  656. package/dist/nuxt/runtime/server/api/editor/themes/[id].delete.js.map +0 -1
  657. package/dist/nuxt/runtime/server/api/editor/themes/[id].get.d.ts +0 -5
  658. package/dist/nuxt/runtime/server/api/editor/themes/[id].get.js +0 -8
  659. package/dist/nuxt/runtime/server/api/editor/themes/[id].get.js.map +0 -1
  660. package/dist/nuxt/runtime/server/api/editor/themes/index.get.d.ts +0 -5
  661. package/dist/nuxt/runtime/server/api/editor/themes/index.get.js +0 -8
  662. package/dist/nuxt/runtime/server/api/editor/themes/index.get.js.map +0 -1
  663. package/dist/nuxt/runtime/server/api/editor/themes/index.post.d.ts +0 -5
  664. package/dist/nuxt/runtime/server/api/editor/themes/index.post.js +0 -8
  665. package/dist/nuxt/runtime/server/api/editor/themes/index.post.js.map +0 -1
  666. package/dist/nuxt/runtime/server/api/editor/users.get.d.ts +0 -5
  667. package/dist/nuxt/runtime/server/api/editor/users.get.js +0 -8
  668. package/dist/nuxt/runtime/server/api/editor/users.get.js.map +0 -1
  669. package/dist/nuxt/runtime/server/plugins/collab.d.ts +0 -5
  670. package/dist/nuxt/runtime/server/plugins/collab.js +0 -10
  671. package/dist/nuxt/runtime/server/plugins/collab.js.map +0 -1
  672. package/dist/nuxt/runtime/server/routes/collab/[id].d.ts +0 -5
  673. package/dist/nuxt/runtime/server/routes/collab/[id].js +0 -35
  674. package/dist/nuxt/runtime/server/routes/collab/[id].js.map +0 -1
  675. package/dist/nuxt/runtime/server/utils/editor-route.d.ts +0 -11
  676. package/dist/nuxt/runtime/server/utils/editor-route.js.map +0 -1
  677. package/dist/presets/advanced.d.ts +0 -33
  678. package/dist/presets/advanced.js +0 -50
  679. package/dist/presets/advanced.js.map +0 -1
  680. package/dist/presets/simple.d.ts +0 -9
  681. package/dist/presets/simple.js +0 -12
  682. package/dist/presets/simple.js.map +0 -1
  683. package/dist/react/advanced-preset.d.ts +0 -11
  684. package/dist/react/advanced-preset.js +0 -31
  685. package/dist/react/advanced-preset.js.map +0 -1
  686. package/dist/vue/advanced-preset.d.ts +0 -13
  687. package/dist/vue/advanced-preset.js +0 -33
  688. package/dist/vue/advanced-preset.js.map +0 -1
  689. package/docs/extending.md +0 -538
  690. package/docs/integration.md +0 -632
  691. package/docs/llms.md +0 -383
  692. package/docs/usage.md +0 -1076
@@ -1 +1 @@
1
- {"version":3,"file":"feature.js","names":[],"sources":["../../../src/features/media/feature.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { FileHandler } from '@tiptap/extension-file-handler'\nimport { Image } from '@tiptap/extension-image'\nimport type { EditorFeature, SlashCommandItem, ToolbarItem } from '../../core'\nimport { ABILITY } from '../../core/schema'\nimport type { MessageKey } from '../../i18n'\nimport { fileImport } from './file-import'\nimport { type MediaNodeViews, mediaBlocks } from './node-views'\nimport { acceptFor, type MediaKind } from './schema'\nimport { Media, openAudioRecorder, pickFiles, uploadOf } from './upload'\n\n/** What a framework binding supplies to the media feature. */\nexport interface MediaUi<C> {\n /** The „Audio aufnehmen“ modal; mounted as an overlay, opened by `openAudioRecorder(editor)`. */\n recorderModal: C\n /** The audio, video and file block views of the framework. */\n nodeViews: MediaNodeViews\n}\n\nconst PICKERS: readonly { id: string, labelKey: MessageKey, icon: string, kind: MediaKind }[] = [\n { id: 'bild', labelKey: 'media.picker.image', icon: 'i-lucide-image', kind: 'image' },\n { id: 'audio', labelKey: 'media.picker.audio', icon: 'i-lucide-file-audio', kind: 'audio' },\n { id: 'video', labelKey: 'media.picker.video', icon: 'i-lucide-video', kind: 'video' },\n]\n\n/**\n * Image, audio, video and file blocks with their views, upload (the surface's `upload` prop, else the document's asset\n * endpoint), drop/paste of files, the audio recorder, and the toolbar and `/` entries that open the file picker or\n * recorder. Files import through the pipeline: text and Markdown files are parsed into blocks, everything else is\n * uploaded; the `wrapImport` stage (identity by default) may wrap the blocks, which are then inserted as they are.\n * Import problems reach the user through the surface's notice seam (`ctx.notify`).\n * Needs a backend (or an `upload` function) only when a file is uploaded.\n */\nexport function mediaFeatureCore<C>(ui: MediaUi<C>): EditorFeature<C> {\n const { AudioBlock, VideoBlock, FileBlock } = mediaBlocks(ui.nodeViews)\n return {\n id: 'media',\n extensions: (ctx) => {\n const { importFiles } = fileImport(ctx.notify, ctx.t)\n return [\n Image,\n AudioBlock,\n VideoBlock,\n FileBlock,\n Media.configure({ upload: uploadOf(ctx), importFiles }),\n FileHandler.configure({\n consumePasteEvent: true,\n onDrop: (editor, files, pos) => importFiles(editor, files, pos),\n onPaste: (editor, files) => importFiles(editor, files),\n }),\n ]\n },\n toolbar: ctx => [{\n id: 'inserts',\n order: 50,\n items: [\n ...PICKERS.map((picker, index): ToolbarItem<C> => ({\n id: picker.id,\n order: 20 + index * 10,\n icon: picker.icon,\n label: ctx.t(picker.labelKey),\n ability: ABILITY.media,\n run: editor => void pickAndImport(editor, acceptFor(picker.kind)),\n })),\n { id: 'aufnahme', order: 50, icon: 'i-lucide-mic', label: ctx.t('media.toolbar.record'), ability: ABILITY.record, run: openAudioRecorder },\n ],\n }],\n slashCommands: (ctx): SlashCommandItem[] => [\n ...PICKERS.map((picker): SlashCommandItem => ({\n id: picker.id,\n label: ctx.t(picker.labelKey),\n icon: picker.icon,\n ability: ABILITY.media,\n run: editor => void pickAndImport(editor, acceptFor(picker.kind), editor.state.selection.from),\n })),\n { id: 'aufnahme', label: ctx.t('media.slash.record'), icon: 'i-lucide-mic', ability: ABILITY.record, run: openAudioRecorder },\n ],\n overlays: [ui.recorderModal],\n }\n}\n\nasync function pickAndImport(editor: Editor, accept: string, pos?: number): Promise<void> {\n const files = await pickFiles(accept)\n if (files.length && !editor.isDestroyed) await editor.storage.media.importFiles(editor, files, pos)\n}\n"],"mappings":";;;;;;;;AAmBA,MAAM,UAA0F;CAC9F;EAAE,IAAI;EAAQ,UAAU;EAAsB,MAAM;EAAkB,MAAM;CAAQ;CACpF;EAAE,IAAI;EAAS,UAAU;EAAsB,MAAM;EAAuB,MAAM;CAAQ;CAC1F;EAAE,IAAI;EAAS,UAAU;EAAsB,MAAM;EAAkB,MAAM;CAAQ;AACvF;;;;;;;;;AAUA,SAAgB,iBAAoB,IAAkC;CACpE,MAAM,EAAE,YAAY,YAAY,cAAc,YAAY,GAAG,SAAS;CACtE,OAAO;EACL,IAAI;EACJ,aAAa,QAAQ;GACnB,MAAM,EAAE,gBAAgB,WAAW,IAAI,QAAQ,IAAI,CAAC;GACpD,OAAO;IACL;IACA;IACA;IACA;IACA,MAAM,UAAU;KAAE,QAAQ,SAAS,GAAG;KAAG;IAAY,CAAC;IACtD,YAAY,UAAU;KACpB,mBAAmB;KACnB,SAAS,QAAQ,OAAO,QAAQ,YAAY,QAAQ,OAAO,GAAG;KAC9D,UAAU,QAAQ,UAAU,YAAY,QAAQ,KAAK;IACvD,CAAC;GACH;EACF;EACA,UAAS,QAAO,CAAC;GACf,IAAI;GACJ,OAAO;GACP,OAAO,CACL,GAAG,QAAQ,KAAK,QAAQ,WAA2B;IACjD,IAAI,OAAO;IACX,OAAO,KAAK,QAAQ;IACpB,MAAM,OAAO;IACb,OAAO,IAAI,EAAE,OAAO,QAAQ;IAC5B,SAAS,QAAQ;IACjB,MAAK,WAAU,KAAK,cAAc,QAAQ,UAAU,OAAO,IAAI,CAAC;GAClE,EAAE,GACF;IAAE,IAAI;IAAY,OAAO;IAAI,MAAM;IAAgB,OAAO,IAAI,EAAE,sBAAsB;IAAG,SAAS,QAAQ;IAAQ,KAAK;GAAkB,CAC3I;EACF,CAAC;EACD,gBAAgB,QAA4B,CAC1C,GAAG,QAAQ,KAAK,YAA8B;GAC5C,IAAI,OAAO;GACX,OAAO,IAAI,EAAE,OAAO,QAAQ;GAC5B,MAAM,OAAO;GACb,SAAS,QAAQ;GACjB,MAAK,WAAU,KAAK,cAAc,QAAQ,UAAU,OAAO,IAAI,GAAG,OAAO,MAAM,UAAU,IAAI;EAC/F,EAAE,GACF;GAAE,IAAI;GAAY,OAAO,IAAI,EAAE,oBAAoB;GAAG,MAAM;GAAgB,SAAS,QAAQ;GAAQ,KAAK;EAAkB,CAC9H;EACA,UAAU,CAAC,GAAG,aAAa;CAC7B;AACF;AAEA,eAAe,cAAc,QAAgB,QAAgB,KAA6B;CACxF,MAAM,QAAQ,MAAM,UAAU,MAAM;CACpC,IAAI,MAAM,UAAU,CAAC,OAAO,aAAa,MAAM,OAAO,QAAQ,MAAM,YAAY,QAAQ,OAAO,GAAG;AACpG"}
1
+ {"version":3,"file":"feature.js","names":[],"sources":["../../../src/features/media/feature.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { FileHandler } from '@tiptap/extension-file-handler'\nimport type { CommandDef, EditorFeature } from '../../core'\nimport { ABILITY } from '../../core/schema'\nimport type { MessageKey } from '../../i18n'\nimport { fileImport } from './file-import'\nimport { type MediaNodeViews, mediaBlocks } from './node-views'\nimport { acceptFor, ImageNode, mediaSchemaExtensions, type MediaKind } from './schema'\nimport { Media, openAudioRecorder, pickFiles, uploadOf } from './upload'\n\n/** What a framework binding supplies to the media feature. */\nexport interface MediaUi<C> {\n /** The „Audio aufnehmen“ modal; mounted as an overlay, opened by `openAudioRecorder(editor)`. */\n recorderModal: C\n /** The audio, video and file block views of the framework. */\n nodeViews: MediaNodeViews\n}\n\nconst PICKERS: readonly { id: string, labelKey: MessageKey, icon: string, kind: MediaKind }[] = [\n { id: 'bild', labelKey: 'media.picker.image', icon: 'i-lucide-image', kind: 'image' },\n { id: 'audio', labelKey: 'media.picker.audio', icon: 'i-lucide-file-audio', kind: 'audio' },\n { id: 'video', labelKey: 'media.picker.video', icon: 'i-lucide-video', kind: 'video' },\n]\n\n/**\n * Image, audio, video and file blocks with their views, upload (`backend.assets`, filed under the surface's `documentId`),\n * drop/paste of files, the audio recorder, and the toolbar and `/` entries that open the file picker or\n * recorder. Files import through the pipeline: text and Markdown files are parsed into blocks, everything else is\n * uploaded; the `wrapImport` stage (identity by default) may wrap the blocks, which are then inserted as they are.\n * Import problems reach the user through the surface's notice seam (`ctx.notify`).\n * Needs a backend with the `assets` capability.\n */\nexport function mediaFeatureCore<C>(ui: MediaUi<C>): EditorFeature<C> {\n const { AudioBlock, VideoBlock, FileBlock } = mediaBlocks(ui.nodeViews)\n return {\n id: 'media',\n dimension: 'content',\n backend: ['assets'],\n schemaExtensions: mediaSchemaExtensions,\n extensions: (ctx) => {\n const { importFiles } = fileImport(ctx.notify, ctx.t)\n return [\n ImageNode,\n AudioBlock,\n VideoBlock,\n FileBlock,\n Media.configure({ upload: uploadOf(ctx), importFiles }),\n FileHandler.configure({\n consumePasteEvent: true,\n onDrop: (editor, files, pos) => importFiles(editor, files, pos),\n onPaste: (editor, files) => importFiles(editor, files),\n }),\n ]\n },\n commands: (ctx): CommandDef<C>[] => [\n ...PICKERS.map((picker, index): CommandDef<C> => ({\n id: picker.id,\n toolbar: { group: 'inserts', order: 20 + index * 10 },\n slash: {},\n icon: picker.icon,\n label: ctx.t(picker.labelKey),\n ability: ABILITY.media,\n // The caret at the click: the picker takes a while and the document may change meanwhile.\n run: editor => void pickAndImport(editor, acceptFor(picker.kind), editor.state.selection.from),\n })),\n { id: 'aufnahme', toolbar: { group: 'inserts', order: 50 }, slash: {}, icon: 'i-lucide-mic', label: ctx.t('media.slash.record'), ability: ABILITY.record, run: openAudioRecorder },\n ],\n overlays: [ui.recorderModal],\n }\n}\n\nasync function pickAndImport(editor: Editor, accept: string, pos?: number): Promise<void> {\n const files = await pickFiles(accept)\n if (files.length && !editor.isDestroyed) await editor.storage.media.importFiles(editor, files, pos)\n}\n"],"mappings":";;;;;;;AAkBA,MAAM,UAA0F;CAC9F;EAAE,IAAI;EAAQ,UAAU;EAAsB,MAAM;EAAkB,MAAM;CAAQ;CACpF;EAAE,IAAI;EAAS,UAAU;EAAsB,MAAM;EAAuB,MAAM;CAAQ;CAC1F;EAAE,IAAI;EAAS,UAAU;EAAsB,MAAM;EAAkB,MAAM;CAAQ;AACvF;;;;;;;;;AAUA,SAAgB,iBAAoB,IAAkC;CACpE,MAAM,EAAE,YAAY,YAAY,cAAc,YAAY,GAAG,SAAS;CACtE,OAAO;EACL,IAAI;EACJ,WAAW;EACX,SAAS,CAAC,QAAQ;EAClB,kBAAkB;EAClB,aAAa,QAAQ;GACnB,MAAM,EAAE,gBAAgB,WAAW,IAAI,QAAQ,IAAI,CAAC;GACpD,OAAO;IACL;IACA;IACA;IACA;IACA,MAAM,UAAU;KAAE,QAAQ,SAAS,GAAG;KAAG;IAAY,CAAC;IACtD,YAAY,UAAU;KACpB,mBAAmB;KACnB,SAAS,QAAQ,OAAO,QAAQ,YAAY,QAAQ,OAAO,GAAG;KAC9D,UAAU,QAAQ,UAAU,YAAY,QAAQ,KAAK;IACvD,CAAC;GACH;EACF;EACA,WAAW,QAAyB,CAClC,GAAG,QAAQ,KAAK,QAAQ,WAA0B;GAChD,IAAI,OAAO;GACX,SAAS;IAAE,OAAO;IAAW,OAAO,KAAK,QAAQ;GAAG;GACpD,OAAO,CAAC;GACR,MAAM,OAAO;GACb,OAAO,IAAI,EAAE,OAAO,QAAQ;GAC5B,SAAS,QAAQ;GAEjB,MAAK,WAAU,KAAK,cAAc,QAAQ,UAAU,OAAO,IAAI,GAAG,OAAO,MAAM,UAAU,IAAI;EAC/F,EAAE,GACF;GAAE,IAAI;GAAY,SAAS;IAAE,OAAO;IAAW,OAAO;GAAG;GAAG,OAAO,CAAC;GAAG,MAAM;GAAgB,OAAO,IAAI,EAAE,oBAAoB;GAAG,SAAS,QAAQ;GAAQ,KAAK;EAAkB,CACnL;EACA,UAAU,CAAC,GAAG,aAAa;CAC7B;AACF;AAEA,eAAe,cAAc,QAAgB,QAAgB,KAA6B;CACxF,MAAM,QAAQ,MAAM,UAAU,MAAM;CACpC,IAAI,MAAM,UAAU,CAAC,OAAO,aAAa,MAAM,OAAO,QAAQ,MAAM,YAAY,QAAQ,OAAO,GAAG;AACpG"}
@@ -19,8 +19,8 @@ async function blocksOf(editor, file) {
19
19
  const blocks = structureBlocks(editor, parseBlocks(editor, await file.text()).blocks);
20
20
  return blocks.length ? blocks : [{ type: "paragraph" }];
21
21
  }
22
- const { url } = await editor.storage.media.upload(file);
23
- return [mediaNodeFor(file, url) ?? fileNodeFor(file, url)];
22
+ const { url, sha256 } = await editor.storage.media.upload(file);
23
+ return [mediaNodeFor(file, url, sha256) ?? fileNodeFor(file, url, sha256)];
24
24
  }
25
25
  /**
26
26
  * Imports files into the editor. Text and Markdown files are parsed through the editor's block pipeline into blocks;
@@ -1 +1 @@
1
- {"version":3,"file":"file-import.js","names":[],"sources":["../../../src/features/media/file-import.ts"],"sourcesContent":["import type { Editor, JSONContent } from '@tiptap/core'\nimport type { Translate } from '../../i18n'\nimport { captureRange, fetchErrorMessage, type Notifier, parseBlocks, releaseRanges, resolveRange, structureBlocks, wrapImportBlocks } from '../../core'\nimport { mediaMime } from './schema'\nimport { fileNodeFor, type ImportFilesFn, mediaNodeFor } from './upload'\n\n/** Text files are read into the document, bigger ones are refused. */\nexport const MAX_TEXT_BYTES = 2 * 1024 * 1024\n/** Browsers report `''` (or something else) as the type of `.md` files, so the name decides too. */\nconst TEXT_EXTENSION = /\\.(md|markdown|txt)$/i\n\n/** Whether `file` is imported as text (parsed Markdown) rather than uploaded. */\nexport function isTextFile(file: File): boolean {\n const mime = mediaMime(file.type)\n return mime === 'text/markdown' || mime === 'text/plain' || TEXT_EXTENSION.test(file.name)\n}\n\n/** Blocks for `file`: its text parsed through the editor's block pipeline (parse, normalize, segment, structure), or the uploaded media/file block. */\nasync function blocksOf(editor: Editor, file: File): Promise<JSONContent[]> {\n if (isTextFile(file)) {\n const blocks = structureBlocks(editor, parseBlocks(editor, await file.text()).blocks)\n return blocks.length ? blocks : [{ type: 'paragraph' }]\n }\n const { url } = await editor.storage.media.upload(file)\n return [mediaNodeFor(file, url) ?? fileNodeFor(file, url)]\n}\n\n/**\n * Imports files into the editor. Text and Markdown files are parsed through the editor's block pipeline into blocks;\n * all other files are uploaded through `editor.storage.media.upload` and appear as their media or file block. The\n * blocks go through the pipeline's `wrapImport` stage (identity unless an installed feature wraps imports) and are\n * inserted as they are. Problems (an oversized text file, a failed upload) are reported through `notify` and the file\n * is skipped. Framework-free: the Vue and React bindings pass their notifier and translator (`useFileImport()`), the feature passes `ctx.notify` and `ctx.t`.\n */\nexport function fileImport(notify: Notifier, t: Translate): { importFiles: ImportFilesFn } {\n // Sequential so files land in selection order; a failed file is reported and skipped. A drop position is anchored,\n // since uploads take a while and the document may change meanwhile; later files follow the first one, because\n // `insertContentAt` leaves the selection right after what it inserted. Resolves whether every file was imported.\n async function importFiles(editor: Editor, files: File[] | FileList, pos?: number): Promise<boolean> {\n let anchor = pos === undefined ? null : captureRange(editor, { from: pos, to: pos })\n let allImported = true\n try {\n for (const file of Array.from(files)) {\n if (isTextFile(file) && file.size > MAX_TEXT_BYTES) {\n notify({ title: t('media.import.tooLarge.title'), description: t('media.import.tooLarge.description', { name: file.name }), kind: 'error' })\n allImported = false\n continue\n }\n try {\n const blocks = wrapImportBlocks(editor, file, await blocksOf(editor, file))\n if (editor.isDestroyed) return false\n const at = (anchor && resolveRange(editor.state, anchor)?.from) ?? editor.state.selection.to\n editor.chain().focus().insertContentAt(at, blocks).run()\n if (anchor) releaseRanges(editor, [anchor])\n anchor = null\n }\n catch (error) {\n notify({ title: t('media.import.failed'), description: fetchErrorMessage(error), kind: 'error' })\n allImported = false\n }\n }\n }\n finally {\n if (anchor) releaseRanges(editor, [anchor])\n }\n return allImported\n }\n\n return { importFiles }\n}\n"],"mappings":";;;;;;;AAOA,MAAa,iBAAiB;;AAE9B,MAAM,iBAAiB;;AAGvB,SAAgB,WAAW,MAAqB;CAC9C,MAAM,OAAO,UAAU,KAAK,IAAI;CAChC,OAAO,SAAS,mBAAmB,SAAS,gBAAgB,eAAe,KAAK,KAAK,IAAI;AAC3F;;AAGA,eAAe,SAAS,QAAgB,MAAoC;CAC1E,IAAI,WAAW,IAAI,GAAG;EACpB,MAAM,SAAS,gBAAgB,QAAQ,YAAY,QAAQ,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM;EACpF,OAAO,OAAO,SAAS,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;CACxD;CACA,MAAM,EAAE,QAAQ,MAAM,OAAO,QAAQ,MAAM,OAAO,IAAI;CACtD,OAAO,CAAC,aAAa,MAAM,GAAG,KAAK,YAAY,MAAM,GAAG,CAAC;AAC3D;;;;;;;;AASA,SAAgB,WAAW,QAAkB,GAA8C;CAIzF,eAAe,YAAY,QAAgB,OAA0B,KAAgC;EACnG,IAAI,SAAS,QAAQ,KAAA,IAAY,OAAO,aAAa,QAAQ;GAAE,MAAM;GAAK,IAAI;EAAI,CAAC;EACnF,IAAI,cAAc;EAClB,IAAI;GACF,KAAK,MAAM,QAAQ,MAAM,KAAK,KAAK,GAAG;IACpC,IAAI,WAAW,IAAI,KAAK,KAAK,OAAA,SAAuB;KAClD,OAAO;MAAE,OAAO,EAAE,6BAA6B;MAAG,aAAa,EAAE,qCAAqC,EAAE,MAAM,KAAK,KAAK,CAAC;MAAG,MAAM;KAAQ,CAAC;KAC3I,cAAc;KACd;IACF;IACA,IAAI;KACF,MAAM,SAAS,iBAAiB,QAAQ,MAAM,MAAM,SAAS,QAAQ,IAAI,CAAC;KAC1E,IAAI,OAAO,aAAa,OAAO;KAC/B,MAAM,MAAM,UAAU,aAAa,OAAO,OAAO,MAAM,CAAC,EAAE,SAAS,OAAO,MAAM,UAAU;KAC1F,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,gBAAgB,IAAI,MAAM,CAAC,CAAC,IAAI;KACvD,IAAI,QAAQ,cAAc,QAAQ,CAAC,MAAM,CAAC;KAC1C,SAAS;IACX,SACO,OAAO;KACZ,OAAO;MAAE,OAAO,EAAE,qBAAqB;MAAG,aAAa,kBAAkB,KAAK;MAAG,MAAM;KAAQ,CAAC;KAChG,cAAc;IAChB;GACF;EACF,UACQ;GACN,IAAI,QAAQ,cAAc,QAAQ,CAAC,MAAM,CAAC;EAC5C;EACA,OAAO;CACT;CAEA,OAAO,EAAE,YAAY;AACvB"}
1
+ {"version":3,"file":"file-import.js","names":[],"sources":["../../../src/features/media/file-import.ts"],"sourcesContent":["import type { Editor, JSONContent } from '@tiptap/core'\nimport type { Translate } from '../../i18n'\nimport { captureRange, fetchErrorMessage, type Notifier, parseBlocks, releaseRanges, resolveRange, structureBlocks, wrapImportBlocks } from '../../core'\nimport { mediaMime } from './schema'\nimport { fileNodeFor, type ImportFilesFn, mediaNodeFor } from './upload'\n\n/** Text files are read into the document, bigger ones are refused. */\nexport const MAX_TEXT_BYTES = 2 * 1024 * 1024\n/** Browsers report `''` (or something else) as the type of `.md` files, so the name decides too. */\nconst TEXT_EXTENSION = /\\.(md|markdown|txt)$/i\n\n/** Whether `file` is imported as text (parsed Markdown) rather than uploaded. */\nexport function isTextFile(file: File): boolean {\n const mime = mediaMime(file.type)\n return mime === 'text/markdown' || mime === 'text/plain' || TEXT_EXTENSION.test(file.name)\n}\n\n/** Blocks for `file`: its text parsed through the editor's block pipeline (parse, normalize, segment, structure), or the uploaded media/file block. */\nasync function blocksOf(editor: Editor, file: File): Promise<JSONContent[]> {\n if (isTextFile(file)) {\n const blocks = structureBlocks(editor, parseBlocks(editor, await file.text()).blocks)\n return blocks.length ? blocks : [{ type: 'paragraph' }]\n }\n const { url, sha256 } = await editor.storage.media.upload(file)\n return [mediaNodeFor(file, url, sha256) ?? fileNodeFor(file, url, sha256)]\n}\n\n/**\n * Imports files into the editor. Text and Markdown files are parsed through the editor's block pipeline into blocks;\n * all other files are uploaded through `editor.storage.media.upload` and appear as their media or file block. The\n * blocks go through the pipeline's `wrapImport` stage (identity unless an installed feature wraps imports) and are\n * inserted as they are. Problems (an oversized text file, a failed upload) are reported through `notify` and the file\n * is skipped. Framework-free: the Vue and React bindings pass their notifier and translator (`useFileImport()`), the feature passes `ctx.notify` and `ctx.t`.\n */\nexport function fileImport(notify: Notifier, t: Translate): { importFiles: ImportFilesFn } {\n // Sequential so files land in selection order; a failed file is reported and skipped. A drop position is anchored,\n // since uploads take a while and the document may change meanwhile; later files follow the first one, because\n // `insertContentAt` leaves the selection right after what it inserted. Resolves whether every file was imported.\n async function importFiles(editor: Editor, files: File[] | FileList, pos?: number): Promise<boolean> {\n let anchor = pos === undefined ? null : captureRange(editor, { from: pos, to: pos })\n let allImported = true\n try {\n for (const file of Array.from(files)) {\n if (isTextFile(file) && file.size > MAX_TEXT_BYTES) {\n notify({ title: t('media.import.tooLarge.title'), description: t('media.import.tooLarge.description', { name: file.name }), kind: 'error' })\n allImported = false\n continue\n }\n try {\n const blocks = wrapImportBlocks(editor, file, await blocksOf(editor, file))\n if (editor.isDestroyed) return false\n const at = (anchor && resolveRange(editor.state, anchor)?.from) ?? editor.state.selection.to\n editor.chain().focus().insertContentAt(at, blocks).run()\n if (anchor) releaseRanges(editor, [anchor])\n anchor = null\n }\n catch (error) {\n notify({ title: t('media.import.failed'), description: fetchErrorMessage(error), kind: 'error' })\n allImported = false\n }\n }\n }\n finally {\n if (anchor) releaseRanges(editor, [anchor])\n }\n return allImported\n }\n\n return { importFiles }\n}\n"],"mappings":";;;;;;;AAOA,MAAa,iBAAiB;;AAE9B,MAAM,iBAAiB;;AAGvB,SAAgB,WAAW,MAAqB;CAC9C,MAAM,OAAO,UAAU,KAAK,IAAI;CAChC,OAAO,SAAS,mBAAmB,SAAS,gBAAgB,eAAe,KAAK,KAAK,IAAI;AAC3F;;AAGA,eAAe,SAAS,QAAgB,MAAoC;CAC1E,IAAI,WAAW,IAAI,GAAG;EACpB,MAAM,SAAS,gBAAgB,QAAQ,YAAY,QAAQ,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM;EACpF,OAAO,OAAO,SAAS,SAAS,CAAC,EAAE,MAAM,YAAY,CAAC;CACxD;CACA,MAAM,EAAE,KAAK,WAAW,MAAM,OAAO,QAAQ,MAAM,OAAO,IAAI;CAC9D,OAAO,CAAC,aAAa,MAAM,KAAK,MAAM,KAAK,YAAY,MAAM,KAAK,MAAM,CAAC;AAC3E;;;;;;;;AASA,SAAgB,WAAW,QAAkB,GAA8C;CAIzF,eAAe,YAAY,QAAgB,OAA0B,KAAgC;EACnG,IAAI,SAAS,QAAQ,KAAA,IAAY,OAAO,aAAa,QAAQ;GAAE,MAAM;GAAK,IAAI;EAAI,CAAC;EACnF,IAAI,cAAc;EAClB,IAAI;GACF,KAAK,MAAM,QAAQ,MAAM,KAAK,KAAK,GAAG;IACpC,IAAI,WAAW,IAAI,KAAK,KAAK,OAAA,SAAuB;KAClD,OAAO;MAAE,OAAO,EAAE,6BAA6B;MAAG,aAAa,EAAE,qCAAqC,EAAE,MAAM,KAAK,KAAK,CAAC;MAAG,MAAM;KAAQ,CAAC;KAC3I,cAAc;KACd;IACF;IACA,IAAI;KACF,MAAM,SAAS,iBAAiB,QAAQ,MAAM,MAAM,SAAS,QAAQ,IAAI,CAAC;KAC1E,IAAI,OAAO,aAAa,OAAO;KAC/B,MAAM,MAAM,UAAU,aAAa,OAAO,OAAO,MAAM,CAAC,EAAE,SAAS,OAAO,MAAM,UAAU;KAC1F,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,gBAAgB,IAAI,MAAM,CAAC,CAAC,IAAI;KACvD,IAAI,QAAQ,cAAc,QAAQ,CAAC,MAAM,CAAC;KAC1C,SAAS;IACX,SACO,OAAO;KACZ,OAAO;MAAE,OAAO,EAAE,qBAAqB;MAAG,aAAa,kBAAkB,KAAK;MAAG,MAAM;KAAQ,CAAC;KAChG,cAAc;IAChB;GACF;EACF,UACQ;GACN,IAAI,QAAQ,cAAc,QAAQ,CAAC,MAAM,CAAC;EAC5C;EACA,OAAO;CACT;CAEA,OAAO,EAAE,YAAY;AACvB"}
@@ -1,7 +1,7 @@
1
1
  import { MAX_MEDIA_BYTES, MEDIA_TYPES, MediaKind, acceptFor, mediaKindOf, mediaMime } from "./schema.js";
2
2
  import { AudioRecorder, AudioRecorderState, AudioRecorderStatus, createAudioRecorder } from "./audio-recorder.js";
3
- import { AUDIO_RECORDER_OPEN, ImportFilesFn, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, postUpload, uploadOf } from "./upload.js";
3
+ import { AUDIO_RECORDER_OPEN, ImportFilesFn, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, uploadOf } from "./upload.js";
4
4
  import { MAX_TEXT_BYTES, fileImport, isTextFile } from "./file-import.js";
5
5
  import { MediaNodeViews, mediaBlocks, playerOptions } from "./node-views.js";
6
6
  import { MediaUi, mediaFeatureCore } from "./feature.js";
7
- export { AUDIO_RECORDER_OPEN, type AudioRecorder, type AudioRecorderState, type AudioRecorderStatus, type ImportFilesFn, MAX_MEDIA_BYTES, MAX_TEXT_BYTES, MEDIA_TYPES, Media, type MediaKind, type MediaNodeViews, type MediaUi, acceptFor, createAudioRecorder, fileImport, fileNodeFor, formatBytes, isTextFile, mediaBlocks, mediaFeatureCore, mediaKindOf, mediaMime, mediaNodeFor, openAudioRecorder, pickFiles, playerOptions, postUpload, uploadOf };
7
+ export { AUDIO_RECORDER_OPEN, type AudioRecorder, type AudioRecorderState, type AudioRecorderStatus, type ImportFilesFn, MAX_MEDIA_BYTES, MAX_TEXT_BYTES, MEDIA_TYPES, Media, type MediaKind, type MediaNodeViews, type MediaUi, acceptFor, createAudioRecorder, fileImport, fileNodeFor, formatBytes, isTextFile, mediaBlocks, mediaFeatureCore, mediaKindOf, mediaMime, mediaNodeFor, openAudioRecorder, pickFiles, playerOptions, uploadOf };
@@ -1,7 +1,7 @@
1
1
  import { MAX_MEDIA_BYTES, MEDIA_TYPES, acceptFor, mediaKindOf, mediaMime } from "./schema.js";
2
2
  import { createAudioRecorder } from "./audio-recorder.js";
3
- import { AUDIO_RECORDER_OPEN, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, postUpload, uploadOf } from "./upload.js";
3
+ import { AUDIO_RECORDER_OPEN, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, uploadOf } from "./upload.js";
4
4
  import { MAX_TEXT_BYTES, fileImport, isTextFile } from "./file-import.js";
5
5
  import { mediaBlocks, playerOptions } from "./node-views.js";
6
6
  import { mediaFeatureCore } from "./feature.js";
7
- export { AUDIO_RECORDER_OPEN, MAX_MEDIA_BYTES, MAX_TEXT_BYTES, MEDIA_TYPES, Media, acceptFor, createAudioRecorder, fileImport, fileNodeFor, formatBytes, isTextFile, mediaBlocks, mediaFeatureCore, mediaKindOf, mediaMime, mediaNodeFor, openAudioRecorder, pickFiles, playerOptions, postUpload, uploadOf };
7
+ export { AUDIO_RECORDER_OPEN, MAX_MEDIA_BYTES, MAX_TEXT_BYTES, MEDIA_TYPES, Media, acceptFor, createAudioRecorder, fileImport, fileNodeFor, formatBytes, isTextFile, mediaBlocks, mediaFeatureCore, mediaKindOf, mediaMime, mediaNodeFor, openAudioRecorder, pickFiles, playerOptions, uploadOf };
@@ -4,6 +4,7 @@ import { Icon } from "../../../react/Icon.js";
4
4
  import { Button } from "../../../react/ui/button.js";
5
5
  import { formatBytes } from "../upload.js";
6
6
  import { TitleField } from "./TitleField.js";
7
+ import { useEffect, useRef, useState } from "react";
7
8
  import { jsx, jsxs } from "react/jsx-runtime";
8
9
  import { NodeViewWrapper } from "@tiptap/react";
9
10
  //#region src/features/media/react/FileView.tsx
@@ -11,7 +12,23 @@ import { NodeViewWrapper } from "@tiptap/react";
11
12
  function FileView({ node, selected, editor, updateAttributes }) {
12
13
  const { t } = useEditorI18n();
13
14
  const isPdf = node.attrs.mimeType === "application/pdf";
15
+ const [reveal, setReveal] = useState(null);
16
+ const figure = useRef(null);
17
+ useEffect(() => {
18
+ const el = figure.current;
19
+ if (!el) return;
20
+ const onReveal = (event) => {
21
+ const detail = event.detail;
22
+ if (typeof detail === "number" && detail > 0) setReveal((prev) => ({
23
+ page: detail,
24
+ n: (prev?.n ?? 0) + 1
25
+ }));
26
+ };
27
+ el.addEventListener("sk-reveal-page", onReveal);
28
+ return () => el.removeEventListener("sk-reveal-page", onReveal);
29
+ }, []);
14
30
  return /* @__PURE__ */ jsxs(NodeViewWrapper, {
31
+ ref: figure,
15
32
  as: "figure",
16
33
  className: cn("media-block flex flex-col gap-1", selected && "is-selected"),
17
34
  "data-id": node.attrs.id || void 0,
@@ -48,11 +65,11 @@ function FileView({ node, selected, editor, updateAttributes }) {
48
65
  })
49
66
  ]
50
67
  }), isPdf && node.attrs.src ? /* @__PURE__ */ jsx("object", {
51
- data: node.attrs.src,
68
+ data: reveal ? `${node.attrs.src}#page=${reveal.page}` : node.attrs.src,
52
69
  type: "application/pdf",
53
70
  contentEditable: false,
54
71
  className: "mt-2 h-[480px] w-full rounded-md border border-border"
55
- }) : null]
72
+ }, reveal?.n ?? 0) : null]
56
73
  });
57
74
  }
58
75
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"FileView.js","names":[],"sources":["../../../../src/features/media/react/FileView.tsx"],"sourcesContent":["import { NodeViewWrapper, type NodeViewProps } from '@tiptap/react'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { formatBytes } from '../upload'\nimport { TitleField } from './TitleField'\n\n/** The file block: icon, name field, size and „Öffnen“ link; PDFs are embedded below. */\nexport function FileView({ node, selected, editor, updateAttributes }: NodeViewProps) {\n const { t } = useEditorI18n()\n const isPdf = node.attrs.mimeType === 'application/pdf'\n return (\n <NodeViewWrapper\n as=\"figure\"\n className={cn('media-block flex flex-col gap-1', selected && 'is-selected')}\n data-id={node.attrs.id || undefined}\n >\n <div className=\"flex items-center gap-2\" contentEditable={false}>\n <Icon name={isPdf ? 'i-lucide-file-text' : 'i-lucide-file'} className=\"size-5 text-muted-foreground\" />\n <TitleField\n value={node.attrs.title}\n placeholder={t('media.view.fileName')}\n label={t('media.view.fileName')}\n readOnly={!editor.isEditable}\n className=\"min-w-0 flex-1\"\n onChange={title => updateAttributes({ title })}\n />\n {node.attrs.size ? <span className=\"shrink-0 text-xs text-muted-foreground/60\">{formatBytes(node.attrs.size)}</span> : null}\n <Button asChild size=\"xs\" variant=\"secondary\">\n <a href={node.attrs.src} target=\"_blank\" rel=\"noopener noreferrer\">\n <Icon name=\"i-lucide-external-link\" />\n {t('media.view.open')}\n </a>\n </Button>\n </div>\n {isPdf && node.attrs.src\n ? <object data={node.attrs.src} type=\"application/pdf\" contentEditable={false} className=\"mt-2 h-[480px] w-full rounded-md border border-border\" />\n : null}\n </NodeViewWrapper>\n )\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,SAAS,EAAE,MAAM,UAAU,QAAQ,oBAAmC;CACpF,MAAM,EAAE,MAAM,cAAc;CAC5B,MAAM,QAAQ,KAAK,MAAM,aAAa;CACtC,OACE,qBAAC,iBAAD;EACE,IAAG;EACH,WAAW,GAAG,mCAAmC,YAAY,aAAa;EAC1E,WAAS,KAAK,MAAM,MAAM,KAAA;EAH5B,UAAA,CAKE,qBAAC,OAAD;GAAK,WAAU;GAA0B,iBAAiB;GAA1D,UAAA;IACE,oBAAC,MAAD;KAAM,MAAM,QAAQ,uBAAuB;KAAiB,WAAU;IAAgC,CAAA;IACtG,oBAAC,YAAD;KACE,OAAO,KAAK,MAAM;KAClB,aAAa,EAAE,qBAAqB;KACpC,OAAO,EAAE,qBAAqB;KAC9B,UAAU,CAAC,OAAO;KAClB,WAAU;KACV,WAAU,UAAS,iBAAiB,EAAE,MAAM,CAAC;IAC9C,CAAA;IACA,KAAK,MAAM,OAAO,oBAAC,QAAD;KAAM,WAAU;KAA6C,UAAA,YAAY,KAAK,MAAM,IAAI;IAAQ,CAAA,IAAI;IACvH,oBAAC,QAAD;KAAQ,SAAA;KAAQ,MAAK;KAAK,SAAQ;KAChC,UAAA,qBAAC,KAAD;MAAG,MAAM,KAAK,MAAM;MAAK,QAAO;MAAS,KAAI;MAA7C,UAAA,CACE,oBAAC,MAAD,EAAM,MAAK,yBAA0B,CAAA,GACpC,EAAE,iBAAiB,CACnB;;IACG,CAAA;GACL;EACJ,CAAA,GAAA,SAAS,KAAK,MAAM,MACjB,oBAAC,UAAD;GAAQ,MAAM,KAAK,MAAM;GAAK,MAAK;GAAkB,iBAAiB;GAAO,WAAU;EAAyD,CAAA,IAChJ,IACW;;AAErB"}
1
+ {"version":3,"file":"FileView.js","names":[],"sources":["../../../../src/features/media/react/FileView.tsx"],"sourcesContent":["import { NodeViewWrapper, type NodeViewProps } from '@tiptap/react'\nimport { useEffect, useRef, useState } from 'react'\nimport { Icon } from '../../../react/Icon'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { formatBytes } from '../upload'\nimport { TitleField } from './TitleField'\n\n/** The file block: icon, name field, size and „Öffnen“ link; PDFs are embedded below. */\nexport function FileView({ node, selected, editor, updateAttributes }: NodeViewProps) {\n const { t } = useEditorI18n()\n const isPdf = node.attrs.mimeType === 'application/pdf'\n // Page the PDF viewer was asked to open at (`revealSource`, locator „S. 3“). `n` counts the requests: the viewer is\n // remounted on every one, so asking for the same page again jumps back after the user scrolled away.\n const [reveal, setReveal] = useState<{ page: number, n: number } | null>(null)\n const figure = useRef<HTMLElement>(null)\n useEffect(() => {\n const el = figure.current\n if (!el) return\n const onReveal = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail\n if (typeof detail === 'number' && detail > 0) setReveal(prev => ({ page: detail, n: (prev?.n ?? 0) + 1 }))\n }\n // Event name of `revealSource` in features/composites/reveal-source.ts (media must not import composites).\n el.addEventListener('sk-reveal-page', onReveal)\n return () => el.removeEventListener('sk-reveal-page', onReveal)\n }, [])\n\n return (\n <NodeViewWrapper\n ref={figure}\n as=\"figure\"\n className={cn('media-block flex flex-col gap-1', selected && 'is-selected')}\n data-id={node.attrs.id || undefined}\n >\n <div className=\"flex items-center gap-2\" contentEditable={false}>\n <Icon name={isPdf ? 'i-lucide-file-text' : 'i-lucide-file'} className=\"size-5 text-muted-foreground\" />\n <TitleField\n value={node.attrs.title}\n placeholder={t('media.view.fileName')}\n label={t('media.view.fileName')}\n readOnly={!editor.isEditable}\n className=\"min-w-0 flex-1\"\n onChange={title => updateAttributes({ title })}\n />\n {node.attrs.size ? <span className=\"shrink-0 text-xs text-muted-foreground/60\">{formatBytes(node.attrs.size)}</span> : null}\n <Button asChild size=\"xs\" variant=\"secondary\">\n <a href={node.attrs.src} target=\"_blank\" rel=\"noopener noreferrer\">\n <Icon name=\"i-lucide-external-link\" />\n {t('media.view.open')}\n </a>\n </Button>\n </div>\n {isPdf && node.attrs.src\n ? <object key={reveal?.n ?? 0} data={reveal ? `${node.attrs.src}#page=${reveal.page}` : node.attrs.src} type=\"application/pdf\" contentEditable={false} className=\"mt-2 h-[480px] w-full rounded-md border border-border\" />\n : null}\n </NodeViewWrapper>\n )\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,SAAS,EAAE,MAAM,UAAU,QAAQ,oBAAmC;CACpF,MAAM,EAAE,MAAM,cAAc;CAC5B,MAAM,QAAQ,KAAK,MAAM,aAAa;CAGtC,MAAM,CAAC,QAAQ,aAAa,SAA6C,IAAI;CAC7E,MAAM,SAAS,OAAoB,IAAI;CACvC,gBAAgB;EACd,MAAM,KAAK,OAAO;EAClB,IAAI,CAAC,IAAI;EACT,MAAM,YAAY,UAAiB;GACjC,MAAM,SAAU,MAA+B;GAC/C,IAAI,OAAO,WAAW,YAAY,SAAS,GAAG,WAAU,UAAS;IAAE,MAAM;IAAQ,IAAI,MAAM,KAAK,KAAK;GAAE,EAAE;EAC3G;EAEA,GAAG,iBAAiB,kBAAkB,QAAQ;EAC9C,aAAa,GAAG,oBAAoB,kBAAkB,QAAQ;CAChE,GAAG,CAAC,CAAC;CAEL,OACE,qBAAC,iBAAD;EACE,KAAK;EACL,IAAG;EACH,WAAW,GAAG,mCAAmC,YAAY,aAAa;EAC1E,WAAS,KAAK,MAAM,MAAM,KAAA;EAJ5B,UAAA,CAME,qBAAC,OAAD;GAAK,WAAU;GAA0B,iBAAiB;GAA1D,UAAA;IACE,oBAAC,MAAD;KAAM,MAAM,QAAQ,uBAAuB;KAAiB,WAAU;IAAgC,CAAA;IACtG,oBAAC,YAAD;KACE,OAAO,KAAK,MAAM;KAClB,aAAa,EAAE,qBAAqB;KACpC,OAAO,EAAE,qBAAqB;KAC9B,UAAU,CAAC,OAAO;KAClB,WAAU;KACV,WAAU,UAAS,iBAAiB,EAAE,MAAM,CAAC;IAC9C,CAAA;IACA,KAAK,MAAM,OAAO,oBAAC,QAAD;KAAM,WAAU;KAA6C,UAAA,YAAY,KAAK,MAAM,IAAI;IAAQ,CAAA,IAAI;IACvH,oBAAC,QAAD;KAAQ,SAAA;KAAQ,MAAK;KAAK,SAAQ;KAChC,UAAA,qBAAC,KAAD;MAAG,MAAM,KAAK,MAAM;MAAK,QAAO;MAAS,KAAI;MAA7C,UAAA,CACE,oBAAC,MAAD,EAAM,MAAK,yBAA0B,CAAA,GACpC,EAAE,iBAAiB,CACnB;;IACG,CAAA;GACL;EACJ,CAAA,GAAA,SAAS,KAAK,MAAM,MACjB,oBAAC,UAAD;GAA6B,MAAM,SAAS,GAAG,KAAK,MAAM,IAAI,QAAQ,OAAO,SAAS,KAAK,MAAM;GAAK,MAAK;GAAkB,iBAAiB;GAAO,WAAU;EAAyD,GAA3M,QAAQ,KAAK,CAA8L,IACxN,IACW;;AAErB"}
@@ -3,6 +3,12 @@ import { AnyExtension, Node } from "@tiptap/core";
3
3
  //#region src/features/media/schema.d.ts
4
4
  /** The media nodes (image, audio, video, file) of the document schema. */
5
5
  export declare const mediaSchemaExtensions: () => AnyExtension[];
6
+ /**
7
+ * The image node with a `hash` attribute. Markdown: `![alt](src "title")`, followed by ` <!-- {"hash":"…"} -->` when the
8
+ * block has a hash; only that form is claimed by `imageBlockTokenizer` (a block-level token that still yields an `image`
9
+ * token), a plain image keeps the stock inline parse.
10
+ */
11
+ export declare const ImageNode: Node<import("@tiptap/extension-image").ImageOptions, any>;
6
12
  export type MediaKind = 'image' | 'audio' | 'video' | 'file';
7
13
  /** Uploadable MIME types → file extension on disk and media kind. `<uuid>.<ext>` asset names derive from this. */
8
14
  export declare const MEDIA_TYPES: Readonly<Record<string, {
@@ -22,6 +28,7 @@ export interface MediaAttrs {
22
28
  src: string;
23
29
  title?: string;
24
30
  mimeType?: string;
31
+ hash?: string | null;
25
32
  }
26
33
  declare module '@tiptap/core' {
27
34
  interface Commands<ReturnType> {
@@ -51,6 +58,7 @@ export interface FileAttrs {
51
58
  title?: string;
52
59
  mimeType?: string;
53
60
  size?: number;
61
+ hash?: string | null;
54
62
  }
55
63
  /** A generic uploaded file (PDF, office document, …) that has no player: shown as a card with an open button. */
56
64
  export declare const FileNode: Node<any, any>;
@@ -3,13 +3,97 @@ import { ASSET_DOCUMENT_ID, ASSET_NAME, SCRATCH_DOCUMENT_ID, assetRefOf, assetUr
3
3
  import { Node, mergeAttributes } from "@tiptap/core";
4
4
  import { Image } from "@tiptap/extension-image";
5
5
  //#region src/features/media/schema.ts
6
+ /** SHA-256 (hex) of the uploaded bytes, kept on media blocks so derived content can tell when its source changed; null when unknown (memory backend, older documents). */
7
+ const hashAttribute = {
8
+ default: null,
9
+ parseHTML: (el) => el.getAttribute("data-hash"),
10
+ renderHTML: (attrs) => attrs.hash ? { "data-hash": attrs.hash } : {}
11
+ };
6
12
  /** The media nodes (image, audio, video, file) of the document schema. */
7
13
  const mediaSchemaExtensions = () => [
8
- Image,
14
+ ImageNode,
9
15
  AudioNode,
10
16
  VideoNode,
11
17
  FileNode
12
18
  ];
19
+ /**
20
+ * The image node with a `hash` attribute. Markdown: `![alt](src "title")`, followed by ` <!-- {"hash":"…"} -->` when the
21
+ * block has a hash; only that form is claimed by `imageBlockTokenizer` (a block-level token that still yields an `image`
22
+ * token), a plain image keeps the stock inline parse.
23
+ */
24
+ const ImageNode = Image.extend({
25
+ addAttributes() {
26
+ return {
27
+ ...this.parent?.(),
28
+ hash: hashAttribute
29
+ };
30
+ },
31
+ markdownTokenizer: {
32
+ name: "imageBlock",
33
+ level: "block",
34
+ start: (src) => src.search(new RegExp(IMAGE_BLOCK_PATTERN, "m")),
35
+ tokenize: (src) => {
36
+ const match = new RegExp(IMAGE_BLOCK_PATTERN).exec(src);
37
+ if (!match) return void 0;
38
+ return {
39
+ type: "image",
40
+ raw: match[0],
41
+ text: match[1] ?? "",
42
+ href: match[2] ?? "",
43
+ title: match[3],
44
+ attrs: parseAttrComment(match[4])
45
+ };
46
+ }
47
+ },
48
+ parseMarkdown: (token, helpers) => helpers.createNode("image", {
49
+ src: token.href,
50
+ title: token.title,
51
+ alt: token.text,
52
+ hash: typeof token.attrs?.hash === "string" ? token.attrs.hash : null
53
+ }),
54
+ renderMarkdown: (node) => {
55
+ const { src = "", alt = "", title = "" } = node.attrs ?? {};
56
+ return `![${alt}](${src}${title ? ` "${title}"` : ""})${attrComment({ hash: node.attrs?.hash })}`;
57
+ }
58
+ });
59
+ const ATTR_COMMENT_PATTERN = "(?: <!-- (\\{.*?\\}) -->)?[ \\t]*(?:\\n|$)";
60
+ const IMAGE_BLOCK_PATTERN = `^!\\[([^\\]\\n]*)\\]\\(([^)\\s]*)(?: "([^"\\n]*)")?\\) <!-- (\\{.*?\\}) -->[ \\t]*(?:\\n|$)`;
61
+ /** ` <!-- {"hash":"…"} -->` for the attributes that are set (empty string when none); `>` is escaped so the JSON cannot end the comment. */
62
+ function attrComment(attrs) {
63
+ const set = Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== null && value !== void 0 && value !== "" && value !== 0));
64
+ return Object.keys(set).length ? ` <!-- ${JSON.stringify(set).replace(/>/g, "\\u003e")} -->` : "";
65
+ }
66
+ /** The attributes of an `attrComment`; empty for a missing or malformed one. */
67
+ function parseAttrComment(json) {
68
+ if (!json) return {};
69
+ try {
70
+ const value = JSON.parse(json);
71
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+ /** Block tokenizer for `[<label>: title](src)` lines of one media node; `labels` are the accepted labels (regex alternatives). */
77
+ function mediaTokenizer(name, labels) {
78
+ const pattern = `^\\[(?:${labels})(?:: ((?:\\\\.|[^\\]\\\\\\n])*))?\\]\\(([^)\\s]*)\\)${ATTR_COMMENT_PATTERN}`;
79
+ return {
80
+ name,
81
+ level: "block",
82
+ start: (src) => src.search(new RegExp(pattern, "m")),
83
+ tokenize: (src) => {
84
+ const match = new RegExp(pattern).exec(src);
85
+ if (!match) return void 0;
86
+ return {
87
+ type: name,
88
+ raw: match[0],
89
+ title: (match[1] ?? "").replace(/\\(.)/g, "$1"),
90
+ href: match[2] ?? "",
91
+ attrs: parseAttrComment(match[3])
92
+ };
93
+ }
94
+ };
95
+ }
96
+ const stringAttr = (value) => typeof value === "string" ? value : "";
13
97
  /** Uploadable MIME types → file extension on disk and media kind. `<uuid>.<ext>` asset names derive from this. */
14
98
  const MEDIA_TYPES = {
15
99
  "image/png": {
@@ -97,11 +181,25 @@ const mediaAttributes = {
97
181
  default: "",
98
182
  parseHTML: (el) => el.getAttribute("data-mime") ?? "",
99
183
  renderHTML: (attrs) => ({ "data-mime": attrs.mimeType })
100
- }
184
+ },
185
+ hash: hashAttribute
101
186
  };
102
- /** Markdown export (AI context, `getMarkdown()`): a link labelled with the kind and, when there is one, the title. */
103
- function mediaMarkdown(label, title, src) {
104
- return `[${title ? `${label}: ${String(title).replace(/[[\]\\]/g, "\\$&")}` : label}](${String(src ?? "")})`;
187
+ /** Markdown export (AI context, `getMarkdown()`, source mode): a link labelled with the kind and, when there is one, the title; `extra` and the hash go into the trailing attribute comment. */
188
+ function mediaMarkdown(label, attrs, extra) {
189
+ const title = attrs?.title;
190
+ return `[${title ? `${label}: ${String(title).replace(/[[\]\\]/g, "\\$&")}` : label}](${String(attrs?.src ?? "")})${attrComment({
191
+ ...extra,
192
+ hash: attrs?.hash
193
+ })}`;
194
+ }
195
+ /** Node attributes of an audio/video Markdown token. */
196
+ function mediaAttrsOfToken(token) {
197
+ return {
198
+ src: token.href,
199
+ title: token.title,
200
+ mimeType: stringAttr(token.attrs?.mimeType),
201
+ hash: stringAttr(token.attrs?.hash) || null
202
+ };
105
203
  }
106
204
  const AudioNode = Node.create({
107
205
  name: "audio",
@@ -115,7 +213,9 @@ const AudioNode = Node.create({
115
213
  controls: "true",
116
214
  preload: "metadata"
117
215
  })],
118
- renderMarkdown: (node) => mediaMarkdown("Audio", node.attrs?.title || "Aufnahme", node.attrs?.src),
216
+ markdownTokenizer: mediaTokenizer("audio", "Audio"),
217
+ parseMarkdown: (token, helpers) => helpers.createNode("audio", mediaAttrsOfToken(token)),
218
+ renderMarkdown: (node) => mediaMarkdown("Audio", node.attrs, { mimeType: node.attrs?.mimeType }),
119
219
  addCommands() {
120
220
  return { insertAudio: (attrs) => ({ commands }) => commands.insertContent({
121
221
  type: this.name,
@@ -135,7 +235,9 @@ const VideoNode = Node.create({
135
235
  controls: "true",
136
236
  preload: "metadata"
137
237
  })],
138
- renderMarkdown: (node) => mediaMarkdown("Video", node.attrs?.title, node.attrs?.src),
238
+ markdownTokenizer: mediaTokenizer("video", "Video"),
239
+ parseMarkdown: (token, helpers) => helpers.createNode("video", mediaAttrsOfToken(token)),
240
+ renderMarkdown: (node) => mediaMarkdown("Video", node.attrs, { mimeType: node.attrs?.mimeType }),
139
241
  addCommands() {
140
242
  return { insertVideo: (attrs) => ({ commands }) => commands.insertContent({
141
243
  type: this.name,
@@ -166,7 +268,8 @@ const FileNode = Node.create({
166
268
  size: {
167
269
  default: 0,
168
270
  parseHTML: (el) => Number(el.getAttribute("data-size")) || 0
169
- }
271
+ },
272
+ hash: hashAttribute
170
273
  }),
171
274
  parseHTML: () => [{ tag: "a[data-type=\"file\"]" }],
172
275
  renderHTML: ({ node }) => [
@@ -176,11 +279,23 @@ const FileNode = Node.create({
176
279
  "href": node.attrs.src,
177
280
  "download": node.attrs.title,
178
281
  "data-mime": node.attrs.mimeType,
179
- "data-size": String(node.attrs.size)
282
+ "data-size": String(node.attrs.size),
283
+ ...node.attrs.hash ? { "data-hash": node.attrs.hash } : {}
180
284
  },
181
285
  node.attrs.title || "Datei"
182
286
  ],
183
- renderMarkdown: (node) => `[Datei: ${String(node.attrs?.title || "Datei").replace(/[[\]\\]/g, "\\$&")}](${String(node.attrs?.src ?? "")})`,
287
+ markdownTokenizer: mediaTokenizer("file", "Datei|File"),
288
+ parseMarkdown: (token, helpers) => helpers.createNode("file", {
289
+ src: token.href,
290
+ title: token.title,
291
+ mimeType: stringAttr(token.attrs?.mimeType),
292
+ size: typeof token.attrs?.size === "number" ? token.attrs.size : 0,
293
+ hash: stringAttr(token.attrs?.hash) || null
294
+ }),
295
+ renderMarkdown: (node) => mediaMarkdown("Datei", node.attrs, {
296
+ mimeType: node.attrs?.mimeType,
297
+ size: node.attrs?.size
298
+ }),
184
299
  addCommands() {
185
300
  return { insertFile: (attrs) => ({ commands }) => commands.insertContent({
186
301
  type: this.name,
@@ -189,6 +304,6 @@ const FileNode = Node.create({
189
304
  }
190
305
  });
191
306
  //#endregion
192
- export { ASSET_DOCUMENT_ID, ASSET_NAME, AudioNode, FileNode, MAX_MEDIA_BYTES, MEDIA_TYPES, SCRATCH_DOCUMENT_ID, VideoNode, acceptFor, assetRefOf, assetUrl, mediaKindOf, mediaMime, mediaSchemaExtensions };
307
+ export { ASSET_DOCUMENT_ID, ASSET_NAME, AudioNode, FileNode, ImageNode, MAX_MEDIA_BYTES, MEDIA_TYPES, SCRATCH_DOCUMENT_ID, VideoNode, acceptFor, assetRefOf, assetUrl, mediaKindOf, mediaMime, mediaSchemaExtensions };
193
308
 
194
309
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/media/schema.ts"],"sourcesContent":["import { type Attributes, type AnyExtension, mergeAttributes, Node } from '@tiptap/core'\nimport { Image } from '@tiptap/extension-image'\nimport { TEXT_ABILITIES } from '../../core/schema'\n\nexport * from './assets'\n\n// Vue-free and server-importable (`@ecsplico/editor/media/schema`): the upload API validates against these types, the\n// schema has the nodes. The client adds the node views in node-views.ts.\n\n/** The media nodes (image, audio, video, file) of the document schema. */\nexport const mediaSchemaExtensions = (): AnyExtension[] => [Image, AudioNode, VideoNode, FileNode]\n\nexport type MediaKind = 'image' | 'audio' | 'video' | 'file'\n\n/** Uploadable MIME types → file extension on disk and media kind. `<uuid>.<ext>` asset names derive from this. */\nexport const MEDIA_TYPES: Readonly<Record<string, { ext: string, kind: MediaKind }>> = {\n 'image/png': { ext: 'png', kind: 'image' },\n 'image/jpeg': { ext: 'jpg', kind: 'image' },\n 'image/gif': { ext: 'gif', kind: 'image' },\n 'image/webp': { ext: 'webp', kind: 'image' },\n 'audio/webm': { ext: 'weba', kind: 'audio' },\n 'audio/mp4': { ext: 'm4a', kind: 'audio' },\n // What Chrome on macOS reports for `.m4a` files picked from disk.\n 'audio/x-m4a': { ext: 'm4a', kind: 'audio' },\n 'audio/mpeg': { ext: 'mp3', kind: 'audio' },\n 'audio/wav': { ext: 'wav', kind: 'audio' },\n 'audio/x-wav': { ext: 'wav', kind: 'audio' },\n 'audio/ogg': { ext: 'ogg', kind: 'audio' },\n 'video/mp4': { ext: 'mp4', kind: 'video' },\n 'video/webm': { ext: 'webm', kind: 'video' },\n 'video/quicktime': { ext: 'mov', kind: 'video' },\n}\n\n/** Upload size limit per kind. */\nexport const MAX_MEDIA_BYTES: Readonly<Record<MediaKind, number>> = {\n image: 5 * 1024 * 1024,\n audio: 50 * 1024 * 1024,\n video: 200 * 1024 * 1024,\n // Generic files (PDF, office documents, …): anything the media kinds do not cover.\n file: 50 * 1024 * 1024,\n}\n\n/** `type` without parameters, lowercased (`audio/webm;codecs=opus` → `audio/webm`): the key into `MEDIA_TYPES`. */\nexport const mediaMime = (type: string): string => (type.split(';')[0] ?? '').trim().toLowerCase()\n\n/** Kind of an uploadable MIME type (parameters such as `;codecs=…` ignored); null when uploads do not accept it. */\nexport function mediaKindOf(mime: string): MediaKind | null {\n return MEDIA_TYPES[mediaMime(mime)]?.kind ?? null\n}\n\n/** `accept` value of a file input for one kind: its MIME types, comma-joined. */\nexport function acceptFor(kind: MediaKind): string {\n return Object.keys(MEDIA_TYPES).filter(mime => MEDIA_TYPES[mime]?.kind === kind).join(',')\n}\n\n/** Attributes of an audio/video node; `title` is the editable caption, `mimeType` the uploaded file's base type. */\nexport interface MediaAttrs {\n src: string\n title?: string\n mimeType?: string\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n audio: {\n /** Inserts an audio block at the selection. */\n insertAudio: (attrs: MediaAttrs) => ReturnType\n }\n video: {\n /** Inserts a video block at the selection. */\n insertVideo: (attrs: MediaAttrs) => ReturnType\n }\n }\n}\n\nconst mediaAttributes: Attributes = {\n src: { default: '' },\n title: {\n default: '',\n parseHTML: el => el.getAttribute('data-title') ?? '',\n renderHTML: attrs => ({ 'data-title': attrs.title }),\n },\n mimeType: {\n default: '',\n parseHTML: el => el.getAttribute('data-mime') ?? '',\n renderHTML: attrs => ({ 'data-mime': attrs.mimeType }),\n },\n}\n\n/** Markdown export (AI context, `getMarkdown()`): a link labelled with the kind and, when there is one, the title. */\nfunction mediaMarkdown(label: string, title: unknown, src: unknown): string {\n const text = title ? `${label}: ${String(title).replace(/[[\\]\\\\]/g, '\\\\$&')}` : label\n return `[${text}](${String(src ?? '')})`\n}\n\nexport const AudioNode = Node.create({\n name: 'audio',\n group: 'block',\n atom: true,\n draggable: true,\n // A player block holds no text: nothing text-related applies inside it.\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => mediaAttributes,\n parseHTML: () => [{ tag: 'audio[src]' }],\n renderHTML: ({ HTMLAttributes }) => ['audio', mergeAttributes(HTMLAttributes, { controls: 'true', preload: 'metadata' })],\n renderMarkdown: node => mediaMarkdown('Audio', node.attrs?.title || 'Aufnahme', node.attrs?.src),\n addCommands() {\n return {\n insertAudio: attrs => ({ commands }) => commands.insertContent({ type: this.name, attrs }),\n }\n },\n})\n\nexport const VideoNode = Node.create({\n name: 'video',\n group: 'block',\n atom: true,\n draggable: true,\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => mediaAttributes,\n parseHTML: () => [{ tag: 'video[src]' }],\n renderHTML: ({ HTMLAttributes }) => ['video', mergeAttributes(HTMLAttributes, { controls: 'true', preload: 'metadata' })],\n renderMarkdown: node => mediaMarkdown('Video', node.attrs?.title, node.attrs?.src),\n addCommands() {\n return {\n insertVideo: attrs => ({ commands }) => commands.insertContent({ type: this.name, attrs }),\n }\n },\n})\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n file: {\n /** Inserts a file block (download link, PDF preview) at the selection. */\n insertFile: (attrs: FileAttrs) => ReturnType\n }\n }\n}\n\n/** Attributes of a file block; `title` is the editable file name, `size` the byte count. */\nexport interface FileAttrs {\n src: string\n title?: string\n mimeType?: string\n size?: number\n}\n\n/** A generic uploaded file (PDF, office document, …) that has no player: shown as a card with an open button. */\nexport const FileNode = Node.create({\n name: 'file',\n group: 'block',\n atom: true,\n draggable: true,\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => ({\n src: { default: '', parseHTML: el => el.getAttribute('href') ?? '' },\n title: { default: '', parseHTML: el => el.getAttribute('download') ?? '' },\n mimeType: { default: '', parseHTML: el => el.getAttribute('data-mime') ?? '' },\n size: { default: 0, parseHTML: el => Number(el.getAttribute('data-size')) || 0 },\n }),\n parseHTML: () => [{ tag: 'a[data-type=\"file\"]' }],\n renderHTML: ({ node }) => ['a', {\n 'data-type': 'file',\n 'href': node.attrs.src,\n 'download': node.attrs.title,\n 'data-mime': node.attrs.mimeType,\n 'data-size': String(node.attrs.size),\n }, node.attrs.title || 'Datei'],\n renderMarkdown: node => `[Datei: ${String(node.attrs?.title || 'Datei').replace(/[[\\]\\\\]/g, '\\\\$&')}](${String(node.attrs?.src ?? '')})`,\n addCommands() {\n return {\n insertFile: attrs => ({ commands }) => commands.insertContent({ type: this.name, attrs }),\n }\n },\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/media/schema.ts"],"sourcesContent":["import { type Attributes, type AnyExtension, type MarkdownToken, type MarkdownTokenizer, mergeAttributes, Node } from '@tiptap/core'\nimport { Image } from '@tiptap/extension-image'\nimport { TEXT_ABILITIES } from '../../core/schema'\n\nexport * from './assets'\n\n// Vue-free and server-importable (`@ecsplico/editor/media/schema`): the upload API validates against these types, the\n// schema has the nodes. The client adds the node views in node-views.ts.\n\n/** SHA-256 (hex) of the uploaded bytes, kept on media blocks so derived content can tell when its source changed; null when unknown (memory backend, older documents). */\nconst hashAttribute: Attributes[string] = {\n default: null,\n parseHTML: el => el.getAttribute('data-hash'),\n renderHTML: attrs => (attrs.hash ? { 'data-hash': attrs.hash } : {}),\n}\n\n/** The media nodes (image, audio, video, file) of the document schema. */\nexport const mediaSchemaExtensions = (): AnyExtension[] => [ImageNode, AudioNode, VideoNode, FileNode]\n\n/**\n * The image node with a `hash` attribute. Markdown: `![alt](src \"title\")`, followed by ` <!-- {\"hash\":\"…\"} -->` when the\n * block has a hash; only that form is claimed by `imageBlockTokenizer` (a block-level token that still yields an `image`\n * token), a plain image keeps the stock inline parse.\n */\nexport const ImageNode = Image.extend({\n addAttributes() {\n return { ...this.parent?.(), hash: hashAttribute }\n },\n markdownTokenizer: {\n name: 'imageBlock',\n level: 'block',\n start: src => src.search(new RegExp(IMAGE_BLOCK_PATTERN, 'm')),\n tokenize: (src) => {\n const match = new RegExp(IMAGE_BLOCK_PATTERN).exec(src)\n if (!match) return undefined\n return { type: 'image', raw: match[0], text: match[1] ?? '', href: match[2] ?? '', title: match[3], attrs: parseAttrComment(match[4]) }\n },\n },\n parseMarkdown: (token, helpers) => helpers.createNode('image', {\n src: token.href,\n title: token.title,\n alt: token.text,\n hash: typeof token.attrs?.hash === 'string' ? token.attrs.hash : null,\n }),\n renderMarkdown: (node) => {\n const { src = '', alt = '', title = '' } = node.attrs ?? {}\n return `![${alt}](${src}${title ? ` \"${title}\"` : ''})${attrComment({ hash: node.attrs?.hash })}`\n },\n})\n\n// Markdown form of the text-less media blocks. Audio, video and file are a paragraph of nothing but a link\n// (`[Audio: name](url)`), optionally followed by an HTML comment holding the attributes the link cannot carry\n// (`hash`, `mimeType`, `size`) as JSON, the way composite fences hold their meta. The block-level tokenizers below parse\n// that line back into the node; labels of every UI language are accepted, the export always writes the German one.\nconst ATTR_COMMENT_PATTERN = '(?: <!-- (\\\\{.*?\\\\}) -->)?[ \\\\t]*(?:\\\\n|$)'\nconst IMAGE_BLOCK_PATTERN = `^!\\\\[([^\\\\]\\\\n]*)\\\\]\\\\(([^)\\\\s]*)(?: \"([^\"\\\\n]*)\")?\\\\) <!-- (\\\\{.*?\\\\}) -->[ \\\\t]*(?:\\\\n|$)`\n\n/** ` <!-- {\"hash\":\"…\"} -->` for the attributes that are set (empty string when none); `>` is escaped so the JSON cannot end the comment. */\nfunction attrComment(attrs: Record<string, unknown>): string {\n const set = Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== null && value !== undefined && value !== '' && value !== 0))\n return Object.keys(set).length ? ` <!-- ${JSON.stringify(set).replace(/>/g, '\\\\u003e')} -->` : ''\n}\n\n/** The attributes of an `attrComment`; empty for a missing or malformed one. */\nfunction parseAttrComment(json: string | undefined): Record<string, unknown> {\n if (!json) return {}\n try {\n const value: unknown = JSON.parse(json)\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}\n }\n catch {\n return {}\n }\n}\n\n/** Block tokenizer for `[<label>: title](src)` lines of one media node; `labels` are the accepted labels (regex alternatives). */\nfunction mediaTokenizer(name: string, labels: string): MarkdownTokenizer {\n const pattern = `^\\\\[(?:${labels})(?:: ((?:\\\\\\\\.|[^\\\\]\\\\\\\\\\\\n])*))?\\\\]\\\\(([^)\\\\s]*)\\\\)${ATTR_COMMENT_PATTERN}`\n return {\n name,\n level: 'block',\n start: src => src.search(new RegExp(pattern, 'm')),\n tokenize: (src) => {\n const match = new RegExp(pattern).exec(src)\n if (!match) return undefined\n return { type: name, raw: match[0], title: (match[1] ?? '').replace(/\\\\(.)/g, '$1'), href: match[2] ?? '', attrs: parseAttrComment(match[3]) }\n },\n }\n}\n\nconst stringAttr = (value: unknown): string => typeof value === 'string' ? value : ''\n\nexport type MediaKind = 'image' | 'audio' | 'video' | 'file'\n\n/** Uploadable MIME types → file extension on disk and media kind. `<uuid>.<ext>` asset names derive from this. */\nexport const MEDIA_TYPES: Readonly<Record<string, { ext: string, kind: MediaKind }>> = {\n 'image/png': { ext: 'png', kind: 'image' },\n 'image/jpeg': { ext: 'jpg', kind: 'image' },\n 'image/gif': { ext: 'gif', kind: 'image' },\n 'image/webp': { ext: 'webp', kind: 'image' },\n 'audio/webm': { ext: 'weba', kind: 'audio' },\n 'audio/mp4': { ext: 'm4a', kind: 'audio' },\n // What Chrome on macOS reports for `.m4a` files picked from disk.\n 'audio/x-m4a': { ext: 'm4a', kind: 'audio' },\n 'audio/mpeg': { ext: 'mp3', kind: 'audio' },\n 'audio/wav': { ext: 'wav', kind: 'audio' },\n 'audio/x-wav': { ext: 'wav', kind: 'audio' },\n 'audio/ogg': { ext: 'ogg', kind: 'audio' },\n 'video/mp4': { ext: 'mp4', kind: 'video' },\n 'video/webm': { ext: 'webm', kind: 'video' },\n 'video/quicktime': { ext: 'mov', kind: 'video' },\n}\n\n/** Upload size limit per kind. */\nexport const MAX_MEDIA_BYTES: Readonly<Record<MediaKind, number>> = {\n image: 5 * 1024 * 1024,\n audio: 50 * 1024 * 1024,\n video: 200 * 1024 * 1024,\n // Generic files (PDF, office documents, …): anything the media kinds do not cover.\n file: 50 * 1024 * 1024,\n}\n\n/** `type` without parameters, lowercased (`audio/webm;codecs=opus` → `audio/webm`): the key into `MEDIA_TYPES`. */\nexport const mediaMime = (type: string): string => (type.split(';')[0] ?? '').trim().toLowerCase()\n\n/** Kind of an uploadable MIME type (parameters such as `;codecs=…` ignored); null when uploads do not accept it. */\nexport function mediaKindOf(mime: string): MediaKind | null {\n return MEDIA_TYPES[mediaMime(mime)]?.kind ?? null\n}\n\n/** `accept` value of a file input for one kind: its MIME types, comma-joined. */\nexport function acceptFor(kind: MediaKind): string {\n return Object.keys(MEDIA_TYPES).filter(mime => MEDIA_TYPES[mime]?.kind === kind).join(',')\n}\n\n/** Attributes of an audio/video node; `title` is the editable caption, `mimeType` the uploaded file's base type. */\nexport interface MediaAttrs {\n src: string\n title?: string\n mimeType?: string\n hash?: string | null\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n audio: {\n /** Inserts an audio block at the selection. */\n insertAudio: (attrs: MediaAttrs) => ReturnType\n }\n video: {\n /** Inserts a video block at the selection. */\n insertVideo: (attrs: MediaAttrs) => ReturnType\n }\n }\n}\n\nconst mediaAttributes: Attributes = {\n src: { default: '' },\n title: {\n default: '',\n parseHTML: el => el.getAttribute('data-title') ?? '',\n renderHTML: attrs => ({ 'data-title': attrs.title }),\n },\n mimeType: {\n default: '',\n parseHTML: el => el.getAttribute('data-mime') ?? '',\n renderHTML: attrs => ({ 'data-mime': attrs.mimeType }),\n },\n hash: hashAttribute,\n}\n\n/** Markdown export (AI context, `getMarkdown()`, source mode): a link labelled with the kind and, when there is one, the title; `extra` and the hash go into the trailing attribute comment. */\nfunction mediaMarkdown(label: string, attrs: Record<string, unknown> | undefined, extra: Record<string, unknown>): string {\n const title = attrs?.title\n const text = title ? `${label}: ${String(title).replace(/[[\\]\\\\]/g, '\\\\$&')}` : label\n return `[${text}](${String(attrs?.src ?? '')})${attrComment({ ...extra, hash: attrs?.hash })}`\n}\n\n/** Node attributes of an audio/video Markdown token. */\nfunction mediaAttrsOfToken(token: MarkdownToken) {\n return { src: token.href, title: token.title, mimeType: stringAttr(token.attrs?.mimeType), hash: stringAttr(token.attrs?.hash) || null }\n}\n\nexport const AudioNode = Node.create({\n name: 'audio',\n group: 'block',\n atom: true,\n draggable: true,\n // A player block holds no text: nothing text-related applies inside it.\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => mediaAttributes,\n parseHTML: () => [{ tag: 'audio[src]' }],\n renderHTML: ({ HTMLAttributes }) => ['audio', mergeAttributes(HTMLAttributes, { controls: 'true', preload: 'metadata' })],\n markdownTokenizer: mediaTokenizer('audio', 'Audio'),\n parseMarkdown: (token, helpers) => helpers.createNode('audio', mediaAttrsOfToken(token)),\n renderMarkdown: node => mediaMarkdown('Audio', node.attrs, { mimeType: node.attrs?.mimeType }),\n addCommands() {\n return {\n insertAudio: attrs => ({ commands }) => commands.insertContent({ type: this.name, attrs }),\n }\n },\n})\n\nexport const VideoNode = Node.create({\n name: 'video',\n group: 'block',\n atom: true,\n draggable: true,\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => mediaAttributes,\n parseHTML: () => [{ tag: 'video[src]' }],\n renderHTML: ({ HTMLAttributes }) => ['video', mergeAttributes(HTMLAttributes, { controls: 'true', preload: 'metadata' })],\n markdownTokenizer: mediaTokenizer('video', 'Video'),\n parseMarkdown: (token, helpers) => helpers.createNode('video', mediaAttrsOfToken(token)),\n renderMarkdown: node => mediaMarkdown('Video', node.attrs, { mimeType: node.attrs?.mimeType }),\n addCommands() {\n return {\n insertVideo: attrs => ({ commands }) => commands.insertContent({ type: this.name, attrs }),\n }\n },\n})\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n file: {\n /** Inserts a file block (download link, PDF preview) at the selection. */\n insertFile: (attrs: FileAttrs) => ReturnType\n }\n }\n}\n\n/** Attributes of a file block; `title` is the editable file name, `size` the byte count. */\nexport interface FileAttrs {\n src: string\n title?: string\n mimeType?: string\n size?: number\n hash?: string | null\n}\n\n/** A generic uploaded file (PDF, office document, …) that has no player: shown as a card with an open button. */\nexport const FileNode = Node.create({\n name: 'file',\n group: 'block',\n atom: true,\n draggable: true,\n abilities: { revokes: TEXT_ABILITIES },\n addAttributes: () => ({\n src: { default: '', parseHTML: el => el.getAttribute('href') ?? '' },\n title: { default: '', parseHTML: el => el.getAttribute('download') ?? '' },\n mimeType: { default: '', parseHTML: el => el.getAttribute('data-mime') ?? '' },\n size: { default: 0, parseHTML: el => Number(el.getAttribute('data-size')) || 0 },\n hash: hashAttribute,\n }),\n parseHTML: () => [{ tag: 'a[data-type=\"file\"]' }],\n renderHTML: ({ node }) => ['a', {\n 'data-type': 'file',\n 'href': node.attrs.src,\n 'download': node.attrs.title,\n 'data-mime': node.attrs.mimeType,\n 'data-size': String(node.attrs.size),\n ...(node.attrs.hash ? { 'data-hash': node.attrs.hash } : {}),\n }, node.attrs.title || 'Datei'],\n markdownTokenizer: mediaTokenizer('file', 'Datei|File'),\n parseMarkdown: (token, helpers) => helpers.createNode('file', {\n src: token.href,\n title: token.title,\n mimeType: stringAttr(token.attrs?.mimeType),\n size: typeof token.attrs?.size === 'number' ? token.attrs.size : 0,\n hash: stringAttr(token.attrs?.hash) || null,\n }),\n renderMarkdown: node => mediaMarkdown('Datei', node.attrs, { mimeType: node.attrs?.mimeType, size: node.attrs?.size }),\n addCommands() {\n return {\n insertFile: attrs => ({ commands }) => commands.insertContent({ type: this.name, attrs }),\n }\n },\n})\n"],"mappings":";;;;;;AAUA,MAAM,gBAAoC;CACxC,SAAS;CACT,YAAW,OAAM,GAAG,aAAa,WAAW;CAC5C,aAAY,UAAU,MAAM,OAAO,EAAE,aAAa,MAAM,KAAK,IAAI,CAAC;AACpE;;AAGA,MAAa,8BAA8C;CAAC;CAAW;CAAW;CAAW;AAAQ;;;;;;AAOrG,MAAa,YAAY,MAAM,OAAO;CACpC,gBAAgB;EACd,OAAO;GAAE,GAAG,KAAK,SAAS;GAAG,MAAM;EAAc;CACnD;CACA,mBAAmB;EACjB,MAAM;EACN,OAAO;EACP,QAAO,QAAO,IAAI,OAAO,IAAI,OAAO,qBAAqB,GAAG,CAAC;EAC7D,WAAW,QAAQ;GACjB,MAAM,QAAQ,IAAI,OAAO,mBAAmB,CAAC,CAAC,KAAK,GAAG;GACtD,IAAI,CAAC,OAAO,OAAO,KAAA;GACnB,OAAO;IAAE,MAAM;IAAS,KAAK,MAAM;IAAI,MAAM,MAAM,MAAM;IAAI,MAAM,MAAM,MAAM;IAAI,OAAO,MAAM;IAAI,OAAO,iBAAiB,MAAM,EAAE;GAAE;EACxI;CACF;CACA,gBAAgB,OAAO,YAAY,QAAQ,WAAW,SAAS;EAC7D,KAAK,MAAM;EACX,OAAO,MAAM;EACb,KAAK,MAAM;EACX,MAAM,OAAO,MAAM,OAAO,SAAS,WAAW,MAAM,MAAM,OAAO;CACnE,CAAC;CACD,iBAAiB,SAAS;EACxB,MAAM,EAAE,MAAM,IAAI,MAAM,IAAI,QAAQ,OAAO,KAAK,SAAS,CAAC;EAC1D,OAAO,KAAK,IAAI,IAAI,MAAM,QAAQ,KAAK,MAAM,KAAK,GAAG,GAAG,YAAY,EAAE,MAAM,KAAK,OAAO,KAAK,CAAC;CAChG;AACF,CAAC;AAMD,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;;AAG5B,SAAS,YAAY,OAAwC;CAC3D,MAAM,MAAM,OAAO,YAAY,OAAO,QAAQ,KAAK,CAAC,CAAC,QAAQ,GAAG,WAAW,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,MAAM,UAAU,CAAC,CAAC;CAChJ,OAAO,OAAO,KAAK,GAAG,CAAC,CAAC,SAAS,SAAS,KAAK,UAAU,GAAG,CAAC,CAAC,QAAQ,MAAM,SAAS,EAAE,QAAQ;AACjG;;AAGA,SAAS,iBAAiB,MAAmD;CAC3E,IAAI,CAAC,MAAM,OAAO,CAAC;CACnB,IAAI;EACF,MAAM,QAAiB,KAAK,MAAM,IAAI;EACtC,OAAO,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,QAAmC,CAAC;CAC3G,QACM;EACJ,OAAO,CAAC;CACV;AACF;;AAGA,SAAS,eAAe,MAAc,QAAmC;CACvE,MAAM,UAAU,UAAU,OAAO,uDAAuD;CACxF,OAAO;EACL;EACA,OAAO;EACP,QAAO,QAAO,IAAI,OAAO,IAAI,OAAO,SAAS,GAAG,CAAC;EACjD,WAAW,QAAQ;GACjB,MAAM,QAAQ,IAAI,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;GAC1C,IAAI,CAAC,OAAO,OAAO,KAAA;GACnB,OAAO;IAAE,MAAM;IAAM,KAAK,MAAM;IAAI,QAAQ,MAAM,MAAM,GAAA,CAAI,QAAQ,UAAU,IAAI;IAAG,MAAM,MAAM,MAAM;IAAI,OAAO,iBAAiB,MAAM,EAAE;GAAE;EAC/I;CACF;AACF;AAEA,MAAM,cAAc,UAA2B,OAAO,UAAU,WAAW,QAAQ;;AAKnF,MAAa,cAA0E;CACrF,aAAa;EAAE,KAAK;EAAO,MAAM;CAAQ;CACzC,cAAc;EAAE,KAAK;EAAO,MAAM;CAAQ;CAC1C,aAAa;EAAE,KAAK;EAAO,MAAM;CAAQ;CACzC,cAAc;EAAE,KAAK;EAAQ,MAAM;CAAQ;CAC3C,cAAc;EAAE,KAAK;EAAQ,MAAM;CAAQ;CAC3C,aAAa;EAAE,KAAK;EAAO,MAAM;CAAQ;CAEzC,eAAe;EAAE,KAAK;EAAO,MAAM;CAAQ;CAC3C,cAAc;EAAE,KAAK;EAAO,MAAM;CAAQ;CAC1C,aAAa;EAAE,KAAK;EAAO,MAAM;CAAQ;CACzC,eAAe;EAAE,KAAK;EAAO,MAAM;CAAQ;CAC3C,aAAa;EAAE,KAAK;EAAO,MAAM;CAAQ;CACzC,aAAa;EAAE,KAAK;EAAO,MAAM;CAAQ;CACzC,cAAc;EAAE,KAAK;EAAQ,MAAM;CAAQ;CAC3C,mBAAmB;EAAE,KAAK;EAAO,MAAM;CAAQ;AACjD;;AAGA,MAAa,kBAAuD;CAClE,OAAO;CACP,OAAO;CACP,OAAO;CAEP,MAAM;AACR;;AAGA,MAAa,aAAa,UAA0B,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;;AAGjG,SAAgB,YAAY,MAAgC;CAC1D,OAAO,YAAY,UAAU,IAAI,EAAE,EAAE,QAAQ;AAC/C;;AAGA,SAAgB,UAAU,MAAyB;CACjD,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC,QAAO,SAAQ,YAAY,KAAK,EAAE,SAAS,IAAI,CAAC,CAAC,KAAK,GAAG;AAC3F;AAuBA,MAAM,kBAA8B;CAClC,KAAK,EAAE,SAAS,GAAG;CACnB,OAAO;EACL,SAAS;EACT,YAAW,OAAM,GAAG,aAAa,YAAY,KAAK;EAClD,aAAY,WAAU,EAAE,cAAc,MAAM,MAAM;CACpD;CACA,UAAU;EACR,SAAS;EACT,YAAW,OAAM,GAAG,aAAa,WAAW,KAAK;EACjD,aAAY,WAAU,EAAE,aAAa,MAAM,SAAS;CACtD;CACA,MAAM;AACR;;AAGA,SAAS,cAAc,OAAe,OAA4C,OAAwC;CACxH,MAAM,QAAQ,OAAO;CAErB,OAAO,IADM,QAAQ,GAAG,MAAM,IAAI,OAAO,KAAK,CAAC,CAAC,QAAQ,YAAY,MAAM,MAAM,MAChE,IAAI,OAAO,OAAO,OAAO,EAAE,EAAE,GAAG,YAAY;EAAE,GAAG;EAAO,MAAM,OAAO;CAAK,CAAC;AAC7F;;AAGA,SAAS,kBAAkB,OAAsB;CAC/C,OAAO;EAAE,KAAK,MAAM;EAAM,OAAO,MAAM;EAAO,UAAU,WAAW,MAAM,OAAO,QAAQ;EAAG,MAAM,WAAW,MAAM,OAAO,IAAI,KAAK;CAAK;AACzI;AAEA,MAAa,YAAY,KAAK,OAAO;CACnC,MAAM;CACN,OAAO;CACP,MAAM;CACN,WAAW;CAEX,WAAW,EAAE,SAAS,eAAe;CACrC,qBAAqB;CACrB,iBAAiB,CAAC,EAAE,KAAK,aAAa,CAAC;CACvC,aAAa,EAAE,qBAAqB,CAAC,SAAS,gBAAgB,gBAAgB;EAAE,UAAU;EAAQ,SAAS;CAAW,CAAC,CAAC;CACxH,mBAAmB,eAAe,SAAS,OAAO;CAClD,gBAAgB,OAAO,YAAY,QAAQ,WAAW,SAAS,kBAAkB,KAAK,CAAC;CACvF,iBAAgB,SAAQ,cAAc,SAAS,KAAK,OAAO,EAAE,UAAU,KAAK,OAAO,SAAS,CAAC;CAC7F,cAAc;EACZ,OAAO,EACL,cAAa,WAAU,EAAE,eAAe,SAAS,cAAc;GAAE,MAAM,KAAK;GAAM;EAAM,CAAC,EAC3F;CACF;AACF,CAAC;AAED,MAAa,YAAY,KAAK,OAAO;CACnC,MAAM;CACN,OAAO;CACP,MAAM;CACN,WAAW;CACX,WAAW,EAAE,SAAS,eAAe;CACrC,qBAAqB;CACrB,iBAAiB,CAAC,EAAE,KAAK,aAAa,CAAC;CACvC,aAAa,EAAE,qBAAqB,CAAC,SAAS,gBAAgB,gBAAgB;EAAE,UAAU;EAAQ,SAAS;CAAW,CAAC,CAAC;CACxH,mBAAmB,eAAe,SAAS,OAAO;CAClD,gBAAgB,OAAO,YAAY,QAAQ,WAAW,SAAS,kBAAkB,KAAK,CAAC;CACvF,iBAAgB,SAAQ,cAAc,SAAS,KAAK,OAAO,EAAE,UAAU,KAAK,OAAO,SAAS,CAAC;CAC7F,cAAc;EACZ,OAAO,EACL,cAAa,WAAU,EAAE,eAAe,SAAS,cAAc;GAAE,MAAM,KAAK;GAAM;EAAM,CAAC,EAC3F;CACF;AACF,CAAC;;AAqBD,MAAa,WAAW,KAAK,OAAO;CAClC,MAAM;CACN,OAAO;CACP,MAAM;CACN,WAAW;CACX,WAAW,EAAE,SAAS,eAAe;CACrC,sBAAsB;EACpB,KAAK;GAAE,SAAS;GAAI,YAAW,OAAM,GAAG,aAAa,MAAM,KAAK;EAAG;EACnE,OAAO;GAAE,SAAS;GAAI,YAAW,OAAM,GAAG,aAAa,UAAU,KAAK;EAAG;EACzE,UAAU;GAAE,SAAS;GAAI,YAAW,OAAM,GAAG,aAAa,WAAW,KAAK;EAAG;EAC7E,MAAM;GAAE,SAAS;GAAG,YAAW,OAAM,OAAO,GAAG,aAAa,WAAW,CAAC,KAAK;EAAE;EAC/E,MAAM;CACR;CACA,iBAAiB,CAAC,EAAE,KAAK,wBAAsB,CAAC;CAChD,aAAa,EAAE,WAAW;EAAC;EAAK;GAC9B,aAAa;GACb,QAAQ,KAAK,MAAM;GACnB,YAAY,KAAK,MAAM;GACvB,aAAa,KAAK,MAAM;GACxB,aAAa,OAAO,KAAK,MAAM,IAAI;GACnC,GAAI,KAAK,MAAM,OAAO,EAAE,aAAa,KAAK,MAAM,KAAK,IAAI,CAAC;EAC5D;EAAG,KAAK,MAAM,SAAS;CAAO;CAC9B,mBAAmB,eAAe,QAAQ,YAAY;CACtD,gBAAgB,OAAO,YAAY,QAAQ,WAAW,QAAQ;EAC5D,KAAK,MAAM;EACX,OAAO,MAAM;EACb,UAAU,WAAW,MAAM,OAAO,QAAQ;EAC1C,MAAM,OAAO,MAAM,OAAO,SAAS,WAAW,MAAM,MAAM,OAAO;EACjE,MAAM,WAAW,MAAM,OAAO,IAAI,KAAK;CACzC,CAAC;CACD,iBAAgB,SAAQ,cAAc,SAAS,KAAK,OAAO;EAAE,UAAU,KAAK,OAAO;EAAU,MAAM,KAAK,OAAO;CAAK,CAAC;CACrH,cAAc;EACZ,OAAO,EACL,aAAY,WAAU,EAAE,eAAe,SAAS,cAAc;GAAE,MAAM,KAAK;GAAM;EAAM,CAAC,EAC1F;CACF;AACF,CAAC"}
@@ -1,35 +1,36 @@
1
- import { FeatureContext, UploadFn } from "../../core/feature.js";
1
+ import { FeatureContext } from "../../core/feature.js";
2
2
  import "../../core/index.js";
3
3
  import { Editor, Extension, JSONContent } from "@tiptap/core";
4
4
  //#region src/features/media/upload.d.ts
5
- /** `importFiles` of the media feature: imports `files` at `pos` (default: the selection), toasting failures; resolves whether every file was imported. */
6
- export type ImportFilesFn = (editor: Editor, files: File[] | FileList, pos?: number) => Promise<boolean>;
7
- /** Default upload: POSTs `file` to the asset endpoint of document `documentId` through the context's HTTP client (size limits per kind, see schema.ts). */
8
- export declare function postUpload(fetch: FeatureContext['fetch'], documentId: string, file: File): Promise<{
5
+ /** Stores a file and resolves with the URL it is addressed by. */
6
+ type UploadFile = (file: File) => Promise<{
9
7
  url: string;
8
+ sha256?: string;
10
9
  }>;
11
- /** The upload of a surface: the host's `upload` prop, else the asset endpoint of its `documentId` (default: the shared `scratch` owner). */
12
- export declare const uploadOf: (ctx: FeatureContext) => UploadFn;
10
+ /** `importFiles` of the media feature: imports `files` at `pos` (default: the selection), toasting failures; resolves whether every file was imported. */
11
+ export type ImportFilesFn = (editor: Editor, files: File[] | FileList, pos?: number) => Promise<boolean>;
12
+ /** The upload of a surface: the backend's asset store for its `documentId` (default: the shared `scratch` owner; size limits per kind, see schema.ts). */
13
+ export declare const uploadOf: (ctx: FeatureContext) => UploadFile;
13
14
  /** Makes the upload and the file import reachable as `editor.storage.media` (drop/paste, toolbar, slash menu, recorder). */
14
15
  export declare const Media: Extension<{
15
- upload: UploadFn;
16
+ upload: UploadFile;
16
17
  importFiles: ImportFilesFn;
17
18
  }, {
18
- upload: UploadFn;
19
+ upload: UploadFile;
19
20
  importFiles: ImportFilesFn;
20
21
  }>;
21
22
  declare module '@tiptap/core' {
22
23
  interface Storage {
23
24
  media: {
24
- upload: UploadFn;
25
+ upload: UploadFile;
25
26
  importFiles: ImportFilesFn;
26
27
  };
27
28
  }
28
29
  }
29
- /** The node that embeds uploaded `file` from `url`; null for files uploads do not accept. */
30
- export declare function mediaNodeFor(file: File, url: string): JSONContent | null;
31
- /** The file block for an uploaded `file` that has no player or image node (PDF, office documents, …). */
32
- export declare function fileNodeFor(file: File, url: string): JSONContent;
30
+ /** The node that embeds uploaded `file` from `url`, remembering the upload's `hash` (SHA-256 hex; none when the backend did not report one); null for files uploads do not accept. */
31
+ export declare function mediaNodeFor(file: File, url: string, hash?: string): JSONContent | null;
32
+ /** The file block for an uploaded `file` that has no player or image node (PDF, office documents, …); `hash` as in `mediaNodeFor`. */
33
+ export declare function fileNodeFor(file: File, url: string, hash?: string): JSONContent;
33
34
  /** Human-readable byte count: 1024-based, one decimal (`1536` → `1.5 KB`), units B/KB/MB/GB. */
34
35
  export declare function formatBytes(bytes: number): string;
35
36
  /** Transaction meta the recorder modal (AudioRecorderModal) listens for: open it, inserting at the current caret. */
@@ -38,4 +39,5 @@ export declare function openAudioRecorder(editor: Editor): void;
38
39
  /** Opens the browser's file picker; resolves the chosen files, `[]` when the picker is cancelled. */
39
40
  export declare function pickFiles(accept: string, multiple?: boolean): Promise<File[]>;
40
41
  //#endregion
42
+ export {};
41
43
  //# sourceMappingURL=upload.d.ts.map
@@ -2,17 +2,8 @@ import "./assets.js";
2
2
  import { mediaKindOf, mediaMime } from "./schema.js";
3
3
  import { Extension } from "@tiptap/core";
4
4
  //#region src/features/media/upload.ts
5
- /** Default upload: POSTs `file` to the asset endpoint of document `documentId` through the context's HTTP client (size limits per kind, see schema.ts). */
6
- async function postUpload(fetch, documentId, file) {
7
- const body = new FormData();
8
- body.append("file", file);
9
- return await fetch(`/api/editor/assets/${documentId}`, {
10
- method: "POST",
11
- body
12
- });
13
- }
14
- /** The upload of a surface: the host's `upload` prop, else the asset endpoint of its `documentId` (default: the shared `scratch` owner). */
15
- const uploadOf = (ctx) => ctx.upload ?? ((file) => postUpload(ctx.fetch, ctx.documentId ?? "scratch", file));
5
+ /** The upload of a surface: the backend's asset store for its `documentId` (default: the shared `scratch` owner; size limits per kind, see schema.ts). */
6
+ const uploadOf = (ctx) => (file) => ctx.backend.assets ? ctx.backend.assets.upload(ctx.documentId ?? "scratch", file) : Promise.reject(/* @__PURE__ */ new Error(`@ecsplico/editor: backend "${ctx.backend.name}" has no assets capability`));
16
7
  /** Makes the upload and the file import reachable as `editor.storage.media` (drop/paste, toolbar, slash menu, recorder). */
17
8
  const Media = Extension.create({
18
9
  name: "media",
@@ -29,15 +20,16 @@ const Media = Extension.create({
29
20
  };
30
21
  }
31
22
  });
32
- /** The node that embeds uploaded `file` from `url`; null for files uploads do not accept. */
33
- function mediaNodeFor(file, url) {
23
+ /** The node that embeds uploaded `file` from `url`, remembering the upload's `hash` (SHA-256 hex; none when the backend did not report one); null for files uploads do not accept. */
24
+ function mediaNodeFor(file, url, hash) {
34
25
  const kind = mediaKindOf(file.type);
35
26
  if (!kind) return null;
36
27
  if (kind === "image") return {
37
28
  type: "image",
38
29
  attrs: {
39
30
  src: url,
40
- alt: file.name
31
+ alt: file.name,
32
+ hash: hash ?? null
41
33
  }
42
34
  };
43
35
  return {
@@ -45,19 +37,21 @@ function mediaNodeFor(file, url) {
45
37
  attrs: {
46
38
  src: url,
47
39
  title: file.name,
48
- mimeType: mediaMime(file.type)
40
+ mimeType: mediaMime(file.type),
41
+ hash: hash ?? null
49
42
  }
50
43
  };
51
44
  }
52
- /** The file block for an uploaded `file` that has no player or image node (PDF, office documents, …). */
53
- function fileNodeFor(file, url) {
45
+ /** The file block for an uploaded `file` that has no player or image node (PDF, office documents, …); `hash` as in `mediaNodeFor`. */
46
+ function fileNodeFor(file, url, hash) {
54
47
  return {
55
48
  type: "file",
56
49
  attrs: {
57
50
  src: url,
58
51
  title: file.name,
59
52
  mimeType: mediaMime(file.type) || "application/octet-stream",
60
- size: file.size
53
+ size: file.size,
54
+ hash: hash ?? null
61
55
  }
62
56
  };
63
57
  }
@@ -101,6 +95,6 @@ function pickFiles(accept, multiple = true) {
101
95
  return promise;
102
96
  }
103
97
  //#endregion
104
- export { AUDIO_RECORDER_OPEN, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, postUpload, uploadOf };
98
+ export { AUDIO_RECORDER_OPEN, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, uploadOf };
105
99
 
106
100
  //# sourceMappingURL=upload.js.map