@dxos/react-ui-editor 0.3.10-main.5515dfe → 0.3.10-main.560f82b

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 (108) hide show
  1. package/dist/lib/browser/index.mjs +1049 -285
  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 +22 -1
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +3 -2
  9. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
  11. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  12. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  13. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
  15. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +11 -0
  17. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  19. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  20. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts +6 -0
  22. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  23. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
  24. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +4 -2
  25. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +2 -1
  27. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  28. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +6 -2
  29. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  30. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  31. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  32. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  33. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -4
  35. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
  36. package/dist/types/src/components/TextEditor/themes/default.d.ts +8 -0
  37. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  38. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +3 -0
  39. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  40. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +16 -0
  41. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  42. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  43. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
  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/automerge/translation/automerge-to-codemirror.d.ts +5 -0
  47. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
  48. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
  49. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
  50. package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
  51. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
  52. package/dist/types/src/hooks/index.d.ts +0 -1
  53. package/dist/types/src/hooks/index.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useTextModel.d.ts +3 -4
  55. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  56. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +1 -1
  57. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  58. package/dist/types/src/styles/markdown.d.ts +3 -2
  59. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  60. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  61. package/package.json +20 -19
  62. package/src/components/TextEditor/MarkdownEditor.stories.tsx +137 -31
  63. package/src/components/TextEditor/TextEditor.tsx +52 -25
  64. package/src/components/TextEditor/extensions/autocomplete.ts +9 -22
  65. package/src/components/TextEditor/extensions/basic.ts +2 -1
  66. package/src/components/TextEditor/extensions/blast.ts +375 -0
  67. package/src/components/TextEditor/extensions/comments.ts +190 -0
  68. package/src/components/TextEditor/extensions/demo.ts +69 -0
  69. package/src/components/TextEditor/extensions/experimental.tsx +1 -0
  70. package/src/components/TextEditor/extensions/image.ts +68 -0
  71. package/src/components/TextEditor/extensions/index.ts +6 -0
  72. package/src/components/TextEditor/extensions/link.ts +25 -25
  73. package/src/components/TextEditor/extensions/listener.ts +4 -3
  74. package/src/components/TextEditor/extensions/markdown/bundle.ts +18 -18
  75. package/src/components/TextEditor/extensions/markdown/highlight.ts +118 -103
  76. package/src/components/TextEditor/extensions/mention.ts +31 -0
  77. package/src/components/TextEditor/extensions/table.ts +123 -0
  78. package/src/components/TextEditor/extensions/tasklist.ts +89 -81
  79. package/src/components/TextEditor/themes/default.ts +69 -2
  80. package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +12 -9
  81. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +5 -3
  82. package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +1 -0
  83. package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/translation/automerge-to-codemirror.ts} +10 -2
  84. package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/translation/codemirror-to-automerge.ts} +7 -4
  85. package/src/hooks/automerge/translation/index.ts +6 -0
  86. package/src/hooks/index.ts +0 -1
  87. package/src/hooks/useTextModel.ts +49 -41
  88. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +1 -1
  89. package/src/styles/markdown.ts +14 -8
  90. package/src/testing/replicator.ts +6 -7
  91. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  92. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
  93. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  94. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
  95. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  96. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  97. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  98. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  99. package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
  100. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  101. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  102. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  103. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  104. package/src/hooks/useCollaboration.ts +0 -45
  105. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  106. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  107. package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  108. package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { keymap } from '@codemirror/view';
