@dxos/react-ui-editor 0.3.9-main.b7e6a67 → 0.3.9-main.c414ce0

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 (144) hide show
  1. package/dist/lib/browser/index.mjs +973 -499
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts +2 -2
  5. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -1
  6. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +2 -2
  7. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -1
  8. package/dist/types/src/automerge/automerge-plugin/handle.d.ts +9 -0
  9. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +1 -0
  10. package/dist/types/src/automerge/automerge-plugin/index.d.ts +2 -2
  11. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -1
  12. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +6 -5
  13. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -1
  14. package/dist/types/src/automerge/automerge.stories.d.ts +2 -1
  15. package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/TextEditor/TextEditor.d.ts +43 -0
  17. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +15 -0
  19. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -0
  20. package/dist/types/{stories/html/code-mirror.stories.d.ts → src/components/TextEditor/codemirror.stories.d.ts} +2 -2
  21. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts +8 -0
  23. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +3 -0
  25. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +1 -0
  26. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +10 -0
  27. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +1 -0
  28. package/dist/types/src/components/TextEditor/extensions/change.d.ts +7 -0
  29. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +1 -0
  30. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +11 -0
  31. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +1 -0
  32. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +8 -0
  33. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +1 -0
  34. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts +10 -0
  35. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +1 -0
  36. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +4 -0
  37. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +1 -0
  38. package/dist/types/src/components/TextEditor/extensions/index.d.ts +5 -0
  39. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -0
  40. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -0
  41. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -0
  42. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +3 -0
  43. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -0
  44. package/dist/types/src/components/{Markdown/markdownTags.d.ts → TextEditor/extensions/markdown/tags.d.ts} +1 -1
  45. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +1 -0
  46. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +21 -0
  47. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +1 -0
  48. package/dist/types/src/components/TextEditor/index.d.ts +4 -0
  49. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -0
  50. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +11 -0
  51. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  52. package/dist/types/src/components/index.d.ts +1 -3
  53. package/dist/types/src/components/index.d.ts.map +1 -1
  54. package/dist/types/src/hooks/index.d.ts +4 -0
  55. package/dist/types/src/hooks/index.d.ts.map +1 -0
  56. package/dist/types/src/hooks/useCollaboration.d.ts +9 -0
  57. package/dist/types/src/hooks/useCollaboration.d.ts.map +1 -0
  58. package/dist/types/src/{model.d.ts → hooks/useTextModel.d.ts} +3 -15
  59. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -0
  60. package/dist/types/src/hooks/yjs/cursors.d.ts.map +1 -0
  61. package/dist/types/src/hooks/yjs/index.d.ts.map +1 -0
  62. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +1 -0
  63. package/dist/types/src/index.d.ts +2 -2
  64. package/dist/types/src/index.d.ts.map +1 -1
  65. package/dist/types/src/styles/tokens.d.ts +4 -0
  66. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  67. package/dist/types/src/testing/replicator.d.ts +1 -1
  68. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  69. package/package.json +34 -27
  70. package/src/automerge/automerge-plugin/PatchSemaphore.ts +5 -5
  71. package/src/automerge/automerge-plugin/codeMirrorToAm.ts +10 -7
  72. package/src/automerge/automerge-plugin/handle.ts +14 -0
  73. package/src/automerge/automerge-plugin/index.ts +2 -2
  74. package/src/automerge/automerge-plugin/plugin.ts +39 -10
  75. package/src/automerge/automerge.stories.tsx +32 -48
  76. package/src/components/TextEditor/TextEditor.stories.tsx +79 -0
  77. package/src/components/TextEditor/TextEditor.tsx +188 -0
  78. package/src/components/TextEditor/codemirror.stories.ts +114 -0
  79. package/src/components/TextEditor/extensions/basic/bundle.ts +26 -0
  80. package/src/components/TextEditor/extensions/basic/index.ts +6 -0
  81. package/src/components/TextEditor/extensions/basic/theme.ts +49 -0
  82. package/src/components/TextEditor/extensions/change.ts +22 -0
  83. package/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.ts +99 -0
  84. package/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.tsx +61 -0
  85. package/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.tsx +119 -0
  86. package/src/components/TextEditor/extensions/hyperlink/index.ts +7 -0
  87. package/src/components/TextEditor/extensions/index.ts +8 -0
  88. package/src/components/TextEditor/extensions/markdown/bundle.ts +99 -0
  89. package/src/components/TextEditor/extensions/markdown/index.ts +6 -0
  90. package/src/components/{Markdown/markdownTheme.ts → TextEditor/extensions/markdown/theme.ts} +42 -27
  91. package/src/components/TextEditor/index.ts +8 -0
  92. package/src/components/TextEditor/yjs.stories.tsx +55 -0
  93. package/src/components/index.ts +1 -3
  94. package/src/hooks/index.ts +7 -0
  95. package/src/hooks/useCollaboration.ts +45 -0
  96. package/src/{model.ts → hooks/useTextModel.ts} +42 -17
  97. package/src/index.ts +3 -3
  98. package/src/styles/tokens.ts +6 -0
  99. package/src/testing/replicator.ts +1 -3
  100. package/dist/types/src/automerge/demo.d.ts +0 -25
  101. package/dist/types/src/automerge/demo.d.ts.map +0 -1
  102. package/dist/types/src/components/Editor/Editor.d.ts +0 -15
  103. package/dist/types/src/components/Editor/Editor.d.ts.map +0 -1
  104. package/dist/types/src/components/Editor/Editor.stories.d.ts +0 -28
  105. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +0 -1
  106. package/dist/types/src/components/Editor/index.d.ts +0 -2
  107. package/dist/types/src/components/Editor/index.d.ts.map +0 -1
  108. package/dist/types/src/components/Markdown/Markdown.d.ts +0 -19
  109. package/dist/types/src/components/Markdown/Markdown.d.ts.map +0 -1
  110. package/dist/types/src/components/Markdown/Markdown.stories.d.ts +0 -19
  111. package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +0 -1
  112. package/dist/types/src/components/Markdown/index.d.ts +0 -2
  113. package/dist/types/src/components/Markdown/index.d.ts.map +0 -1
  114. package/dist/types/src/components/Markdown/markdownTags.d.ts.map +0 -1
  115. package/dist/types/src/components/Markdown/markdownTheme.d.ts +0 -107
  116. package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +0 -1
  117. package/dist/types/src/components/RichText/RichText.d.ts +0 -20
  118. package/dist/types/src/components/RichText/RichText.d.ts.map +0 -1
  119. package/dist/types/src/components/RichText/RichText.stories.d.ts +0 -26
  120. package/dist/types/src/components/RichText/RichText.stories.d.ts.map +0 -1
  121. package/dist/types/src/components/RichText/index.d.ts +0 -2
  122. package/dist/types/src/components/RichText/index.d.ts.map +0 -1
  123. package/dist/types/src/model.d.ts.map +0 -1
  124. package/dist/types/src/yjs/cursors.d.ts.map +0 -1
  125. package/dist/types/src/yjs/index.d.ts.map +0 -1
  126. package/dist/types/src/yjs/space-provider.d.ts.map +0 -1
  127. package/dist/types/stories/html/code-mirror.stories.d.ts.map +0 -1
  128. package/src/automerge/demo.ts +0 -106
  129. package/src/components/Editor/Editor.stories.tsx +0 -80
  130. package/src/components/Editor/Editor.tsx +0 -33
  131. package/src/components/Editor/index.ts +0 -5
  132. package/src/components/Markdown/Markdown.stories.tsx +0 -56
  133. package/src/components/Markdown/Markdown.tsx +0 -206
  134. package/src/components/Markdown/index.ts +0 -5
  135. package/src/components/RichText/RichText.stories.tsx +0 -68
  136. package/src/components/RichText/RichText.tsx +0 -181
  137. package/src/components/RichText/index.ts +0 -5
  138. /package/dist/types/src/{yjs → hooks/yjs}/cursors.d.ts +0 -0
  139. /package/dist/types/src/{yjs → hooks/yjs}/index.d.ts +0 -0
  140. /package/dist/types/src/{yjs → hooks/yjs}/space-provider.d.ts +0 -0
  141. /package/src/components/{Markdown/markdownTags.ts → TextEditor/extensions/markdown/tags.ts} +0 -0
  142. /package/src/{yjs → hooks/yjs}/cursors.ts +0 -0
  143. /package/src/{yjs → hooks/yjs}/index.ts +0 -0
  144. /package/src/{yjs → hooks/yjs}/space-provider.ts +0 -0
