@dxos/react-ui-editor 0.3.10-main.16895f7 → 0.3.10-main.375af01

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 (121) hide show
  1. package/dist/lib/browser/index.mjs +728 -732
  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 +35 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +4 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
  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 +9 -0
  11. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +1 -1
  13. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
  15. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/index.d.ts +5 -2
  17. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  18. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  19. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +7 -0
  21. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  22. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  23. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +31 -0
  24. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  25. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  26. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  27. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +5 -0
  28. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  29. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  30. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  31. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  32. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  33. package/dist/types/src/components/TextEditor/themes/default.d.ts +17 -0
  34. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  35. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  36. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  37. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
  38. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
  39. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  40. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +1 -0
  41. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  42. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +1 -0
  43. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  44. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  45. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  46. package/dist/types/src/hooks/index.d.ts +0 -1
  47. package/dist/types/src/hooks/index.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  49. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  50. package/dist/types/src/styles/markdown.d.ts +12 -10
  51. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  52. package/package.json +20 -32
  53. package/src/components/TextEditor/MarkdownEditor.stories.tsx +242 -0
  54. package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
  55. package/src/components/TextEditor/TextEditor.tsx +59 -45
  56. package/src/components/TextEditor/extensions/autocomplete.ts +72 -0
  57. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
  58. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +17 -2
  59. package/src/components/TextEditor/extensions/index.ts +5 -2
  60. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +68 -31
  61. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +3 -3
  62. package/src/components/TextEditor/extensions/markdown/bundle.ts +15 -11
  63. package/src/components/TextEditor/extensions/markdown/highlight.ts +166 -0
  64. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  65. package/src/components/TextEditor/extensions/tasklist.ts +113 -0
  66. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
  67. package/src/components/TextEditor/index.ts +3 -0
  68. package/src/components/TextEditor/themes/default.ts +170 -0
  69. package/src/components/TextEditor/themes/index.ts +5 -0
  70. package/src/components/TextEditor/yjs.stories.tsx +1 -0
  71. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
  72. package/src/hooks/index.ts +0 -1
  73. package/src/hooks/useTextModel.ts +50 -40
  74. package/src/styles/markdown.ts +26 -24
  75. package/src/testing/replicator.ts +2 -2
  76. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  77. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  78. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  79. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  80. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  81. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  82. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  83. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  84. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  85. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  86. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  87. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  88. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  89. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  91. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  92. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  93. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  94. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  95. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  96. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  97. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  98. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  99. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  100. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  101. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  102. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  103. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  104. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  105. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  106. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  107. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  108. package/src/hooks/useCollaboration.ts +0 -45
  109. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +0 -0
  110. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.d.ts +0 -0
  111. /package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
  112. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.d.ts +0 -0
  113. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  114. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +0 -0
  115. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  116. /package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +0 -0
  117. /package/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.ts +0 -0
  118. /package/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.ts +0 -0
  119. /package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  120. /package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +0 -0
  121. /package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +0 -0
@@ -7,23 +7,24 @@ import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
- import get from 'lodash.get';
11
10
  import React, {
11
+ type ComponentProps,
12
12
  type KeyboardEvent,
13
13
  forwardRef,
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
17
  useState,
18
- type ComponentProps,
19
18
  } from 'react';
20
19
 
21
- import { isDocAccessor } from '@dxos/echo-schema';
20
+ import { generateName } from '@dxos/display-name';
22
21
  import { useThemeContext } from '@dxos/react-ui';
22
+ import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
23
 
24
- import { basicBundle, basicTheme, markdownBundle, markdownTheme } from './extensions';
25
- import { type EditorModel, useCollaboration } from '../../hooks';
26
- import type { ThemeStyles } from '../../styles';
24
+ import { basicBundle, markdownBundle } from './extensions';
25
+ import { defaultTheme, textTheme } from './themes';
26
+ import { type EditorModel } from '../../hooks';
27
+ import { type ThemeStyles } from '../../styles';
27
28
 
28
29
  export const EditorModes = ['default', 'vim'] as const;
29
30
  export type EditorMode = (typeof EditorModes)[number];
@@ -37,7 +38,7 @@ export type CursorInfo = {
37
38
  };
38
39
 
39
40
  export type TextEditorRef = {
40
- editor: HTMLDivElement | null;
41
+ root: HTMLDivElement | null;
41
42
  state?: EditorState;
42
43
  view?: EditorView;
43
44
  };