7
+
8
+ // TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
9
+
10
+ export type DemoOptions = {
11
+ delay?: number;
12
+ items?: string[];
13
+ };
14
+
15
+ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
16
+
17
+ /**
18
+ * Demo script.
19
+ * Configurable plugin that let's user cycle through pre-configured input script.
20
+ */
21
+ export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
22
+ let t: any;
23
+ let idx = 0;
24
+
25
+ return [
26
+ keymap.of([
27
+ {
28
+ // Reset.
29
+ key: "alt-meta-'",
30
+ run: (view) => {
31
+ clearTimeout(t);
32
+ idx = 0;
33
+ return true;
34
+ },
35
+ },
36
+ {
37
+ // Next prompt.
38
+ // TODO(burdon): Press 1-9 to select prompt?
39
+ key: "shift-meta-'",
40
+ run: (view) => {
41
+ clearTimeout(t);
42
+ // TODO(burdon): Add space if needed.
43
+ const text = items[idx++];
44
+ if (idx === items?.length) {
45
+ idx = 0;
46
+ }
47
+
48
+ let i = 0;
49
+ const insert = (d = 0) => {
50
+ t = setTimeout(() => {
51
+ const pos = view.state.selection.main.head;
52
+ view.dispatch({
53
+ changes: { from: pos, insert: text[i++] },
54
+ selection: { anchor: pos + 1 },
55
+ });
56
+
57
+ if (i < text.length) {
58
+ insert(Math.random() * delay * (text[i] === ' ' ? 2 : 1));
59
+ }
60
+ }, d);
61
+ };
62
+
63
+ insert();
64
+ return true;
65
+ },
66
+ },
67
+ ]),
68
+ ];
69
+ };
@@ -19,6 +19,7 @@ import {
19
19
  // TODO(burdon): Add-on: dialog.
20
20
  // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
21
  // TODO(burdon): Split view: https://codemirror.net/examples/split
22
+ // TODO(burdon): https://codemirror.net/5/demo/simplemode.html
22
23
 
23
24
  // https://codemirror.net/examples/autocompletion
24
25
  // https://codemirror.net/docs/ref/#autocomplete.autocompletion
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import {
7
+ type EditorState,
8
+ type Extension,
9
+ type RangeSet,
10
+ RangeSetBuilder,
11
+ StateField,
12
+ type Transaction,
13
+ } from '@codemirror/state';
14
+ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
15
+
16
+ export type ImageOptions = {};
17
+
18
+ export const image = (options: ImageOptions = {}): Extension => {
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
+ const update = (state: EditorState, options: ImageOptions) => {
27
+ const builder = new RangeSetBuilder();
28
+
29
+ // TODO(burdon): If not-readonly then show text.
30
+ // if (state.readOnly) {
31
+ syntaxTree(state).iterate({
32
+ enter: (node) => {
33
+ if (node.name === 'Image') {
34
+ const urlNode = node.node.getChild('URL');
35
+ if (urlNode) {
36
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
37
+ builder.add(
38
+ state.readOnly ? node.from : node.to,
39
+ node.to,
40
+ Decoration.widget({
41
+ widget: new ImageWidget(url),
42
+ }),
43
+ );
44
+ }
45
+ }
46
+ },
47
+ });
48
+ // }
49
+
50
+ return builder.finish();
51
+ };
52
+
53
+ class ImageWidget extends WidgetType {
54
+ constructor(readonly _url: string) {
55
+ super();
56
+ }
57
+
58
+ override eq(other: WidgetType) {
59
+ return this._url === (other as any as ImageWidget)?._url;
60
+ }
61
+
62
+ toDOM(view: EditorView) {
63
+ const img = document.createElement('img');
64
+ img.setAttribute('src', this._url);
65
+ img.setAttribute('class', 'cm-image');
66
+ return img;
67
+ }
68
+ }
@@ -4,8 +4,14 @@
4
4
 
5
5
  export * from './autocomplete';
6
6
  export * from './basic';
7
+ export * from './blast';
8
+ export * from './demo';
9
+ export * from './comments';
10
+ export * from './image';
7
11
  export * from './link';
8
12
  export * from './listener';
9
13
  export * from './markdown';
14
+ export * from './mention';
15
+ export * from './table';
10
16
  export * from './tasklist';
11
17
  export * from './tooltip';
@@ -18,13 +18,22 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
18
18
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
19
19
  // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
20
20
 
21
- // TODO(burdon): Adapt to make all markdown editable?
21
+ export type LinkOptions = {
22
+ link?: boolean;
23
+ onRender?: (el: Element, url: string) => void;
24
+ };
22
25
 
23
- // TODO(burdon): Research overlays.
24
- // https://stackoverflow.com/questions/26933344/codemirror-detect-and-create-links-inside-editor
25
- // https://codemirror.net/5/doc/manual.html#addOverlay
26
- // https://codemirror.net/5/demo/simplemode.html
27
- // https://codemirror.net/5/demo/search.html (Search dialog).
26
+ /**
27
+ * Creates a state field to replace AST elements with a hyperlink widget.
28
+ * https://codemirror.net/docs/ref/#state.StateField
29
+ */
30
+ export const link = (options: LinkOptions = {}): Extension => {
31
+ return StateField.define<RangeSet<any>>({
32
+ create: (state) => update(state, options),
33
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
34
+ provide: (field) => EditorView.decorations.from(field),
35
+ });
36
+ };
28
37
 
29
38
  class LinkButton extends WidgetType {
30
39
  constructor(
@@ -77,6 +86,11 @@ class LinkText extends WidgetType {
77
86
  }
78
87
  }
79
88
 
89
+ /**
90
+ * Range sets provide a data structure that can hold a collection of tagged,
91
+ * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
92
+ * https://codemirror.net/docs/ref/#state
93
+ */
80
94
  const update = (state: EditorState, options: LinkOptions) => {
81
95
  const builder = new RangeSetBuilder();
82
96
  const cursor = state.selection.main.head;
@@ -89,6 +103,9 @@ const update = (state: EditorState, options: LinkOptions) => {
89
103
 
90
104
  const urlNode = node.node.getChild('URL');
91
105
  const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
106
+ if (!url) {
107
+ return false;
108
+ }
92
109
 
93
110
  if (cursor < node.from || cursor > node.to) {
94
111
  builder.add(
@@ -109,28 +126,11 @@ const update = (state: EditorState, options: LinkOptions) => {
109
126
  }),
110
127
  );
111
128
  }
112
- }
113
129
 
114
- return true;
130
+ return false;
131
+ }
115
132
  },
116
133
  });
