@dxos/react-ui-editor 0.3.11-main.fbbdc2a → 0.3.11-main.fc97a54

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 (202) hide show
  1. package/dist/lib/browser/index.mjs +2904 -1394
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +16 -14
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
  12. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Toolbar/Toolbar.d.ts +29 -0
  14. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +25 -0
  16. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  18. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  19. package/dist/types/src/components/index.d.ts +1 -0
  20. package/dist/types/src/components/index.d.ts.map +1 -1
  21. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  23. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  24. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  26. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  28. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  29. package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
  30. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  31. package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
  32. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  33. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  34. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  35. package/dist/types/src/extensions/awareness.d.ts +2 -2
  36. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  37. package/dist/types/src/extensions/basic.d.ts +4 -5
  38. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  39. package/dist/types/src/extensions/comments.d.ts +41 -20
  40. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  41. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  42. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  43. package/dist/types/src/extensions/index.d.ts +2 -8
  44. package/dist/types/src/extensions/index.d.ts.map +1 -1
  45. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  46. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  48. package/dist/types/src/extensions/markdown/formatting.d.ts +58 -0
  49. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
  51. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  53. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  54. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  55. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  56. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  58. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  59. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  60. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  61. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  62. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  63. package/dist/types/src/extensions/outliner.d.ts +4 -0
  64. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  65. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  66. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  67. package/dist/types/src/hooks/defs.d.ts +25 -0
  68. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  69. package/dist/types/src/hooks/index.d.ts +4 -2
  70. package/dist/types/src/hooks/index.d.ts.map +1 -1
  71. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  72. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  74. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  76. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTextModel.d.ts +14 -32
  78. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  79. package/dist/types/src/index.d.ts +3 -0
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/styles/markdown.d.ts +2 -2
  82. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  83. package/dist/types/src/styles/tokens.d.ts +1 -0
  84. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  85. package/dist/types/src/themes/default.d.ts.map +1 -1
  86. package/dist/types/src/util.d.ts +7 -0
  87. package/dist/types/src/util.d.ts.map +1 -0
  88. package/package.json +48 -37
  89. package/src/components/TextEditor/MarkdownEditor.stories.tsx +85 -92
  90. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  91. package/src/components/TextEditor/TextEditor.tsx +135 -95
  92. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  93. package/src/components/TextEditor/hooks.stories.tsx +111 -0
  94. package/src/components/Toolbar/Toolbar.stories.tsx +102 -0
  95. package/src/components/Toolbar/Toolbar.tsx +264 -0
  96. package/src/components/Toolbar/index.ts +5 -0
  97. package/src/components/index.ts +1 -0
  98. package/src/extensions/autocomplete.ts +23 -19
  99. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  100. package/src/extensions/automerge/automerge.test.ts +13 -0
  101. package/src/extensions/automerge/automerge.ts +55 -46
  102. package/src/extensions/automerge/cursor.ts +5 -4
  103. package/src/extensions/automerge/defs.ts +13 -34
  104. package/src/extensions/automerge/semaphore.ts +33 -32
  105. package/src/extensions/automerge/update-automerge.ts +14 -8
  106. package/src/extensions/automerge/update-codemirror.ts +5 -5
  107. package/src/extensions/awareness.ts +46 -49
  108. package/src/extensions/basic.ts +17 -10
  109. package/src/extensions/blast.ts +4 -1
  110. package/src/extensions/comments.ts +332 -267
  111. package/src/extensions/cursor.ts +50 -0
  112. package/src/extensions/index.ts +2 -8
  113. package/src/extensions/markdown/bundle.ts +23 -28
  114. package/src/extensions/markdown/code.ts +150 -0
  115. package/src/extensions/markdown/formatting.test.ts +482 -0
  116. package/src/extensions/markdown/formatting.ts +1237 -0
  117. package/src/extensions/markdown/heading.ts +59 -0
  118. package/src/extensions/markdown/highlight.ts +11 -26
  119. package/src/extensions/markdown/hr.ts +71 -0
  120. package/src/extensions/markdown/image.ts +87 -0
  121. package/src/extensions/markdown/index.ts +8 -0
  122. package/src/extensions/{link.ts → markdown/link.ts} +90 -83
  123. package/src/extensions/{table.ts → markdown/table.ts} +10 -4
  124. package/src/extensions/markdown/tasklist.ts +110 -0
  125. package/src/extensions/mention.ts +9 -2
  126. package/src/extensions/outliner.ts +12 -0
  127. package/src/hooks/awareness-provider.ts +24 -20
  128. package/src/hooks/defs.ts +42 -0
  129. package/src/hooks/index.ts +5 -2
  130. package/src/hooks/useActionHandler.ts +93 -0
  131. package/src/hooks/useEditorView.ts +29 -0
  132. package/src/hooks/useTextEditor.ts +153 -0
  133. package/src/hooks/useTextModel.ts +67 -60
  134. package/src/index.ts +3 -0
  135. package/src/styles/markdown.ts +9 -12
  136. package/src/styles/tokens.ts +3 -0
  137. package/src/themes/default.ts +59 -83
  138. package/src/util.ts +41 -0
  139. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  140. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  141. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  142. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  143. package/dist/types/src/extensions/code.d.ts.map +0 -1
  144. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  145. package/dist/types/src/extensions/image.d.ts.map +0 -1
  146. package/dist/types/src/extensions/link.d.ts.map +0 -1
  147. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  148. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  149. package/dist/types/src/extensions/table.d.ts.map +0 -1
  150. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  151. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  152. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  153. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  154. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  155. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  156. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  157. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  158. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  159. package/dist/types/src/hooks/automerge.d.ts +0 -3
  160. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  161. package/dist/types/src/hooks/yjs.d.ts +0 -27
  162. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  163. package/dist/types/src/testing/index.d.ts +0 -3
  164. package/dist/types/src/testing/index.d.ts.map +0 -1
  165. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  166. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  167. package/dist/types/src/testing/proto/index.d.ts +0 -2
  168. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  169. package/dist/types/src/testing/replicator.d.ts +0 -45
  170. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  171. package/dist/types/src/util/cursor.d.ts.map +0 -1
  172. package/dist/types/src/util/index.d.ts +0 -3
  173. package/dist/types/src/util/index.d.ts.map +0 -1
  174. package/dist/types/src/util/util.d.ts +0 -5
  175. package/dist/types/src/util/util.d.ts.map +0 -1
  176. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  177. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  178. package/src/extensions/code.ts +0 -99
  179. package/src/extensions/hr.ts +0 -74
  180. package/src/extensions/image.ts +0 -69
  181. package/src/extensions/mermaid.ts +0 -11
  182. package/src/extensions/tasklist.ts +0 -121
  183. package/src/extensions/yjs/cursor.ts +0 -21
  184. package/src/extensions/yjs/index.ts +0 -5
  185. package/src/extensions/yjs/yjs.test.ts +0 -35
  186. package/src/extensions/yjs/yjs.ts +0 -16
  187. package/src/hooks/automerge.ts +0 -52
  188. package/src/hooks/yjs.ts +0 -178
  189. package/src/testing/index.ts +0 -6
  190. package/src/testing/proto/gen/schema.ts +0 -49
  191. package/src/testing/proto/index.ts +0 -5
  192. package/src/testing/proto/schema.proto +0 -15
  193. package/src/testing/replicator.ts +0 -184
  194. package/src/util/cursor.ts +0 -29
  195. package/src/util/index.ts +0 -6
  196. package/src/util/util.ts +0 -18
  197. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  198. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  199. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  200. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  201. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  202. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
