@dxos/ui-editor 0.0.0 → 0.8.4-main.05e74ebcff

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 (263) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/browser/index.mjs +8657 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/lib/browser/types/index.mjs +33 -0
  7. package/dist/lib/browser/types/index.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +8659 -0
  9. package/dist/lib/node-esm/index.mjs.map +7 -0
  10. package/dist/lib/node-esm/meta.json +1 -0
  11. package/dist/lib/node-esm/types/index.mjs +35 -0
  12. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  13. package/dist/types/src/defaults.d.ts +6 -0
  14. package/dist/types/src/defaults.d.ts.map +1 -0
  15. package/dist/types/src/extensions/annotations.d.ts +9 -0
  16. package/dist/types/src/extensions/annotations.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 +30 -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/scrolling/auto-scroll.d.ts +18 -0
  142. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -0
  143. package/dist/types/src/extensions/scrolling/crawler.d.ts +75 -0
  144. package/dist/types/src/extensions/scrolling/crawler.d.ts.map +1 -0
  145. package/dist/types/src/extensions/scrolling/index.d.ts +5 -0
  146. package/dist/types/src/extensions/scrolling/index.d.ts.map +1 -0
  147. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts +3 -0
  148. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +1 -0
  149. package/dist/types/src/extensions/scrolling/scroller.d.ts +16 -0
  150. package/dist/types/src/extensions/scrolling/scroller.d.ts.map +1 -0
  151. package/dist/types/src/extensions/selection.d.ts +24 -0
  152. package/dist/types/src/extensions/selection.d.ts.map +1 -0
  153. package/dist/types/src/extensions/snippets.d.ts +10 -0
  154. package/dist/types/src/extensions/snippets.d.ts.map +1 -0
  155. package/dist/types/src/extensions/state.d.ts +2 -0
  156. package/dist/types/src/extensions/state.d.ts.map +1 -0
  157. package/dist/types/src/extensions/submit.d.ts +10 -0
  158. package/dist/types/src/extensions/submit.d.ts.map +1 -0
  159. package/dist/types/src/extensions/tags/extended-markdown.d.ts +10 -0
  160. package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +1 -0
  161. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts +2 -0
  162. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +1 -0
  163. package/dist/types/src/extensions/tags/fader.d.ts +12 -0
  164. package/dist/types/src/extensions/tags/fader.d.ts.map +1 -0
  165. package/dist/types/src/extensions/tags/index.d.ts +7 -0
  166. package/dist/types/src/extensions/tags/index.d.ts.map +1 -0
  167. package/dist/types/src/extensions/tags/typewriter.d.ts +43 -0
  168. package/dist/types/src/extensions/tags/typewriter.d.ts.map +1 -0
  169. package/dist/types/src/extensions/tags/typewriter.test.d.ts +2 -0
  170. package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +1 -0
  171. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +31 -0
  172. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +1 -0
  173. package/dist/types/src/extensions/tags/xml-formatting.d.ts +24 -0
  174. package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +1 -0
  175. package/dist/types/src/extensions/tags/xml-tags.d.ts +117 -0
  176. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -0
  177. package/dist/types/src/extensions/tags/xml-util.d.ts +10 -0
  178. package/dist/types/src/extensions/tags/xml-util.d.ts.map +1 -0
  179. package/dist/types/src/extensions/tags/xml-util.test.d.ts +2 -0
  180. package/dist/types/src/extensions/tags/xml-util.test.d.ts.map +1 -0
  181. package/dist/types/src/index.d.ts +8 -0
  182. package/dist/types/src/index.d.ts.map +1 -0
  183. package/dist/types/src/styles/index.d.ts +2 -0
  184. package/dist/types/src/styles/index.d.ts.map +1 -0
  185. package/dist/types/src/styles/theme.d.ts +58 -0
  186. package/dist/types/src/styles/theme.d.ts.map +1 -0
  187. package/dist/types/src/types/index.d.ts +2 -0
  188. package/dist/types/src/types/index.d.ts.map +1 -0
  189. package/dist/types/src/types/types.d.ts +21 -0
  190. package/dist/types/src/types/types.d.ts.map +1 -0
  191. package/dist/types/src/util/cursor.d.ts +31 -0
  192. package/dist/types/src/util/cursor.d.ts.map +1 -0
  193. package/dist/types/src/util/debug.d.ts +17 -0
  194. package/dist/types/src/util/debug.d.ts.map +1 -0
  195. package/dist/types/src/util/decorations.d.ts +4 -0
  196. package/dist/types/src/util/decorations.d.ts.map +1 -0
  197. package/dist/types/src/util/dom.d.ts +10 -0
  198. package/dist/types/src/util/dom.d.ts.map +1 -0
  199. package/dist/types/src/util/facet.d.ts +3 -0
  200. package/dist/types/src/util/facet.d.ts.map +1 -0
  201. package/dist/types/src/util/index.d.ts +7 -0
  202. package/dist/types/src/util/index.d.ts.map +1 -0
  203. package/dist/types/src/util/util.d.ts +8 -0
  204. package/dist/types/src/util/util.d.ts.map +1 -0
  205. package/dist/types/tsconfig.tsbuildinfo +1 -0
  206. package/package.json +43 -44
  207. package/src/defaults.ts +33 -20
  208. package/src/extensions/annotations.ts +1 -1
  209. package/src/extensions/autocomplete/placeholder.ts +37 -18
  210. package/src/extensions/automerge/automerge.test.tsx +37 -11
  211. package/src/extensions/automerge/automerge.ts +5 -7
  212. package/src/extensions/blocks.ts +5 -5
  213. package/src/extensions/comments.ts +5 -6
  214. package/src/extensions/dnd.ts +2 -2
  215. package/src/extensions/factories.test.ts +88 -0
  216. package/src/extensions/factories.ts +32 -15
  217. package/src/extensions/folding.ts +5 -22
  218. package/src/extensions/index.ts +2 -3
  219. package/src/extensions/markdown/action.ts +0 -1
  220. package/src/extensions/markdown/bundle.ts +23 -9
  221. package/src/extensions/markdown/decorate.ts +15 -12
  222. package/src/extensions/markdown/formatting.ts +5 -10
  223. package/src/extensions/markdown/highlight.ts +15 -7
  224. package/src/extensions/markdown/link.ts +27 -33
  225. package/src/extensions/markdown/parser.test.ts +0 -1
  226. package/src/extensions/markdown/styles.ts +42 -9
  227. package/src/extensions/markdown/table.ts +24 -2
  228. package/src/extensions/outliner/outliner.test.ts +0 -1
  229. package/src/extensions/outliner/outliner.ts +3 -4
  230. package/src/extensions/outliner/tree.test.ts +0 -1
  231. package/src/extensions/preview/preview.ts +62 -15
  232. package/src/extensions/scrolling/auto-scroll.ts +244 -0
  233. package/src/extensions/scrolling/crawler.ts +263 -0
  234. package/src/extensions/scrolling/index.ts +8 -0
  235. package/src/extensions/scrolling/scroll-past-end.ts +32 -0
  236. package/src/extensions/scrolling/scroller.ts +27 -0
  237. package/src/extensions/selection.ts +1 -1
  238. package/src/extensions/snippets.ts +67 -0
  239. package/src/extensions/tags/extended-markdown.test.ts +120 -2
  240. package/src/extensions/tags/extended-markdown.ts +80 -1
  241. package/src/extensions/tags/fader.ts +195 -0
  242. package/src/extensions/tags/index.ts +4 -1
  243. package/src/extensions/tags/testing/text.md +36 -0
  244. package/src/extensions/tags/testing/text.txt +35 -0
  245. package/src/extensions/tags/typewriter.test.ts +65 -0
  246. package/src/extensions/tags/typewriter.ts +594 -0
  247. package/src/extensions/tags/xml-block-decoration.ts +123 -0
  248. package/src/extensions/tags/xml-formatting.ts +125 -0
  249. package/src/extensions/tags/xml-tags.ts +186 -35
  250. package/src/extensions/tags/xml-util.test.ts +199 -24
  251. package/src/extensions/tags/xml-util.ts +62 -5
  252. package/src/index.ts +0 -1
  253. package/src/styles/index.ts +0 -2
  254. package/src/styles/theme.ts +125 -33
  255. package/src/types/types.ts +10 -2
  256. package/src/typings.d.ts +8 -0
  257. package/src/util/cursor.ts +1 -2
  258. package/src/extensions/autoscroll.ts +0 -165
  259. package/src/extensions/scrolling.ts +0 -189
  260. package/src/extensions/tags/streamer.ts +0 -243
  261. package/src/extensions/typewriter.ts +0 -68
  262. package/src/styles/markdown.ts +0 -26
  263. package/src/styles/tokens.ts +0 -17
