@dxos/ui-editor 0.0.0 → 0.8.4-main.03d5cd7b56

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 (255) hide show
  1. package/dist/lib/browser/index.mjs +8633 -0
  2. package/dist/lib/browser/index.mjs.map +7 -0
  3. package/dist/lib/browser/meta.json +1 -0
  4. package/dist/lib/browser/types/index.mjs +33 -0
  5. package/dist/lib/browser/types/index.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +8635 -0
  7. package/dist/lib/node-esm/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/meta.json +1 -0
  9. package/dist/lib/node-esm/types/index.mjs +35 -0
  10. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  11. package/dist/types/src/defaults.d.ts +6 -0
  12. package/dist/types/src/defaults.d.ts.map +1 -0
  13. package/dist/types/src/extensions/annotations.d.ts +9 -0
  14. package/dist/types/src/extensions/annotations.d.ts.map +1 -0
  15. package/dist/types/src/extensions/auto-scroll.d.ts +18 -0
  16. package/dist/types/src/extensions/auto-scroll.d.ts.map +1 -0
  17. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts +17 -0
  18. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  19. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  20. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  21. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  22. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  23. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +23 -0
  24. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  25. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  26. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/automerge.d.ts +4 -0
  28. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  30. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  31. package/dist/types/src/extensions/automerge/cursor.d.ts +4 -0
  32. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  33. package/dist/types/src/extensions/automerge/defs.d.ts +17 -0
  34. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  35. package/dist/types/src/extensions/automerge/index.d.ts +2 -0
  36. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  37. package/dist/types/src/extensions/automerge/sync.d.ts +17 -0
  38. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -0
  39. package/dist/types/src/extensions/automerge/update-automerge.d.ts +6 -0
  40. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  41. package/dist/types/src/extensions/automerge/update-codemirror.d.ts +5 -0
  42. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  43. package/dist/types/src/extensions/awareness/awareness-provider.d.ts +31 -0
  44. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
  45. package/dist/types/src/extensions/awareness/awareness.d.ts +46 -0
  46. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
  47. package/dist/types/src/extensions/awareness/index.d.ts +3 -0
  48. package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
  49. package/dist/types/src/extensions/blast.d.ts +25 -0
  50. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  51. package/dist/types/src/extensions/blocks.d.ts +2 -0
  52. package/dist/types/src/extensions/blocks.d.ts.map +1 -0
  53. package/dist/types/src/extensions/bookmarks.d.ts +12 -0
  54. package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
  55. package/dist/types/src/extensions/comments.d.ts +90 -0
  56. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  57. package/dist/types/src/extensions/debug.d.ts +3 -0
  58. package/dist/types/src/extensions/debug.d.ts.map +1 -0
  59. package/dist/types/src/extensions/dnd.d.ts +9 -0
  60. package/dist/types/src/extensions/dnd.d.ts.map +1 -0
  61. package/dist/types/src/extensions/factories.d.ts +88 -0
  62. package/dist/types/src/extensions/factories.d.ts.map +1 -0
  63. package/dist/types/src/extensions/factories.test.d.ts +2 -0
  64. package/dist/types/src/extensions/factories.test.d.ts.map +1 -0
  65. package/dist/types/src/extensions/focus.d.ts +7 -0
  66. package/dist/types/src/extensions/focus.d.ts.map +1 -0
  67. package/dist/types/src/extensions/folding.d.ts +6 -0
  68. package/dist/types/src/extensions/folding.d.ts.map +1 -0
  69. package/dist/types/src/extensions/hashtag.d.ts +3 -0
  70. package/dist/types/src/extensions/hashtag.d.ts.map +1 -0
  71. package/dist/types/src/extensions/index.d.ts +32 -0
  72. package/dist/types/src/extensions/index.d.ts.map +1 -0
  73. package/dist/types/src/extensions/json.d.ts +7 -0
  74. package/dist/types/src/extensions/json.d.ts.map +1 -0
  75. package/dist/types/src/extensions/listener.d.ts +13 -0
  76. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  77. package/dist/types/src/extensions/markdown/action.d.ts +12 -0
  78. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  79. package/dist/types/src/extensions/markdown/bundle.d.ts +25 -0
  80. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  81. package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
  82. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
  83. package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
  84. package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
  85. package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
  86. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
  87. package/dist/types/src/extensions/markdown/decorate.d.ts +25 -0
  88. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
  89. package/dist/types/src/extensions/markdown/formatting.d.ts +63 -0
  90. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  91. package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
  92. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
  93. package/dist/types/src/extensions/markdown/highlight.d.ts +37 -0
  94. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  95. package/dist/types/src/extensions/markdown/image.d.ts +7 -0
  96. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  97. package/dist/types/src/extensions/markdown/index.d.ts +10 -0
  98. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  99. package/dist/types/src/extensions/markdown/link.d.ts +7 -0
  100. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  101. package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
  102. package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
  103. package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
  104. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
  105. package/dist/types/src/extensions/markdown/table.d.ts +8 -0
  106. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  107. package/dist/types/src/extensions/mention.d.ts +7 -0
  108. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  109. package/dist/types/src/extensions/modal.d.ts +7 -0
  110. package/dist/types/src/extensions/modal.d.ts.map +1 -0
  111. package/dist/types/src/extensions/modes.d.ts +10 -0
  112. package/dist/types/src/extensions/modes.d.ts.map +1 -0
  113. package/dist/types/src/extensions/outliner/commands.d.ts +10 -0
  114. package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
  115. package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
  116. package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
  117. package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
  118. package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
  119. package/dist/types/src/extensions/outliner/index.d.ts +4 -0
  120. package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
  121. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  122. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  123. package/dist/types/src/extensions/outliner/outliner.d.ts +11 -0
  124. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
  125. package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
  126. package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
  127. package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
  128. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
  129. package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
  130. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
  131. package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
  132. package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
  133. package/dist/types/src/extensions/preview/index.d.ts +2 -0
  134. package/dist/types/src/extensions/preview/index.d.ts.map +1 -0
  135. package/dist/types/src/extensions/preview/preview.d.ts +34 -0
  136. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -0
  137. package/dist/types/src/extensions/replacer.d.ts +21 -0
  138. package/dist/types/src/extensions/replacer.d.ts.map +1 -0
  139. package/dist/types/src/extensions/replacer.test.d.ts +2 -0
  140. package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
  141. package/dist/types/src/extensions/scroll-past-end.d.ts +3 -0
  142. package/dist/types/src/extensions/scroll-past-end.d.ts.map +1 -0
  143. package/dist/types/src/extensions/scroller.d.ts +68 -0
  144. package/dist/types/src/extensions/scroller.d.ts.map +1 -0
  145. package/dist/types/src/extensions/selection.d.ts +24 -0
  146. package/dist/types/src/extensions/selection.d.ts.map +1 -0
  147. package/dist/types/src/extensions/snippets.d.ts +10 -0
  148. package/dist/types/src/extensions/snippets.d.ts.map +1 -0
  149. package/dist/types/src/extensions/state.d.ts +2 -0
  150. package/dist/types/src/extensions/state.d.ts.map +1 -0
  151. package/dist/types/src/extensions/submit.d.ts +10 -0
  152. package/dist/types/src/extensions/submit.d.ts.map +1 -0
  153. package/dist/types/src/extensions/tags/extended-markdown.d.ts +10 -0
  154. package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -0
  155. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts +2 -0
  156. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +1 -0
  157. package/dist/types/src/extensions/tags/fader.d.ts +12 -0
  158. package/dist/types/src/extensions/tags/fader.d.ts.map +1 -0
  159. package/dist/types/src/extensions/tags/index.d.ts +7 -0
  160. package/dist/types/src/extensions/tags/index.d.ts.map +1 -0
  161. package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
  162. package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
  163. package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
  164. package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
  165. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
  166. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
  167. package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
  168. package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
  169. package/dist/types/src/extensions/tags/xml-tags.d.ts +117 -0
  170. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -0
  171. package/dist/types/src/extensions/tags/xml-util.d.ts +10 -0
  172. package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -0
  173. package/dist/types/src/extensions/tags/xml-util.test.d.ts +2 -0
  174. package/dist/types/src/extensions/tags/xml-util.test.d.ts.map +1 -0
  175. package/dist/types/src/index.d.ts +8 -0
  176. package/dist/types/src/index.d.ts.map +1 -0
  177. package/dist/types/src/styles/index.d.ts +2 -0
  178. package/dist/types/src/styles/index.d.ts.map +1 -0
  179. package/dist/types/src/styles/theme.d.ts +58 -0
  180. package/dist/types/src/styles/theme.d.ts.map +1 -0
  181. package/dist/types/src/types/index.d.ts +2 -0
  182. package/dist/types/src/types/index.d.ts.map +1 -0
  183. package/dist/types/src/types/types.d.ts +21 -0
  184. package/dist/types/src/types/types.d.ts.map +1 -0
  185. package/dist/types/src/util/cursor.d.ts +31 -0
  186. package/dist/types/src/util/cursor.d.ts.map +1 -0
  187. package/dist/types/src/util/debug.d.ts +17 -0
  188. package/dist/types/src/util/debug.d.ts.map +1 -0
  189. package/dist/types/src/util/decorations.d.ts +4 -0
  190. package/dist/types/src/util/decorations.d.ts.map +1 -0
  191. package/dist/types/src/util/dom.d.ts +10 -0
  192. package/dist/types/src/util/dom.d.ts.map +1 -0
  193. package/dist/types/src/util/facet.d.ts +3 -0
  194. package/dist/types/src/util/facet.d.ts.map +1 -0
  195. package/dist/types/src/util/index.d.ts +7 -0
  196. package/dist/types/src/util/index.d.ts.map +1 -0
  197. package/dist/types/src/util/util.d.ts +8 -0
  198. package/dist/types/src/util/util.d.ts.map +1 -0
  199. package/dist/types/tsconfig.tsbuildinfo +1 -0
  200. package/package.json +42 -43
  201. package/src/defaults.ts +33 -20
  202. package/src/extensions/annotations.ts +1 -1
  203. package/src/extensions/auto-scroll.ts +234 -0
  204. package/src/extensions/autocomplete/placeholder.ts +37 -18
  205. package/src/extensions/automerge/automerge.test.tsx +37 -11
  206. package/src/extensions/automerge/automerge.ts +5 -7
  207. package/src/extensions/blocks.ts +5 -5
  208. package/src/extensions/comments.ts +5 -6
  209. package/src/extensions/dnd.ts +2 -2
  210. package/src/extensions/factories.test.ts +88 -0
  211. package/src/extensions/factories.ts +32 -15
  212. package/src/extensions/folding.ts +5 -22
  213. package/src/extensions/index.ts +4 -3
  214. package/src/extensions/markdown/action.ts +0 -1
  215. package/src/extensions/markdown/bundle.ts +23 -9
  216. package/src/extensions/markdown/decorate.ts +15 -12
  217. package/src/extensions/markdown/formatting.ts +5 -10
  218. package/src/extensions/markdown/highlight.ts +15 -7
  219. package/src/extensions/markdown/link.ts +27 -33
  220. package/src/extensions/markdown/parser.test.ts +0 -1
  221. package/src/extensions/markdown/styles.ts +42 -9
  222. package/src/extensions/markdown/table.ts +24 -2
  223. package/src/extensions/outliner/outliner.test.ts +0 -1
  224. package/src/extensions/outliner/outliner.ts +3 -4
  225. package/src/extensions/outliner/tree.test.ts +0 -1
  226. package/src/extensions/preview/preview.ts +62 -15
  227. package/src/extensions/scroll-past-end.ts +32 -0
  228. package/src/extensions/scroller.ts +256 -0
  229. package/src/extensions/selection.ts +1 -1
  230. package/src/extensions/snippets.ts +67 -0
  231. package/src/extensions/tags/extended-markdown.test.ts +120 -2
  232. package/src/extensions/tags/extended-markdown.ts +80 -1
  233. package/src/extensions/tags/fader.ts +195 -0
  234. package/src/extensions/tags/index.ts +4 -1
  235. package/src/extensions/tags/testing/text.md +36 -0
  236. package/src/extensions/tags/testing/text.txt +35 -0
  237. package/src/extensions/tags/typewriter.test.ts +65 -0
  238. package/src/extensions/tags/typewriter.ts +594 -0
  239. package/src/extensions/tags/xml-block-decoration.ts +123 -0
  240. package/src/extensions/tags/xml-formatting.ts +125 -0
  241. package/src/extensions/tags/xml-tags.ts +186 -35
  242. package/src/extensions/tags/xml-util.test.ts +199 -24
  243. package/src/extensions/tags/xml-util.ts +62 -5
  244. package/src/index.ts +0 -1
  245. package/src/styles/index.ts +0 -2
  246. package/src/styles/theme.ts +124 -33
  247. package/src/types/types.ts +10 -2
  248. package/src/typings.d.ts +8 -0
  249. package/src/util/cursor.ts +1 -2
  250. package/src/extensions/autoscroll.ts +0 -165
  251. package/src/extensions/scrolling.ts +0 -189
  252. package/src/extensions/tags/streamer.ts +0 -243
  253. package/src/extensions/typewriter.ts +0 -68
  254. package/src/styles/markdown.ts +0 -26
  255. package/src/styles/tokens.ts +0 -17
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+
8
+ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
9
+
10
+ export type SnippetsOptions = {
11
+ delay?: number;
12
+ items?: string[];
13
+ };
14
+
15
+ /**
16
+ * Configurable plugin that lets users cycle through pre-configured input snippets.
17
+ */
18
+ // TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
19
+ export const snippets = ({ delay = 75, items = defaultItems }: SnippetsOptions = {}): Extension => {
20
+ let timer: ReturnType<typeof setTimeout> | undefined;
21
+ let index = 0; // TODO(burdon): Make global.
22
+
23
+ return [
24
+ keymap.of([
25
+ {
26
+ // Reset.
27
+ key: "alt-meta-'",
28
+ run: () => {
29
+ clearTimeout(timer);
30
+ index = 0;
31
+ return true;
32
+ },
33
+ },
34
+ {
35
+ // Next snippet.
36
+ // TODO(burdon): Press 1-9 to select snippet?
37
+ key: "Shift-Meta-'",
38
+ run: (view) => {
39
+ clearTimeout(timer);
40
+ // TODO(burdon): Add space if needed.
41
+ const text = items[index++];
42
+ if (index === items?.length) {
43
+ index = 0;
44
+ }
45
+
46
+ let offset = 0;
47
+ const insert = (delayMs = 0) => {
48
+ timer = setTimeout(() => {
49
+ const pos = view.state.selection.main.head;
50
+ view.dispatch({
51
+ changes: { from: pos, insert: text[offset++] },
52
+ selection: { anchor: pos + 1 },
53
+ });
54
+
55
+ if (offset < text.length) {
56
+ insert(Math.random() * delay * (text[offset] === ' ' ? 2 : 1));
57
+ }
58
+ }, delayMs);
59
+ };
60
+
61
+ insert();
62
+ return true;
63
+ },
64
+ },
65
+ ]),
66
+ ];
67
+ };
@@ -12,11 +12,24 @@ import { trim } from '@dxos/util';
12
12
  import { extendedMarkdown } from './extended-markdown';