@@ -0,0 +1,26 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { closeBrackets } from '@codemirror/autocomplete';
6
+ import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
+ import { type Extension } from '@codemirror/state';
8
+ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
+ import { placeholder } from '@codemirror/view';
10
+
11
+ import type { ThemeMode } from '@dxos/react-ui';
12
+
13
+ export type BasicBundleOptions = {
14
+ themeMode?: ThemeMode;
15
+ placeholder?: string;
16
+ };
17
+
18
+ export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
19
+ [
20
+ bracketMatching(),
21
+ closeBrackets(),
22
+ _placeholder && placeholder(_placeholder),
23
+
24
+ // TODO(burdon): Is this required?
25
+ themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
26
+ ].filter(Boolean) as Extension[];
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './bundle';
6
+ export * from './theme';
@@ -0,0 +1,49 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import get from 'lodash.get';
6
+ import { type StyleSpec } from 'style-mod';
7
+
8
+ import { tokens } from '../../../../styles';
9
+
10
+ /**
11
+ * Minimal styles.
12
+ * NOTE: The '&' prefix denotes the CM editor root.
13
+ * https://codemirror.net/examples/styling
14
+ */
15
+ // TODO(burdon): Remove?
16
+ export const basicTheme: {
17
+ [selector: string]: StyleSpec;
18
+ } = {
19
+ '&.cm-focused': {
20
+ outline: 'none',
21
+ },
22
+ '& .cm-line': {
23
+ paddingInline: 0,
24
+ lineHeight: 1.6,
25
+ minBlockSize: '1.6em',
26
+ },
27
+ '& .cm-line *': {
28
+ lineHeight: 1.6,
29
+ },
30
+ '& .cm-scroller': {
31
+ fontFamily: get(tokens, 'fontFamily.body', []).join(','),
32
+ overflow: 'visible',
33
+ },
34
+ '& .cm-content': {
35
+ padding: 0,
36
+ caretColor: 'black',
37
+ },
38
+ '.dark & .cm-content': {
39
+ caretColor: 'white',
40
+ },
41
+ '& .cm-tooltip': {
42
+ backgroundColor: 'transparent',
43
+ border: 'none',
44
+ },
45
+ '& .cm-link': {
46
+ color: get(tokens, 'extend.colors.primary.500'),
47
+ textDecoration: 'underline',
48
+ },
49
+ };
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { StateField } from '@codemirror/state';
6
+
7
+ /**
8
+ * Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
9
+ * @param onChange
10
+ */
11
+ // TODO(burdon): Name?
12
+ export const onChangeExtension = (onChange: (text: string) => void) =>
13
+ StateField.define({
14
+ create: () => null,
15
+ update: (_value, transaction) => {
16
+ if (transaction.docChanged && onChange) {
17
+ onChange(transaction.newDoc.toString());
18
+ }
19
+
20
+ return null;
21
+ },
22
+ });
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ // Copyright CodeMirror
4
+ //
5
+
6
+ import { syntaxTree } from '@codemirror/language';
7
+ import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
8
+ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
+
10
+ // Adapted from:
11
+ // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
12
+ // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
13
+
14
+ // TODO(burdon): Adapt to make all markdown editable?
15
+
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).
21
+
22
+ class Link extends WidgetType {
23
+ constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
24
+ super();
25
+ }
26
+
27
+ override eq(other: any) {
28
+ return this._text === other.text && this._url === other.url;
29
+ }
30
+
31
+ toDOM(view: EditorView) {
32
+ const link = document.createElement('a');
33
+ link.setAttribute('class', 'cm-link');
34
+ link.textContent = this._text;
35
+ if (this._url) {
36
+ link.setAttribute('target', '_blank');
37
+ link.href = this._url;
38
+ } else {
39
+ link.onclick = () => {
40
+ view.dispatch({
41
+ selection: {
42
+ anchor: this._anchor,
43
+ },
44
+ });
45
+ };
46
+ }
47
+
48
+ return link;
49
+ }
50
+ }
51
+
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) => {
69
+ const builder = new RangeSetBuilder();
70
+ const cursor = state.selection.main.head;
71
+ syntaxTree(state).iterate({
72
+ enter: (node) => {
73
+ // 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) : '';
81
+
82
+ builder.add(
83
+ node.from,
84
+ node.to,
85
+ Decoration.replace({
86
+ widget: new Link(node.from, text, link ? url : undefined),
87
+ }),
88
+ );
89
+
90
+ return false;
91
+ }
92
+ }
93
+
94
+ return true;
95
+ },
96
+ });
97
+
98
+ return builder.finish();
99
+ };
@@ -0,0 +1,61 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { hoverTooltip } from '@codemirror/view';
6
+
7
+ import { tooltipContent } from '@dxos/react-ui-theme';
8
+
9
+ const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
10
+
11
+ export type OnHyperlinkHover = (el: Element, url: string) => void;
12
+
13
+ /**
14
+ * https://codemirror.net/examples/tooltip
15
+ * https://codemirror.net/docs/ref/#view.hoverTooltip
16
+ * https://github.com/codemirror/view/blob/main/src/tooltip.ts
17
+ */
18
+ // TODO(burdon): Create config object.
19
+ export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
20
+ hoverTooltip((view, pos) => {
21
+ const { from, text } = view.state.doc.lineAt(pos);
22
+ const p = pos - from;
23
+
24
+ let idx = 0;
25
+ let match;
26
+ do {
27
+ match = text.substring(idx).match(regexp);
28
+ if (!match) {
29
+ match = undefined;
30
+ break;
31
+ }
32
+
33
+ idx = text.indexOf(match[0]);
34
+ if (p >= idx && p < idx + match[0].length) {
35
+ break;
36
+ }
37
+
38
+ idx += match[0].length;
39
+ } while (true);
40
+
41
+ if (!match) {
42
+ return null;
43
+ }
44
+
45
+ const [, , url] = match ?? [];
46
+
47
+ return {
48
+ pos: idx + from,
49
+ end: idx + from + match[0].length,
50
+ above: true,
51
+ create: () => {
52
+ const el = document.createElement('a');
53
+ el.innerText = '_'; // Required so doesn't collapse.
54
+ el.className = tooltipContent({}, 'mb-2 p-1');
55
+ el.setAttribute('target', '_blank');
56
+ el.setAttribute('href', url);
57
+ onHover(el, url);
58
+ return { dom: el };
59
+ },
60
+ };
61
+ });
@@ -0,0 +1,119 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ // Adapted from: https://github.com/uiwjs/react-codemirror/blob/master/extensions/hyper-link/src/index.ts
4
+ //
5
+
6
+ import { type Extension } from '@codemirror/state';
7
+ import {
8
+ Decoration,
9
+ type DecorationSet,
10
+ EditorView,
11
+ MatchDecorator,
12
+ ViewPlugin,
13
+ type ViewUpdate,
14
+ } from '@codemirror/view';
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.
21
+
22
+ const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
23
+
24
+ /**
25
+ * Custom link decorator.
26
+ * NOTE: The default markdown extension formats links by default.
27
+ * @deprecated
28
+ */
29
+ const linkDecorator = () =>
30
+ new MatchDecorator({
31
+ regexp: markdownLinkRegexp,
32
+ decorate: (add, from, to, match, view) => {
33
+ const [_, label, url] = match;
34
+ const p0 = {
35
+ start: from,
36
+ end: from + 1,
37
+ };
38
+
39
+ const p1 = {
40
+ start: from + 1,
41
+ end: from + 1 + label.length,
42
+ };
43
+
44
+ const p2 = {
45
+ start: p1.end,
46
+ end: p1.end + 1,
47
+ };
48
+
49
+ const p3 = {
50
+ start: p1.end + 1,
51
+ end: p1.end + 1 + url.length + 2,
52
+ };
53
+
54
+ add(
55
+ p0.start,
56
+ p0.end,
57
+ Decoration.mark({ class: 'cm-hyperlink-bracket', _attributes: { style: 'display: none' } }),
58
+ );
59
+ add(p1.start, p1.end, Decoration.mark({ class: 'cm-hyperlink-label' }));
60
+ add(
61
+ p2.start,
62
+ p2.end,
63
+ Decoration.mark({ class: 'cm-hyperlink-bracket', _attributes: { style: 'display: none' } }),
64
+ );
65
+ add(p3.start, p3.end, Decoration.mark({ class: 'cm-hyperlink-url', _attributes: { style: 'display: none' } }));
66
+ },
67
+ });
68
+
69
+ export function hyperLinkExtension() {
70
+ return ViewPlugin.fromClass(
71
+ class HyperLinkView {
72
+ readonly decorator: MatchDecorator;
73
+ decorations: DecorationSet;
74
+ constructor(view: EditorView) {
75
+ this.decorator = linkDecorator();
76
+ this.decorations = this.decorator.createDeco(view);
77
+ }
78
+
79
+ update(update: ViewUpdate) {
80
+ if (update.docChanged || update.viewportChanged) {
81
+ this.decorations = this.decorator.updateDeco(update, this.decorations);
82
+ }
83
+ }
84
+ },
85
+ {
86
+ decorations: (view) => view.decorations,
87
+ eventHandlers: {
88
+ // TODO(burdon): CLick to expand link.
89
+ // https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
90
+ mousedown: function (event, view) {
91
+ const pos = view.posAtDOM(event.target as Node);
92
+ if (pos !== 0) {
93
+ console.log(this);
94
+ }
95
+ },
96
+ // TODO(burdon): Hide when cursor moves away.
97
+ keydown: (event, view) => {
98
+ view.update([]);
99
+ },
100
+ },
101
+ },
102
+ );
103
+ }
104
+
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];
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './hyperlinkDecoration';
6
+ export * from './hyperlinkTooltip';
7
+ export * from './hyperlinkWidget';
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './basic';
6
+ export * from './change';
7
+ export * from './hyperlink';
8
+ export * from './markdown';
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
7
+ import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
+ import {
9
+ bracketMatching,
10
+ defaultHighlightStyle,
11
+ foldKeymap,
12
+ indentOnInput,
13
+ syntaxHighlighting,
14
+ } from '@codemirror/language';
15
+ import { languages } from '@codemirror/language-data';
16
+ import { lintKeymap } from '@codemirror/lint';
17
+ import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
18
+ import { EditorState, type Extension } from '@codemirror/state';
19
+ 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';
33
+
34
+ import type { ThemeMode } from '@dxos/react-ui';
35
+
36
+ import { markdownTagsExtension } from './tags';
37
+ import { markdownHighlightStyle } from './theme';
38
+
39
+ export type MarkdownBundleOptions = {
40
+ themeMode?: ThemeMode;
41
+ placeholder?: string;
42
+ };
43
+
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
47
+ return [
48
+ bracketMatching(),
49
+ closeBrackets(),
50
+ _placeholder && placeholder(_placeholder),
51
+
52
+ EditorState.allowMultipleSelections.of(true),
53
+ EditorView.lineWrapping,
54
+
55
+ // autocompletion(),
56
+ crosshairCursor(),
57
+ dropCursor(),
58
+ drawSelection(),
59
+ highlightActiveLine(),
60
+ highlightActiveLineGutter(),
61
+ highlightSelectionMatches(),
62
+ highlightSpecialChars(),
63
+ history(),
64
+ indentOnInput(),
65
+ rectangularSelection(),
66
+
67
+ keymap.of([
68
+ ...closeBracketsKeymap,
69
+ // ...completionKeymap,
70
+ ...defaultKeymap,
71
+ ...foldKeymap,
72
+ ...historyKeymap,
73
+ ...lintKeymap,
74
+ ...searchKeymap,
75
+ indentWithTab,
76
+ ]),
77
+
78
+ // Main extension.
79
+ // https://github.com/codemirror/lang-markdown
80
+ markdown({
81
+ base: markdownLanguage,
82
+ 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
+
90
+ // Custom styling.
91
+ markdownTagsExtension,
92
+ ],
93
+ }),
94
+
95
+ // TODO(thure): All but one rule here apply to both themes; rename or refactor.
96
+ syntaxHighlighting(markdownHighlightStyle),
97
+ themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
98
+ ].filter(Boolean) as Extension[];
99
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './bundle';
6
+ export * from './theme';
@@ -7,12 +7,10 @@ import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags } from '@lezer/highlight';
8
8
  import get from 'lodash.get';
