@dxos/ui-editor 0.0.0 → 0.8.4-main.1c7ec43d41

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
@@ -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 `scroller.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,30 @@ 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',
148
230
  },
149
231
  '.cm-link > span': {
150
- color: 'var(--dx-accentText)',
232
+ color: 'var(--color-accent-text)',
233
+ },
234
+ '.cm-link > span:hover': {
235
+ color: 'var(--color-accent-text-hover)',
151
236
  },
152
237
 
153
238
  /**
154
239
  * Tooltip.
155
240
  */
156
241
  '.cm-tooltip': {
157
- background: 'var(--dx-baseSurface)',
242
+ background: 'var(--color-modal-surface)',
158
243
  },
159
244
  '.cm-tooltip-below': {},
245
+ '.cm-tooltip-hover': {
246
+ background: 'var(--color-modal-surface)',
247
+ border: '1px solid var(--color-separator)',
248
+ borderRadius: '4px',
249
+ overflow: 'hidden',
250
+ },
160
251
 
161
252
  /**
162
253
  * Autocomplete.
@@ -165,7 +256,7 @@ export const baseTheme = EditorView.baseTheme({
165
256
  '.cm-tooltip.cm-tooltip-autocomplete': {
166
257
  marginTop: '6px',
167
258
  marginLeft: '-10px',
168
- border: '2px solid var(--dx-separator)',
259
+ border: '2px solid var(--color-separator)',
169
260
  borderRadius: '4px',
170
261
  },
171
262
  '.cm-tooltip.cm-tooltip-autocomplete > ul': {
@@ -175,12 +266,12 @@ export const baseTheme = EditorView.baseTheme({
175
266
  padding: '4px',
176
267
  },
177
268
  '.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {
178
- background: 'var(--dx-activeSurface)',
179
- color: 'var(--dx-activeSurfaceText)',
269
+ background: 'var(--color-active-surface)',
270
+ color: 'var(--color-base-surface-text)',
180
271
  },
181
272
  '.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
182
273
  paddingLeft: '4px !important',
183
- color: 'var(--dx-hoverSurfaceText)',
274
+ color: 'var(--color-base-surface-text)',
184
275
  },
185
276
 
186
277
  /**
@@ -190,17 +281,17 @@ export const baseTheme = EditorView.baseTheme({
190
281
  width: '360px !important',
191
282
  margin: '-10px 1px 0 1px',
192
283
  padding: '8px !important',
193
- borderColor: 'var(--dx-separator)',
284
+ borderColor: 'var(--color-separator)',
194
285
  },
195
286
  '.cm-completionIcon': {
196
287
  display: 'none',
197
288
  },
198
289
  '.cm-completionLabel': {
199
- color: 'var(--dx-description)',
290
+ color: 'var(--color-description)',
200
291
  padding: '0 4px',
201
292
  },
202
293
  '.cm-completionMatchedText': {
203
- color: 'var(--dx-baseText)',
294
+ color: 'var(--color-base-surface-text)',
204
295
  textDecoration: 'none !important',
205
296
  },
206
297
 
@@ -225,7 +316,7 @@ export const baseTheme = EditorView.baseTheme({
225
316
  backgroundColor: 'var(--surface-bg)',
226
317
  },
227
318
  '.cm-panel input, .cm-panel button, .cm-panel label': {
228
- color: 'var(--dx-subdued)',
319
+ color: 'var(--color-subdued)',
229
320
  fontSize: '14px',
230
321
  all: 'unset',
231
322
  margin: '3px !important',
@@ -233,10 +324,10 @@ export const baseTheme = EditorView.baseTheme({
233
324
  outline: '1px solid transparent',
234
325
  },
235
326
  '.cm-panel input, .cm-panel button': {
236
- backgroundColor: 'var(--dx-inputSurface)',
327
+ backgroundColor: 'var(--color-input-surface)',
237
328
  },
238
329
  '.cm-panel input:focus, .cm-panel button:focus': {
239
- outline: '1px solid var(--dx-neutralFocusIndicator)',
330
+ outline: '1px solid var(--color-neutral-focus-indicator)',
240
331
  },
241
332
  '.cm-panel label': {
242
333
  display: 'inline-flex',
@@ -249,26 +340,27 @@ export const baseTheme = EditorView.baseTheme({
249
340
  height: '8px',
250
341
  marginRight: '6px !important',
251
342
  padding: '2px !important',
252
- color: 'var(--dx-neutralFocusIndicator)',
343
+ color: 'var(--color-neutral-focus-indicator)',
253
344
  },
254
345
  '.cm-panel button': {
255
346
  '&:hover': {
256
- backgroundColor: 'var(--dx-accentSurfaceHover) !important',
347
+ // TODO(burdon): Replace with layer and @apply bg-accent-surface-hover
348
+ backgroundColor: 'var(--color-accent-surface-hover) !important',
257
349
  },
258
350
  '&:active': {
259
- backgroundColor: 'var(--dx-accentSurfaceHover)',
351
+ backgroundColor: 'var(--color-accent-surface-hover)',
260
352
  },
261
353
  },
262
354
  '.cm-panel.cm-search': {
263
355
  padding: '4px',
264
- borderTop: '1px solid var(--dx-separator)',
356
+ borderTop: '1px solid var(--color-separator)',
265
357
  },
266
358
  });
267
359
 
268
360
  export const editorGutter: Extension = EditorView.theme({
269
361
  '.cm-gutters': {
270
362
  // NOTE: Non-transparent background required to cover content if scrolling horizontally.
271
- background: 'var(--dx-baseSurface) !important',
363
+ background: 'var(--color-base-surface) !important',
272
364
  paddingRight: '1rem',
273
365
  },
274
366
  });
@@ -279,10 +371,9 @@ export type FontOptions = {
279
371
 
280
372
  export const createFontTheme = ({ monospace }: FontOptions = {}) =>
281
373
  EditorView.theme({
282
- // Set metrics on the scroller (this is often what CM uses for layout).
374
+ // Main content.
283
375
  '.cm-scroller': {
284
376
  fontFamily: monospace ? fontMono : fontBody,
285
- fontSize: '16px',
286
377
  },
287
378
 
288
379
  // 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>;