13
13
  import { nodeToJson } from './xml-util';
14
14
 
15
+ const testRegistry = {
16
+ prompt: { block: true },
17
+ suggestion: { block: true },
18
+ choice: { block: true },
19
+ toolkit: { block: true },
20
+ toolCall: { block: true },
21
+ summary: { block: true },
22
+ reasoning: { block: true },
23
+ stats: { block: true },
24
+ reference: { block: false },
25
+ select: { block: true },
26
+ };
27
+
15
28
  describe('extended-markdown', () => {
16
- const createEditorState = (doc: string) => {
29
+ const createEditorState = (doc: string, registry?: Record<string, any>) => {
17
30
  return EditorState.create({
18
31
  doc,
19
- extensions: [extendedMarkdown()],
32
+ extensions: [extendedMarkdown({ registry: registry ?? testRegistry })],
20
33
  });
21
34
  };
22
35
 
@@ -121,6 +134,111 @@ describe('extended-markdown', () => {
121
134
  });
122
135
  });
123
136
 
137
+ test('self-closing tags with attributes should not consume subsequent content', ({ expect }) => {
138
+ const doc = trim`
139
+ <toolCall id="toolu_01ABC123" />
140
+
141
+ ## Architecture
142
+
143
+ Regular paragraph.
144
+ `;
145
+
146
+ const state = createEditorState(doc);
147
+ const tree = syntaxTree(state);
148
+
149
+ const nodeNames: string[] = [];
150
+ tree.iterate({
151
+ enter: (node) => {
152
+ nodeNames.push(node.name);
153
+ },
154
+ });
155
+
156
+ // The heading must be parsed as ATXHeading2, not swallowed into an HTMLBlock.
157
+ expect(nodeNames).toContain('ATXHeading2');
158
+ });
159
+
160
+ test('self-closing tags without attributes should parse correctly', ({ expect }) => {
161
+ const doc = trim`
162
+ <toolkit />
163
+
164
+ ## Heading After Self-Close
165
+
166
+ Paragraph text.
167
+ `;
168
+
169
+ const state = createEditorState(doc);
170
+ const tree = syntaxTree(state);
171
+
172
+ const nodeNames: string[] = [];
173
+ tree.iterate({
174
+ enter: (node) => {
175
+ nodeNames.push(node.name);
176
+ },
177
+ });
178
+
179
+ expect(nodeNames).toContain('ATXHeading2');
180
+ });
181
+
182
+ test('self-closing tags with trailing text should not consume subsequent content', ({ expect }) => {
183
+ const doc = trim`
184
+ <toolkit /> trailing text
185
+
186
+ ## Heading After Trailing
187
+ `;
188
+
189
+ const state = createEditorState(doc);
190
+ const tree = syntaxTree(state);
191
+
192
+ const nodeNames: string[] = [];
193
+ tree.iterate({
194
+ enter: (node) => {
195
+ nodeNames.push(node.name);
196
+ },
197
+ });
198
+
199
+ // The trailing text line should not be swallowed as an HTMLBlock.
200
+ expect(nodeNames).toContain('ATXHeading2');
201
+ });
202
+
203
+ test('mixed self-closing and block tags should not break markdown parsing', ({ expect }) => {
204
+ const doc = trim`
205
+ <prompt>What is markdown?</prompt>
206
+
207
+ <reasoning>
208
+ The user is asking about markdown.
209
+ </reasoning>
210
+
211
+ Some explanation text.
212
+
213
+ <toolCall id="toolu_01ABC123analyze" />
214
+
215
+ <summary>Analyzed 42 files.</summary>
216
+
217
+ ## Architecture
218
+
219
+ The project follows a modular pattern.
220
+
221
+ <stats>3 tool uses · 12.4k tokens</stats>
222
+
223
+ ### Sub-heading
224
+
225
+ More text.
226
+ `;
227
+
228
+ const state = createEditorState(doc);
229
+ const tree = syntaxTree(state);
230
+
231
+ const nodeNames: string[] = [];
232
+ tree.iterate({
233
+ enter: (node) => {
234
+ nodeNames.push(node.name);
235
+ },
236
+ });
237
+
238
+ expect(nodeNames).toContain('ATXHeading2');
239
+ expect(nodeNames).toContain('ATXHeading3');
240
+ });
241
+
124
242
  //
125
243
  // TODO(burdon): All tests below should test the tree.
126
244
  //
@@ -5,15 +5,18 @@
5
5
  import { xmlLanguage } from '@codemirror/lang-xml';
6
6
  import { type Extension } from '@codemirror/state';
7
7
  import { type ParseWrapper, parseMixed } from '@lezer/common';
8
+ import { type BlockParser } from '@lezer/markdown';
8
9
 
9
10
  import { createMarkdownExtensions } from '../markdown';
10
-
11
11
  import { type XmlWidgetRegistry } from './xml-tags';
12
12
 
13
13
  export type ExtendedMarkdownOptions = {
14
14
  registry?: XmlWidgetRegistry;
15
15
  };
16
16
 
17
+ /** Escapes a string for safe embedding in RegExp source. */
18
+ const escapeRegExpSource = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
19
+
17
20
  /**
18
21
  * Extended markdown parser with mixed parser for custom rendering of XML tags.
19
22
  */
@@ -29,6 +32,9 @@ export const extendedMarkdown = ({ registry }: ExtendedMarkdownOptions = {}): Ex
29
32
  name: 'SetextHeading',
30
33
  parse: () => false,
31
34
  },