117
134
 
118
135
  return builder.finish();
119
136
  };
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,13 +4,13 @@
4
4
 
5
5
  import { StateField } from '@codemirror/state';
6
6
 
7
- export type TextListener = (text: string) => void;
7
+ export type ListenerOptions = { onChange: (text: string) => void };
8
8
 
9
9
  /**
10
10
  * Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
11
11
  */
12
- export const listener = (onChange: TextListener) =>
13
- StateField.define({
12
+ export const listener = ({ onChange }: ListenerOptions) => {
13
+ return StateField.define({
14
14
  create: () => null,
15
15
  update: (_value, transaction) => {
16
16
  if (transaction.docChanged && onChange) {
@@ -20,3 +20,4 @@ export const listener = (onChange: TextListener) =>
20
20
  return null;
21
21
  },
22
22
  });
23
+ };
@@ -5,15 +5,8 @@
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
6
  import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
- import {
9
- bracketMatching,
10
- defaultHighlightStyle,
11
- foldKeymap,
12
- indentOnInput,
13
- syntaxHighlighting,
14
- } from '@codemirror/language';
8
+ import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
15
9
  import { languages } from '@codemirror/language-data';
16
- import { lintKeymap } from '@codemirror/lint';
17
10
  import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
18
11
  import { EditorState, type Extension } from '@codemirror/state';
19
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
@@ -35,11 +28,16 @@ import type { ThemeMode } from '@dxos/react-ui';
35
28
  import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
36
29
 
37
30
  export type MarkdownBundleOptions = {
31
+ readonly?: boolean;
38
32
  themeMode?: ThemeMode;
39
33
  placeholder?: string;
40
34
  };
41
35
 
42
- export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
36
+ export const markdownBundle = ({
37
+ readonly,
38
+ themeMode,
39
+ placeholder: _placeholder,
40
+ }: MarkdownBundleOptions): Extension[] => {
43
41
  // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
44
42
  // https://codemirror.net/docs/ref/#codemirror.basicSetup
45
43
  return [
@@ -50,7 +48,6 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
50
48
  EditorState.allowMultipleSelections.of(true),
51
49
  EditorView.lineWrapping,
52
50
 
53
- // autocompletion(),
54
51
  crosshairCursor(),
55
52
  dropCursor(),
56
53
  drawSelection(),
@@ -62,14 +59,17 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
62
59
  indentOnInput(),
63
60
  rectangularSelection(),
64
61
 
62
+ // https://codemirror.net/docs/ref/#view.keymap
65
63
  keymap.of([
66
- ...closeBracketsKeymap,
67
- // ...completionKeymap,
64
+ // https://codemirror.net/docs/ref/#commands.defaultKeymap
68
65
  ...defaultKeymap,
69
- ...foldKeymap,
66
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
70
67
  ...historyKeymap,
71
- ...lintKeymap,
68
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
69
+ ...closeBracketsKeymap,
70
+ // https://codemirror.net/docs/ref/#search.searchKeymap
72
71
  ...searchKeymap,
72
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
73
73
  indentWithTab,
74
74
  ]),
75
75
 
@@ -94,10 +94,10 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
94
94
  ],
95
95
  }),
96
96
 
97
- // Custom styles.
98
- syntaxHighlighting(markdownHighlightStyle),
99
-
100
- // TODO(thure): All but one rule here apply to both themes; rename or refactor.
97
+ // https://github.com/codemirror/theme-one-dark
101
98
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
99
+
100
+ // Custom styles.
101
+ syntaxHighlighting(markdownHighlightStyle(readonly)),
102
102
  ].filter(Boolean) as Extension[];
103
103
  };
