@dxos/react-ui-editor 0.3.10-next.c3f2d1d → 0.3.10-next.cec8a55

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 (101) hide show
  1. package/dist/lib/browser/index.mjs +1270 -372
  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 +23 -2
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
  12. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  14. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  15. package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
  16. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
  17. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +11 -7
  18. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  19. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +11 -0
  20. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  21. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
  22. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  23. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
  24. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
  25. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  26. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  27. package/dist/types/src/components/TextEditor/extensions/index.d.ts +7 -0
  28. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  29. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
  30. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +4 -2
  31. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
  32. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +10 -1
  33. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  34. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +9 -3
  35. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  37. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  39. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -1
  41. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
  42. package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -0
  43. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  44. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts +3 -0
  45. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -1
  46. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +8 -1
  47. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  48. package/dist/types/src/hooks/automerge/index.d.ts +1 -1
  49. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -1
  50. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
  51. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
  52. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
  53. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
  54. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
  55. package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
  56. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTextModel.d.ts +6 -0
  58. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  59. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -2
  60. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  61. package/dist/types/src/styles/markdown.d.ts +3 -2
  62. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  63. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  64. package/package.json +23 -20
  65. package/src/components/TextEditor/MarkdownEditor.stories.tsx +128 -34
  66. package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
  67. package/src/components/TextEditor/TextEditor.tsx +30 -9
  68. package/src/components/TextEditor/extensions/autocomplete.ts +5 -20
  69. package/src/components/TextEditor/extensions/basic.ts +6 -4
  70. package/src/components/TextEditor/extensions/blast.ts +373 -0
  71. package/src/components/TextEditor/extensions/code.ts +99 -0
  72. package/src/components/TextEditor/extensions/comments.ts +67 -67
  73. package/src/components/TextEditor/extensions/demo.ts +69 -0
  74. package/src/components/TextEditor/extensions/experimental.tsx +15 -36
  75. package/src/components/TextEditor/extensions/highlight.ts +128 -0
  76. package/src/components/TextEditor/extensions/image.ts +67 -0
  77. package/src/components/TextEditor/extensions/index.ts +7 -0
  78. package/src/components/TextEditor/extensions/link.ts +17 -25
  79. package/src/components/TextEditor/extensions/listener.ts +4 -4
  80. package/src/components/TextEditor/extensions/markdown/bundle.ts +45 -34
  81. package/src/components/TextEditor/extensions/markdown/highlight.ts +148 -108
  82. package/src/components/TextEditor/extensions/mention.ts +31 -0
  83. package/src/components/TextEditor/extensions/table.ts +119 -0
  84. package/src/components/TextEditor/extensions/tasklist.ts +42 -28
  85. package/src/components/TextEditor/themes/default.ts +98 -4
  86. package/src/hooks/automerge/PatchSemaphore.ts +12 -9
  87. package/src/hooks/automerge/automerge.stories.tsx +4 -2
  88. package/src/hooks/automerge/index.ts +1 -1
  89. package/src/hooks/automerge/plugin.ts +1 -0
  90. package/src/hooks/automerge/{amToCodemirror.ts → translation/automerge-to-codemirror.ts} +10 -2
  91. package/src/hooks/automerge/{codeMirrorToAm.ts → translation/codemirror-to-automerge.ts} +7 -4
  92. package/src/hooks/automerge/translation/index.ts +6 -0
  93. package/src/hooks/useTextModel.ts +9 -0
  94. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -2
  95. package/src/styles/markdown.ts +15 -8
  96. package/src/testing/replicator.ts +1 -0
  97. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  98. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts +0 -6
  99. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +0 -1
  100. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts +0 -7
  101. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +0 -1
@@ -3,11 +3,11 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, indentWithTab } from '@codemirror/commands';
6
+ import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
8
  import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
10
- import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
10
+ import { searchKeymap } from '@codemirror/search';
11
11
  import { EditorState, type Extension } from '@codemirror/state';