35
+ // Custom XML block parser that keeps registered tags as a single HTMLBlock
36
+ // even when their content contains blank lines.
37
+ ...xmlBlockParsers(registry),
32
38
  ],
33
39
  },
34
40
  ],
@@ -36,6 +42,79 @@ export const extendedMarkdown = ({ registry }: ExtendedMarkdownOptions = {}): Ex
36
42
  ];
37
43
  };
38
44
 
45
+ /**
46
+ * Creates block parsers for registered XML tags that may contain blank lines.
47
+ *
48
+ * By default, the markdown parser treats custom HTML tags as type 6 HTML blocks,
49
+ * which end at blank lines. This causes tags like `<reasoning>...\n\n...</reasoning>`
50
+ * to be split into separate blocks, preventing the XML mixed parser from seeing the
51
+ * full element. This custom parser consumes all lines (including blanks) until the
52
+ * matching closing tag, emitting a single HTMLBlock.
53
+ */
54
+ const xmlBlockParsers = (registry?: XmlWidgetRegistry): BlockParser[] => {
55
+ const customTags = Object.keys(registry ?? {});
56
+ if (customTags.length === 0) {
57
+ return [];
58
+ }
59
+
60
+ const tagPattern = customTags.map(escapeRegExpSource).join('|');
61
+ const selfClosePattern = new RegExp(`^\\s*<(${tagPattern})(\\s[^>]*)?\\/>\\s*$`);
62
+ const openPattern = new RegExp(`^\\s*<(${tagPattern})(\\s[^>]*)?\\/?>`);
63
+
64
+ return [
65
+ {
66
+ name: 'XMLBlock',
67
+ before: 'HTMLBlock',
68
+ parse: (cx, line) => {
69
+ const match = openPattern.exec(line.text);
70
+ if (!match) {
71
+ return false;
72
+ }
73
+
74
+ // Self-closing tag (e.g., `<tag />` or `<tag id="x" />`).
75
+ if (selfClosePattern.test(line.text)) {
76
+ const end = cx.lineStart + line.text.length;
77
+ cx.addElement(cx.elt('HTMLBlock', cx.lineStart, end));
78
+ cx.nextLine();
79
+ return true;
80
+ }
81
+
82
+ // Self-closing tag with trailing text (e.g., `<tag /> text`).
83
+ // Let markdown parse it as a normal paragraph.
84
+ if (match[0].trimEnd().endsWith('/>')) {
85
+ return false;
86
+ }
87
+
88
+ const tagName = match[1];
89
+ const closeTag = `</${tagName}>`;
90
+ const start = cx.lineStart;
91
+
92
+ // Check if closing tag is on the same line.
93
+ if (line.text.includes(closeTag)) {
94
+ cx.addElement(cx.elt('HTMLBlock', start, start + line.text.length));
95
+ cx.nextLine();
96
+ return true;
97
+ }
98
+
99
+ // Consume lines (including blank lines) until the closing tag.
100
+ let end = cx.lineStart + line.text.length;
101
+ while (cx.nextLine()) {
102
+ end = cx.lineStart + line.text.length;
103
+ if (line.text.includes(closeTag)) {
104
+ cx.addElement(cx.elt('HTMLBlock', start, end));
105
+ cx.nextLine();
106
+ return true;
107
+ }
108
+ }
109
+
110
+ // Unclosed tag (e.g., still streaming) — emit what we have.
111
+ cx.addElement(cx.elt('HTMLBlock', start, end));
112
+ return true;
113
+ },
114
+ },
115
+ ];
116
+ };
117
+
39
118
  /**
40
119
  * Configure mixed parser to recognize custom tags.
41
120
  */
