@ecsplico/editor 2.0.0 → 2.1.1

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 (278) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +4 -3
  3. package/dist/adapters/anchors.d.ts +80 -0
  4. package/dist/adapters/anchors.js +270 -0
  5. package/dist/adapters/anchors.js.map +1 -0
  6. package/dist/adapters/comments/client.d.ts +64 -0
  7. package/dist/adapters/comments/client.js +536 -0
  8. package/dist/adapters/comments/client.js.map +1 -0
  9. package/dist/adapters/comments/drizzle.d.ts +430 -0
  10. package/dist/adapters/comments/drizzle.js +236 -0
  11. package/dist/adapters/comments/drizzle.js.map +1 -0
  12. package/dist/adapters/comments/errors.d.ts +9 -0
  13. package/dist/adapters/comments/errors.js +19 -0
  14. package/dist/adapters/comments/errors.js.map +1 -0
  15. package/dist/adapters/comments/json.d.ts +19 -0
  16. package/dist/adapters/comments/json.js +94 -0
  17. package/dist/adapters/comments/json.js.map +1 -0
  18. package/dist/adapters/comments/record.js +78 -0
  19. package/dist/adapters/comments/record.js.map +1 -0
  20. package/dist/adapters/comments/server.d.ts +23 -0
  21. package/dist/adapters/comments/server.js +102 -0
  22. package/dist/adapters/comments/server.js.map +1 -0
  23. package/dist/adapters/comments/types.d.ts +109 -0
  24. package/dist/adapters/documents.js +108 -0
  25. package/dist/adapters/documents.js.map +1 -0
  26. package/dist/adapters/drizzle.d.ts +293 -0
  27. package/dist/adapters/drizzle.js +157 -0
  28. package/dist/adapters/drizzle.js.map +1 -0
  29. package/dist/adapters/files.js +30 -0
  30. package/dist/adapters/files.js.map +1 -0
  31. package/dist/adapters/forge.d.ts +58 -0
  32. package/dist/adapters/forge.js +91 -0
  33. package/dist/adapters/forge.js.map +1 -0
  34. package/dist/adapters/index.d.ts +13 -0
  35. package/dist/adapters/index.js +0 -0
  36. package/dist/adapters/json.d.ts +24 -0
  37. package/dist/adapters/json.js +128 -0
  38. package/dist/adapters/json.js.map +1 -0
  39. package/dist/adapters/nitro.d.ts +61 -0
  40. package/dist/adapters/nitro.js +212 -0
  41. package/dist/adapters/nitro.js.map +1 -0
  42. package/dist/adapters/routes.js +16 -0
  43. package/dist/adapters/routes.js.map +1 -0
  44. package/dist/adapters/shared.d.ts +37 -0
  45. package/dist/adapters/shared.js +32 -0
  46. package/dist/adapters/shared.js.map +1 -0
  47. package/dist/adapters/vue.d.ts +67 -0
  48. package/dist/adapters/vue.js +187 -0
  49. package/dist/adapters/vue.js.map +1 -0
  50. package/dist/code/CodeSurface.vue.d.ts +3 -3
  51. package/dist/core/doc-anchors.d.ts +5 -0
  52. package/dist/core/doc-anchors.js +4 -3
  53. package/dist/core/doc-anchors.js.map +1 -1
  54. package/dist/core/editor-ui.js +57 -1
  55. package/dist/core/editor-ui.js.map +1 -1
  56. package/dist/core/surface-session.js +139 -0
  57. package/dist/core/surface-session.js.map +1 -0
  58. package/dist/document/DocumentSurface.d.ts +1 -1
  59. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  60. package/dist/features/ai/agent-op-review.d.ts +2 -2
  61. package/dist/features/ai/agent-op-review.js.map +1 -1
  62. package/dist/features/ai/agent-ops.d.ts +1 -1
  63. package/dist/features/ai/agent-ops.js +9 -6
  64. package/dist/features/ai/agent-ops.js.map +1 -1
  65. package/dist/features/ai/ai-apply.d.ts +1 -1
  66. package/dist/features/ai/ai-review.js +10 -1
  67. package/dist/features/ai/ai-review.js.map +1 -1
  68. package/dist/features/ai/components/AiSelectionMenu.js +78 -252
  69. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  70. package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
  71. package/dist/features/ai/components/AiSidebar.js +2 -2
  72. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  73. package/dist/features/ai/components/DocumentChatOps.js +1 -0
  74. package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
  75. package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
  76. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  77. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  78. package/dist/features/ai/document-chat.d.ts +3 -2
  79. package/dist/features/ai/document-chat.js +16 -19
  80. package/dist/features/ai/document-chat.js.map +1 -1
  81. package/dist/features/ai/index.d.ts +5 -3
  82. package/dist/features/ai/index.js +4 -2
  83. package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
  84. package/dist/features/ai/protocol/agent-ops.js +65 -39
  85. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  86. package/dist/features/ai/protocol/block-lines.d.ts +89 -0
  87. package/dist/features/ai/protocol/block-lines.js +177 -0
  88. package/dist/features/ai/protocol/block-lines.js.map +1 -0
  89. package/dist/features/ai/protocol/chat.js +10 -10
  90. package/dist/features/ai/protocol/chat.js.map +1 -1
  91. package/dist/features/ai/protocol/index.d.ts +3 -2
  92. package/dist/features/ai/protocol/index.js +3 -2
  93. package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
  94. package/dist/features/ai/react/AiSelectionMenu.js +64 -255
  95. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  96. package/dist/features/ai/react/AiSidebar.js +2 -2
  97. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  98. package/dist/features/ai/react/DocumentChatOps.js +1 -0
  99. package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
  100. package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
  101. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  102. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  103. package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
  104. package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
  105. package/dist/features/ai/selection-flow.d.ts +133 -0
  106. package/dist/features/ai/selection-flow.js +283 -0
  107. package/dist/features/ai/selection-flow.js.map +1 -0
  108. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  109. package/dist/features/ai/use-document-chat.d.ts +1 -1
  110. package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
  111. package/dist/features/authors/feature.js +1 -1
  112. package/dist/features/authors/index.js +1 -1
  113. package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
  114. package/dist/features/comments/components/CommentBubble.js +1 -1
  115. package/dist/features/comments/components/CommentComposer.js +1 -1
  116. package/dist/features/comments/components/CommentThreadView.js +5 -2
  117. package/dist/features/comments/components/CommentThreadView.js.map +1 -1
  118. package/dist/features/comments/components/CommentsMargin.js +108 -230
  119. package/dist/features/comments/components/CommentsMargin.js.map +1 -1
  120. package/dist/features/comments/components/CommentsOverlay.js +1 -1
  121. package/dist/features/comments/feature.js +1 -1
  122. package/dist/features/comments/index.d.ts +4 -2
  123. package/dist/features/comments/index.js +4 -2
  124. package/dist/features/comments/margin-controller.d.ts +44 -0
  125. package/dist/features/comments/margin-controller.js +132 -0
  126. package/dist/features/comments/margin-controller.js.map +1 -0
  127. package/dist/features/comments/margin-layout.d.ts +55 -0
  128. package/dist/features/comments/margin-layout.js +79 -0
  129. package/dist/features/comments/margin-layout.js.map +1 -0
  130. package/dist/features/comments/model.d.ts +8 -0
  131. package/dist/features/comments/model.js +60 -34
  132. package/dist/features/comments/model.js.map +1 -1
  133. package/dist/features/comments/react/CommentBubble.js +1 -1
  134. package/dist/features/comments/react/CommentComposer.js +1 -1
  135. package/dist/features/comments/react/CommentThreadView.js +2 -2
  136. package/dist/features/comments/react/CommentThreadView.js.map +1 -1
  137. package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
  138. package/dist/features/comments/react/CommentsMargin.js +85 -251
  139. package/dist/features/comments/react/CommentsMargin.js.map +1 -1
  140. package/dist/features/comments/react/CommentsOverlay.js +1 -1
  141. package/dist/features/comments/react/use-comments.js +1 -1
  142. package/dist/features/comments/react/use-margin-layout.js +45 -0
  143. package/dist/features/comments/react/use-margin-layout.js.map +1 -0
  144. package/dist/features/comments/react.d.ts +1 -1
  145. package/dist/features/comments/schema.d.ts +23 -0
  146. package/dist/features/comments/schema.js.map +1 -1
  147. package/dist/features/comments/snapshot-equality.js +40 -0
  148. package/dist/features/comments/snapshot-equality.js.map +1 -0
  149. package/dist/features/comments/thread-anchor.js +33 -0
  150. package/dist/features/comments/thread-anchor.js.map +1 -0
  151. package/dist/features/comments/use-comments.d.ts +3 -1
  152. package/dist/features/comments/use-comments.js +1 -0
  153. package/dist/features/comments/use-comments.js.map +1 -1
  154. package/dist/features/comments/use-margin-layout.js +29 -0
  155. package/dist/features/comments/use-margin-layout.js.map +1 -0
  156. package/dist/features/comments/vue.d.ts +1 -1
  157. package/dist/features/composites/accepted-children.d.ts +21 -0
  158. package/dist/features/composites/accepted-children.js +126 -0
  159. package/dist/features/composites/accepted-children.js.map +1 -0
  160. package/dist/features/composites/components/CompositeDetails.js +1 -1
  161. package/dist/features/composites/components/CompositeInspector.js +38 -164
  162. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  163. package/dist/features/composites/feature.d.ts +2 -1
  164. package/dist/features/composites/feature.js +8 -2
  165. package/dist/features/composites/feature.js.map +1 -1
  166. package/dist/features/composites/index.d.ts +5 -3
  167. package/dist/features/composites/index.js +8 -6
  168. package/dist/features/composites/inspector-model.d.ts +97 -0
  169. package/dist/features/composites/inspector-model.js +255 -0
  170. package/dist/features/composites/inspector-model.js.map +1 -0
  171. package/dist/features/composites/kinds.d.ts +11 -0
  172. package/dist/features/composites/kinds.js +17 -2
  173. package/dist/features/composites/kinds.js.map +1 -1
  174. package/dist/features/composites/outline.d.ts +18 -5
  175. package/dist/features/composites/outline.js +44 -12
  176. package/dist/features/composites/outline.js.map +1 -1
  177. package/dist/features/composites/react/CompositeDetails.js +1 -1
  178. package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
  179. package/dist/features/composites/react/CompositeInspector.js +21 -180
  180. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  181. package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
  182. package/dist/features/composites/react/CompositeView.js +1 -1
  183. package/dist/features/composites/react/use-composite-inspector.js +29 -0
  184. package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
  185. package/dist/features/composites/schema.d.ts +2 -2
  186. package/dist/features/composites/schema.js +2 -2
  187. package/dist/features/composites/schema.js.map +1 -1
  188. package/dist/features/composites/use-composite-anchors.d.ts +1 -1
  189. package/dist/features/composites/use-composite-inspector.js +41 -0
  190. package/dist/features/composites/use-composite-inspector.js.map +1 -0
  191. package/dist/features/composites/use-composite-rows.d.ts +1 -1
  192. package/dist/features/media/index.d.ts +1 -1
  193. package/dist/features/media/vue.d.ts +1 -1
  194. package/dist/features/mentions/index.d.ts +3 -3
  195. package/dist/features/mentions/index.js +3 -3
  196. package/dist/features/mentions/schema.d.ts +1 -1
  197. package/dist/features/mentions/schema.js +1 -1
  198. package/dist/features/themes/index.d.ts +1 -1
  199. package/dist/features/themes/index.js +1 -1
  200. package/dist/features/transcript/feature.js +1 -1
  201. package/dist/features/transcript/index.js +2 -2
  202. package/dist/i18n/de/ai.js +2 -0
  203. package/dist/i18n/de/ai.js.map +1 -1
  204. package/dist/i18n/de/composites.js +8 -1
  205. package/dist/i18n/de/composites.js.map +1 -1
  206. package/dist/i18n/de/tree.js +14 -1
  207. package/dist/i18n/de/tree.js.map +1 -1
  208. package/dist/i18n/de.d.ts +22 -0
  209. package/dist/i18n/en/ai.js +2 -0
  210. package/dist/i18n/en/ai.js.map +1 -1
  211. package/dist/i18n/en/composites.js +8 -1
  212. package/dist/i18n/en/composites.js.map +1 -1
  213. package/dist/i18n/en/tree.js +14 -1
  214. package/dist/i18n/en/tree.js.map +1 -1
  215. package/dist/integrations/composite-ai/composite-fill.js +2 -2
  216. package/dist/integrations/composite-ai/composite-summary.js +2 -2
  217. package/dist/integrations/composite-ai/index.js +2 -2
  218. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  219. package/dist/markdown/MarkdownSurface.d.ts +1 -1
  220. package/dist/markdown/MarkdownSurface.js +2 -2
  221. package/dist/markdown/MarkdownSurface.vue.d.ts +9 -9
  222. package/dist/markdown/index.d.ts +2 -2
  223. package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
  224. package/dist/react/EditorSurface.d.ts +1 -1
  225. package/dist/react/EditorSurface.js +43 -149
  226. package/dist/react/EditorSurface.js.map +1 -1
  227. package/dist/react/EditorToolbar.js +3 -40
  228. package/dist/react/EditorToolbar.js.map +1 -1
  229. package/dist/schema/advanced.js +2 -2
  230. package/dist/server/adapter.d.ts +2 -2
  231. package/dist/server/collab.js +1 -1
  232. package/dist/server/handlers/ai.d.ts +4 -2
  233. package/dist/server/handlers/ai.js +11 -9
  234. package/dist/server/handlers/ai.js.map +1 -1
  235. package/dist/server/handlers/context.d.ts +1 -1
  236. package/dist/server/handlers/editor.d.ts +1 -1
  237. package/dist/server/handlers/editor.js +2 -2
  238. package/dist/server/host.d.ts +1 -1
  239. package/dist/server/index.d.ts +2 -2
  240. package/dist/settings/index.d.ts +1 -1
  241. package/dist/styles/tokens.css +1 -1
  242. package/dist/tree/DocumentTree.js +159 -387
  243. package/dist/tree/DocumentTree.js.map +1 -1
  244. package/dist/tree/drop-rules.js +37 -0
  245. package/dist/tree/drop-rules.js.map +1 -0
  246. package/dist/tree/react/DocumentTree.d.ts +2 -2
  247. package/dist/tree/react/DocumentTree.js +135 -397
  248. package/dist/tree/react/DocumentTree.js.map +1 -1
  249. package/dist/tree/react/use-tree-model.js +129 -0
  250. package/dist/tree/react/use-tree-model.js.map +1 -0
  251. package/dist/tree/react.js +1 -1
  252. package/dist/tree/tree-host.js +100 -0
  253. package/dist/tree/tree-host.js.map +1 -0
  254. package/dist/tree/tree-items.js +22 -43
  255. package/dist/tree/tree-items.js.map +1 -1
  256. package/dist/tree/tree-model.js +580 -0
  257. package/dist/tree/tree-model.js.map +1 -0
  258. package/dist/tree/tree-rows.js +77 -0
  259. package/dist/tree/tree-rows.js.map +1 -0
  260. package/dist/tree/use-tree-model.js +94 -0
  261. package/dist/tree/use-tree-model.js.map +1 -0
  262. package/dist/tree/vue.js +1 -1
  263. package/dist/vue/AdvancedEditor.vue.d.ts +6 -6
  264. package/dist/vue/SimpleEditor.vue.d.ts +2 -2
  265. package/dist/vue/components/EditorSurface.js +27 -87
  266. package/dist/vue/components/EditorSurface.js.map +1 -1
  267. package/dist/vue/components/EditorSurface.vue.d.ts +6 -6
  268. package/dist/vue/components/EditorToolbar.js +7 -44
  269. package/dist/vue/components/EditorToolbar.js.map +1 -1
  270. package/dist/vue/context.d.ts +1 -1
  271. package/dist/vue/context.js +1 -1
  272. package/dist/vue/context.js.map +1 -1
  273. package/dist/vue/use-editor-ui.d.ts +1 -1
  274. package/dist/vue/use-notifier.d.ts +1 -1
  275. package/docs/integration.md +97 -4
  276. package/docs/llms.md +9 -1
  277. package/docs/usage.md +10 -5
  278. package/package.json +67 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1