12
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
13
13
  import {
@@ -16,54 +16,50 @@ import {
16
16
  dropCursor,
17
17
  EditorView,
18
18
  highlightActiveLine,
19
- highlightActiveLineGutter,
20
- highlightSpecialChars,
21
19
  keymap,
22
20
  placeholder,
23
- rectangularSelection,
24
21
  } from '@codemirror/view';
25
22
 
26
23
  import type { ThemeMode } from '@dxos/react-ui';
27
24
 
28
- import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
25
+ import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
29
26
 
30
27
  export type MarkdownBundleOptions = {
28
+ readonly?: boolean;
31
29
  themeMode?: ThemeMode;
32
30
  placeholder?: string;
33
31
  };
34
32
 
35
- export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
36
- // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
37
- // https://codemirror.net/docs/ref/#codemirror.basicSetup
33
+ /**
34
+ * Markdown bundle.
35
+ * Refs:
36
+ * https://github.com/codemirror/basic-setup
37
+ * https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
38
+ * https://codemirror.net/docs/community
39
+ * https://codemirror.net/docs/ref/#codemirror.basicSetup
40
+ */
41
+ // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
42
+ export const markdownBundle = ({
43
+ readonly,
44
+ themeMode,
45
+ placeholder: _placeholder,
46
+ }: MarkdownBundleOptions): Extension[] => {
38
47
  return [
39
- bracketMatching(),
40
- closeBrackets(),
41
- _placeholder && placeholder(_placeholder),
42
-
43
48
  EditorState.allowMultipleSelections.of(true),
49
+ EditorState.tabSize.of(2),
44
50
  EditorView.lineWrapping,
45
51
 
52
+ customKeymap(),
53
+
54
+ bracketMatching(),
55
+ closeBrackets(),
46
56
  crosshairCursor(),
47
57
  dropCursor(),
48
58
  drawSelection(),
49
59
  highlightActiveLine(),
50
- highlightActiveLineGutter(),
51
- highlightSelectionMatches(),
52
- highlightSpecialChars(),
53
60
  history(),
54
61
  indentOnInput(),
55
- rectangularSelection(),
56
-
57
- // TODO(burdon): Review.
58
- keymap.of([
59
- ...closeBracketsKeymap,
60
- ...defaultKeymap,
61
- // ...foldKeymap,
62
- // ...historyKeymap,
63
- // ...lintKeymap,
64
- ...searchKeymap,
65
- indentWithTab,
66
- ]),
62
+ _placeholder && placeholder(_placeholder),
67
63
 
68
64
  // Main extension.
69
65
  // https://github.com/codemirror/lang-markdown
@@ -81,15 +77,30 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
81
77
 
82
78
  // Parser extensions.
83
79
  extensions: [
84
- // GFM, // TODO(burdon): Provided by default?
85
- markdownTagsExtension,
80
+ // GFM provided by default.
81
+ markdownTagsExtensions,
86
82
  ],
87
83
  }),
88
84
 
89
- // Custom styles.
90
- syntaxHighlighting(markdownHighlightStyle),
91
-
92
- // TODO(thure): All but one rule here apply to both themes; rename or refactor.
85
+ // https://github.com/codemirror/theme-one-dark
93
86
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
87
+
88
+ // Custom styles.
89
+ syntaxHighlighting(markdownHighlightStyle(readonly)),
94
90
  ].filter(Boolean) as Extension[];
95
91
  };
92
+
93
+ // https://codemirror.net/docs/ref/#view.keymap
94
+ const customKeymap = () =>
95
+ keymap.of([
96
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
97
+ indentWithTab,
98
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
99
+ ...standardKeymap,
100
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
101
+ ...historyKeymap,
102
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
103
+ ...closeBracketsKeymap,
104
+ // https://codemirror.net/docs/ref/#search.searchKeymap
105
+ ...searchKeymap,
106
+ ]);
@@ -5,9 +5,11 @@
5
5
  import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags, styleTags, Tag } from '@lezer/highlight';
8
- import { type MarkdownConfig } from '@lezer/markdown';
8
+ import { type MarkdownConfig, Table } from '@lezer/markdown';
9
9
  import get from 'lodash.get';
10
10
 
