@dxos/react-ui-editor 0.3.10-main.5c4230b → 0.3.10-main.5e11230

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 (148) hide show
  1. package/dist/lib/browser/index.mjs +1644 -791
  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 +56 -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 +7 -3
  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 +10 -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} +3 -2
  13. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  15. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
  17. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +11 -0
  19. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
  21. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
  22. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  23. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/index.d.ts +11 -2
  25. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  27. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  28. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
  29. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  30. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +9 -1
  31. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  32. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +37 -0
  33. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  35. package/dist/types/src/components/TextEditor/extensions/markdown/index.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 +3 -0
  41. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  42. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  43. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  44. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  45. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TextEditor/themes/default.d.ts +25 -0
  47. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  48. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  49. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  50. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +3 -0
  51. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  52. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +16 -0
  53. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  54. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  55. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
  56. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  57. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  58. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
  59. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
  60. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
  61. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
  62. package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
  63. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
  64. package/dist/types/src/hooks/index.d.ts +0 -1
  65. package/dist/types/src/hooks/index.d.ts.map +1 -1
  66. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  67. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  68. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -1
  69. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  70. package/dist/types/src/styles/markdown.d.ts +14 -11
  71. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  72. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  73. package/package.json +21 -32
  74. package/src/components/TextEditor/MarkdownEditor.stories.tsx +350 -0
  75. package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
  76. package/src/components/TextEditor/TextEditor.tsx +71 -45
  77. package/src/components/TextEditor/extensions/autocomplete.ts +59 -0
  78. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +6 -3
  79. package/src/components/TextEditor/extensions/blast.ts +387 -0
  80. package/src/components/TextEditor/extensions/comments.ts +172 -0
  81. package/src/components/TextEditor/extensions/demo.ts +69 -0
  82. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +15 -20
  83. package/src/components/TextEditor/extensions/image.ts +67 -0
  84. package/src/components/TextEditor/extensions/index.ts +11 -2
  85. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +75 -38
  86. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +5 -4
  87. package/src/components/TextEditor/extensions/markdown/bundle.ts +56 -59
  88. package/src/components/TextEditor/extensions/markdown/highlight.ts +207 -0
  89. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  90. package/src/components/TextEditor/extensions/mention.ts +31 -0
  91. package/src/components/TextEditor/extensions/table.ts +122 -0
  92. package/src/components/TextEditor/extensions/tasklist.ts +121 -0
  93. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
  94. package/src/components/TextEditor/index.ts +3 -0
  95. package/src/components/TextEditor/themes/default.ts +274 -0
  96. package/src/components/TextEditor/themes/index.ts +5 -0
  97. package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +12 -9
  98. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +5 -3
  99. package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +1 -0
  100. package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/translation/automerge-to-codemirror.ts} +10 -2
  101. package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/translation/codemirror-to-automerge.ts} +7 -4
  102. package/src/hooks/automerge/translation/index.ts +6 -0
  103. package/src/hooks/index.ts +0 -1
  104. package/src/hooks/useTextModel.ts +52 -39
  105. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -1
  106. package/src/styles/markdown.ts +32 -24
  107. package/src/testing/replicator.ts +6 -6
  108. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  109. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
  110. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  111. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
  112. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  113. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  114. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  115. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  116. package/dist/types/src/automerge/automerge.stories.d.ts +0 -9
  117. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  118. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  119. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  120. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  121. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  122. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  123. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  124. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  125. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  126. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  127. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  128. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  129. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  130. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  131. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  133. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  135. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  137. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  138. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  139. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  140. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  141. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  142. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  143. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  144. package/src/hooks/useCollaboration.ts +0 -45
  145. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  146. package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  147. package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  148. package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
@@ -13,11 +13,20 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
- // Notes: Can't edit widget content (or cursor nav through them).
17
- // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
18
- // Widgets intentionally always get set to contenteditable=false,
19
- // or they would become part of CodeMirror’s editable content element.
20
- // You should be able to introduce new contenteditable=true child elements inside of them.
16
+ // TODO(burdon): Reconcile with theme.
17
+ export const styles = EditorView.baseTheme({
18
+ '.cm-hyperlink-label': {
19
+ cursor: 'pointer',
20
+ textDecoration: 'underline',
21
+ },
22
+ '.cm-hyperlink-bracket': {
23
+ opacity: 0.2,
24
+ },
25
+ '.cm-hyperlink-url': {
26
+ opacity: 0.5,
27
+ fontFamily: 'monospace',
28
+ },
29
+ });
21
30
 
22
31
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
23
32
 
@@ -102,18 +111,4 @@ export function hyperLinkExtension() {
102
111
  );
103
112
  }
104
113
 