4
+
5
+ ### Changed
6
+
7
+ - The comment stores and `handleCommentsRequest` (`/comments/server`, `/comments/json`, `/comments/drizzle`) answer refused requests (`400`/`404`/`405`/`409`) with German `error` texts, like the editor's other server routes (e.g. `Kommentar-Thread „t1“ nicht gefunden`). Status codes and `CommentStoreError.code` are unchanged; clients that matched the old English texts must match the status or code instead.
8
+ - Errors thrown by `useEditorDocument` (`/adapters/vue`) and `createRestCommentsModel` (`/comments/client`) start with `@ecsplico/editor:` like the editor's other developer errors.
9
+
10
+ ## 2.1.0
11
+
12
+ ### Added
13
+
14
+ - The storage and connection layer of the former `@ecsplico/editor-adapters`, as 11 subpath entries (guide: `docs/integration.md` §14): `@ecsplico/editor/adapters` (types only), `/adapters/json`, `/adapters/drizzle`, `/adapters/forge`, `/adapters/vue` (`useEditorDocument`), `/anchors`, `/comments/server`, `/comments/json`, `/comments/drizzle`, `/comments/client` and `/server/nitro` (`installEditorServer`, the Nuxt-free Nitro host). Exported names are unchanged.
15
+ - Optional peer dependency `drizzle-orm` (`^0.45.0`), needed only by `/adapters/drizzle` and `/comments/drizzle`.
16
+ - Bundle-isolation fixtures `adapters-node`, `adapters-drizzle`, `adapters-nitro`, `adapters-vue` and `headless-comments-client`: Vue is reachable only from `/adapters/vue`, Nitro only from `/server/nitro`, drizzle only from the two drizzle entries, React from none.
17
+ - Example `examples/vue-nitro-json` (`bun run proto:vue-nitro-json`): a Vue editor on a bare Nitro server with JSON-file storage.
18
+ - Framework-free models behind the Vue and React components: `createCompositeInspector` (`/composites`), `createAiSelectionFlow` (`/ai`) and the comments margin (`createMarginController`, `layoutMargin`, `marginCards` in `/comments`; orphaned threads sit at their deletion point). The document tree reports why a drop is refused and filters by title.
19
+ - Accepted children per Baustein kind, enforced in the editor: `AcceptedChildren`, `kindAccepts`, `moveIntoVerdict` (`/composites`).
20
+ - The block-lines module owning the `[id] …` line format the AI sees (`readBlockLines`, `BlockLines` and the `BLOCK_LINE_*` texts in `/ai` and `/ai/protocol`): IDs inside fenced code are ignored, `add` requires `afterId`, operations on a block inside a block another operation replaces or deletes are dropped (`AgentOpProblemReason` `inside`), and operations on blocks deleted since the document went to the model are reported as `stale` (`AgentOpOutcome`).
21
+
22
+ ### Changed (breaking)
23
+
24
+ These shipped in a minor release by mistake; the only known consumer (skaile-forge) is migrated.
25
+
26
+ - `documentBlockIds(text)` is removed from `/ai` and `/ai/protocol`: use `readBlockLines(text).ids`.
27
+ - `AgentOpContext` (`checkAgentOps`): `known: (id) => boolean` is replaced by `lines: BlockLines`; pass `lines: readBlockLines(documentChatText(editor))`.
28
+ - `CommentsSnapshot` has a required `orphanAt` (thread id → deletion position); a host's own `CommentsModel` must fill it (an empty map is valid).
29
+ - `AgentOpOutcome` and `AgentOpProblemReason` gained `stale` and `inside`; exhaustive maps over them need the new keys.
30
+
31
+ ### Notes
32
+
33
+ - `@ecsplico/editor-adapters` is deprecated and will receive no further releases. `@ecsplico/editor-adapters/vue` is now `@ecsplico/editor/adapters/vue`; every other subpath keeps its name under `@ecsplico/editor` (for example `@ecsplico/editor-adapters/adapters/json` → `@ecsplico/editor/adapters/json`, `@ecsplico/editor-adapters/server/nitro` → `@ecsplico/editor/server/nitro`, `@ecsplico/editor-adapters/anchors` → `@ecsplico/editor/anchors`). The root entry of the old package (types) is `@ecsplico/editor/adapters`.
34
+ - `./adapters/react`, `./server/nest` and `./adapters/prisma` are reserved names and not exported.
35
+
36
+ ### Fixed
37
+
38
+ - `createEditor()` without arguments threw a `TypeError` (destructuring `undefined`); `app.use(createEditor())` now installs with the default `nuxtUiKit()` as documented.
39
+
3
40
  ## 2.0.0