11
+ import { mx } from '@dxos/react-ui-theme';
12
+
11
13
  import {
12
14
  blockquote,
13
15
  bold,
@@ -40,128 +42,166 @@ export const markdownTags = {
40
42
  ListMark: Tag.define(),
41
43
  QuoteMark: Tag.define(),
42
44
  URL: Tag.define(),
43
- };
44
45
 
45
- export const markdownTagsExtension: MarkdownConfig = {
46
- props: [styleTags(markdownTags)],
46
+ // Custom.
47
+ TableCell: Tag.define(),
47
48
  };
48
49
 
50
+ // TODO(burdon): Customize table parser (make all content monospace).
51
+ // https://github.com/lezer-parser/markdown/blob/main/src/extension.ts
52
+ Table.defineNodes?.forEach((node: any) => {
53
+ switch (node?.name) {
54
+ case 'TableCell': {
55
+ node.style = markdownTags.TableCell;
56
+ break;
57
+ }
58
+ }
59
+ });
60
+
61
+ export const markdownTagsExtensions: MarkdownConfig[] = [
62
+ Table,
63
+ {
64
+ props: [styleTags(markdownTags)],
65
+ },
66
+ ];
67
+
49
68
  /**
50
69
  * Styling based on `lezer` parser tags.
51
70
  * https://codemirror.net/examples/styling
52
71
  * https://github.com/lezer-parser/highlight
53
- * https://lezer.codemirror.net/docs/ref/#highlight (list of tags)
72
+ * https://github.com/lezer-parser/highlight/blob/main/src/highlight.ts#L427
73
+ * https://lezer.codemirror.net/docs/ref/#highlight.tags (list of tags)
74
+ *
75
+ * Examples:
76
+ * - https://github.com/codemirror/language/blob/main/src/highlight.ts#L194
77
+ * - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts#L115
54
78
  */
55
- export const markdownHighlightStyle = HighlightStyle.define(
56
- [
57
- {
58
- tag: [
59
- tags.keyword,
60
- tags.name,
61
- tags.deleted,
62
- tags.character,
63
- tags.propertyName,
64
- tags.macroName,
65
- tags.color,
66
- tags.constant(tags.name),
67
- tags.standard(tags.name),
68
- tags.definition(tags.name),
69
- tags.separator,
70
- tags.typeName,
71
- tags.className,
72
- tags.number,
73
- tags.changed,
74
- tags.annotation,
75
- tags.modifier,
76
- tags.self,
77
- tags.namespace,
78
- tags.operator,
79
- tags.operatorKeyword,
80
- tags.escape,
81
- tags.regexp,
82
- tags.special(tags.string),
83
- tags.meta,
84
- tags.comment,
85
- tags.atom,
86
- tags.bool,
87
- tags.special(tags.variableName),
88
- tags.processingInstruction,
89
- tags.string,
90
- tags.inserted,
91
- tags.invalid,
92
- ],
93
- color: 'inherit !important',
94
- },
79
+ export const markdownHighlightStyle = (readonly?: boolean) => {
80
+ return HighlightStyle.define(
81
+ [
82
+ {
83
+ tag: [
84
+ tags.keyword,
85
+ tags.name,
86
+ tags.deleted,
87
+ tags.character,
88
+ tags.propertyName,
89
+ tags.macroName,
90
+ tags.color,
91
+ tags.constant(tags.name),
92
+ tags.standard(tags.name),
93
+ tags.definition(tags.name),
94
+ tags.separator,
95
+ tags.typeName,
96
+ tags.className,
97
+ tags.number,
98
+ tags.changed,
99
+ tags.annotation,
100
+ tags.modifier,
101
+ tags.self,
102
+ tags.namespace,
103
+ tags.operator,
104
+ tags.operatorKeyword,
105
+ tags.escape,
106
+ tags.regexp,
107
+ tags.special(tags.string),
108
+ tags.meta,
109
+ tags.comment,
110
+ tags.atom,
111
+ tags.bool,
112
+ tags.special(tags.variableName),
113
+ tags.processingInstruction,
114
+ tags.string,
115
+ tags.inserted,
116
+ tags.invalid,
117
+ ],
118
+ color: 'inherit !important',
119
+ },
95
120
 
96
- // Markdown marks.
97
- {
98
- tag: [
99
- tags.meta,
100
- tags.processingInstruction,
101
- markdownTags.CodeMark,
102
- markdownTags.EmphasisMark,
103
- markdownTags.HeaderMark,
104
- markdownTags.LinkLabel,
105
- markdownTags.LinkReference,
106
- markdownTags.ListMark,
107
- markdownTags.QuoteMark,
108
- ],
109
- class: mark,
110
- },
121
+ // Markdown marks.
122
+ {
123
+ tag: [
124
+ tags.meta,
125
+ tags.processingInstruction,
126
+ markdownTags.LinkLabel,
127
+ markdownTags.LinkReference,
128
+ markdownTags.ListMark,
129
+ ],
130
+ class: mark,
131
+ },
111
132
 
112
- // Headings.
113
- { tag: tags.heading1, class: heading[1] },
114
- { tag: tags.heading2, class: heading[2] },
115
- { tag: tags.heading3, class: heading[3] },
116
- { tag: tags.heading4, class: heading[4] },
117
- { tag: tags.heading5, class: heading[5] },
118
- { tag: tags.heading6, class: heading[6] },
133
+ // Markdown marks.
134
+ {
135
+ tag: [markdownTags.CodeMark, markdownTags.HeaderMark, markdownTags.QuoteMark, markdownTags.EmphasisMark],
136
+ class: readonly ? 'hidden' : mark,
137
+ },
119
138
 
120
- // Emphasis.
121
- { tag: tags.emphasis, class: italic },
122
- { tag: tags.strong, class: bold },
123
- { tag: tags.strikethrough, class: strikethrough },
139
+ // E.g., code block language (after ```).
140
+ {
141
+ tag: [tags.function(tags.variableName), tags.labelName],
142
+ class: readonly ? 'hidden' : codeMark,
143
+ },
124
144
 
125
- // Naked URLs.
126
- {
127
- tag: [markdownTags.URL],
128
- class: inlineUrl,
129
- },
145
+ {
146
+ tag: [tags.monospace],
147
+ class: 'font-mono',
148
+ },
130
149
 
131
- // E.g., code block language (after ```).
132
- {
133
- tag: [tags.function(tags.variableName), tags.labelName],
134
- class: codeMark,
135
- },
150
+ // Headings.
151
+ { tag: tags.heading1, class: heading(1, readonly) },
152
+ { tag: tags.heading2, class: heading(2, readonly) },
153
+ { tag: tags.heading3, class: heading(3, readonly) },
154
+ { tag: tags.heading4, class: heading(4, readonly) },
155
+ { tag: tags.heading5, class: heading(5, readonly) },
156
+ { tag: tags.heading6, class: heading(6, readonly) },
136
157
 
137
- // NOTE: The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
138
- // However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
139
- // when a language is specified. In this case, the syntax highlighting extensions will colorize
140
- // the code, but all other CSS properties will be inherited.
141
- // IMPORTANT: Therefore, the fenced code block will use the base editor font.
142
- {
143
- tag: [markdownTags.CodeText, markdownTags.InlineCode],
144
- class: code,
145
- },
158
+ // Emphasis.
159
+ { tag: tags.emphasis, class: italic },
160
+ { tag: tags.strong, class: bold },
161
+ { tag: tags.strikethrough, class: strikethrough },
146
162
 
147
- {
148
- tag: [markdownTags.QuoteMark],
149
- class: blockquote,
150
- },
163
+ // Naked URLs.
164
+ {
165
+ tag: [markdownTags.URL],
166
+ class: inlineUrl,
167
+ },
168
+
169
+ // NOTE: The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
170
+ // However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
171
+ // when a language is specified. In this case, the syntax highlighting extensions will colorize
172
+ // the code, but all other CSS properties will be inherited.
173
+ // IMPORTANT: Therefore, the fenced code block will use the base editor font.
174
+ {
175
+ tag: [markdownTags.CodeText, markdownTags.InlineCode],
176
+ class: code,
177
+ },
178
+
179
+ {
180
+ tag: [markdownTags.QuoteMark],
181
+ class: blockquote,
182
+ },
151
183
 
184
+ // TODO(burdon): Replace with extension.
185
+ {
186
+ tag: [markdownTags.HorizontalRule],
187
+ class: mx(horizontalRule, readonly && '-indent-[100rem]'),
188
+ },
189
+
190
+ // TODO(burdon): Only if being edited.
191
+ {
192
+ tag: [markdownTags.TableCell],
193
+ class: 'font-mono',
194
+ },
195
+
196
+ // Main content, paragraphs, etc.
197
+ // {
198
+ // tag: [tags.content],
199
+ // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
200
+ // },
201
+ ],
152
202
  {
153
- tag: [markdownTags.HorizontalRule],
154
- class: horizontalRule,
203
+ scope: markdownLanguage,
204
+ all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
155
205
  },
156
-
157
- // Main content, paragraphs, etc.
158
- // {
159
- // tag: [tags.content],
160
- // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
161
- // },
162
- ],
163
- {
164
- scope: markdownLanguage,
165
- all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
166
- },
167
- );
206
+ );
207
+ };
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
6
+ import type { Extension } from '@codemirror/state';
7
+
8
+ export type MentionOptions = {
9
+ onSearch: (text: string) => string[];
10
+ };
11
+
12
+ export const mention = ({ onSearch }: MentionOptions): Extension => {
13
+ return autocompletion({
14
+ closeOnBlur: false, // For debugging.
15
+ override: [
16
+ (context: CompletionContext): CompletionResult | null => {
17
+ const match = context.matchBefore(/@(\w+)?/);
18
+ if (!match || (match.from === match.to && !context.explicit)) {
19
+ return null;
20
+ }
21
+
22
+ console.log(match);
23
+
24
+ return {
25
+ from: match.from,
26
+ options: onSearch(match.text.slice(1).toLowerCase()).map((value) => ({ label: `@${value}` })),
27
+ };
28
+ },
29
+ ],
30
+ });
31
+ };
@@ -0,0 +1,119 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
7
+ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
8
+
9
+ // TODO(burdon): Snippet to create basic table.
10
+ // https://codemirror.net/docs/ref/#autocomplete.snippet
11
+
12
+ export type TableOptions = {};
13
+
14
+ /**
15
+ * GFM tables.
16
+ * https://github.github.com/gfm/#tables-extension
17
+ */
18
+ export const table = (options: TableOptions = {}) => {
19
+ return StateField.define<RangeSet<any>>({
20
+ create: (state) => update(state, options),
21
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
22
+ provide: (field) => EditorView.decorations.from(field),
23
+ });
24
+ };
25
+
26
+ type Table = {
27
+ from: number;
28
+ to: number;
29
+ header?: string[];
30
+ rows?: string[][];
31
+ };
32
+
33
+ const update = (state: EditorState, options: TableOptions) => {
34
+ const builder = new RangeSetBuilder();
35
+ const cursor = state.selection.main.head;
36
+
37
+ const tables: Table[] = [];
38
+ const getTable = () => tables[tables.length - 1];
39
+ const getRow = () => {
40
+ const table = getTable();
41
+ return table.rows?.[table.rows.length - 1];
42
+ };
43
+
44
+ // Parse table.
45
+ syntaxTree(state).iterate({
46
+ enter: (node) => {
47
+ // Check if cursor is inside text.
48
+ switch (node.name) {
49
+ case 'Table': {
50
+ tables.push({ from: node.from, to: node.to });
51
+ break;
52
+ }
53
+ case 'TableHeader': {
54
+ getTable().header = [];
55
+ break;
56
+ }
57
+ case 'TableRow': {
58
+ (getTable().rows ??= []).push([]);
59
+ break;
60
+ }
61
+ case 'TableCell': {
62
+ const row = getRow();
63
+ if (row) {
64
+ row.push(state.sliceDoc(node.from, node.to));
65
+ } else {
66
+ getTable().header?.push(state.sliceDoc(node.from, node.to));
67
+ }
68
+ break;
69
+ }
70
+ }
71
+ },
72
+ });
73
+
74
+ tables.forEach((table) => {
75
+ const hide = state.readOnly || cursor < table.from || cursor > table.to;
76
+ hide && builder.add(table.from, table.to, Decoration.replace({ widget: new TableWidget(table) }));
77
+ builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
78
+ });
79
+
80
+ return builder.finish();
81
+ };
82
+
83
+ class TableWidget extends WidgetType {
84
+ constructor(readonly _table: Table) {
85
+ super();
86
+ }
87
+
88
+ override eq(other: WidgetType) {
89
+ return this._table.from === (other as any as TableWidget)?._table?.from;
90
+ }
91
+
92
+ toDOM(view: EditorView) {
93
+ const table = document.createElement('table');
94
+
95
+ {
96
+ const header = table.appendChild(document.createElement('thead'));
97
+ const tr = header.appendChild(document.createElement('tr'));
98
+ this._table.header?.forEach((cell) => {
99
+ const th = document.createElement('th');
100
+ th.setAttribute('class', 'cm-table-head');
101
+ tr.appendChild(th).textContent = cell;
102
+ });
103
+ }
104
+
105
+ {
106
+ const body = table.appendChild(document.createElement('tbody'));
107
+ this._table.rows?.forEach((row) => {
108
+ const tr = body.appendChild(document.createElement('tr'));
109
+ row.forEach((cell) => {
110
+ const td = document.createElement('td');
111
+ td.setAttribute('class', 'cm-table-cell');
112
+ tr.appendChild(td).textContent = cell;
113
+ });
114
+ });
115
+ }
116
+
117
+ return table;
118
+ }
119
+ }
@@ -12,12 +12,20 @@ import {
12
12
  type ViewUpdate,
13
13
  } from '@codemirror/view';
14
14
 
15
+ // TODO(burdon): Reconcile with theme.
16
+ const styles = EditorView.baseTheme({
17
+ '& .cm-task-item': {
18
+ paddingLeft: '4px',
19
+ paddingRight: '8px',
20
+ },
21
+ });
22
+
15
23
  class CheckboxWidget extends WidgetType {
16
24
  constructor(
17
25
  private readonly _pos: number,
18
26
  private _checked: boolean,
19
27
  private readonly _indent: number,
20
- private readonly _onCheck: (check: boolean) => void,
28
+ private readonly _onCheck?: (check: boolean) => void,
21
29
  ) {
22
30
  super();
23
31
  }
@@ -35,10 +43,15 @@ class CheckboxWidget extends WidgetType {
35
43
  const input = wrap.appendChild(document.createElement('input'));
36
44
  input.type = 'checkbox';
37
45
  input.checked = this._checked;
38
- input.onchange = (event: Event) => {
39
- this._onCheck((event.target as any).checked);
40
- return true;
41
- };
46
+ if (!this._onCheck) {
47
+ input.setAttribute('disabled', 'true');
48
+ }
49
+ if (this._onCheck) {
50
+ input.onchange = (event: Event) => {
51
+ this._onCheck?.((event.target as any).checked);
52
+ return true;
53
+ };
54
+ }
42
55
 
43
56
  return wrap;
44
57
  }
@@ -48,15 +61,9 @@ class CheckboxWidget extends WidgetType {
48
61
  }
49
62
  }
50
63
 
51
- // TODO(burdon): Reconcile with theme.
52
- const styles = EditorView.baseTheme({
53
- '& .cm-task-item': {
54
- paddingLeft: '4px',
55
- paddingRight: '8px',
56
- },
57
- });
64
+ export type TasklistOptions = {};
58
65
 
59
- export const tasklist = () => {
66
+ export const tasklist = (options: TasklistOptions = {}) => {
60
67
  // TODO(burdon): Matcher isn't as precise as syntax tree.
61
68
  // Allows for indents to be greater than AST would allow.
62
69
  const taskMatcher = new MatchDecorator({
@@ -65,20 +72,27 @@ export const tasklist = () => {
65
72
  const indent = Math.floor(match[1].length / 2);
66
73
  const checked = match[2] === 'x' || match[2] === 'X';
67
74
  return Decoration.replace({
68
- widget: new CheckboxWidget(pos, checked, indent, (checked) => {
69
- const idx = pos + match[0].indexOf('[') + 1;
70
- view.dispatch({
71
- changes: {
72
- from: idx,
73
- to: idx + 1,
74
- insert: checked ? 'x' : ' ',
75
- },
76
- // TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
77
- selection: {
78
- anchor: idx + 3,
79
- },
80
- });
81
- }),
75
+ widget: new CheckboxWidget(
76
+ pos,
77
+ checked,
78
+ indent,
79
+ view.state.readOnly
80
+ ? undefined
81
+ : (checked) => {
82
+ const idx = pos + match[0].indexOf('[') + 1;
83
+ view.dispatch({
84
+ changes: {
85
+ from: idx,
86
+ to: idx + 1,
87
+ insert: checked ? 'x' : ' ',
88
+ },
89
+ // TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
90
+ selection: {
91
+ anchor: idx + 3,
92
+ },
93
+ });
94
+ },
95
+ ),
82
96
  });
83
97
  },
84
98
  });
@@ -95,7 +109,7 @@ export const tasklist = () => {
95
109
  }
96
110
  },
97
111
  {
98
- decorations: (instance) => instance.tasks,
112
+ decorations: (value) => value.tasks,
99
113
  provide: (plugin) =>
100
114
  EditorView.atomicRanges.of((view) => {
101
115
  return view.plugin(plugin)?.tasks || Decoration.none;