105
- export const hyperlinkStyle = EditorView.baseTheme({
106
- '.cm-hyperlink-label': {
107
- cursor: 'pointer',
108
- textDecoration: 'underline',
109
- },
110
- '.cm-hyperlink-bracket': {
111
- opacity: 0.2,
112
- },
113
- '.cm-hyperlink-url': {
114
- opacity: 0.5,
115
- fontFamily: 'monospace',
116
- },
117
- });
118
-
119
- export const hyperlinkWidget: Extension = [hyperLinkExtension(), hyperlinkStyle];
114
+ export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
@@ -0,0 +1,67 @@
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
+ const cursor = state.selection.main.head;
29
+
30
+ syntaxTree(state).iterate({
31
+ enter: (node) => {
32
+ if (node.name === 'Image') {
33
+ const urlNode = node.node.getChild('URL');
34
+ if (urlNode) {
35
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
36
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
37
+ builder.add(
38
+ hide ? node.from : node.to,
39
+ node.to,
40
+ Decoration.replace({
41
+ widget: new ImageWidget(url),
42
+ }),
43
+ );
44
+ }
45
+ }
46
+ },
47
+ });
48
+
49
+ return builder.finish();
50
+ };
51
+
52
+ class ImageWidget extends WidgetType {
53
+ constructor(readonly _url: string) {
54
+ super();
55
+ }
56
+
57
+ override eq(other: WidgetType) {
58
+ return this._url === (other as any as ImageWidget)?._url;
59
+ }
60
+
61
+ toDOM(view: EditorView) {
62
+ const img = document.createElement('img');
63
+ img.setAttribute('src', this._url);
64
+ img.setAttribute('class', 'cm-image');
65
+ return img;
66
+ }
67
+ }
@@ -2,7 +2,16 @@
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 './blast';
8
+ export * from './demo';
9
+ export * from './comments';
10
+ export * from './image';
11
+ export * from './link';
12
+ export * from './listener';
8
13
  export * from './markdown';
14
+ export * from './mention';
15
+ export * from './table';
16
+ export * from './tasklist';
17
+ export * from './tooltip';
@@ -4,28 +4,64 @@
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:
11
18
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
12
19
  // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
13
20
 
14
- // TODO(burdon): Adapt to make all markdown editable?
21
+ export type LinkOptions = {
22
+ link?: boolean;
23
+ onRender?: (el: Element, url: string) => void;
24
+ };
25
+
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
+ };
15
37
 