@@ -8,6 +8,8 @@ import { tags, styleTags, Tag } from '@lezer/highlight';
8
8
  import { type MarkdownConfig } 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,
@@ -22,6 +24,11 @@ import {
22
24
  tokens,
23
25
  } from '../../../../styles';
24
26
 
27
+ // TODO(burdon): Style table in monospace.
28
+ // https://discuss.codemirror.net/t/markdown-table-highlighting/658
29
+
30
+ // TODO(burdon): Readonly: use remark rather than hiding markup chars (which cursor can still move into).
31
+
25
32
  /**
26
33
  * Custom tags defined and processed by the GFM lezer extension.
27
34
  * https://github.com/lezer-parser/markdown
@@ -50,117 +57,125 @@ export const markdownTagsExtension: MarkdownConfig = {
50
57
  * Styling based on `lezer` parser tags.
51
58
  * https://codemirror.net/examples/styling
52
59
  * https://github.com/lezer-parser/highlight
53
- * https://lezer.codemirror.net/docs/ref/#highlight (list of tags)
60
+ * https://lezer.codemirror.net/docs/ref/#highlight.tags (list of tags)
61
+ *
62
+ * Examples:
63
+ * - https://github.com/codemirror/language/blob/main/src/highlight.ts#L194
64
+ * - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts#L115
54
65
  */
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
- },
66
+ export const markdownHighlightStyle = (readonly?: boolean) =>
67
+ HighlightStyle.define(
68
+ [
69
+ {
70
+ tag: [
71
+ tags.keyword,
72
+ tags.name,
73
+ tags.deleted,
74
+ tags.character,
75
+ tags.propertyName,
76
+ tags.macroName,
77
+ tags.color,
78
+ tags.constant(tags.name),
79
+ tags.standard(tags.name),
80
+ tags.definition(tags.name),
81
+ tags.separator,
82
+ tags.typeName,
83
+ tags.className,
84
+ tags.number,
85
+ tags.changed,
86
+ tags.annotation,
87
+ tags.modifier,
88
+ tags.self,
89
+ tags.namespace,
90
+ tags.operator,
91
+ tags.operatorKeyword,
92
+ tags.escape,
93
+ tags.regexp,
94
+ tags.special(tags.string),
95
+ tags.meta,
96
+ tags.comment,
97
+ tags.atom,
98
+ tags.bool,
99
+ tags.special(tags.variableName),
100
+ tags.processingInstruction,
101
+ tags.string,
102
+ tags.inserted,
103
+ tags.invalid,
104
+ ],
105
+ color: 'inherit !important',
106
+ },
95
107
 
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
- },
108
+ // Markdown marks.
109
+ {
110
+ tag: [
111
+ tags.meta,
112
+ tags.processingInstruction,
113
+ markdownTags.LinkLabel,
114
+ markdownTags.LinkReference,
115
+ markdownTags.ListMark,
116
+ ],
117
+ class: mark,
118
+ },
111
119
 
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] },
120
+ // Markdown marks.
121
+ {
122
+ tag: [markdownTags.CodeMark, markdownTags.HeaderMark, markdownTags.QuoteMark, markdownTags.EmphasisMark],
123
+ class: readonly ? 'hidden' : mark,
124
+ },
119
125
 