9
9
 
10
- import { markdownTags } from './markdownTags';
11
- import { bold, heading, italic, mark, strikethrough, tokens } from '../../styles';
12
-
13
- // TODO(burdon): Use theme colors.
14
- // TODO(burdon): Light mode.
10
+ import { markdownTags } from './tags';
11
+ import { bold, heading, italic, mark, strikethrough, tokens, type ThemeStyles } from '../../../../styles';
15
12
 
13
+ // TODO(burdon): Rationalize theme/colors. Hoist onto single property object.
16
14
  export const chalky = '#e5c07b';
17
15
  export const coral = '#e06c75';
18
16
  export const cyan = '#56b6c2';
@@ -28,12 +26,15 @@ export const highlightBackground = '#2c313a';
28
26
  const _background = '#282c34';
29
27
  export const tooltipBackground = '#353a42';
30
28
  const _selection = '#3E4451';
31
- export const cursor = '#ffffff';
32
29
 
33
30
  const monospace = get(tokens, 'fontFamily.mono', ['monospace']).join(',');
34
31
 
35
- export const markdownTheme = {
36
- // TODO(thure): consider whether these commented-out rules from one-dark-theme should be integrated.
32
+ /**
33
+ * NOTE: The '&' prefix denotes the CM editor root.
34
+ * https://codemirror.net/examples/styling
35
+ */
36
+ export const markdownTheme: ThemeStyles = {
37
+ // TODO(thure): Consider whether these commented-out rules from one-dark-theme should be integrated.
37
38
  // '&': {
38
39
  // color: ivory,
39
40
  // backgroundColor: background
@@ -71,29 +72,34 @@ export const markdownTheme = {
71
72
  // color: '#ddd'
72
73
  // },
73
74
 
74
- '.dark & .cm-tooltip': {
75
+ '& .cm-tooltip': {
76
+ backgroundColor: 'transparent',
75
77
  border: 'none',
76
- backgroundColor: tooltipBackground,
77
- },
78
- '.dark & .cm-tooltip .cm-tooltip-arrow:before': {
79
- borderTopColor: 'transparent',
80
- borderBottomColor: 'transparent',
81
- },
82
- '.dark & .cm-tooltip .cm-tooltip-arrow:after': {
83
- borderTopColor: tooltipBackground,
84
- borderBottomColor: tooltipBackground,
85
- },
86
- '.dark & .cm-tooltip-autocomplete': {
87
- '& > ul > li[aria-selected]': {
88
- backgroundColor: highlightBackground,
89
- color: ivory,
90
- },
91
78
  },
79
+ // '.dark & .cm-tooltip': {
80
+ // border: 'none',
81
+ // backgroundColor: tooltipBackground,
82
+ // },
83
+ // '.dark & .cm-tooltip .cm-tooltip-arrow:before': {
84
+ // borderTopColor: 'transparent',
85
+ // borderBottomColor: 'transparent',
86
+ // },
87
+ // '.dark & .cm-tooltip .cm-tooltip-arrow:after': {
88
+ // borderTopColor: tooltipBackground,
89
+ // borderBottomColor: tooltipBackground,
90
+ // },
91
+ // '.dark & .cm-tooltip-autocomplete': {
92
+ // '& > ul > li[aria-selected]': {
93
+ // backgroundColor: highlightBackground,
94
+ // color: ivory,
95
+ // },
96
+ // },
92
97
  '&.cm-focused': {
93
98
  outline: 'none',
94
99
  },
95
100
  '& .cm-line': {
96
101
  paddingInline: 0,
102
+ lineHeight: 1.6,
97
103
  minBlockSize: '1.6em',
98
104
  },
99
105
  '& .cm-line *': {
@@ -112,16 +118,17 @@ export const markdownTheme = {
112
118
  background: get(tokens, 'extend.colors.primary.600', '#00ffff') + '44',
113
119
  },
114
120
  '& .cm-content': {
121
+ padding: 0,
115
122
  caretColor: 'black',
116
123
  },
117
124
  '.dark & .cm-content': {
118
- caretColor: cursor,
125
+ caretColor: 'white',
119
126
  },
120
127
  '& .cm-cursor': {
121
128
  borderLeftColor: 'black',
122
129
  },
123
130
  '.dark & .cm-cursor': {
124
- borderLeftColor: cursor,
131
+ borderLeftColor: 'white',
125
132
  },
126
133
  '.cm-placeholder': {
127
134
  fontFamily: get(tokens, 'fontFamily.body', []).join(','),
@@ -160,6 +167,14 @@ export const markdownTheme = {
160
167
  '& .cm-yLineSelection': {
161
168
  margin: '0',
162
169
  },
170
+ '.cm-link': {
171
+ color: 'rgb(20 89 208)', // TODO(burdon): Rationalize.
172
+ textDecorationLine: 'underline',
173
+ textDecorationThickness: '1px',
174
+ textUnderlineOffset: '2px',
175
+ borderRadius: '.125rem',
176
+ },
177
+ // TODO(burdon): Document.
163
178
  ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
164
179
  const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
165
180
  // TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
@@ -170,7 +185,7 @@ export const markdownTheme = {
170
185
  }, {}),
171
186
  };
172
187
 
173
- export const markdownDarkHighlighting = HighlightStyle.define(
188
+ export const markdownHighlightStyle = HighlightStyle.define(
174
189
  [
175
190
  {
176
191
  tag: [
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { tags } from '@lezer/highlight';
6
+
7
+ export * from './extensions';
8
+ export * from './TextEditor';