@@ -2,47 +2,222 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { syntaxTree } from '@codemirror/language';
5
+ import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
6
6
  import { EditorState } from '@codemirror/state';
7
- import { describe, it } from 'vitest';
7
+ import { describe, test } from 'vitest';
8
8
 
9
- import { Trigger } from '@dxos/async';
10
9
  import { trim } from '@dxos/util';
11
10
 
12
11
  import { extendedMarkdown } from './extended-markdown';
12
+ import TEXT from './testing/text.md?raw';
13
13
  import { xmlTags } from './xml-tags';
14
- import { nodeToJson } from './xml-util';
14
+ import { nodeToJson, type Tag } from './xml-util';
15
+
16
+ type ParsedElement = Tag & {
17
+ /** Element spans the entire document (opening through closing tag). */
18
+ complete: boolean;
19
+ };
20
+
21
+ /**
22
+ * Helper to extract all parsed XML elements from a document.
23
+ * Checks completeness by verifying the Element node has a CloseTag child.
24
+ */
25
+ const parseElements = (doc: string, registry: Record<string, any> = {}): ParsedElement[] => {
26
+ const state = EditorState.create({
27
+ doc,
28
+ extensions: [extendedMarkdown({ registry }), xmlTags({ registry })],
29
+ });
30
+
31
+ const tree = ensureSyntaxTree(state, doc.length, 5000) ?? syntaxTree(state);
32
+ const elements: ParsedElement[] = [];
33
+ tree.iterate({
34
+ enter: (node) => {
35
+ if (node.type.name === 'Element') {
36
+ const tag = nodeToJson(state, node.node);
37
+ if (tag) {
38
+ const hasCloseTag = !!node.node.getChild('CloseTag');
39
+ const hasSelfClose = !!node.node.getChild('SelfClosingTag');
40
+ elements.push({
41
+ ...tag,
42
+ complete: hasCloseTag || hasSelfClose,
43
+ });
44
+ }
45
+ return false;
46
+ }
47
+ },
48
+ });
49
+
50
+ return elements;
51
+ };
15
52
 