8
+
9
+ const buildDecorations = (view: EditorView): DecorationSet => {
10
+ const builder = new RangeSetBuilder<Decoration>();
11
+ const { state } = view;
12
+ const cursor = state.selection.main.head;
13
+
14
+ for (const { from, to } of view.visibleRanges) {
15
+ syntaxTree(state).iterate({
16
+ enter: (node) => {
17
+ switch (node.name) {
18
+ case 'ATXHeading1':
19
+ case 'ATXHeading2':
20
+ case 'ATXHeading3':
21
+ case 'ATXHeading4':
22
+ case 'ATXHeading5':
23
+ case 'ATXHeading6': {
24
+ const mark = node.node.getChild('HeaderMark');
25
+ if (mark) {
26
+ if (!view.hasFocus || view.state.readOnly || cursor < node.from || cursor > node.to) {
27
+ builder.add(mark.from, mark.to + 1, Decoration.replace({}));
28
+ }
29
+ }
30
+ }
31
+ }
32
+ },
33
+ from,
34
+ to,
35
+ });
36
+ }
37
+
38
+ return builder.finish();
39
+ };
40
+
41
+ export const heading = () => {
42
+ return [
43
+ ViewPlugin.fromClass(
44
+ class {
45
+ decorations: DecorationSet;
46
+ constructor(view: EditorView) {
47
+ this.decorations = buildDecorations(view);
48
+ }
49
+
50
+ update(update: ViewUpdate) {
51
+ this.decorations = buildDecorations(update.view);
52
+ }
53
+ },
54
+ {
55
+ decorations: (value) => value.decorations,
56
+ },
57
+ ),
58
+ ];
59
+ };
@@ -6,19 +6,18 @@ import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags, styleTags, Tag } from '@lezer/highlight';
8
8
  import { type MarkdownConfig, Table } from '@lezer/markdown';