4
41
 
5
42
  ### Removed (breaking)
package/README.md CHANGED
@@ -123,10 +123,10 @@ Every entry is an explicit subpath of `package.json#exports`; there is no wildca
123
123
  | `@ecsplico/editor/transcript`, `/transcript/vue`, `/transcript/react`, `/transcript/schema` | `/vue` and `/react`: `transcriptFeature`, `TranscriptSegmentView`; framework-free: `transcriptFeatureCore`, `transcriptSegment`, `speakerConversions`, `segmentSpeakerLines`, `transcriptSchemaExtensions()`, `speakerParagraphsToSegments`, `lastSpeaker`, `speakersIn` |
124
124
  | `@ecsplico/editor/authors`, `/authors/vue`, `/authors/react`, `/authors/schema` | `/vue` and `/react`: `authorsFeature`, `AuthorColorsPanel`, `AuthorColorsButton`, `SettingsAuthorsTab`; framework-free: `authorsFeatureCore`, `currentAuthor`, `AuthorAttribution`, `authorsSchemaExtensions()`, `AuthorMark`, `documentAuthors`, `AuthorIdentity`, `AUTHOR_COLOR_STYLES`, `AuthorColorStyle` |
125
125
  | `@ecsplico/editor/themes`, `/themes/vue`, `/themes/react`, `/themes/schema` | `/vue` and `/react`: `themesFeature()` (site-wide Obsidian themes: its `setup` hook starts the theme runtime and returns its teardown, its `appearance` settings tab picks the theme), `SettingsAppearanceTab`; framework-free: `themesFeatureCore`, `startObsidianTheme(ctx)` (reference-counted per app, returns the release), `scopeThemeCss`, `THEME_SCOPE_CLASS`; Vue-free schema: `ThemeRecord`, `ThemeManifest`, `themeManifestSchema`, `THEME_ID`, `MAX_THEME_BYTES`. Needs an adapter with `listThemes`/`loadTheme`; see the [usage guide](./docs/usage.md#themes-obsidian) |
126
- | `@ecsplico/editor/comments`, `/comments/vue`, `/comments/react`, `/comments/schema` | `/vue` and `/react`: `commentsFeature` (requires `collab`), `useComments`, `CommentsMargin`, `CommentsPopover`, `CommentsListMenu`, `CommentThreadView`, `CommentComposer`, `CommentBubble`, `SettingsCommentsTab` (`/vue` also the type `CommentsStore`); framework-free: `commentsFeatureCore`, `createCommentsModel`, `Comments` plugin helpers, `commentsSchemaExtensions()`, `CommentMark`, `CommentThread`, `CommentReply`, `COMMENT_LIMITS`, `COMMENTS_LAYOUTS`, `CommentsLayout` |
126
+ | `@ecsplico/editor/comments`, `/comments/vue`, `/comments/react`, `/comments/schema` | `/vue` and `/react`: `commentsFeature` (requires `collab`), `useComments`, `CommentsMargin`, `CommentsPopover`, `CommentsListMenu`, `CommentThreadView`, `CommentComposer`, `CommentBubble`, `SettingsCommentsTab` (`/vue` also the type `CommentsStore`); framework-free: `commentsFeatureCore`, `createCommentsModel`, `Comments` plugin helpers, `commentsSchemaExtensions()`, `CommentMark`, `CommentThread`, `CommentReply`, `COMMENT_LIMITS`, `COMMENTS_LAYOUTS`, `CommentsLayout`, and the margin's layout (the active thread pinned to its text's line, the others one-line cards out of its way): `layoutMargin(entries, activeId, { gap, minTop })` (with `MarginEntry`, `MarginLayout`, `MARGIN_GAP`), `marginCards` (with `MarginCard`, `DRAFT_CARD`; an orphaned thread sits at its deletion point, `CommentsSnapshot.orphanAt`, which host models may omit), `createMarginController` (with `MarginController`, `MarginSource`, `MarginState`: measuring and scheduling for both `CommentsMargin`s) |
127
127
  | `@ecsplico/editor/composites/vue`, `/composites/react` | `compositeFeature()` (Vue: Nuxt UI, React: shadcn), the host-placed `CompositeTable` and `CompositeInspector`, `SettingsEditingTab`, `CompositeChrome`, `CompositeDetails`, `CompositeView` and editors, `useCompositeRows`, `useCompositeAnchors` |