16
53
  describe('nodeToJson', () => {
17
- it('should parse a simple element', async ({ expect }) => {
54
+ test('should parse a simple element', ({ expect }) => {
18
55
  const xml = trim`
19
56
  # Test
20
57
 
21
58
  <test id="123" foo="100" />
22
59
  `;
23
60
 
24
- const state = EditorState.create({
25
- doc: xml,
26
- extensions: [extendedMarkdown(), xmlTags()],
27
- });
28
-
29
- const value = new Trigger<any>();
30
- syntaxTree(state).iterate({
31
- enter: (node) => {
32
- switch (node.type.name) {
33
- case 'Element': {
34
- const args = nodeToJson(state, node.node);
35
- value.wake(args);
36
- break;
37
- }
38
- }
39
- },
40
- });
41
-
42
- expect(await value.wait()).toEqual({
61
+ const elements = parseElements(xml);
62
+ expect(elements).toHaveLength(1);
63
+ expect(elements[0]).toMatchObject({
43
64
  _tag: 'test',
44
65
  id: '123',
45
66
  foo: '100',
46
67
  });
47
68
  });
69
+
70
+ test('should parse tag with single-line content', ({ expect }) => {
71
+ const xml = trim`
72
+ <reasoning>The user is asking about markdown.</reasoning>
73
+ `;
74
+
75
+ const registry = { reasoning: { block: true } };
76
+ const elements = parseElements(xml, registry);
77
+ expect(elements).toHaveLength(1);
78
+ expect(elements[0]._tag).toBe('reasoning');
79
+ expect(elements[0].complete).toBe(true);
80
+ expect(elements[0].children).toEqual(['The user is asking about markdown.']);
81
+ });
82
+
83
+ test('should parse tag with multi-line content without blank lines', ({ expect }) => {
84
+ const xml = trim`
85
+ <reasoning>
86
+ The user is asking about markdown.
87
+ This is a follow-up thought.
88
+ </reasoning>
89
+ `;
90
+
91
+ const registry = { reasoning: { block: true } };
92
+ const elements = parseElements(xml, registry);
93
+ expect(elements).toHaveLength(1);
94
+ expect(elements[0]._tag).toBe('reasoning');
95
+ expect(elements[0].complete).toBe(true);
96
+ });
97
+
98
+ // BUG: Blank lines inside XML tags cause the markdown parser to split the content
99
+ // into multiple blocks (HTMLBlock + Paragraph), preventing the XML mixed parser from
100
+ // seeing the full element. The first HTMLBlock gets an incomplete Element (no CloseTag),
101
+ // and the closing tag ends up in a Paragraph that's never parsed as XML.
102
+ //
103
+ // Syntax tree with blank lines:
104
+ // Document [0-28] → Element (incomplete: OpenTag + Text + ⚠ — no CloseTag)
105
+ // Paragraph [30-60] → "Second paragraph.\n</reasoning>"
106
+
107
+ test('should parse tag with blank lines in content', ({ expect }) => {
108
+ const xml = trim`
109
+ <reasoning>
110
+ The user is asking me to think deeply about what markdown is.
111
+
112
+ 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.
113
+ </reasoning>
114
+ `;
115
+
116
+ const registry = { reasoning: { block: true } };
117
+ const elements = parseElements(xml, registry);
118
+ expect(elements).toHaveLength(1);
119
+ expect(elements[0]._tag).toBe('reasoning');
120
+ expect(elements[0].complete).toBe(true);
121
+ });
122
+
123
+ // Regression: when reasoning text contains escaped XML (e.g. `&lt;foo&gt;`), Lezer XML
124
+ // splits the inline content into Text + EntityReference + Text + EntityReference + Text
125
+ // siblings. The walker used to skip entity references and push each text segment as a
126
+ // separate child, so `getXmlTextChild(children)` only saw the prefix before the first
127
+ // `&lt;` (e.g. ``"The user sent `"``).
128
+ test('should preserve text containing entity references as a single child', ({ expect }) => {
129
+ const xml = trim`
130
+ <reasoning>The user sent \`&lt;foo&gt;\`, which appears to be an XML-like tag.</reasoning>
131
+ `;
132
+
133
+ const registry = { reasoning: { block: true } };
134
+ const elements = parseElements(xml, registry);
135
+ expect(elements).toHaveLength(1);
136
+ expect(elements[0]._tag).toBe('reasoning');
137
+ expect(elements[0].children).toEqual(['The user sent `<foo>`, which appears to be an XML-like tag.']);
138
+ });
139
+
140
+ test('should decode numeric character references', ({ expect }) => {
141
+ const xml = trim`
142
+ <reasoning>arrow &#8594; and hex &#x2192;</reasoning>
143
+ `;
144
+
145
+ const registry = { reasoning: { block: true } };
146
+ const elements = parseElements(xml, registry);
147
+ expect(elements).toHaveLength(1);
148
+ expect(elements[0].children).toEqual(['arrow → and hex →']);
149
+ });
150
+
151
+ test('should parse tag with multiple blank lines in content', ({ expect }) => {
152
+ const xml = trim`
153
+ <reasoning>
154
+ First paragraph.
155
+
156
+
157
+ Second paragraph after two blank lines.
158
+ </reasoning>
159
+ `;
160
+
161
+ const registry = { reasoning: { block: true } };
162
+ const elements = parseElements(xml, registry);
163
+ expect(elements).toHaveLength(1);
164
+ expect(elements[0]._tag).toBe('reasoning');
165
+ expect(elements[0].complete).toBe(true);
166
+ });
167
+
168
+ // Regression: when an unregistered XML tag like `<prompt>` appears earlier in the doc,
169
+ // the markdown parser treats it as a paragraph that lazy-continues into subsequent lines,
170
+ // and a later multi-line tag (e.g. `<reasoning>` whose body contains an ordered list) gets
171
+ // its HTMLBlock truncated to its first line — losing the closing tag and breaking widget
172
+ // rendering. Including the surrounding tag in the registry as a block-only entry (no
173
+ // factory/Component) lets `xmlBlockParsers` keep each tag as its own block.
174
+ test('multi-line reasoning is complete when preceded by a registered prompt block', ({ expect }) => {
175
+ const xml = [
176
+ '<prompt>summarize the posts</prompt>',
177
+ '<toolCall id="x" />',
178
+ '<reasoning>multi line content',
179
+ '1. "First" - desc',
180
+ '5. "Last" - desc</reasoning>',
181
+ ].join('\n');
182
+ const registry = {
183
+ prompt: { block: true },
184
+ reasoning: { block: true },
185
+ toolCall: { block: true },
186
+ };
187
+ const elements = parseElements(xml, registry);
188
+ expect(elements.map((e) => `${e._tag}${e.complete ? '' : '!'}`)).toEqual(['prompt', 'toolCall', 'reasoning']);
189
+ });
190
+
191
+ test('should parse text.md', ({ expect }) => {
192
+ // Mirror the live ChatThread registry, including `prompt` as a block-only entry so
193
+ // unregistered-paragraph lazy-continuation does not break later multi-line tags.
194
+ const registry = {
195
+ prompt: { block: true },
196
+ reasoning: { block: true },
197
+ status: { block: true },
198
+ toolCall: { block: true },
199
+ name: { block: true },
200
+ };
201
+ const elements = parseElements(TEXT, registry);
202
+ const tags = elements.map((element) => element._tag);
203
+ expect(tags).toEqual([
204
+ 'prompt',
205
+ 'reasoning',
206
+ 'status',
207
+ 'toolCall',
208
+ 'toolCall',
209
+ 'reasoning',
210
+ 'status',
211
+ 'toolCall',
212
+ 'reasoning',
213
+ 'prompt',
214
+ 'reasoning',
215
+ 'prompt',
216
+ 'name',
217
+ ]);
218
+ // Every element must be `complete` so the widget renderer wraps it. The bug we fixed:
219
+ // third multi-line `<reasoning>` (lines 9-14) used to be truncated by markdown's
220
+ // OrderedList parser breaking the HTMLBlock, leaving an Element node with no CloseTag.
221
+ expect(elements.filter((e) => !e.complete)).toEqual([]);
222
+ });
48
223
  });
@@ -65,14 +65,25 @@ export const nodeToJson = (state: EditorState, node: SyntaxNode): Tag | undefine
65
65
  const children: any[] = [];
66
66
  let child = node.node.firstChild;
67
67
 
68
+ const appendText = (raw: string) => {
69
+ if (raw.length === 0) {
70
+ return;
71
+ }
72
+ const last = children[children.length - 1];
73
+ if (typeof last === 'string') {
74
+ children[children.length - 1] = last + raw;
75
+ } else {
76
+ children.push(raw);
77
+ }
78
+ };
79
+
68
80
  while (child) {
69
81
  // Skip the opening and closing tags.
70
82
  if (child.type.name !== 'OpenTag' && child.type.name !== 'CloseTag') {
71
83
  if (child.type.name === 'Text') {
72
- const text = state.doc.sliceString(child.from, child.to).trim();
73
- if (text) {
74
- children.push(text);
75
- }
84
+ appendText(state.doc.sliceString(child.from, child.to));
85
+ } else if (child.type.name === 'EntityReference' || child.type.name === 'CharacterReference') {
86
+ appendText(decodeXmlEntity(state.doc.sliceString(child.from, child.to)));
76
87
  } else if (child.type.name === 'Element') {
77
88
  const data = nodeToJson(state, child);
78
89
  if (data) {
@@ -83,11 +94,57 @@ export const nodeToJson = (state: EditorState, node: SyntaxNode): Tag | undefine
83
94
  child = child.nextSibling;
84
95
  }
85
96
 
97
+ // Trim only leading/trailing whitespace on the outer-boundary string segments —
98
+ // interior strings are preserved verbatim so meaningful whitespace around inline
99
+ // child elements (e.g. `<reasoning>foo <ref/> bar</reasoning>`) is not collapsed.
100
+ if (children.length > 0 && typeof children[0] === 'string') {
101
+ children[0] = children[0].trimStart();
102
+ }
86
103
  if (children.length > 0) {
87
- tag.children = children;
104
+ const lastIndex = children.length - 1;
105
+ const last = children[lastIndex];
106
+ if (typeof last === 'string') {
107
+ children[lastIndex] = last.trimEnd();
108
+ }
109
+ }
110
+ const trimmed = children.filter((value) => typeof value !== 'string' || value.length > 0);
111
+
112
+ if (trimmed.length > 0) {
113
+ tag.children = trimmed;
88
114
  }
89
115
  }
90
116
 
91
117
  return tag;
92
118
  }
93
119
  };
120
+
121
+ /**
122
+ * Decode the common XML named entities and numeric character references that
123
+ * Lezer XML produces as `EntityReference` / `CharacterReference` nodes.
124
+ */
125
+ const XML_NAMED_ENTITIES: Record<string, string> = {
126
+ '&lt;': '<',
127
+ '&gt;': '>',
128
+ '&amp;': '&',
129
+ '&quot;': '"',
130
+ '&apos;': "'",
131
+ };
132
+
133
+ const decodeXmlEntity = (raw: string): string => {
134
+ const named = XML_NAMED_ENTITIES[raw];
135
+ if (named !== undefined) {
136
+ return named;
137
+ }
138
+ const numeric = /^&#(x?)([0-9a-fA-F]+);$/.exec(raw);
139
+ if (numeric) {
140
+ const code = parseInt(numeric[2], numeric[1] ? 16 : 10);
141
+ if (Number.isFinite(code)) {
142
+ try {
143
+ return String.fromCodePoint(code);
144
+ } catch {
145
+ // Fall through and return the raw text on out-of-range code points.
146
+ }
147
+ }
148
+ }
149
+ return raw;
150
+ };
package/src/index.ts CHANGED
@@ -10,5 +10,4 @@ export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
10
10
 
11
11
  export * from './defaults';
12
12
  export * from './extensions';
13
- export * from './types';
14
13
  export * from './util';
@@ -2,6 +2,4 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './markdown';
6
5
  export * from './theme';
7
- export * from './tokens';
@@ -5,7 +5,60 @@
5
5
  import { type Extension } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
7
 
8
- import { fontBody, fontMono } from './tokens';
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
11
+
12
+ // https://tailwindcss.com/docs/font-weight
13
+ const headings: Record<HeadingLevel, { className: string; fontSize: string; lineHeight: string }> = {
14
+ 1: {
15
+ className: 'text-3xl',
16
+ fontSize: 'var(--text-3xl)',
17
+ lineHeight: 'var(--text-4xl--line-height)',
18
+ },
19
+ 2: {
20
+ className: 'text-2xl',
21
+ fontSize: 'var(--text-2xl)',
22
+ lineHeight: 'var(--text-3xl--line-height)',
23
+ },
24
+ 3: {
25
+ className: 'text-xl',
26
+ fontSize: 'var(--text-xl)',
27
+ lineHeight: 'var(--text-2xl--line-height)',
28
+ },
29
+ 4: {
30
+ className: 'text-lg',
31
+ fontSize: 'var(--text-lg)',
32
+ lineHeight: 'var(--text-xl--line-height)',
33
+ },
34
+ 5: {
35
+ className: 'text-base',
36
+ fontSize: 'var(--text-base)',
37
+ lineHeight: 'var(--text-lg--line-height)',
38
+ },
39
+ 6: {
40
+ className: 'text-base',
41
+ fontSize: 'var(--text-base)',
42
+ lineHeight: 'var(--text-base--line-height)',
43
+ },
44
+ };
45
+
46
+ // Font families matching --font-body and --font-mono in theme.css.
47
+ export const fontBody = '"Inter Variable", ui-sans-serif, system-ui, sans-serif';
48
+ export const fontMono = '"JetBrains Mono Variable", ui-monospace, "Cascadia Code", "Source Code Pro", monospace';
49
+
50
+ export const markdownTheme = {
51
+ code: 'font-mono! cm-code-inline',
52
+ codeMark: 'font-mono! cm-code-mark',
53
+ mark: 'font-mono!',
54
+ heading: (level: HeadingLevel) => ({
55
+ className: mx(headings[level].className, 'font-light text-(--color-cm-heading-number)'),
56
+ color: 'var(--color-cm-heading) !important',
57
+ lineHeight: headings[level].lineHeight,
58
+ fontSize: headings[level].fontSize,
59
+ fontWeight: '100 !important',
60
+ }),
61
+ };
9
62
 
10
63
  /**
11
64
  * Global base theme.
@@ -43,6 +96,9 @@ import { fontBody, fontMono } from './tokens';
43
96
  * </div>
44
97
  */
45
98
  export const baseTheme = EditorView.baseTheme({
99
+ /**
100
+ * Outer frame.
101
+ */
46
102
  '&': {},
47
103
  '&.cm-focused': {
48
104
  outline: 'none',
@@ -52,7 +108,27 @@ export const baseTheme = EditorView.baseTheme({
52
108
  * Scroller
53
109
  */
54
110
  '.cm-scroller': {
55
- overflowY: 'auto',
111
+ // Browser scroll-anchoring: see comment in `scrolling/crawler.ts`. `auto` lets the browser pin a
112
+ // stable element near the viewport top so widget resizes (e.g. tool-block TogglePanel
113
+ // open/close) don't jump the user's view.
114
+ overflowAnchor: 'auto',
115
+ },
116
+ '.cm-scroller::-webkit-scrollbar': {
117
+ width: 'var(--scrollbar-size,8px)',
118
+ height: 'var(--scrollbar-size,8px)',
119
+ },
120
+ '.cm-scroller::-webkit-scrollbar-corner': {
121
+ background: 'transparent',
122
+ },
123
+ '.cm-scroller::-webkit-scrollbar-track': {
124
+ background: 'transparent',
125
+ },
126
+ '.cm-scroller::-webkit-scrollbar-thumb': {
127
+ background: 'transparent',
128
+ transition: 'background 0.15s',
129
+ },
130
+ '&:hover .cm-scroller::-webkit-scrollbar-thumb': {
131
+ background: 'var(--color-scrollbar-thumb)',
56
132
  },
57
133
 
58
134
  /**
@@ -61,7 +137,6 @@ export const baseTheme = EditorView.baseTheme({
61
137
  */
62
138
  '.cm-content': {
63
139
  padding: 'unset',
64
- lineHeight: '24px',
65
140
  color: 'unset',
66
141
  },
67
142
 
@@ -76,8 +151,8 @@ export const baseTheme = EditorView.baseTheme({
76
151
  '.cm-gutter': {},
77
152
  '.cm-gutter.cm-lineNumbers': {
78
153
  paddingRight: '4px',
79
- borderRight: '1px solid var(--dx-subduedSeparator)',
80
- color: 'var(--dx-subduedText)',
154
+ borderRight: '1px solid var(--color-subdued-separator)',
155
+ color: 'var(--color-subdued)',
81
156
  },
82
157
  '.cm-gutter.cm-lineNumbers .cm-gutterElement': {
83
158
  minWidth: '40px',
@@ -94,31 +169,38 @@ export const baseTheme = EditorView.baseTheme({
94
169
  * Line.
95
170
  */
96
171
  '.cm-line': {
97
- lineHeight: '24px',
172
+ lineHeight: 1.5,
98
173
  paddingInline: 0,
99
174
  },
175
+ /**
176
+ * Force all inline children to inherit line-height to prevent monospace font metrics
177
+ * (JetBrains Mono ascent/descent) from inflating the line box beyond 24px.
178
+ */
179
+ '.cm-line *': {
180
+ lineHeight: 'inherit',
181
+ },
100
182
  '.cm-activeLine': {
101
- background: 'var(--dx-cmActiveLine)',
183
+ background: 'var(--color-cm-active-line)',
102
184
  },
103
185
 
104
186
  /**
105
187
  * Cursor (layer).
106
188
  */
107
189
  '.cm-cursor, .cm-dropCursor': {
108
- borderLeft: '2px solid var(--dx-cmCursor)',
190
+ borderLeft: '2px solid var(--color-cm-cursor)',
109
191
  },
110
192
  '.cm-placeholder': {
111
- color: 'var(--dx-placeholder)',
193
+ color: 'var(--color-placeholder)',
112
194
  },
113
195
 
114
196
  /**
115
197
  * Selection (layer).
116
198
  */
117
199
  '.cm-selectionBackground': {
118
- background: 'var(--dx-cmSelection)',
200
+ background: 'var(--color-cm-selection)',
119
201
  },
120
202
  '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
121
- background: 'var(--dx-cmFocusedSelection)',
203
+ background: 'var(--color-cm-focused-selection)',
122
204
  },
123
205
 
124
206
  /**
@@ -129,8 +211,8 @@ export const baseTheme = EditorView.baseTheme({
129
211
  margin: '0 -3px',
130
212
  padding: '3px',
131
213
  borderRadius: '3px',
132
- background: 'var(--dx-cmHighlightSurface)',
133
- color: 'var(--dx-cmHighlight)',
214
+ background: 'var(--color-cm-highlight-surface)',
215
+ color: 'var(--color-cm-highlight)',
134
216
  },
135
217
  '.cm-searchMatch-selected': {
136
218
  textDecoration: 'underline',
@@ -142,21 +224,31 @@ export const baseTheme = EditorView.baseTheme({
142
224
  '.cm-link': {
143
225
  textDecorationLine: 'underline',
144
226
  textDecorationThickness: '1px',
145
- textDecorationColor: 'var(--dx-separator)',
227
+ textDecorationColor: 'var(--color-separator)',
146
228
  textUnderlineOffset: '2px',
147
229
  borderRadius: '.125rem',
230
+ cursor: 'pointer',
148
231
  },
149
232
  '.cm-link > span': {
150
- color: 'var(--dx-accentText)',
233
+ color: 'var(--color-accent-text)',
234
+ },
235
+ '.cm-link > span:hover': {
236
+ color: 'var(--color-accent-text-hover)',
151
237
  },
152
238
 
153
239
  /**
154
240
  * Tooltip.
155
241
  */
156
242
  '.cm-tooltip': {
157
- background: 'var(--dx-baseSurface)',
243
+ background: 'var(--color-modal-surface)',
158
244
  },
159
245
  '.cm-tooltip-below': {},
246
+ '.cm-tooltip-hover': {
247
+ background: 'var(--color-modal-surface)',
248
+ border: '1px solid var(--color-separator)',
249
+ borderRadius: '4px',
250
+ overflow: 'hidden',
251
+ },
160
252
 
161
253
  /**
162
254
  * Autocomplete.
@@ -165,7 +257,7 @@ export const baseTheme = EditorView.baseTheme({
165
257
  '.cm-tooltip.cm-tooltip-autocomplete': {
166
258
  marginTop: '6px',
167
259
  marginLeft: '-10px',
168
- border: '2px solid var(--dx-separator)',
260
+ border: '2px solid var(--color-separator)',
169
261
  borderRadius: '4px',
170
262
  },
171
263
  '.cm-tooltip.cm-tooltip-autocomplete > ul': {
@@ -175,12 +267,12 @@ export const baseTheme = EditorView.baseTheme({
175
267
  padding: '4px',
176
268
  },
177
269
  '.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {
178
- background: 'var(--dx-activeSurface)',
179
- color: 'var(--dx-activeSurfaceText)',
270
+ background: 'var(--color-current-surface)',
271
+ color: 'var(--color-base-foreground)',
180
272
  },
181
273
  '.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
182
274
  paddingLeft: '4px !important',
183
- color: 'var(--dx-hoverSurfaceText)',
275
+ color: 'var(--color-base-foreground)',
184
276
  },
185
277
 
186
278
  /**
@@ -190,17 +282,17 @@ export const baseTheme = EditorView.baseTheme({
190
282
  width: '360px !important',
191
283
  margin: '-10px 1px 0 1px',
192
284
  padding: '8px !important',
193
- borderColor: 'var(--dx-separator)',
285
+ borderColor: 'var(--color-separator)',
194
286
  },
195
287
  '.cm-completionIcon': {
196
288
  display: 'none',
197
289
  },
198
290
  '.cm-completionLabel': {
199
- color: 'var(--dx-description)',
291
+ color: 'var(--color-description)',
200
292
  padding: '0 4px',
201
293
  },
202
294
  '.cm-completionMatchedText': {
203
- color: 'var(--dx-baseText)',
295
+ color: 'var(--color-base-foreground)',
204
296
  textDecoration: 'none !important',
205
297
  },
206
298
 
@@ -225,7 +317,7 @@ export const baseTheme = EditorView.baseTheme({
225
317
  backgroundColor: 'var(--surface-bg)',
226
318
  },
227
319
  '.cm-panel input, .cm-panel button, .cm-panel label': {
228
- color: 'var(--dx-subdued)',
320
+ color: 'var(--color-subdued)',
229
321
  fontSize: '14px',
230
322
  all: 'unset',
231
323
  margin: '3px !important',
@@ -233,10 +325,10 @@ export const baseTheme = EditorView.baseTheme({
233
325
  outline: '1px solid transparent',
234
326
  },
235
327
  '.cm-panel input, .cm-panel button': {
236
- backgroundColor: 'var(--dx-inputSurface)',
328
+ backgroundColor: 'var(--color-input-surface)',
237
329
  },
238
330
  '.cm-panel input:focus, .cm-panel button:focus': {
239
- outline: '1px solid var(--dx-neutralFocusIndicator)',
331
+ outline: '1px solid var(--color-focus-ring-subtle)',
240
332
  },
241
333
  '.cm-panel label': {
242
334
  display: 'inline-flex',
@@ -249,26 +341,27 @@ export const baseTheme = EditorView.baseTheme({
249
341
  height: '8px',
250
342
  marginRight: '6px !important',
251
343
  padding: '2px !important',
252
- color: 'var(--dx-neutralFocusIndicator)',
344
+ color: 'var(--color-focus-ring-subtle)',
253
345
  },
254
346
  '.cm-panel button': {
255
347
  '&:hover': {
256
- backgroundColor: 'var(--dx-accentSurfaceHover) !important',
348
+ // TODO(burdon): Replace with layer and @apply bg-accent-surface-hover
349
+ backgroundColor: 'var(--color-accent-surface-hover) !important',
257
350
  },
258
351
  '&:active': {
259
- backgroundColor: 'var(--dx-accentSurfaceHover)',
352
+ backgroundColor: 'var(--color-accent-surface-hover)',
260
353
  },
261
354
  },
262
355
  '.cm-panel.cm-search': {
263
356
  padding: '4px',
264
- borderTop: '1px solid var(--dx-separator)',
357
+ borderTop: '1px solid var(--color-separator)',
265
358
  },
266
359
  });
267
360
 
268
361
  export const editorGutter: Extension = EditorView.theme({
269
362
  '.cm-gutters': {
270
363
  // NOTE: Non-transparent background required to cover content if scrolling horizontally.
271
- background: 'var(--dx-baseSurface) !important',
364
+ background: 'var(--color-base-surface) !important',
272
365
  paddingRight: '1rem',
273
366
  },
274
367
  });
@@ -279,10 +372,9 @@ export type FontOptions = {
279
372
 
280
373
  export const createFontTheme = ({ monospace }: FontOptions = {}) =>
281
374
  EditorView.theme({
282
- // Set metrics on the scroller (this is often what CM uses for layout).
375
+ // Main content.
283
376
  '.cm-scroller': {
284
377
  fontFamily: monospace ? fontMono : fontBody,
285
- fontSize: '16px',
286
378
  },
287
379
 
288
380
  // Maintain defaults for UI components.
@@ -24,9 +24,17 @@ export type Comment = {
24
24
  export type RenderCallback<Props extends object> = (el: HTMLElement, props: Props, view: EditorView) => void;
25
25
 
26
26
  export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
27
- export const EditorViewMode = Schema.Union(...EditorViewModes.map((mode) => Schema.Literal(mode)));
27
+ export const EditorViewMode = Schema.Union(
28
+ Schema.Literal('preview').annotations({ title: 'Preview' }),
29
+ Schema.Literal('readonly').annotations({ title: 'Read-only' }),
30
+ Schema.Literal('source').annotations({ title: 'Source' }),
31
+ );
28
32
  export type EditorViewMode = Schema.Schema.Type<typeof EditorViewMode>;
29
33
 
30
34
  export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
31
- export const EditorInputMode = Schema.Union(...EditorInputModes.map((mode) => Schema.Literal(mode)));
35
+ export const EditorInputMode = Schema.Union(
36
+ Schema.Literal('default').annotations({ title: 'Default' }),
37
+ Schema.Literal('vim').annotations({ title: 'Vim' }),
38
+ Schema.Literal('vscode').annotations({ title: 'VS Code' }),
39
+ );
32
40
  export type EditorInputMode = Schema.Schema.Type<typeof EditorInputMode>;