9
- import get from 'lodash.get';
10
9
 
11
10
  import {
12
11
  blockquote,
13
12
  bold,
14
13
  code,
15
14
  codeMark,
15
+ getToken,
16
16
  heading,
17
17
  inlineUrl,
18
18
  italic,
19
19
  mark,
20
20
  strikethrough,
21
- tokens,
22
21
  } from '../../styles';
23
22
 
24
23
  /**
@@ -32,7 +31,6 @@ export const markdownTags = {
32
31
  CodeText: Tag.define(),
33
32
  EmphasisMark: Tag.define(),
34
33
  HeaderMark: Tag.define(),
35
- // HorizontalRule: Tag.define(),
36
34
  InlineCode: Tag.define(),
37
35
  LinkLabel: Tag.define(),
38
36
  LinkReference: Tag.define(),
@@ -130,13 +128,13 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
130
128
  // Markdown marks.
131
129
  {
132
130
  tag: [markdownTags.CodeMark, markdownTags.HeaderMark, markdownTags.QuoteMark, markdownTags.EmphasisMark],
133
- class: readonly ? 'hidden' : mark,
131
+ class: mark,
134
132
  },
135
133
 
136
134
  // E.g., code block language (after ```).
137
135
  {
138
136
  tag: [tags.function(tags.variableName), tags.labelName],
139
- class: readonly ? 'hidden' : codeMark,
137
+ class: codeMark,
140
138
  },
141
139
 
142
140
  {
@@ -145,12 +143,12 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
145
143
  },
146
144
 
147
145
  // Headings.
148
- { tag: tags.heading1, class: heading(1, readonly) },
149
- { tag: tags.heading2, class: heading(2, readonly) },
150
- { tag: tags.heading3, class: heading(3, readonly) },
151
- { tag: tags.heading4, class: heading(4, readonly) },
152
- { tag: tags.heading5, class: heading(5, readonly) },
153
- { tag: tags.heading6, class: heading(6, readonly) },
146
+ { tag: tags.heading1, class: heading(1) },
147
+ { tag: tags.heading2, class: heading(2) },
148
+ { tag: tags.heading3, class: heading(3) },
149
+ { tag: tags.heading4, class: heading(4) },
150
+ { tag: tags.heading5, class: heading(5) },
151
+ { tag: tags.heading6, class: heading(6) },
154
152
 
155
153
  // Emphasis.
156
154
  { tag: tags.emphasis, class: italic },
@@ -167,7 +165,7 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
167
165
  // However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
168
166
  // when a language is specified. In this case, the syntax highlighting extensions will colorize
169
167
  // the code, but all other CSS properties will be inherited.
170
- // IMPORTANT: Therefore, the fenced code block will use the base editor font.
168
+ // IMPORTANT: Therefore, the fenced code block will use the base editor font unless changed by an extension.
171
169
  {
172
170
  tag: [markdownTags.CodeText, markdownTags.InlineCode],
173
171
  class: code,
@@ -178,27 +176,14 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
178
176
  class: blockquote,
179
177
  },
180
178
 
181
- // TODO(burdon): Replace with extension.
182
- // {
183
- // tag: [markdownTags.HorizontalRule],
184
- // class: mx(horizontalRule, readonly && '-indent-[100rem]'),
185
- // },
186
-
187
- // TODO(burdon): Only if being edited.
188
179
  {
189
180
  tag: [markdownTags.TableCell],
190
181
  class: 'font-mono',
191
182
  },
192
-
193
- // Main content, paragraphs, etc.
194
- // {
195
- // tag: [tags.content],
196
- // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
197
- // },
198
183
  ],
199
184
  {
200
185
  scope: markdownLanguage,
201
- all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
186
+ all: { fontFamily: getToken('fontFamily.body', []).join(',') },
202
187
  },
203
188
  );
204
189
  };
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
8
+
9
+ import { getToken } from '../../styles';
10
+
11
+ // TODO(burdon): Reconcile with theme.
12
+ const styles = EditorView.baseTheme({
13
+ '& .cm-hr': {
14
+ // Note that block-level decorations should not have vertical margins,
15
+ borderTop: `1px solid ${getToken('extend.colors.neutral.200')}`,
16
+ },
17
+ });
18
+
19
+ class HorizontalRuleWidget extends WidgetType {
20
+ override toDOM() {
21
+ const el = document.createElement('div');
22
+ el.className = 'cm-hr';
23
+ return el;
24
+ }
25
+ }
26
+
27
+ const decoration = Decoration.replace({ widget: new HorizontalRuleWidget() });
28
+
29
+ const buildDecorations = (view: EditorView): DecorationSet => {
30
+ const builder = new RangeSetBuilder<Decoration>();
31
+ const { state } = view;
32
+ const cursor = state.selection.main.head;
33
+
34
+ for (const { from, to } of view.visibleRanges) {
35
+ syntaxTree(state).iterate({
36
+ enter: (node) => {
37
+ if (node.name === 'HorizontalRule') {
38
+ // Check if cursor is inside text.
39
+ if (cursor <= node.from || cursor >= node.to) {
40
+ builder.add(node.from, node.to, decoration);
41
+ }
42
+ }
43
+ },
44
+ from,
45
+ to,
46
+ });
47
+ }
48
+
49
+ return builder.finish();
50
+ };
51
+
52
+ export const hr = () => {
53
+ return [
54
+ ViewPlugin.fromClass(
55
+ class {
56
+ decorations: DecorationSet;
57
+ constructor(view: EditorView) {
58
+ this.decorations = buildDecorations(view);
59
+ }
60
+
61
+ update(update: ViewUpdate) {
62
+ this.decorations = buildDecorations(update.view);
63
+ }
64
+ },
65
+ {
66
+ decorations: (value) => value.decorations,
67
+ },
68
+ ),
69
+ styles,
70
+ ];
71
+ };
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
8
+
9
+ export type ImageOptions = {};
10
+
11
+ export const image = (options: ImageOptions = {}): Extension => {
12
+ return StateField.define<DecorationSet>({
13
+ create: (state) => {
14
+ return Decoration.set(buildDecorations(0, state.doc.length, state));
15
+ },
16
+ update: (value: DecorationSet, tr: Transaction) => {
17
+ if (!tr.docChanged && !tr.selection) {
18
+ return value;
19
+ }
20
+ // Find range of changes and cursor changes.
21
+ const cursor = tr.state.selection.main.head;
22
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
23
+ let from = Math.min(cursor, oldCursor);
24
+ let to = Math.max(cursor, oldCursor);
25
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
26
+ from = Math.min(from, fromB);
27
+ to = Math.max(to, toB);
28
+ });
29
+ // Expand to cover lines
30
+ from = tr.state.doc.lineAt(from).from;
31
+ to = tr.state.doc.lineAt(to).to;
32
+ return value.map(tr.changes).update({
33
+ filterFrom: from,
34
+ filterTo: to,
35
+ filter: () => false,
36
+ add: buildDecorations(from, to, tr.state),
37
+ });
38
+ },
39
+ provide: (field) => EditorView.decorations.from(field),
40
+ });
41
+ };
42
+
43
+ const buildDecorations = (from: number, to: number, state: EditorState) => {
44
+ const decorations: Range<Decoration>[] = [];
45
+ const cursor = state.selection.main.head;
46
+
47
+ syntaxTree(state).iterate({
48
+ enter: (node) => {
49
+ if (node.name === 'Image') {
50
+ const urlNode = node.node.getChild('URL');
51
+ if (urlNode) {
52
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
53
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
54
+ decorations.push(
55
+ Decoration.replace({
56
+ block: true, // Prevent cursor from entering.
57
+ widget: new ImageWidget(url),
58
+ }).range(hide ? node.from : node.to, node.to),
59
+ );
60
+ }
61
+ }
62
+ },
63
+ from,
64
+ to,
65
+ });
66
+
67
+ return decorations;
68
+ };
69
+
70
+ class ImageWidget extends WidgetType {
71
+ constructor(readonly _url: string) {
72
+ super();
73
+ }
74
+
75
+ override eq(other: this) {
76
+ return this._url === (other as any as ImageWidget)._url;
77
+ }
78
+
79
+ override toDOM(view: EditorView) {
80
+ const img = document.createElement('img');
81
+ img.setAttribute('src', this._url);
82
+ img.setAttribute('class', 'cm-image');
83
+ // Images are hidden until successfully loaded to avoid flickering effects.
84
+ img.onload = () => img.classList.add('cm-loaded-image');
85
+ return img;
86
+ }
87
+ }
@@ -3,4 +3,12 @@
3
3
  //
4
4
 
5
5
  export * from './bundle';
6
+ export * from './code';
7
+ export * from './formatting';
8
+ export * from './heading';
6
9
  export * from './highlight';
10
+ export * from './hr';
11
+ export * from './image';
12
+ export * from './link';
13
+ export * from './table';
14
+ export * from './tasklist';
@@ -4,24 +4,25 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
+ import { type Extension, RangeSetBuilder } from '@codemirror/state';
7
8
  import {
8
- type EditorState,
9
- type Extension,
10
- type RangeSet,
11
- RangeSetBuilder,
12
- StateField,
13
- type Transaction,
14
- } from '@codemirror/state';
15
- import { Decoration, EditorView, WidgetType, hoverTooltip } from '@codemirror/view';
9
+ hoverTooltip,
10
+ Decoration,
11
+ type DecorationSet,
12
+ type EditorView,
13
+ ViewPlugin,
14
+ type ViewUpdate,
15
+ WidgetType,
16
+ } from '@codemirror/view';
17
+ import { type SyntaxNode } from '@lezer/common';
16
18
 
17
19
  import { tooltipContent } from '@dxos/react-ui-theme';
18
20
 
19
- const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
20
-
21
21
  // Adapted from:
22
22
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
23
23
  // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
24
24
 
25
+ // TODO(burdon): Callback to dynamically lookup title of linked pages.
25
26
  export type LinkOptions = {
26
27
  link?: boolean;
27
28
  onHover?: (el: Element, url: string) => void;
@@ -34,11 +35,23 @@ export type LinkOptions = {
34
35
  */