@@ -0,0 +1,195 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Extension, StateEffect, StateField } from '@codemirror/state';
6
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin } from '@codemirror/view';
7
+
8
+ const DEFAULT_REMOVAL_DELAY = 5_000; // ms.
9
+ const DEFAULT_COALESCE_WINDOW = 100; // ms.
10
+ const CLEANUP_INTERVAL = 1_000; // ms.
11
+
12
+ export type FaderOptions = {
13
+ /** Time window in ms to coalesce adjacent appends into a single decoration. */
14
+ coalesce?: number;
15
+ };
16
+
17
+ /**
18
+ * Extension that decorates appended text with a fade-in effect.
19
+ * Adjacent appends within the coalesce window are merged into a single decoration.
20
+ * Decorations are queued with an expiry and removed by a timer.
21
+ */
22
+ export const fader = (options: FaderOptions = {}): Extension => {
23
+ const removalDelay = DEFAULT_REMOVAL_DELAY;
24
+ const coalesceWindow = options.coalesce ?? DEFAULT_COALESCE_WINDOW;
25
+
26
+ let lastCount = -1;
27
+ const log = (expiries: number[]) => {
28
+ if (expiries.length !== lastCount) {
29
+ lastCount = expiries.length;
30
+ // console.log('[fader] decorations:', lastCount);
31
+ }
32
+ };
33
+
34
+ // Effect to remove all expired decorations.
35
+ const dequeue = StateEffect.define<number>();
36
+
37
+ // FIFO of expiry timestamps paired with a decoration set.
38
+ // Tracks `batchStart` — the time the current coalesce batch began.
39
+ type FaderState = {
40
+ decorations: DecorationSet;
41
+ expiries: number[];
42
+ batchStart: number;
43
+ };
44
+
45
+ const fadeField = StateField.define<FaderState>({
46
+ create: () => ({ decorations: Decoration.none, expiries: [], batchStart: 0 }),
47
+ update: ({ decorations, expiries, batchStart }, tr) => {
48
+ // Remove expired entries by count (FIFO order matches decoration insertion order).
49
+ for (const effect of tr.effects) {
50
+ if (effect.is(dequeue)) {
51
+ const now = effect.value;
52
+ let removeCount = 0;
53
+ while (removeCount < expiries.length && expiries[removeCount] <= now) {
54
+ removeCount++;
55
+ }
56
+ if (removeCount > 0) {
57
+ expiries = expiries.slice(removeCount);
58
+ let skipped = 0;
59
+ decorations = decorations.update({
60
+ filter: () => {
61
+ if (skipped < removeCount) {
62
+ skipped++;
63
+ return false;
64
+ }
65
+ return true;
66
+ },
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ if (!tr.docChanged) {
73
+ log(expiries);
74
+ return { decorations, expiries, batchStart };
75
+ }
76
+
77
+ // Reset when document is cleared or fully replaced.
78
+ let isReset = tr.state.doc.length === 0;
79
+ if (!isReset && tr.startState.doc.length > 0) {
80
+ tr.changes.iterChanges((fromA, toA) => {
81
+ if (fromA === 0 && toA === tr.startState.doc.length) {
82
+ isReset = true;
83
+ }
84
+ });
85
+ }
86
+ if (isReset) {
87
+ log([]);
88
+ return { decorations: Decoration.none, expiries: [], batchStart: 0 };
89
+ }
90
+
91
+ // Collect appended ranges.
92
+ const now = Date.now();
93
+ const add: { from: number; to: number }[] = [];
94
+ tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
95
+ if (toA === tr.startState.doc.length && inserted.length > 0) {
96
+ add.push({ from: fromB, to: toB });
97
+ }
98
+ });
99
+
100
+ if (add.length > 0) {
101
+ const canCoalesce = expiries.length > 0 && batchStart > 0 && now - batchStart < coalesceWindow;
102
+ if (canCoalesce) {
103
+ // Extend the last decoration: remove it, then add a merged one.
104
+ let lastFrom = -1;
105
+ let lastTo = -1;
106
+ decorations.between(0, tr.state.doc.length, (from, to) => {
107
+ lastFrom = from;
108
+ lastTo = to;
109
+ });
110
+
111
+ if (lastFrom >= 0) {
112
+ decorations = decorations.update({
113
+ filter: (from, to) => !(from === lastFrom && to === lastTo),
114
+ });
115
+
116
+ const mergedFrom = Math.min(lastFrom, add[0].from);
117
+ const mergedTo = add[add.length - 1].to;
118
+ decorations = decorations.update({
119
+ add: [Decoration.mark({ class: 'cm-fader' }).range(mergedFrom, mergedTo)],
120
+ });
121
+
122
+ // Update the last expiry rather than adding a new one.
123
+ expiries = [...expiries.slice(0, -1), now + removalDelay];
124
+ }
125
+ } else {
126
+ // New batch — record when it started.
127
+ batchStart = now;
128
+ expiries = [...expiries, now + removalDelay];
129
+ decorations = decorations.update({
130
+ add: add.map(({ from, to }) => Decoration.mark({ class: 'cm-fader' }).range(from, to)),
131
+ });
132
+ }
133
+ }
134
+
135
+ log(expiries);
136
+ return { decorations, expiries, batchStart };
137
+ },
138
+ provide: (f) => EditorView.decorations.from(f, (value) => value.decorations),
139
+ });
140
+
141
+ // Timer that schedules removal based on the next pending expiry.
142
+ const cleanup = ViewPlugin.fromClass(
143
+ class {
144
+ #timer: ReturnType<typeof setTimeout> | undefined;
145
+
146
+ constructor(private view: EditorView) {
147
+ this.#schedule();
148
+ }
149
+
150
+ update() {
151
+ this.#schedule();
152
+ }
153
+
154
+ #schedule() {
155
+ const { expiries } = this.view.state.field(fadeField);
156
+ if (expiries.length === 0) {
157
+ clearTimeout(this.#timer);
158
+ this.#timer = undefined;
159
+ return;
160
+ }
161
+
162
+ if (this.#timer !== undefined) {
163
+ return;
164
+ }
165
+
166
+ // Wait at least CLEANUP_INTERVAL after the oldest expiry to batch removals.
167
+ const delay = Math.max(CLEANUP_INTERVAL, expiries[0] - Date.now());
168
+ this.#timer = setTimeout(() => {
169
+ this.#timer = undefined;
170
+ this.view.dispatch({ effects: dequeue.of(Date.now()) });
171
+ }, delay);
172
+ }
173
+
174
+ destroy() {
175
+ clearTimeout(this.#timer);
176
+ }
177
+ },
178
+ );
179
+
180
+ return [
181
+ fadeField,
182
+ cleanup,
183
+ EditorView.theme({
184
+ '.cm-fader': {
185
+ animation: 'fader 1s ease-out forwards',
186
+ },
187
+ '@keyframes fader': {
188
+ '0%': {
189
+ textShadow: '0 0 16px rgba(100, 200, 255, 1), 0 0 32px rgba(100, 200, 255, 0.6)',
190
+ },
191
+ '100%': {},
192
+ },
193
+ }),
194
+ ];
195
+ };
@@ -3,5 +3,8 @@
3
3
  //
4
4
 
5
5
  export * from './extended-markdown';
6
- export * from './streamer';
6
+ export * from './fader';
7
+ export * from './typewriter';
8
+ export * from './xml-block-decoration';
9
+ export * from './xml-formatting';
7
10
  export * from './xml-tags';
@@ -0,0 +1,36 @@
1
+ <prompt>summarize the posts</prompt>
2
+ <reasoning>The user wants me to summarize the posts related to the magazine in context. Let me first load the magazine object to understand its structure, and then list the candidate posts.</reasoning>
3
+ <status>Loading the magazine and listing candidate posts…</status>
4
+ <toolCall id="toolu_01WMMcsaHxTvuGxcMh2CkfL4" />
5
+ <toolCall id="toolu_01CxWNvXZ4jExfLEJumfMzqm" />
6
+ <reasoning>The magazine has 10 posts already curated, and there are no uncurated candidate posts. The user wants me to summarize the posts that are already in the magazine. Let me load them.</reasoning>
7
+ <status>No uncurated candidates — loading the 10 posts already in the magazine…</status>
8
+ <toolCall id="toolu_0152ftz33GL5M1DDi8GGDHi2" />
9
+ <reasoning>The magazine has 10 posts, but several are duplicates. Let me deduplicate by title/guid and summarize the unique articles. Unique articles:
10
+
11
+ 1. "FBI affidavit quotes White House press dinner shooting suspect expressing rage at 'a pedophile, rapist and traitor' – US politics live" - About Cole Allen's manifesto and charges, plus King Charles visiting Trump at the White House.
12
+ 2. "Australia news live: UK inquiry says 'cracks already beginning to show' on Aukus; search for missing five-year-old near Alice Springs grows" - Live blog covering Aukus inquiry, David Brat nominated as ambassador to Australia, fuel reserve crisis.
13
+ 3. "Rebel Wilson gives evidence in high-profile defamation case against star of directorial debut" - Rebel Wilson testifying in defamation case brought by Charlotte MacInnes.
14
+ 4. "Baby on board: US woman gives birth on Delta flight" - Ashley Blair delivered her baby aboard a Delta flight traveling from Atlanta to Portland.
15
+ 5. "'Shortcomings and failures' could sink Aukus nuclear submarines plan, UK inquiry warns" - A UK House of Commons defence committee report identifies significant funding gaps threatening the Aukus submarine program.</reasoning>
16
+ Here's a summary of the **5 unique articles** currently in the magazine (some posts are duplicates):
17
+
18
+ 6. **FBI affidavit quotes White House press dinner shooting suspect**
19
+ Federal charges filed against Cole Allen include excerpts from a manifesto he emailed to family and friends before attempting to charge into the White House Correspondents' Dinner. Separately, King Charles and Queen Camilla visited Donald Trump at the White House for tea and a beehive tour.
20
+
21
+ 7. **Australia news live: Aukus 'cracks' & missing child search**
22
+ A UK House of Commons inquiry reveals "shortcomings and failings" in the Aukus submarine program. Also covered: David Brat nominated as the next US ambassador to Australia, and a push to extend Australia's fuel reserves from 30 to 60 days amid rising security concerns.
23
+
24
+ 8. **Rebel Wilson testifies in defamation case**
25
+ Hollywood star Rebel Wilson gave evidence in a defamation lawsuit brought by Charlotte MacInnes, the lead actor of Wilson's directorial debut _The Deb_. The case centres on claims MacInnes confided to Wilson about being uncomfortable with a bathing request from a colleague.
26
+
27
+ 9. **Baby born on Delta flight**
28
+ Passenger Ashley Blair went into labor on a packed Delta flight from Atlanta to Portland. Two off-duty EMTs on the plane delivered baby Brielle Renee just before landing — using a shoelace as a tourniquet for an IV. Mother and baby were healthy on arrival.
29
+
30
+ 10. **UK inquiry warns Aukus submarine plan at risk**
31
+ A detailed House of Commons defence committee report finds UK submarine funding has "cracks already beginning to show," with chronic under-investment in shipbuilding and only one submarine yard (Barrow-in-Furness) already behind schedule. Australia has committed A$4.6 billion to help, but the committee warns political drift could derail the entire program.
32
+ <prompt>how many articles are there?</prompt>
33
+ <reasoning>The user is asking about the number of articles. From my previous analysis, there are 10 posts in the magazine, but only 5 unique articles (the rest are duplicates). Let me answer based on what I already know.</reasoning>
34
+ There are **10 posts** in the magazine, but only **5 unique articles** — each article appears twice (as duplicates from the two feeds).
35
+ <prompt>say your name inside an xml tag</prompt>
36
+ <name>Kai</name>
@@ -0,0 +1,35 @@
1
+ <prompt>summarize the posts</prompt>
2
+ <reasoning>The user wants me to summarize the posts related to the magazine in context. Let me first load the magazine object to understand its structure, and then list the candidate posts.</reasoning>
3
+ <status>Loading the magazine and listing candidate posts…</status>
4
+ <toolCall id="toolu_01WMMcsaHxTvuGxcMh2CkfL4" />
5
+ <toolCall id="toolu_01CxWNvXZ4jExfLEJumfMzqm" />
6
+ <reasoning>The magazine has 10 posts already curated, and there are no uncurated candidate posts. The user wants me to summarize the posts that are already in the magazine. Let me load them.</reasoning>
7
+ <status>No uncurated candidates — loading the 10 posts already in the magazine…</status>
8
+ <toolCall id="toolu_0152ftz33GL5M1DDi8GGDHi2" />
9
+ <reasoning>The magazine has 10 posts, but several are duplicates. Let me deduplicate by title/guid and summarize the unique articles. Unique articles:
10
+ 1. "FBI affidavit quotes White House press dinner shooting suspect expressing rage at 'a pedophile, rapist and traitor' – US politics live" - About Cole Allen's manifesto and charges, plus King Charles visiting Trump at the White House.
11
+ 2. "Australia news live: UK inquiry says 'cracks already beginning to show' on Aukus; search for missing five-year-old near Alice Springs grows" - Live blog covering Aukus inquiry, David Brat nominated as ambassador to Australia, fuel reserve crisis.
12
+ 3. "Rebel Wilson gives evidence in high-profile defamation case against star of directorial debut" - Rebel Wilson testifying in defamation case brought by Charlotte MacInnes.
13
+ 4. "Baby on board: US woman gives birth on Delta flight" - Ashley Blair delivered her baby aboard a Delta flight traveling from Atlanta to Portland.
14
+ 5. "'Shortcomings and failures' could sink Aukus nuclear submarines plan, UK inquiry warns" - A UK House of Commons defence committee report identifies significant funding gaps threatening the Aukus submarine program.</reasoning>
15
+ Here's a summary of the **5 unique articles** currently in the magazine (some posts are duplicates):
16
+
17
+ 1. **FBI affidavit quotes White House press dinner shooting suspect**
18
+ Federal charges filed against Cole Allen include excerpts from a manifesto he emailed to family and friends before attempting to charge into the White House Correspondents' Dinner. Separately, King Charles and Queen Camilla visited Donald Trump at the White House for tea and a beehive tour.
19
+
20
+ 2. **Australia news live: Aukus 'cracks' & missing child search**
21
+ A UK House of Commons inquiry reveals "shortcomings and failings" in the Aukus submarine program. Also covered: David Brat nominated as the next US ambassador to Australia, and a push to extend Australia's fuel reserves from 30 to 60 days amid rising security concerns.
22
+
23
+ 3. **Rebel Wilson testifies in defamation case**
24
+ Hollywood star Rebel Wilson gave evidence in a defamation lawsuit brought by Charlotte MacInnes, the lead actor of Wilson's directorial debut *The Deb*. The case centres on claims MacInnes confided to Wilson about being uncomfortable with a bathing request from a colleague.
25
+
26
+ 4. **Baby born on Delta flight**
27
+ Passenger Ashley Blair went into labor on a packed Delta flight from Atlanta to Portland. Two off-duty EMTs on the plane delivered baby Brielle Renee just before landing — using a shoelace as a tourniquet for an IV. Mother and baby were healthy on arrival.
28
+
29
+ 5. **UK inquiry warns Aukus submarine plan at risk**
30
+ A detailed House of Commons defence committee report finds UK submarine funding has "cracks already beginning to show," with chronic under-investment in shipbuilding and only one submarine yard (Barrow-in-Furness) already behind schedule. Australia has committed A$4.6 billion to help, but the committee warns political drift could derail the entire program.
31
+ <prompt>how many articles are there?</prompt>
32
+ <reasoning>The user is asking about the number of articles. From my previous analysis, there are 10 posts in the magazine, but only 5 unique articles (the rest are duplicates). Let me answer based on what I already know.</reasoning>
33
+ There are **10 posts** in the magazine, but only **5 unique articles** — each article appears twice (as duplicates from the two feeds).
34
+ <prompt>say your name inside an xml tag</prompt>
35
+ <name>Kai</name>
@@ -0,0 +1,65 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { trim } from '@dxos/util';
8
+
9
+ import { xmlElementLength } from './typewriter';
10
+
11
+ describe('typewriter', () => {
12
+ test('xmlElementLength handles hyphenated custom element names', ({ expect }) => {
13
+ const inner = 'Hello';
14
+ const el = `<dom-widget>${inner}</dom-widget>`;
15
+ expect(xmlElementLength(el)).toBe(el.length);
16
+ });
17
+
18
+ test('xmlElementLength handle multi-line tags', ({ expect }) => {
19
+ const el = trim`
20
+ <summary>
21
+ Line 1
22
+
23
+ Line 2
24
+ </summary>
25
+ `;
26
+ expect(xmlElementLength(el)).toBe(el.length);
27
+ });
28
+
29
+ test('xmlElementLength handles simple tag names', ({ expect }) => {
30
+ const el = '<prompt>Hi</prompt>';
31
+ expect(xmlElementLength(el)).toBe(el.length);
32
+ });
33
+
34
+ test('xmlElementLength returns 0 until closing tag is complete', ({ expect }) => {
35
+ expect(xmlElementLength('<dom-widget>x')).toBe(0);
36
+ });
37
+
38
+ test('xmlElementLength handles tags with blank lines in content', ({ expect }) => {
39
+ const el = trim`
40
+ <reasoning>
41
+ The user is asking me to think deeply about what markdown is.
42
+
43
+ But given the context of our conversation - we've been trying to scrape slab data from a website - I think they might be hinting at something specific.
44
+ </reasoning>
45
+ `;
46
+ expect(xmlElementLength(el)).toBe(el.length);
47
+ });
48
+
49
+ test('xmlElementLength handles tags with multiple blank lines', ({ expect }) => {
50
+ const el = trim`
51
+ <reasoning>
52
+ First paragraph.
53
+
54
+
55
+ Second paragraph after two blank lines.
56
+ </reasoning>
57
+ `;
58
+ expect(xmlElementLength(el)).toBe(el.length);
59
+ });
60
+
61
+ test('xmlElementLength handles tags with only blank lines', ({ expect }) => {
62
+ const el = '<reasoning>\n\n\n</reasoning>';
63
+ expect(xmlElementLength(el)).toBe(el.length);
64
+ });
65
+ });