120
- // Emphasis.
121
- { tag: tags.emphasis, class: italic },
122
- { tag: tags.strong, class: bold },
123
- { tag: tags.strikethrough, class: strikethrough },
126
+ // E.g., code block language (after ```).
127
+ {
128
+ tag: [tags.function(tags.variableName), tags.labelName],
129
+ class: readonly ? 'hidden' : codeMark,
130
+ },
124
131
 
125
- // Naked URLs.
126
- {
127
- tag: [markdownTags.URL],
128
- class: inlineUrl,
129
- },
132
+ // Headings.
133
+ { tag: tags.heading1, class: heading(1, readonly) },
134
+ { tag: tags.heading2, class: heading(2, readonly) },
135
+ { tag: tags.heading3, class: heading(3, readonly) },
136
+ { tag: tags.heading4, class: heading(4, readonly) },
137
+ { tag: tags.heading5, class: heading(5, readonly) },
138
+ { tag: tags.heading6, class: heading(6, readonly) },
130
139
 
131
- // E.g., code block language (after ```).
132
- {
133
- tag: [tags.function(tags.variableName), tags.labelName],
134
- class: codeMark,
135
- },
140
+ // Emphasis.
141
+ { tag: tags.emphasis, class: italic },
142
+ { tag: tags.strong, class: bold },
143
+ { tag: tags.strikethrough, class: strikethrough },
136
144
 
137
- // 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
- {
142
- tag: [markdownTags.CodeText, markdownTags.InlineCode],
143
- class: code,
144
- },
145
+ // Naked URLs.
146
+ {
147
+ tag: [markdownTags.URL],
148
+ class: inlineUrl,
149
+ },
145
150
 
146
- {
147
- tag: [markdownTags.QuoteMark],
148
- class: blockquote,
149
- },
151
+ // NOTE: The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
152
+ // However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
153
+ // when a language is specified. In this case, the syntax highlighting extensions will colorize
154
+ // the code, but all other CSS properties will be inherited.
155
+ // IMPORTANT: Therefore, the fenced code block will use the base editor font.
156
+ {
157
+ tag: [markdownTags.CodeText, markdownTags.InlineCode],
158
+ class: code,
159
+ },
160
+
161
+ {
162
+ tag: [markdownTags.QuoteMark],
163
+ class: blockquote,
164
+ },
150
165
 
166
+ {
167
+ tag: [markdownTags.HorizontalRule],
168
+ class: mx(horizontalRule, readonly && '-indent-[100rem]'),
169
+ },
170
+
171
+ // Main content, paragraphs, etc.
172
+ // {
173
+ // tag: [tags.content],
174
+ // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
175
+ // },
176
+ ],
151
177
  {
152
- tag: [markdownTags.HorizontalRule],
153
- class: horizontalRule,
178
+ scope: markdownLanguage,
179
+ all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
154
180
  },
155
-
156
- // Main content, paragraphs, etc.
157
- // {
158
- // tag: [tags.content],
159
- // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
160
- // },
161
- ],
162
- {
163
- scope: markdownLanguage,
164
- all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
165
- },
166
- );
181
+ );
@@ -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
+ };