35
36
  export const link = (options: LinkOptions = {}): Extension => {
36
37
  const extensions: Extension[] = [
37
- StateField.define<RangeSet<any>>({
38
- create: (state) => update(state, options),
39
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
40
- provide: (field) => EditorView.decorations.from(field),
41
- }),
38
+ ViewPlugin.fromClass(
39
+ class {
40
+ decorations: DecorationSet;
41
+ constructor(view: EditorView) {
42
+ this.decorations = buildDecorations(view, options);
43
+ }
44
+
45
+ update(update: ViewUpdate) {
46
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
47
+ this.decorations = buildDecorations(update.view, options);
48
+ }
49
+ }
50
+ },
51
+ {
52
+ decorations: (value) => value.decorations,
53
+ },
54
+ ),
42
55
  ];
43
56
 
44
57
  if (options.onHover) {
@@ -46,41 +59,27 @@ export const link = (options: LinkOptions = {}): Extension => {
46
59
  // https://codemirror.net/examples/tooltip
47
60
  // https://codemirror.net/docs/ref/#view.hoverTooltip
48
61
  // https://github.com/codemirror/view/blob/main/src/tooltip.ts
49
- hoverTooltip((view, pos) => {
50
- const { from, text } = view.state.doc.lineAt(pos);
51
- const p = pos - from;
52
-
53
- let idx = 0;
54
- let match;
55
- do {
56
- match = text.substring(idx).match(markdownLinkRegexp);
57
- if (!match) {
58
- match = undefined;
59
- break;
60
- }
61
-
62
- idx = text.indexOf(match[0]);
63
- if (p >= idx && p < idx + match[0].length) {
64
- break;
65
- }
66
-
67
- idx += match[0].length;
68
- } while (true);
62
+ hoverTooltip((view, pos, side) => {
63
+ const syntax = syntaxTree(view.state).resolveInner(pos, side);
64
+ let link = null;
65
+ for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
66
+ link = node.name === 'Link' ? node : null;
67
+ }
69
68
 
70
- if (!match) {
69
+ const url = link && link.getChild('URL');
70
+ if (!url || !link) {
71
71
  return null;
72
72
  }
73
73
 
74
- const [, , url] = match ?? [];
75
-
74
+ const urlText = view.state.sliceDoc(url.from, url.to);
76
75
  return {
77
- pos: idx + from,
78
- end: idx + from + match[0].length,
76
+ pos: link.from,
77
+ end: link.to,
79
78
  above: true,
80
79
  create: () => {
81
80
  const el = document.createElement('div');
82
81
  el.className = tooltipContent({}, 'pli-2 plb-1');
83
- options.onHover!(el, url);
82
+ options.onHover!(el, urlText);
84
83
  return { dom: el, offset: { x: 0, y: 4 } };
85
84
  },
86
85
  };
@@ -93,18 +92,17 @@ export const link = (options: LinkOptions = {}): Extension => {
93
92
 
94
93
  class LinkButton extends WidgetType {
95
94
  constructor(
96
- private readonly _pos: number,
97
95
  private readonly _url: string,
98
96
  private readonly _onAttach: LinkOptions['onRender'],
99
97
  ) {
100
98
  super();
101
99
  }
102
100
 
103
- override eq(other: LinkButton) {
104
- return this._pos === other._pos && this._url === other._url;
101
+ override eq(other: this) {
102
+ return this._url === other._url;
105
103
  }
106
104
 
107
- toDOM(view: EditorView) {
105
+ override toDOM(view: EditorView) {
108
106
  const el = document.createElement('span');
109
107
  this._onAttach!(el, this._url);
110
108
  return el;
@@ -112,15 +110,18 @@ class LinkButton extends WidgetType {
112
110
  }
113
111
 
114
112
  class LinkText extends WidgetType {
115
- constructor(private readonly _text: string, private readonly _url?: string) {
113
+ constructor(
114
+ private readonly _text: string,
115
+ private readonly _url?: string,
116
+ ) {
116
117
  super();
117
118
  }
118
119
 
119
- override eq(other: LinkText) {
120
+ override eq(other: this) {
120
121
  return this._url === other._url;
121
122
  }
122
123
 
123
- toDOM(view: EditorView) {
124
+ override toDOM(view: EditorView) {
124
125
  const link = document.createElement('a');
125
126
  link.setAttribute('class', 'cm-link');
126
127
  link.textContent = this._text;
@@ -147,46 +148,52 @@ class LinkText extends WidgetType {
147
148
  * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
148
149
  * https://codemirror.net/docs/ref/#state
149
150
  */
150
- const update = (state: EditorState, options: LinkOptions) => {
151
- const builder = new RangeSetBuilder();
151
+ const buildDecorations = (view: EditorView, options: LinkOptions): DecorationSet => {
152
+ const builder = new RangeSetBuilder<Decoration>();
153
+ const { state } = view;
152
154
  const cursor = state.selection.main.head;
153
155
 
154
- syntaxTree(state).iterate({
155
- enter: (node) => {
156
- // Check if cursor is inside text.
157
- if (node.name === 'Link') {
158
- const marks = node.node.getChildren('LinkMark');
159
- const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
160
- const urlNode = node.node.getChild('URL');
161
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
162
- if (!url) {
163
- return false;
164
- }
156
+ for (const { from, to } of view.visibleRanges) {
157
+ syntaxTree(state).iterate({
158
+ enter: (node) => {
159
+ // Check if cursor is inside text.
160
+ if (node.name === 'Link') {
161
+ const marks = node.node.getChildren('LinkMark');
162
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
163
+ const urlNode = node.node.getChild('URL');
164
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
165
+ if (!url) {
166
+ return false;
167
+ }
165
168
 
166
- if (state.readOnly || cursor < node.from || cursor > node.to) {
167
- builder.add(
168
- node.from,
169
- node.to,
170
- Decoration.replace({
171
- widget: new LinkText(text, options.link ? url : undefined),
172
- }),
173
- );
174
- }
169
+ // TODO(burdon): Don't expand if moving cursor up/down.
170
+ if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
171
+ builder.add(
172
+ node.from,
173
+ node.to,
174
+ Decoration.replace({
175
+ widget: new LinkText(text, options.link ? url : undefined),
176
+ }),
177
+ );
178
+ }
175
179
 
176
- if (options.onRender) {
177
- builder.add(
178
- node.to,
179
- node.to,
180
- Decoration.replace({
181
- widget: new LinkButton(node.from, url, options.onRender),
182
- }),
183
- );
184
- }
180
+ if (options.onRender) {
181
+ builder.add(
182
+ node.to,
183
+ node.to,
184
+ Decoration.replace({
185
+ widget: new LinkButton(url, options.onRender),
186
+ }),
187
+ );
188
+ }
185
189
 
186
- return false;
187
- }
188
- },
189
- });
190
+ return false;
191
+ }
192
+ },
193
+ from,
194
+ to,
195
+ });
196
+ }
190
197
 
191
198
  return builder.finish();
192
199
  };
@@ -81,7 +81,6 @@ const update = (state: EditorState, options: TableOptions) => {
81
81
  table.from,
82
82
  table.to,
83
83
  Decoration.replace({
84
- block: true,
85
84
  widget: new TableWidget(table),
86
85
  }),
87
86
  );
@@ -98,11 +97,14 @@ class TableWidget extends WidgetType {
98
97
  super();
99
98
  }
100
99
 
101
- override eq(other: WidgetType) {
102
- return this._table.from === (other as any as TableWidget)?._table?.from;
100
+ override eq(other: this) {
101
+ return (
102
+ this._table.header?.join() === other._table.header?.join() &&
103
+ this._table.rows?.join() === other._table.rows?.join()
104
+ );
103
105
  }
104
106
 
105
- toDOM(view: EditorView) {
107
+ override toDOM(view: EditorView) {
106
108
  const table = document.createElement('table');
107
109
 
108
110
  {
@@ -129,4 +131,8 @@ class TableWidget extends WidgetType {
129
131
 
130
132
  return table;
131
133
  }
134
+
135
+ override ignoreEvent(e: Event) {
136
+ return !/^mouse/.test(e.type);
137
+ }
132
138
  }