128
- | `@ecsplico/editor/composites`, `/composites/schema` | Framework-free: `compositeFeatureCore`, `Composite`, `defaultCompositeKinds`, `compositeKind`, `compositeOutline`, `moveBlockInto`, `canMoveBlockInto`, `headingsMarkdown`, `documentCompositeMarkdown`, `Structure`, `createCompositeRows`, `observeCompositeAnchors`, `compositeRows`, `setCompositeMetaField`, `setCompositeTags`, `setCompositeDescription`, `compositeDescriptionText`, `compositeSchemaExtensions()`, `compositeJSON`, `blocksFromMarkdown`, `splitCompositeFences`, `migrateLegacyContent`, conversions and heading↔composite transforms |
129
- | `@ecsplico/editor/ai`, `/ai/protocol` | Framework-free: `aiFeatureCore`, `modelProfiles(app)`, `createAiAssistant`, `createDocumentChat`, `createAgentOpReview({ notify, t })` (host-driven agent edit review), `notifyAiRateLimited(notify, t)`, review/apply helpers (`startAiReview`, `applyAiResult`, …) and the protocol (`AI_TASKS`, `ModelRef`, `AGENT_OP_TYPES`, `CHAT_LIMITS`, …), importable alone from `/ai/protocol`; the model-facing agent texts (`checkAgentOps`, `agentShownMessage`, `agentFeedbackText`, `agentRetryMessage`) take a `locale: 'de' \| 'en'` option (default `de`), and `agentRetryMessage(problems, scoped, { locale, tool })` names the `tool` the model must call again (default `edit_document`) |
128
+ | `@ecsplico/editor/composites`, `/composites/schema` | Framework-free: `compositeFeatureCore`, `Composite`, `defaultCompositeKinds`, `compositeKind`, `kindAccepts` (the kinds' accepted children, `CompositeKind.accepts`), `AcceptedChildren` (the editor extension that enforces them; registered by `compositeFeatureCore`), `compositeOutline`, `moveBlockInto` (returns whether the document changed), `moveIntoVerdict(editor, id, targetId, t)` (with `MoveVerdict`: `{ ok: true }` or `{ ok: false, reason }` in the language of `t`), `canMoveBlockInto` (`moveIntoVerdict(...).ok`), `headingsMarkdown`, `documentCompositeMarkdown`, `Structure`, `createCompositeRows`, `createCompositeInspector`, `compositeFieldText`, `observeCompositeAnchors`, `compositeRows`, `setCompositeMetaField`, `setCompositeTags`, `setCompositeDescription`, `compositeDescriptionText`, `compositeSchemaExtensions()`, `compositeJSON`, `blocksFromMarkdown`, `splitCompositeFences`, `migrateLegacyContent`, conversions and heading↔composite transforms |
129
+ | `@ecsplico/editor/ai`, `/ai/protocol` | Framework-free: `aiFeatureCore`, `modelProfiles(app)`, `createAiAssistant`, `createDocumentChat`, `createAiSelectionFlow` (with `AI_SELECTION_MENU_KEY`), `createAgentOpReview({ notify, t })` (host-driven agent edit review), `notifyAiRateLimited(notify, t)`, review/apply helpers (`startAiReview`, `applyAiResult`, …) and the protocol (`AI_TASKS`, `ModelRef`, `AGENT_OP_TYPES`, `AGENT_OP_OUTCOMES`, `CHAT_LIMITS`, …), importable alone from `/ai/protocol`; the model-facing agent texts (`checkAgentOps`, `agentShownMessage`, `agentFeedbackText`, `agentRetryMessage`) take a `locale: 'de' \| 'en'` option (default `de`), and `agentRetryMessage(problems, scoped, { locale, tool })` names the `tool` the model must call again (default `edit_document`). The `[id]` line format of the chat's document text has one owner there, `block-lines`: writer helpers (`blockLine`, `singleBlockLine`, `nestedLinePrefix`, `quoteLinePrefix`, `listLinePrefix`, `continuedLinePrefix`, `compositeChromePart`, `compositeLineHeader`, `compositeLineClose`, `TRUNCATED_LINE`), the reader `readBlockLines` (the block ids and which blocks lie inside which; lines in fenced code are not block lines; type `BlockLines`), the echo strippers `stripBlockLineEcho` and `stripListMarkerEcho`, and the (German, model-facing) prompt texts `BLOCK_LINES_LABEL`, `BLOCK_LINE_INTRO`, `BLOCK_LINE_MARKERS`, `BLOCK_LINE_LIST_TEXT`, `BLOCK_LINE_HOST_TEXT`, `BLOCK_LINE_COMPOSITE_TEXT`, `BLOCK_LINE_ECHO_TEXT`, `BLOCK_LINE_IDS_HINT` with the example (`BLOCK_LINE_EXAMPLE`, `BLOCK_LINE_EXAMPLE_ID`, `BLOCK_LINE_EXAMPLE_TEXT`) |
130
130
  | `@ecsplico/editor/ai/vue`, `/ai/react` | `aiFeature()`, `AiSidebar`, `ModelQuickSwitch`, `AiReviewBar`, `AiSelectionMenu`, `SettingsAiTab`, `SettingsModelProfiles`; `/ai/vue` also `useAiAssistant`, `useDocumentChat`, `useModelProfiles` (thin hooks over the models) |
131
131
  | `@ecsplico/editor/integrations/composite-ai` | Framework-free: `compositeAiFeature` (requires `composites`, `ai`, `media`), `fillComposite`, `summarizeComposite`, `transcribeDocument`, `compositeSourceText`, `mediaAssetOf` |
132
132
  | `@ecsplico/editor/integrations/composite-media` | Framework-free: `compositeMediaFeature` (requires `composites`, `media`): imported files become `document` composites |
@@ -213,6 +213,7 @@ bun run typecheck:editor # vue-tsc --noEmit -p packages/edi
213
213
  bun run check:editor-entries # production build per fixture, module-graph isolation rules
214
214
  bun run check:editor-markdown-clipboard # MarkdownClipboard paste/copy cases (HTML, Markdown, plain text, data-URL opt-in) in happy-dom
215
215
  bun run check:editor-composition # defineEditor rejects missing `requires`; accepted compositions; one formatAiResult contributor
216
+ bun run test:editor # bun test over packages/editor/test: framework-free models on a mounted editor (happy-dom)
216
217
  ```
217
218
 
218
219
  On a clean checkout run the example build once before `typecheck:editor`.
@@ -0,0 +1,80 @@
1
+ import { Node } from "@tiptap/pm/model";
2
+ //#region src/adapters/anchors.d.ts
3
+ /** A half-open range of ProseMirror positions; `from === to` is a cursor position. */
4
+ export interface DocRange {
5
+ from: number;
6
+ to: number;
7
+ }
8
+ /** Selected text. */
9
+ export interface RangeAnchor {
10
+ kind: 'range';
11
+ from: number;
12
+ to: number;
13
+ quote: string;
14
+ contextBefore?: string;
15
+ contextAfter?: string;
16
+ }
17
+ /** A zero-width cursor position. */
18
+ export interface PointAnchor {
19
+ kind: 'point';
20
+ position: number;
21
+ contextBefore?: string;
22
+ contextAfter?: string;
23
+ }
24
+ /** What remains of an anchor whose target was deleted or cannot be found: the last good anchor plus its quote and context. */
25
+ export interface OrphanedAnchor {
26
+ kind: 'orphan';
27
+ previous: RangeAnchor | PointAnchor;
28
+ quote?: string;
29
+ contextBefore?: string;
30
+ contextAfter?: string;
31
+ reason: 'target-changed' | 'target-deleted' | 'unresolvable';
32
+ }
33
+ /** The stored form of an anchor; the layout of skaile-forge's `concept-discussions.json`. */
34
+ export type PortableAnchor = RangeAnchor | PointAnchor | OrphanedAnchor;
35
+ /** Characters of quote a range anchor keeps. */
36
+ export declare const QUOTE_LENGTH = 200;
37
+ /** Characters of surrounding text kept on each side. */
38
+ export declare const CONTEXT_LENGTH = 80;
39
+ /** What `captureAnchor` and `capturePoint` read: a tiptap `Editor`, or anything with `state.doc`. */
40
+ export interface AnchorSource {
41
+ state: {
42
+ doc: Node;
43
+ };
44
+ }
45
+ /** What `mapAnchor` reads of a ProseMirror transaction. */
46
+ export interface AnchorTransaction {
47
+ /** The document after the transaction. */
48
+ doc: Node;
49
+ mapping: {
50
+ mapResult(pos: number, assoc?: number): {
51
+ pos: number;
52
+ deleted: boolean;
53
+ };
54
+ };
55
+ }
56
+ /** The anchor of the text in `range` (clamped to the document); its `quote` is the trimmed text, cut to `QUOTE_LENGTH`. */
57
+ export declare function captureAnchor(source: AnchorSource, range: DocRange): RangeAnchor;
58
+ /** The anchor of the cursor position `position` (clamped to the document). */
59
+ export declare function capturePoint(source: AnchorSource, position: number): PointAnchor;
60
+ /**
61
+ * The range `anchor` points at in `doc`, or `null` when it cannot be found (an orphan). Orphaned anchors resolve to
62
+ * `null` too: a deleted target is never silently re-attached to other text. A point resolves to `{ from, to }` with
63
+ * `from === to`.
64
+ *
65
+ * 1. The stored positions, if they lie in the document and the text there still reads as the quote (a point: the
66
+ * text around still reads as its context).
67
+ * 2. A search for the quote (a point: for its context); among repeats the one whose surroundings agree most with the
68
+ * stored context wins, ties go to the one nearest the stored position.
69
+ */
70
+ export declare function resolveAnchor(doc: Node, anchor: PortableAnchor): DocRange | null;
71
+ /**
72
+ * `anchor` carried through a transaction: positions follow the edit, quote and context are read again from the new
73
+ * document (so an edit inside the range changes the quote). The same object when nothing changed. A range collapsed by
74
+ * the edit, or a point inside deleted text, becomes an `orphan` that keeps the last good anchor; orphans stay as they are.
75
+ */
76
+ export declare function mapAnchor(anchor: PortableAnchor, transaction: AnchorTransaction): PortableAnchor;
77
+ /** Whether `value` has the shape of a stored anchor (range, point or orphan); for input from the network. */
78
+ export declare function isPortableAnchor(value: unknown): value is PortableAnchor;
79
+ //#endregion
80
+ //# sourceMappingURL=anchors.d.ts.map
@@ -0,0 +1,270 @@
1
+ //#region src/adapters/anchors.ts
2
+ /** Characters of quote a range anchor keeps. */
3
+ const QUOTE_LENGTH = 200;
4
+ /** Characters of surrounding text kept on each side. */
5
+ const CONTEXT_LENGTH = 80;
6
+ /** Characters of context compared to accept an exact position and to search a moved one. */
7
+ const MATCH_LENGTH = 24;
8
+ const projections = /* @__PURE__ */ new WeakMap();
9
+ const isSpace = (char) => /\s/.test(char);
10
+ function projectionOf(doc) {
11
+ const cached = projections.get(doc);
12
+ if (cached) return cached;
13
+ const chars = [];
14
+ const starts = [];
15
+ const ends = [];
16
+ const push = (char, start, end) => {
17
+ if (isSpace(char)) {
18
+ if (chars.length === 0 || chars[chars.length - 1] === " ") return;
19
+ char = " ";
20
+ }
21
+ chars.push(char);
22
+ starts.push(start);
23
+ ends.push(end);
24
+ };
25
+ doc.descendants((node, pos) => {
26
+ if (node.isText) {
27
+ const text = node.text ?? "";
28
+ for (let i = 0; i < text.length; i++) push(text[i], pos + i, pos + i + 1);
29
+ return;
30
+ }
31
+ if (node.isTextblock || node.isBlock && node.isLeaf) push(" ", pos, pos);
32
+ if (node.isLeaf) {
33
+ const leaf = node.type.spec.leafText?.(node) ?? "";
34
+ for (let i = 0; i < leaf.length; i++) push(leaf[i], pos, pos + node.nodeSize);
35
+ }
36
+ });
37
+ const projection = {
38
+ text: chars.join(""),
39
+ starts,
40
+ ends
41
+ };
42
+ projections.set(doc, projection);
43
+ return projection;
44
+ }
45
+ /** First index in the non-decreasing `values` whose value satisfies `ok` (monotone: false… then true…). */
46
+ function firstIndex(values, ok) {
47
+ let low = 0;
48
+ let high = values.length;
49
+ while (low < high) {
50
+ const middle = low + high >> 1;
51
+ if (ok(values[middle])) high = middle;
52
+ else low = middle + 1;
53
+ }
54
+ return low;
55
+ }
56
+ /** The characters of `projection` inside `[from, to]`: `[first, last)` indexes. */
57
+ function span(projection, from, to) {
58
+ const first = firstIndex(projection.starts, (start) => start >= from);
59
+ const last = firstIndex(projection.ends, (end) => end > to);
60
+ return [first, Math.max(first, last)];
61
+ }
62
+ /** `value` with whitespace collapsed, as the projection holds text. */
63
+ const normalize = (value) => value.replace(/\s+/g, " ");
64
+ const clampPosition = (doc, position) => Math.max(0, Math.min(doc.content.size, Math.floor(position)));
65
+ /** The anchor of the text in `range` (clamped to the document); its `quote` is the trimmed text, cut to `QUOTE_LENGTH`. */
66
+ function captureAnchor(source, range) {
67
+ const { doc } = source.state;
68
+ const from = clampPosition(doc, Math.min(range.from, range.to));
69
+ const to = clampPosition(doc, Math.max(range.from, range.to));
70
+ const projection = projectionOf(doc);
71
+ const [first, last] = span(projection, from, to);
72
+ return {
73
+ kind: "range",
74
+ from,
75
+ to,
76
+ quote: projection.text.slice(first, last).trim().slice(0, 200),
77
+ contextBefore: projection.text.slice(Math.max(0, first - 80), first),
78
+ contextAfter: projection.text.slice(last, last + 80)
79
+ };
80
+ }
81
+ /** The anchor of the cursor position `position` (clamped to the document). */
82
+ function capturePoint(source, position) {
83
+ const { doc } = source.state;
84
+ const at = clampPosition(doc, position);
85
+ const projection = projectionOf(doc);
86
+ const [first, last] = span(projection, at, at);
87
+ return {
88
+ kind: "point",
89
+ position: at,
90
+ contextBefore: projection.text.slice(Math.max(0, last - 80), last),
91
+ contextAfter: projection.text.slice(first, first + 80)
92
+ };
93
+ }
94
+ const commonSuffix = (text, end, context) => {
95
+ let count = 0;
96
+ while (count < context.length && count < end && text[end - 1 - count] === context[context.length - 1 - count]) count++;
97
+ return count;
98
+ };
99
+ const commonPrefix = (text, start, context) => {
100
+ let count = 0;
101
+ while (count < context.length && start + count < text.length && text[start + count] === context[count]) count++;
102
+ return count;
103
+ };
104
+ /** Every index where `needle` occurs in `text` (overlaps included). */
105
+ function occurrences(text, needle) {
106
+ const found = [];
107
+ if (!needle) return found;
108
+ for (let index = text.indexOf(needle); index >= 0; index = text.indexOf(needle, index + 1)) found.push(index);
109
+ return found;
110
+ }
111
+ const matchTail = (context) => context.slice(-24);
112
+ const matchHead = (context) => context.slice(0, MATCH_LENGTH);
113
+ /**
114
+ * The range `anchor` points at in `doc`, or `null` when it cannot be found (an orphan). Orphaned anchors resolve to
115
+ * `null` too: a deleted target is never silently re-attached to other text. A point resolves to `{ from, to }` with
116
+ * `from === to`.
117
+ *
118
+ * 1. The stored positions, if they lie in the document and the text there still reads as the quote (a point: the
119
+ * text around still reads as its context).
120
+ * 2. A search for the quote (a point: for its context); among repeats the one whose surroundings agree most with the
121
+ * stored context wins, ties go to the one nearest the stored position.
122
+ */
123
+ function resolveAnchor(doc, anchor) {
124
+ if (anchor.kind === "orphan") return null;
125
+ const projection = projectionOf(doc);
126
+ const size = doc.content.size;
127
+ const contextBefore = normalize(anchor.contextBefore ?? "");
128
+ const contextAfter = normalize(anchor.contextAfter ?? "");
129
+ if (anchor.kind === "point") {
130
+ const stored = Number.isInteger(anchor.position) && anchor.position >= 0 && anchor.position <= size ? anchor.position : null;
131
+ if (stored !== null) {
132
+ const [first, last] = span(projection, stored, stored);
133
+ const beforeOk = !contextBefore || projection.text.slice(0, last).endsWith(matchTail(contextBefore));
134
+ const afterOk = !contextAfter || projection.text.startsWith(matchHead(contextAfter), first);
135
+ if (beforeOk && afterOk) return {
136
+ from: stored,
137
+ to: stored
138
+ };
139
+ }
140
+ const [firstIndex, ...otherIndexes] = pointCandidates(projection, contextBefore, contextAfter);
141
+ if (firstIndex === void 0) return null;
142
+ const near = anchor.position;
143
+ const entryOf = (index) => ({
144
+ index,
145
+ position: boundaryPosition(projection, index),
146
+ score: commonSuffix(projection.text, index, contextBefore) + commonPrefix(projection.text, index, contextAfter)
147
+ });
148
+ const best = pickBest(entryOf(firstIndex), otherIndexes.map(entryOf), (entry) => Math.abs(entry.position - near));
149
+ return {
150
+ from: best.position,
151
+ to: best.position
152
+ };
153
+ }
154
+ const quote = normalize(anchor.quote).trim();
155
+ if (Number.isInteger(anchor.from) && Number.isInteger(anchor.to) && anchor.from >= 0 && anchor.from <= anchor.to && anchor.to <= size) {
156
+ if (!quote) return {
157
+ from: anchor.from,
158
+ to: anchor.to
159
+ };
160
+ const [first, last] = span(projection, anchor.from, anchor.to);
161
+ if (projection.text.slice(first, last).trim().slice(0, 200) === quote) return {
162
+ from: anchor.from,
163
+ to: anchor.to
164
+ };
165
+ }
166
+ if (!quote) return null;
167
+ const [firstMatch, ...otherMatches] = occurrences(projection.text, quote);
168
+ if (firstMatch === void 0) return null;
169
+ const truncated = quote.length >= 200;
170
+ const entryOf = (index) => {
171
+ const from = projection.starts[index];
172
+ return {
173
+ index,
174
+ from,
175
+ to: truncated ? Math.min(size, from + Math.max(0, anchor.to - anchor.from)) : projection.ends[index + quote.length - 1],
176
+ score: commonSuffix(projection.text, index, contextBefore) + commonPrefix(projection.text, index + quote.length, contextAfter)
177
+ };
178
+ };
179
+ const best = pickBest(entryOf(firstMatch), otherMatches.map(entryOf), (entry) => Math.abs(entry.from - anchor.from));
180
+ return {
181
+ from: best.from,
182
+ to: best.to
183
+ };
184
+ }
185
+ /** The boundaries (between characters) where the context of a point reads: both sides, else either side alone. */
186
+ function pointCandidates(projection, contextBefore, contextAfter) {
187
+ const before = matchTail(contextBefore);
188
+ const after = matchHead(contextAfter);
189
+ if (before && after) {
190
+ const both = occurrences(projection.text, before + after).map((index) => index + before.length);
191
+ if (both.length) return both;
192
+ }
193
+ if (before) {
194
+ const found = occurrences(projection.text, before).map((index) => index + before.length);
195
+ if (found.length) return found;
196
+ }
197
+ if (after) return occurrences(projection.text, after);
198
+ return [];
199
+ }
200
+ /** The cursor position of the boundary before character `index`. */
201
+ function boundaryPosition(projection, index) {
202
+ const { text, starts, ends } = projection;
203
+ if (index >= text.length) return ends[text.length - 1] ?? 0;
204
+ if (text[index] === " " && starts[index] === ends[index] && index > 0) return ends[index - 1];
205
+ return starts[index];
206
+ }
207
+ /** The entry with the highest `score` among `first` and `rest`; among equals the one with the lowest `distance`. */
208
+ function pickBest(first, rest, distance) {
209
+ let best = first;
210
+ for (const entry of rest) if (entry.score > best.score || entry.score === best.score && distance(entry) < distance(best)) best = entry;
211
+ return best;
212
+ }
213
+ const sameAnchor = (a, b) => JSON.stringify(a) === JSON.stringify(b);
214
+ /**
215
+ * `anchor` carried through a transaction: positions follow the edit, quote and context are read again from the new
216
+ * document (so an edit inside the range changes the quote). The same object when nothing changed. A range collapsed by
217
+ * the edit, or a point inside deleted text, becomes an `orphan` that keeps the last good anchor; orphans stay as they are.
218
+ */
219
+ function mapAnchor(anchor, transaction) {
220
+ if (anchor.kind === "orphan") return anchor;
221
+ const source = { state: { doc: transaction.doc } };
222
+ let next;
223
+ if (anchor.kind === "range") {
224
+ const from = transaction.mapping.mapResult(anchor.from, 1).pos;
225
+ const to = transaction.mapping.mapResult(anchor.to, -1).pos;
226
+ if (from >= to) next = {
227
+ kind: "orphan",
228
+ previous: anchor,
229
+ quote: anchor.quote,
230
+ contextBefore: anchor.contextBefore,
231
+ contextAfter: anchor.contextAfter,
232
+ reason: from === to ? "target-deleted" : "target-changed"
233
+ };
234
+ else next = captureAnchor(source, {
235
+ from,
236
+ to
237
+ });
238
+ } else {
239
+ const after = transaction.mapping.mapResult(anchor.position, 1);
240
+ const before = transaction.mapping.mapResult(anchor.position, -1);
241
+ next = after.deleted && before.deleted ? {
242
+ kind: "orphan",
243
+ previous: anchor,
244
+ contextBefore: anchor.contextBefore,
245
+ contextAfter: anchor.contextAfter,
246
+ reason: "target-deleted"
247
+ } : capturePoint(source, after.pos);
248
+ }
249
+ return sameAnchor(anchor, next) ? anchor : next;
250
+ }
251
+ const isPosition = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
252
+ const isOptionalText = (value) => value === void 0 || typeof value === "string";
253
+ function isLiveAnchor(value) {
254
+ if (typeof value !== "object" || value === null) return false;
255
+ const anchor = value;
256
+ if (!isOptionalText(anchor.contextBefore) || !isOptionalText(anchor.contextAfter)) return false;
257
+ if (anchor.kind === "range") return isPosition(anchor.from) && isPosition(anchor.to) && anchor.from <= anchor.to && typeof anchor.quote === "string";
258
+ return anchor.kind === "point" && isPosition(anchor.position);
259
+ }
260
+ /** Whether `value` has the shape of a stored anchor (range, point or orphan); for input from the network. */
261
+ function isPortableAnchor(value) {
262
+ if (isLiveAnchor(value)) return true;
263
+ if (typeof value !== "object" || value === null) return false;
264
+ const anchor = value;
265
+ return anchor.kind === "orphan" && isLiveAnchor(anchor.previous) && isOptionalText(anchor.quote) && isOptionalText(anchor.contextBefore) && isOptionalText(anchor.contextAfter) && (anchor.reason === "target-changed" || anchor.reason === "target-deleted" || anchor.reason === "unresolvable");
266
+ }
267
+ //#endregion
268
+ export { CONTEXT_LENGTH, QUOTE_LENGTH, captureAnchor, capturePoint, isPortableAnchor, mapAnchor, resolveAnchor };
269
+
270
+ //# sourceMappingURL=anchors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchors.js","names":[],"sources":["../../src/adapters/anchors.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\n\n// Portable text anchors: a durable reference from a comment to a place in a document that survives edits, re-parses\n// and content swaps. Pure and framework-free: no runtime import, so the entry loads in a browser, a worker or Node.\n//\n// An anchor stores ProseMirror positions (fast path) plus the quoted text and its surroundings (fallback). Resolving\n// trusts the positions while the text there still reads as the quote, otherwise searches the document for the quote\n// and lets the context decide between repeats; no match at all is an orphan (`null`), never a guess.\n\n/** A half-open range of ProseMirror positions; `from === to` is a cursor position. */\nexport interface DocRange {\n from: number\n to: number\n}\n\n/** Selected text. */\nexport interface RangeAnchor {\n kind: 'range'\n from: number\n to: number\n quote: string\n contextBefore?: string\n contextAfter?: string\n}\n\n/** A zero-width cursor position. */\nexport interface PointAnchor {\n kind: 'point'\n position: number\n contextBefore?: string\n contextAfter?: string\n}\n\n/** What remains of an anchor whose target was deleted or cannot be found: the last good anchor plus its quote and context. */\nexport interface OrphanedAnchor {\n kind: 'orphan'\n previous: RangeAnchor | PointAnchor\n quote?: string\n contextBefore?: string\n contextAfter?: string\n reason: 'target-changed' | 'target-deleted' | 'unresolvable'\n}\n\n/** The stored form of an anchor; the layout of skaile-forge's `concept-discussions.json`. */\nexport type PortableAnchor = RangeAnchor | PointAnchor | OrphanedAnchor\n\n/** Characters of quote a range anchor keeps. */\nexport const QUOTE_LENGTH = 200\n/** Characters of surrounding text kept on each side. */\nexport const CONTEXT_LENGTH = 80\n/** Characters of context compared to accept an exact position and to search a moved one. */\nconst MATCH_LENGTH = 24\n\n/** What `captureAnchor` and `capturePoint` read: a tiptap `Editor`, or anything with `state.doc`. */\nexport interface AnchorSource {\n state: { doc: PMNode }\n}\n\n/** What `mapAnchor` reads of a ProseMirror transaction. */\nexport interface AnchorTransaction {\n /** The document after the transaction. */\n doc: PMNode\n mapping: { mapResult(pos: number, assoc?: number): { pos: number, deleted: boolean } }\n}\n\n// -- Text projection ------------------------------------------------------------------------------------------------\n\n/**\n * The document as one string with its positions: runs of whitespace collapse to one space, a text block boundary is a\n * space (`starts`/`ends` = the boundary position), a non-text leaf is its `leafText` or U+FFFC. `starts[i]`/`ends[i]`\n * are the document positions before and after character `i`; both are non-decreasing.\n */\ninterface Projection {\n text: string\n starts: number[]\n ends: number[]\n}\n\nconst projections = new WeakMap<PMNode, Projection>()\nconst isSpace = (char: string) => /\\s/.test(char)\n\nfunction projectionOf(doc: PMNode): Projection {\n const cached = projections.get(doc)\n if (cached) return cached\n const chars: string[] = []\n const starts: number[] = []\n const ends: number[] = []\n const push = (char: string, start: number, end: number) => {\n if (isSpace(char)) {\n if (chars.length === 0 || chars[chars.length - 1] === ' ') return\n char = ' '\n }\n chars.push(char)\n starts.push(start)\n ends.push(end)\n }\n doc.descendants((node, pos) => {\n if (node.isText) {\n const text = node.text ?? ''\n for (let i = 0; i < text.length; i++) push(text[i]!, pos + i, pos + i + 1)\n return\n }\n if (node.isTextblock || (node.isBlock && node.isLeaf)) push(' ', pos, pos)\n if (node.isLeaf) {\n const leaf = node.type.spec.leafText?.(node) ?? '\\ufffc'\n for (let i = 0; i < leaf.length; i++) push(leaf[i]!, pos, pos + node.nodeSize)\n }\n })\n const projection = { text: chars.join(''), starts, ends }\n projections.set(doc, projection)\n return projection\n}\n\n/** First index in the non-decreasing `values` whose value satisfies `ok` (monotone: false… then true…). */\nfunction firstIndex(values: readonly number[], ok: (value: number) => boolean): number {\n let low = 0\n let high = values.length\n while (low < high) {\n const middle = (low + high) >> 1\n if (ok(values[middle]!)) high = middle\n else low = middle + 1\n }\n return low\n}\n\n/** The characters of `projection` inside `[from, to]`: `[first, last)` indexes. */\nfunction span(projection: Projection, from: number, to: number): [number, number] {\n const first = firstIndex(projection.starts, start => start >= from)\n const last = firstIndex(projection.ends, end => end > to)\n return [first, Math.max(first, last)]\n}\n\n/** `value` with whitespace collapsed, as the projection holds text. */\nconst normalize = (value: string) => value.replace(/\\s+/g, ' ')\n\nconst clampPosition = (doc: PMNode, position: number) => Math.max(0, Math.min(doc.content.size, Math.floor(position)))\n\n// -- Capture ----------------------------------------------------------------------------------------------------------\n\n/** The anchor of the text in `range` (clamped to the document); its `quote` is the trimmed text, cut to `QUOTE_LENGTH`. */\nexport function captureAnchor(source: AnchorSource, range: DocRange): RangeAnchor {\n const { doc } = source.state\n const from = clampPosition(doc, Math.min(range.from, range.to))\n const to = clampPosition(doc, Math.max(range.from, range.to))\n const projection = projectionOf(doc)\n const [first, last] = span(projection, from, to)\n return {\n kind: 'range',\n from,\n to,\n quote: projection.text.slice(first, last).trim().slice(0, QUOTE_LENGTH),\n contextBefore: projection.text.slice(Math.max(0, first - CONTEXT_LENGTH), first),\n contextAfter: projection.text.slice(last, last + CONTEXT_LENGTH),\n }\n}\n\n/** The anchor of the cursor position `position` (clamped to the document). */\nexport function capturePoint(source: AnchorSource, position: number): PointAnchor {\n const { doc } = source.state\n const at = clampPosition(doc, position)\n const projection = projectionOf(doc)\n const [first, last] = span(projection, at, at)\n return {\n kind: 'point',\n position: at,\n contextBefore: projection.text.slice(Math.max(0, last - CONTEXT_LENGTH), last),\n contextAfter: projection.text.slice(first, first + CONTEXT_LENGTH),\n }\n}\n\n// -- Resolve ----------------------------------------------------------------------------------------------------------\n\nconst commonSuffix = (text: string, end: number, context: string): number => {\n let count = 0\n while (count < context.length && count < end && text[end - 1 - count] === context[context.length - 1 - count]) count++\n return count\n}\n\nconst commonPrefix = (text: string, start: number, context: string): number => {\n let count = 0\n while (count < context.length && start + count < text.length && text[start + count] === context[count]) count++\n return count\n}\n\n/** Every index where `needle` occurs in `text` (overlaps included). */\nfunction occurrences(text: string, needle: string): number[] {\n const found: number[] = []\n if (!needle) return found\n for (let index = text.indexOf(needle); index >= 0; index = text.indexOf(needle, index + 1)) found.push(index)\n return found\n}\n\nconst matchTail = (context: string) => context.slice(-MATCH_LENGTH)\nconst matchHead = (context: string) => context.slice(0, MATCH_LENGTH)\n\n/**\n * The range `anchor` points at in `doc`, or `null` when it cannot be found (an orphan). Orphaned anchors resolve to\n * `null` too: a deleted target is never silently re-attached to other text. A point resolves to `{ from, to }` with\n * `from === to`.\n *\n * 1. The stored positions, if they lie in the document and the text there still reads as the quote (a point: the\n * text around still reads as its context).\n * 2. A search for the quote (a point: for its context); among repeats the one whose surroundings agree most with the\n * stored context wins, ties go to the one nearest the stored position.\n */\nexport function resolveAnchor(doc: PMNode, anchor: PortableAnchor): DocRange | null {\n if (anchor.kind === 'orphan') return null\n const projection = projectionOf(doc)\n const size = doc.content.size\n const contextBefore = normalize(anchor.contextBefore ?? '')\n const contextAfter = normalize(anchor.contextAfter ?? '')\n\n if (anchor.kind === 'point') {\n const stored = Number.isInteger(anchor.position) && anchor.position >= 0 && anchor.position <= size ? anchor.position : null\n if (stored !== null) {\n const [first, last] = span(projection, stored, stored)\n const beforeOk = !contextBefore || projection.text.slice(0, last).endsWith(matchTail(contextBefore))\n const afterOk = !contextAfter || projection.text.startsWith(matchHead(contextAfter), first)\n if (beforeOk && afterOk) return { from: stored, to: stored }\n }\n const [firstIndex, ...otherIndexes] = pointCandidates(projection, contextBefore, contextAfter)\n if (firstIndex === undefined) return null\n const near = anchor.position\n const entryOf = (index: number) => ({\n index,\n position: boundaryPosition(projection, index),\n score: commonSuffix(projection.text, index, contextBefore) + commonPrefix(projection.text, index, contextAfter),\n })\n const best = pickBest(entryOf(firstIndex), otherIndexes.map(entryOf), entry => Math.abs(entry.position - near))\n return { from: best.position, to: best.position }\n }\n\n const quote = normalize(anchor.quote).trim()\n if (Number.isInteger(anchor.from) && Number.isInteger(anchor.to) && anchor.from >= 0 && anchor.from <= anchor.to && anchor.to <= size) {\n if (!quote) return { from: anchor.from, to: anchor.to }\n const [first, last] = span(projection, anchor.from, anchor.to)\n if (projection.text.slice(first, last).trim().slice(0, QUOTE_LENGTH) === quote) return { from: anchor.from, to: anchor.to }\n }\n if (!quote) return null\n const [firstMatch, ...otherMatches] = occurrences(projection.text, quote)\n if (firstMatch === undefined) return null\n // A quote cut at `QUOTE_LENGTH` says where the text starts, not where it ends: keep the stored length.\n const truncated = quote.length >= QUOTE_LENGTH\n const entryOf = (index: number) => {\n const from = projection.starts[index]!\n const to = truncated ? Math.min(size, from + Math.max(0, anchor.to - anchor.from)) : projection.ends[index + quote.length - 1]!\n return {\n index,\n from,\n to,\n score: commonSuffix(projection.text, index, contextBefore) + commonPrefix(projection.text, index + quote.length, contextAfter),\n }\n }\n const best = pickBest(entryOf(firstMatch), otherMatches.map(entryOf), entry => Math.abs(entry.from - anchor.from))\n return { from: best.from, to: best.to }\n}\n\n/** The boundaries (between characters) where the context of a point reads: both sides, else either side alone. */\nfunction pointCandidates(projection: Projection, contextBefore: string, contextAfter: string): number[] {\n const before = matchTail(contextBefore)\n const after = matchHead(contextAfter)\n if (before && after) {\n const both = occurrences(projection.text, before + after).map(index => index + before.length)\n if (both.length) return both\n }\n if (before) {\n const found = occurrences(projection.text, before).map(index => index + before.length)\n if (found.length) return found\n }\n if (after) return occurrences(projection.text, after)\n return []\n}\n\n/** The cursor position of the boundary before character `index`. */\nfunction boundaryPosition(projection: Projection, index: number): number {\n const { text, starts, ends } = projection\n if (index >= text.length) return ends[text.length - 1] ?? 0\n // Before a block boundary the cursor belongs at the end of the text before it, not in the gap between blocks.\n if (text[index] === ' ' && starts[index] === ends[index] && index > 0) return ends[index - 1]!\n return starts[index]!\n}\n\n/** The entry with the highest `score` among `first` and `rest`; among equals the one with the lowest `distance`. */\nfunction pickBest<T extends { score: number }>(first: T, rest: readonly T[], distance: (entry: T) => number): T {\n let best = first\n for (const entry of rest) {\n if (entry.score > best.score || (entry.score === best.score && distance(entry) < distance(best))) best = entry\n }\n return best\n}\n\n// -- Map --------------------------------------------------------------------------------------------------------------\n\nconst sameAnchor = (a: PortableAnchor, b: PortableAnchor) => JSON.stringify(a) === JSON.stringify(b)\n\n/**\n * `anchor` carried through a transaction: positions follow the edit, quote and context are read again from the new\n * document (so an edit inside the range changes the quote). The same object when nothing changed. A range collapsed by\n * the edit, or a point inside deleted text, becomes an `orphan` that keeps the last good anchor; orphans stay as they are.\n */\nexport function mapAnchor(anchor: PortableAnchor, transaction: AnchorTransaction): PortableAnchor {\n if (anchor.kind === 'orphan') return anchor\n const source = { state: { doc: transaction.doc } }\n let next: PortableAnchor\n if (anchor.kind === 'range') {\n const from = transaction.mapping.mapResult(anchor.from, 1).pos\n const to = transaction.mapping.mapResult(anchor.to, -1).pos\n if (from >= to) {\n next = { kind: 'orphan', previous: anchor, quote: anchor.quote, contextBefore: anchor.contextBefore, contextAfter: anchor.contextAfter, reason: from === to ? 'target-deleted' : 'target-changed' }\n }\n else {\n next = captureAnchor(source, { from, to })\n }\n }\n else {\n const after = transaction.mapping.mapResult(anchor.position, 1)\n const before = transaction.mapping.mapResult(anchor.position, -1)\n next = after.deleted && before.deleted\n ? { kind: 'orphan', previous: anchor, contextBefore: anchor.contextBefore, contextAfter: anchor.contextAfter, reason: 'target-deleted' }\n : capturePoint(source, after.pos)\n }\n return sameAnchor(anchor, next) ? anchor : next\n}\n\n// -- Validation -------------------------------------------------------------------------------------------------------\n\nconst isPosition = (value: unknown): value is number => typeof value === 'number' && Number.isInteger(value) && value >= 0\nconst isOptionalText = (value: unknown) => value === undefined || typeof value === 'string'\n\nfunction isLiveAnchor(value: unknown): value is RangeAnchor | PointAnchor {\n if (typeof value !== 'object' || value === null) return false\n const anchor = value as Record<string, unknown>\n if (!isOptionalText(anchor.contextBefore) || !isOptionalText(anchor.contextAfter)) return false\n if (anchor.kind === 'range') return isPosition(anchor.from) && isPosition(anchor.to) && anchor.from <= anchor.to && typeof anchor.quote === 'string'\n return anchor.kind === 'point' && isPosition(anchor.position)\n}\n\n/** Whether `value` has the shape of a stored anchor (range, point or orphan); for input from the network. */\nexport function isPortableAnchor(value: unknown): value is PortableAnchor {\n if (isLiveAnchor(value)) return true\n if (typeof value !== 'object' || value === null) return false\n const anchor = value as Record<string, unknown>\n return anchor.kind === 'orphan'\n && isLiveAnchor(anchor.previous)\n && isOptionalText(anchor.quote) && isOptionalText(anchor.contextBefore) && isOptionalText(anchor.contextAfter)\n && (anchor.reason === 'target-changed' || anchor.reason === 'target-deleted' || anchor.reason === 'unresolvable')\n}\n"],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import { AuthorIdentity } from "../../core/feature.js";
2
+ import "../../core/index.js";
3
+ import { CommentsModel } from "../../features/comments/model.js";
4
+ import "../../features/comments/index.js";
5
+ import { Editor } from "@tiptap/core";
6
+ //#region src/adapters/comments/client.d.ts
7
+ /** The part of a `Response` the model reads; `globalThis.fetch` fits. */
8
+ export interface CommentsFetchResponse {
9
+ ok: boolean;
10
+ status: number;
11
+ json(): Promise<unknown>;
12
+ }
13
+ export type CommentsFetch = (url: string, init?: {
14
+ method?: string;
15
+ headers?: Record<string, string>;
16
+ body?: string;
17
+ }) => Promise<CommentsFetchResponse>;
18
+ export interface RestCommentsModelOptions {
19
+ /** How requests are sent: `globalThis.fetch`, or a wrapper that adds the base URL and credentials. */
20
+ fetch: CommentsFetch;
21
+ /** The document the threads belong to. */
22
+ documentId: string;
23
+ /** The document's comments endpoint (`GET` → `Discussion`, `POST` → `CommentAction`); default `/comments/<encoded documentId>`. */
24
+ endpoint?: string;
25
+ /** Who writes: the author of new comments and the only one who may edit or delete them; `null` while nobody is signed in (no comments then). */
26
+ user: AuthorIdentity | null | (() => AuthorIdentity | null);
27
+ /** Whether the user may compose and change comments now; readers of a read-only document may be allowed. */
28
+ canComment: boolean | (() => boolean);
29
+ /** The editor the threads belong to; may return no editor yet or a destroyed one. */
30
+ getEditor: () => Editor | undefined;
31
+ /** Milliseconds a changed anchor waits before it is written back (typing moves many); default 500. `idle()` writes at once. */
32
+ persistDelay?: number;
33
+ /** Called with every failed request; the model reloads the threads afterwards, so the local state falls back to the server's. */
34
+ onError?: (error: Error) => void;
35
+ }
36
+ export interface RestCommentsModel extends CommentsModel {
37
+ /** Fetches the threads again and resolves their anchors against the current document. */
38
+ reload(): Promise<void>;
39
+ /** Resolves once every change made so far has been sent (and the threads loaded, if a load was requested). */
40
+ idle(): Promise<void>;
41
+ }
42
+ /**
43
+ * The comments of one editor over a REST endpoint, as a `CommentsModel` (the host passes `() => model` as the
44
+ * `model` factory of `commentsFeature`/`commentsFeatureCore`; one instance serves the plugin and every hook).
45
+ *
46
+ * - **Load.** `refresh()` (called by the feature once the editor exists) fetches `GET endpoint`; each annotation's
47
+ * anchor is resolved against the document (exact offsets, else quote and context search). An anchor that moved is
48
+ * re-captured and written back; an anchor whose text is gone becomes an `orphan` (the thread stays, without a range)
49
+ * and an orphan whose text is back with the same surroundings is re-attached. While the document is still empty the
50
+ * resolving waits for its content.
51
+ * - **Edits.** Every transaction carries all anchors with it (`mapAnchor`), so ranges, quotes and contexts follow the
52
+ * text; changed anchors are written back after `persistDelay`. Text deleted under an anchor orphans it for good (it is
53
+ * never silently re-attached to other text during editing; `reload()` re-attaches it when its text and context
54
+ * are found again, e.g. after the content was replaced).
55
+ * - **Writes** are applied locally at once and sent in order; a failed request is reported to `onError` and the threads
56
+ * are reloaded. Without a `user` or while `canComment` is false nothing is written. Only the author may edit or
57
+ * delete a comment.
58
+ * - **Lifecycle.** The model follows the editor `getEditor` names until that editor is destroyed; `dispose()` writes
59
+ * pending anchor changes and does not stop it (the feature and its hooks share the instance and dispose it on their
60
+ * own schedule).
61
+ */
62
+ export declare function createRestCommentsModel(options: RestCommentsModelOptions): RestCommentsModel;
63
+ //#endregion
64
+ //# sourceMappingURL=client.d.ts.map