16
- // TODO(burdon): Research overlays.
17
- // https://stackoverflow.com/questions/26933344/codemirror-detect-and-create-links-inside-editor
18
- // https://codemirror.net/5/doc/manual.html#addOverlay
19
- // https://codemirror.net/5/demo/simplemode.html
20
- // https://codemirror.net/5/demo/search.html (Search dialog).
38
+ class LinkButton extends WidgetType {
39
+ constructor(
40
+ private readonly _pos: number,
41
+ private readonly _url: string,
42
+ private readonly _onAttach: LinkOptions['onRender'],
43
+ ) {
44
+ super();
45
+ }
46
+
47
+ override eq(other: LinkButton) {
48
+ return this._pos === other._pos && this._url === other._url;
49
+ }
21
50
 
22
- class Link extends WidgetType {
23
- constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
51
+ toDOM(view: EditorView) {
52
+ const el = document.createElement('span');
53
+ this._onAttach!(el, this._url);
54
+ return el;
55
+ }
56
+ }
57
+
58
+ class LinkText extends WidgetType {
59
+ constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
24
60
  super();
25
61
  }
26
62
 
27
- override eq(other: any) {
28
- return this._text === other.text && this._url === other.url;
63
+ override eq(other: LinkText) {
64
+ return this._pos === other._pos && this._url === other._url;
29
65
  }
30
66
 
31
67
  toDOM(view: EditorView) {
@@ -33,13 +69,14 @@ class Link extends WidgetType {
33
69
  link.setAttribute('class', 'cm-link');
34
70
  link.textContent = this._text;
35
71
  if (this._url) {
72
+ link.setAttribute('rel', 'noreferrer');
36
73
  link.setAttribute('target', '_blank');
37
74
  link.href = this._url;
38
75
  } else {
39
76
  link.onclick = () => {
40
77
  view.dispatch({
41
78
  selection: {
42
- anchor: this._anchor,
79
+ anchor: this._pos,
43
80
  },
44
81
  });
45
82
  };
@@ -49,49 +86,49 @@ class Link extends WidgetType {
49
86
  }
50
87
  }
51
88
 
52
- type HyperlinkDecorationConfig = {
53
- link?: boolean;
54
- };
55
-
56
89
  /**
57
- * Creates a state field to replace AST elements with a hyperlink widget.
58
- * https://codemirror.net/docs/ref/#state.StateField
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
59
93
  */
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) => {
94
+ const update = (state: EditorState, options: LinkOptions) => {
69
95
  const builder = new RangeSetBuilder();
70
96
  const cursor = state.selection.main.head;
97
+
71
98
  syntaxTree(state).iterate({
72
99
  enter: (node) => {
73
100
  // 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) : '';
78
-
79
- const urlNode = node.node.getChild('URL');
80
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
101
+ if (node.name === 'Link') {
102
+ const marks = node.node.getChildren('LinkMark');
103
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
104
+ const urlNode = node.node.getChild('URL');
105
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
106
+ if (!url) {
107
+ return false;
108
+ }
81
109
 
110
+ if (state.readOnly || cursor < node.from || cursor > node.to) {
82
111
  builder.add(
83
112
  node.from,
84
113
  node.to,
85
114
  Decoration.replace({
86
- widget: new Link(node.from, text, link ? url : undefined),
115
+ widget: new LinkText(node.from, text, options.link ? url : undefined),
87
116
  }),
88
117
  );
118
+ }
89
119
 
90
- return false;
120
+ if (options.onRender) {
121
+ builder.add(
122
+ node.to,
123
+ node.to,
124
+ Decoration.replace({
125
+ widget: new LinkButton(node.from, url, options.onRender),
126
+ }),
127
+ );
91
128
  }
92
- }
93
129
 
94
- return true;
130
+ return false;
131
+ }
95
132
  },
96
133
  });
97
134
 
@@ -4,13 +4,13 @@
4
4
 
5
5
  import { StateField } from '@codemirror/state';
6
6
 
7
+ export type ListenerOptions = { onChange: (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) =>
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 onChangeExtension = (onChange: (text: string) => void) =>
20
20
  return null;
21
21
  },
22
22
  });
23
+ };
@@ -3,97 +3,94 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
6
+ import { history, historyKeymap, indentWithTab, standardKeymap } 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
- import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
10
+ import { searchKeymap } from '@codemirror/search';
18
11
  import { EditorState, type Extension } from '@codemirror/state';
19
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
20
- import {
21
- crosshairCursor,
22
- drawSelection,
23
- dropCursor,
24
- EditorView,
25
- highlightActiveLine,
26
- highlightActiveLineGutter,
27
- highlightSpecialChars,
28
- keymap,
29
- placeholder,
30
- rectangularSelection,
31
- } from '@codemirror/view';
32
- // import { GFM } from '@lezer/markdown';
13
+ import { crosshairCursor, drawSelection, dropCursor, EditorView, keymap, placeholder } from '@codemirror/view';
33
14
 
34
15
  import type { ThemeMode } from '@dxos/react-ui';
35
16
 
36
- import { markdownTagsExtension } from './tags';
37
- import { markdownHighlightStyle } from './theme';
17
+ import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
38
18
 
39
19
  export type MarkdownBundleOptions = {
20
+ readonly?: boolean;
40
21
  themeMode?: ThemeMode;
41
22
  placeholder?: string;
42
23
  };
43
24
 
44
- export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
45
- // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
46
- // https://codemirror.net/docs/ref/#codemirror.basicSetup
25
+ /**
26
+ * Markdown bundle.
27
+ * Refs:
28
+ * https://github.com/codemirror/basic-setup
29
+ * https://codemirror.net/docs/community
30
+ * https://codemirror.net/docs/ref/#codemirror.basicSetup
31
+ */
32
+ // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
33
+ export const markdownBundle = ({
34
+ readonly,
35
+ themeMode,
36
+ placeholder: _placeholder,
37
+ }: MarkdownBundleOptions): Extension[] => {
47
38
  return [
48
- bracketMatching(),
49
- closeBrackets(),
50
- _placeholder && placeholder(_placeholder),
51
-
52
39
  EditorState.allowMultipleSelections.of(true),
40
+ EditorState.tabSize.of(2),
53
41
  EditorView.lineWrapping,
54
42
 
55
- // autocompletion(),
43
+ customKeymap(),
44
+
45
+ bracketMatching(),
46
+ closeBrackets(),
56
47
  crosshairCursor(),
57
48
  dropCursor(),
58
49
  drawSelection(),
59
- highlightActiveLine(),
60
- highlightActiveLineGutter(),
61
- highlightSelectionMatches(),
62
- highlightSpecialChars(),
63
50
  history(),
64
51
  indentOnInput(),
65
- rectangularSelection(),
66
-
67
- keymap.of([
68
- ...closeBracketsKeymap,
69
- // ...completionKeymap,
70
- ...defaultKeymap,
71
- ...foldKeymap,
72
- ...historyKeymap,
73
- ...lintKeymap,
74
- ...searchKeymap,
75
- indentWithTab,
76
- ]),
52
+ _placeholder && placeholder(_placeholder),
77
53
 
78
54
  // Main extension.
79
55
  // https://github.com/codemirror/lang-markdown
56
+ // https://codemirror.net/5/mode/markdown/index.html (demo).
80
57
  markdown({
58
+ // GRM by default (vs strict CommonMark):
59
+ // Table, TaskList, Strikethrough, and Autolink.
60
+ // NOTE: This extends the parser; it doesn't affect rendering.
61
+ // https://github.github.com/gfm
62
+ // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
81
63
  base: markdownLanguage,
64
+
65
+ // Languages for syntax highlighting fenced code blocks.
82
66
  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
67
 
90
- // Custom styling.
91
- markdownTagsExtension,
68
+ // Parser extensions.
69
+ extensions: [
70
+ // GFM provided by default.
71
+ markdownTagsExtensions,
92
72
  ],
93
73
  }),
94
74
 
95
- // TODO(thure): All but one rule here apply to both themes; rename or refactor.
96
- syntaxHighlighting(markdownHighlightStyle),
75
+ // https://github.com/codemirror/theme-one-dark
97
76
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
77
+
78
+ // Custom styles.
79
+ syntaxHighlighting(markdownHighlightStyle(readonly)),
98
80
  ].filter(Boolean) as Extension[];
99
81
  };
82
+
83
+ // https://codemirror.net/docs/ref/#view.keymap
84
+ const customKeymap = () =>
85
+ keymap.of([
86
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
87
+ indentWithTab,
88
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
89
+ ...standardKeymap,
90
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
91
+ ...historyKeymap,
92
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
93
+ ...closeBracketsKeymap,
94
+ // https://codemirror.net/docs/ref/#search.searchKeymap
95
+ ...searchKeymap,
96
+ ]);
@@ -0,0 +1,207 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { markdownLanguage } from '@codemirror/lang-markdown';
6
+ import { HighlightStyle } from '@codemirror/language';
7
+ import { tags, styleTags, Tag } from '@lezer/highlight';
8
+ import { type MarkdownConfig, Table } from '@lezer/markdown';
9
+ import get from 'lodash.get';
10
+
11
+ import { mx } from '@dxos/react-ui-theme';
12
+
13
+ import {
14
+ blockquote,
15
+ bold,
16
+ code,
17
+ codeMark,
18
+ heading,
19
+ horizontalRule,
20
+ inlineUrl,
21
+ italic,
22
+ mark,
23
+ strikethrough,
24
+ tokens,
25
+ } from '../../../../styles';
26
+
27
+ /**
28
+ * Custom tags defined and processed by the GFM lezer extension.
29
+ * https://github.com/lezer-parser/markdown
30
+ * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
31
+ */
32
+ export const markdownTags = {
33
+ Blockquote: Tag.define(),
34
+ CodeMark: Tag.define(),
35
+ CodeText: Tag.define(),
36
+ EmphasisMark: Tag.define(),
37
+ HeaderMark: Tag.define(),
38
+ HorizontalRule: Tag.define(),
39
+ InlineCode: Tag.define(),
40
+ LinkLabel: Tag.define(),
41
+ LinkReference: Tag.define(),
42
+ ListMark: Tag.define(),
43
+ QuoteMark: Tag.define(),
44
+ URL: Tag.define(),
45
+
46
+ // Custom.
47
+ TableCell: Tag.define(),
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
+
68
+ /**
69
+ * Styling based on `lezer` parser tags.
70
+ * https://codemirror.net/examples/styling
71
+ * https://github.com/lezer-parser/highlight
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
78
+ */
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
+ },
120
+
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
+ },
132
+
133
+ // Markdown marks.
134
+ {
135
+ tag: [markdownTags.CodeMark, markdownTags.HeaderMark, markdownTags.QuoteMark, markdownTags.EmphasisMark],
136
+ class: readonly ? 'hidden' : mark,
137
+ },
138
+
139
+ // E.g., code block language (after ```).
140
+ {
141
+ tag: [tags.function(tags.variableName), tags.labelName],
142
+ class: readonly ? 'hidden' : codeMark,
143
+ },
144
+
145
+ {
146
+ tag: [tags.monospace],
147
+ class: 'font-mono',
148
+ },
149
+
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) },
157
+
158
+ // Emphasis.
159
+ { tag: tags.emphasis, class: italic },
160
+ { tag: tags.strong, class: bold },
161
+ { tag: tags.strikethrough, class: strikethrough },
162
+
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
+ },
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
+ ],
202
+ {
203
+ scope: markdownLanguage,
204
+ all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
205
+ },
206
+ );
207
+ };