@@ -65,66 +66,65 @@ export type TextEditorProps = {
65
66
  * NOTE: Rather than adding properties, try to create extensions that can be reused.
66
67
  */
67
68
  export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
68
- ({ model, extensions = [], slots, editorMode }, forwardedRef) => {
69
+ ({ model, extensions = [], slots = defaultSlots, editorMode }, forwardedRef) => {
69
70
  const { themeMode } = useThemeContext();
70
71
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
71
72
 
72
- const [parent, setParent] = useState<HTMLDivElement | null>(null);
73
+ // TODO(burdon): Factor out?
74
+ const { awareness, peer } = model;
75
+ useEffect(() => {
76
+ if (awareness && peer) {
77
+ awareness.setLocalStateField('user', {
78
+ name: peer.name ?? generateName(peer.id),
79
+ color: getColorForValue({ value: peer.id, type: 'color' }),
80
+ colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
81
+ });
82
+ }
83
+ }, [awareness, peer, themeMode]);
84
+
85
+ const [root, setRoot] = useState<HTMLDivElement | null>(null);
73
86
  const [state, setState] = useState<EditorState>();
74
87
  const [view, setView] = useState<EditorView>();
75
- useImperativeHandle(
76
- forwardedRef,
77
- () => ({
78
- editor: parent,
79
- state,
80
- view,
81
- }),
82
- [view, state, parent],
83
- );
84
-
85
- // TODO(burdon): Pass in extension?
86
- const collaboration = useCollaboration(model, themeMode);
88
+ useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
87
89
 
88
90
  useEffect(() => {
89
- if (!parent) {
91
+ if (!root) {
90
92
  return;
91
93
  }
92
94
 
93
- const initialText = isDocAccessor(model.content)
94
- ? get(model.content.handle.docSync(), model.content.path)
95
- : model.content?.toString();
96
-
97
95
  const state = EditorState.create({
98
- doc: initialText,
96
+ doc: model.text(),
99
97
  extensions: [
100
98
  // TODO(burdon): Factor out VIM mode?
101
99
  editorMode === 'vim' && vim(),
102
100
 
103
- // Replication.
104
- collaboration,
105
-
106
101
  // Theme.
107
- slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
102
+ EditorView.baseTheme(defaultTheme),
103
+ EditorView.theme(slots?.editor?.theme ?? {}),
104
+ // TODO(burdon): themeMode doesn't change in storybooks.
105
+ EditorView.darkTheme.of(themeMode === 'dark'),
106
+
107
+ // Storage and replication.
108
+ model.extension,
108
109
 
109
110
  // Custom.
110
111
  ...extensions,
111
112
  ].filter(Boolean) as Extension[],
112
113
  });
113
114
 
114
- setState(state);
115
-
116
115
  // NOTE: This repaints the editor.
117
116
  // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
118
117
  // Ideally this should not be hit except when changing between text objects.
119
118
  view?.destroy();
120
- setView(new EditorView({ state, parent }));
119
+ setView(new EditorView({ state, parent: root }));
120
+ setState(state);
121
121
 
122
122
  return () => {
123
123
  view?.destroy();
124
124
  setView(undefined);
125
125
  setState(undefined);
126
126
  };
127
- }, [parent, model.content, themeMode, editorMode]);
127
+ }, [root, model, themeMode, editorMode]);
128
128
 
129
129
  const handleKeyUp = useCallback(
130
130
  (event: KeyboardEvent) => {
@@ -136,7 +136,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
136
136
  }
137
137
 
138
138
  case 'Escape': {
139
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && parent?.focus();
139
+ editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
140
140
  break;
141
141
  }
142
142
  }
@@ -147,7 +147,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
147
147
  return (
148
148
  <div
149
149
  key={model.id}
150
- ref={setParent}
150
+ ref={setRoot}
151
151
  tabIndex={0}
152
152
  {...slots?.root}
153
153
  {...(editorMode !== 'vim' && tabsterDOMAttribute)}
@@ -157,15 +157,16 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
157
157
  },
158
158
  );
159
159
 
160
+ // TODO(burdon): Set default text theme?
160
161
  export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
161
- ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
162
+ ({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
162
163
  const { themeMode } = useThemeContext();
163
- const extensions = [...(_extensions ?? []), basicBundle({ themeMode, placeholder: slots?.editor?.placeholder })];
164
+ const slots = defaultsDeep({}, _slots, defaultTextSlots);
164
165
  return (
165
166
  <BaseTextEditor
166
167
  ref={forwardedRef}
167
- extensions={extensions}
168
- slots={defaultsDeep({}, slots, { editor: { theme: basicTheme } })}
168
+ extensions={[basicBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
169
+ slots={slots}
169
170
  {...props}
170
171
  />
171
172
  );
@@ -173,16 +174,29 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
173
174
  );
174
175
 
175
176
  export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
176
- ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
177
+ ({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
177
178
  const { themeMode } = useThemeContext();
178
- const extensions = [...(_extensions ?? []), markdownBundle({ themeMode })];
179
+ const slots = defaultsDeep({}, _slots, defaultSlots);
179
180
  return (
180
181
  <BaseTextEditor
181
182
  ref={forwardedRef}
182
- extensions={extensions}
183
- slots={defaultsDeep({}, slots, { editor: { theme: markdownTheme } })}
183
+ extensions={[markdownBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
184
+ slots={slots}
184
185
  {...props}
185
186
  />
186
187
  );
187
188
  },
188
189
  );
190
+
191
+ export const defaultSlots: TextEditorSlots = {
192
+ root: {
193
+ className: mx('p-2', inputSurface),
194
+ },
195
+ };
196
+
197
+ export const defaultTextSlots: TextEditorSlots = {
198
+ ...defaultSlots,
199
+ editor: {
200
+ theme: textTheme,
201
+ },
202
+ };
@@ -0,0 +1,72 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ // TODO(burdon): Automcomplete: https://codemirror.net/5/doc/manual.html#addon_runmode
6
+ // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
7
+ // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
8
+ // https://codemirror.net/5/doc/manual.html#addon_runmode
9
+ // TODO(burdon): Add-on: dialog.
10
+ // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
11
+ // TODO(burdon): Split view: https://codemirror.net/examples/split
12
+
13
+ // https://codemirror.net/examples/autocompletion
14
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
15
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
16
+
17
+ import {
18
+ autocompletion,
19
+ type Completion,
20
+ type CompletionContext,
21
+ completionKeymap,
22
+ type CompletionResult,
23
+ } from '@codemirror/autocomplete';
24
+ import { keymap } from '@codemirror/view';
25
+
26
+ export type AutocompleteOptions = {
27
+ getOptions: (text: string) => Completion[];
28
+ };
29
+
30
+ /**
31
+ * Autocomplete extension.
32
+ */
33
+ export const autocomplete = ({ getOptions }: AutocompleteOptions) => {
34
+ return [
35
+ // https://codemirror.net/docs/ref/#view.keymap
36
+ // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
37
+ keymap.of(completionKeymap),
38
+
39
+ // https://codemirror.net/examples/autocompletion
40
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
41
+ autocompletion({
42
+ // TODO(burdon): Set custom keymap.
43
+ // defaultKeymap: false,
44
+
45
+ // Don't start unless key press.
46
+ activateOnTyping: false,
47
+
48
+ // TODO(burdon): Option to create new page?
49
+ // TODO(burdon): Optional decoration via addToOptions
50
+ override: [
51
+ (context: CompletionContext): CompletionResult | null => {
52
+ const word = context.matchBefore(/\w*/);
53
+ if (!word || (word.from === word.to && !context.explicit)) {
54
+ return null;
55
+ }
56
+
57
+ // TODO(burdon): Option to convert to links?
58
+ return {
59
+ from: word.from,
60
+ options: getOptions(word.text.toLowerCase()),
61
+ // options: [
62
+ // { label: 'apple', type: 'keyword' },
63
+ // { label: 'amazon', type: 'keyword' },
64
+ // { label: 'hello', type: 'variable', info: '(World)' },
65
+ // { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
66
+ // ],
67
+ };
68
+ },
69
+ ],
70
+ }),
71
+ ];
72
+ };
@@ -17,12 +17,14 @@ export type BasicBundleOptions = {
17
17
 
18
18
  export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
19
19
  [
20
+ // TODO(burdon): Compare with minimalSetup.
21
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
20
22
  bracketMatching(),
21
23
  closeBrackets(),
22
- _placeholder && placeholder(_placeholder),
23
-
24
24
  EditorView.lineWrapping,
25
25
 
26
+ _placeholder && placeholder(_placeholder),
27
+
26
28
  // TODO(burdon): Is this required?
27
29
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
28
30
  ].filter(Boolean) as Extension[];
@@ -13,12 +13,27 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
+ // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
17
+ // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
18
+ // https://codemirror.net/5/doc/manual.html#addon_runmode
19
+ // TODO(burdon): Add-on: dialog.
20
+ // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
+ // TODO(burdon): Split view: https://codemirror.net/examples/split
22
+
23
+ // https://codemirror.net/examples/autocompletion
24
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
25
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
26
+
27
+ // TODO(burdon): Hint to customize?
28
+ // https://codemirror.net/examples/autocompletion
29
+
16
30
  // Notes: Can't edit widget content (or cursor nav through them).
17
31
  // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
18
32
  // Widgets intentionally always get set to contenteditable=false,
19
33
  // or they would become part of CodeMirror’s editable content element.
20
34
  // You should be able to introduce new contenteditable=true child elements inside of them.
21
35
 
36
+ // TODO(burdon): URL match?
22
37
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
23
38
 
24
39
  /**
@@ -102,7 +117,7 @@ export function hyperLinkExtension() {
102
117
  );
103
118
  }
104
119
 
105
- export const hyperlinkStyle = EditorView.baseTheme({
120
+ export const styles = EditorView.baseTheme({
106
121
  '.cm-hyperlink-label': {
107
122
  cursor: 'pointer',
108
123
  textDecoration: 'underline',
@@ -116,4 +131,4 @@ export const hyperlinkStyle = EditorView.baseTheme({
116
131
  },
117
132
  });
118
133
 
119
- export const hyperlinkWidget: Extension = [hyperLinkExtension(), hyperlinkStyle];
134
+ export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
@@ -2,7 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './autocomplete';
5
6
  export * from './basic';
6
- export * from './change';
7
- export * from './hyperlink';
7
+ export * from './link';
8
+ export * from './listener';
8
9
  export * from './markdown';
10
+ export * from './tasklist';
11
+ export * from './tooltip';
@@ -4,7 +4,14 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
- import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
7
+ import {
8
+ type EditorState,
9
+ type Extension,
10
+ type RangeSet,
11
+ RangeSetBuilder,
12
+ StateField,
13
+ type Transaction,
14
+ } from '@codemirror/state';
8
15
  import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
16
 
10
17
  // Adapted from:
@@ -19,13 +26,33 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
19
26
  // https://codemirror.net/5/demo/simplemode.html
20
27
  // https://codemirror.net/5/demo/search.html (Search dialog).
21
28
 
22
- class Link extends WidgetType {
23
- constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
29
+ class LinkButton extends WidgetType {
30
+ constructor(
31
+ private readonly _pos: number,
32
+ private readonly _url: string,
33
+ private readonly _onAttach: LinkOptions['onRender'],
34
+ ) {
24
35
  super();
25
36
  }
26
37
 
27
- override eq(other: any) {
28
- return this._text === other.text && this._url === other.url;
38
+ override eq(other: LinkButton) {
39
+ return this._pos === other._pos && this._url === other._url;
40
+ }
41
+
42
+ toDOM(view: EditorView) {
43
+ const el = document.createElement('span');
44
+ this._onAttach!(el, this._url);
45
+ return el;
46
+ }
47
+ }
48
+
49
+ class LinkText extends WidgetType {
50
+ constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
51
+ super();
52
+ }
53
+
54
+ override eq(other: LinkText) {
55
+ return this._pos === other._pos && this._url === other._url;
29
56
  }
30
57
 
31
58
  toDOM(view: EditorView) {
@@ -33,13 +60,14 @@ class Link extends WidgetType {
33
60
  link.setAttribute('class', 'cm-link');
34
61
  link.textContent = this._text;
35
62
  if (this._url) {
63
+ link.setAttribute('rel', 'noreferrer');
36
64
  link.setAttribute('target', '_blank');
37
65
  link.href = this._url;
38
66
  } else {
39
67
  link.onclick = () => {
40
68
  view.dispatch({
41
69
  selection: {
42
- anchor: this._anchor,
70
+ anchor: this._pos,
43
71
  },
44
72
  });
45
73
  };
@@ -49,45 +77,37 @@ class Link extends WidgetType {
49
77
  }
50
78
  }
51
79
 
52
- type HyperlinkDecorationConfig = {
53
- link?: boolean;
54
- };
55
-
56
- /**
57
- * Creates a state field to replace AST elements with a hyperlink widget.
58
- * https://codemirror.net/docs/ref/#state.StateField
59
- */
60
- export const hyperlinkDecoration = ({ link }: HyperlinkDecorationConfig = {}) => {
61
- return StateField.define<RangeSet<any>>({
62
- create: (state) => update(state, link),
63
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, link),
64
- provide: (field) => EditorView.decorations.from(field),
65
- });
66
- };
67
-
68
- const update = (state: EditorState, link?: boolean) => {
80
+ const update = (state: EditorState, options: LinkOptions) => {
69
81
  const builder = new RangeSetBuilder();
70
82
  const cursor = state.selection.main.head;
71
83
  syntaxTree(state).iterate({
72
84
  enter: (node) => {
73
85
  // Check if cursor is inside text.
74
- if (cursor < node.from || cursor > node.to) {
75
- if (node.name === 'Link') {
76
- const marks = node.node.getChildren('LinkMark');
77
- const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
86
+ if (node.name === 'Link') {
87
+ const marks = node.node.getChildren('LinkMark');
88
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
78
89
 
79
- const urlNode = node.node.getChild('URL');
80
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
90
+ const urlNode = node.node.getChild('URL');
91
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
81
92
 
93
+ if (cursor < node.from || cursor > node.to) {
82
94
  builder.add(
83
95
  node.from,
84
96
  node.to,
85
97
  Decoration.replace({
86
- widget: new Link(node.from, text, link ? url : undefined),
98
+ widget: new LinkText(node.from, text, options.link ? url : undefined),
87
99
  }),
88
100
  );
101
+ }
89
102
 
90
- return false;
103
+ if (options.onRender) {
104
+ builder.add(
105
+ node.to,
106
+ node.to,
107
+ Decoration.replace({
108
+ widget: new LinkButton(node.from, url, options.onRender),
109
+ }),
110
+ );
91
111
  }
92
112
  }
93
113
 
@@ -97,3 +117,20 @@ const update = (state: EditorState, link?: boolean) => {
97
117
 
98
118
  return builder.finish();
99
119
  };
120
+
121
+ export type LinkOptions = {
122
+ link?: boolean;
123
+ onRender?: (el: Element, url: string) => void;
124
+ };
125
+
126
+ /**
127
+ * Creates a state field to replace AST elements with a hyperlink widget.
128
+ * https://codemirror.net/docs/ref/#state.StateField
129
+ */
130
+ export const link = (options: LinkOptions = {}): Extension => {
131
+ return StateField.define<RangeSet<any>>({
132
+ create: (state) => update(state, options),
133
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
134
+ provide: (field) => EditorView.decorations.from(field),
135
+ });
136
+ };
@@ -4,12 +4,12 @@
4
4
 
5
5
  import { StateField } from '@codemirror/state';
6
6
 
7
+ export type TextListener = (text: string) => void;
8
+
7
9
  /**
8
10
  * Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
9
- * @param onChange
10
11
  */
11
- // TODO(burdon): Name?
12
- export const onChangeExtension = (onChange: (text: string) => void) =>
12
+ export const listener = (onChange: TextListener) =>
13
13
  StateField.define({
14
14
  create: () => null,
15
15
  update: (_value, transaction) => {
@@ -29,12 +29,10 @@ import {
29
29
  placeholder,
30
30
  rectangularSelection,
31
31
  } from '@codemirror/view';
32
- // import { GFM } from '@lezer/markdown';
33
32
 
34
33
  import type { ThemeMode } from '@dxos/react-ui';
35
34
 
36
- import { markdownTagsExtension } from './tags';
37
- import { markdownHighlightStyle } from './theme';
35
+ import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
38
36
 
39
37
  export type MarkdownBundleOptions = {
40
38
  themeMode?: ThemeMode;
@@ -77,23 +75,29 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
77
75
 
78
76
  // Main extension.
79
77
  // https://github.com/codemirror/lang-markdown
78
+ // https://codemirror.net/5/mode/markdown/index.html (demo).
80
79
  markdown({
80
+ // GRM by default (vs strict CommonMark):
81
+ // Table, TaskList, Strikethrough, and Autolink.
82
+ // NOTE: This extends the parser; it doesn't affect rendering.
83
+ // https://github.github.com/gfm
84
+ // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
81
85
  base: markdownLanguage,
86
+
87
+ // Languages for syntax highlighting fenced code blocks.
82
88
  codeLanguages: languages,
83
- extensions: [
84
- // TODO(burdon): This seems to upgrade the parser.
85
- // GitHub flavored markdown bundle: Table, TaskList, Strikethrough, and Autolink.
86
- // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
87
- // https://github.github.com/gfm
88
- // GFM,
89
89
 
90
- // Custom styling.
90
+ // Parser extensions.
91
+ extensions: [
92
+ // GFM, // TODO(burdon): Provided by default?
91
93
  markdownTagsExtension,
92
94
  ],
93
95
  }),
94
96
 
95
- // TODO(thure): All but one rule here apply to both themes; rename or refactor.
97
+ // Custom styles.
96
98
  syntaxHighlighting(markdownHighlightStyle),
99
+
100
+ // TODO(thure): All but one rule here apply to both themes; rename or refactor.
97
101
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
98
102
  ].filter(Boolean) as Extension[];
99
103
  };