@dxos/react-ui-editor 0.3.9 → 0.3.10-main.04816d3

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 (128) hide show
  1. package/dist/lib/browser/index.mjs +952 -736
  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 +38 -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 +5 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +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} +1 -1
  13. package/dist/types/src/components/TextEditor/extensions/basic.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 +10 -0
  17. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
  19. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/index.d.ts +7 -2
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  22. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  23. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
  25. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  26. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  27. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +31 -0
  28. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  29. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  30. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  31. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -0
  32. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  33. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  34. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  35. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  36. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  37. package/dist/types/src/components/TextEditor/themes/default.d.ts +20 -0
  38. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  39. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  40. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  41. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
  42. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
  43. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  44. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +1 -0
  45. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  46. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +1 -0
  47. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  48. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  49. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  50. package/dist/types/src/hooks/index.d.ts +0 -1
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  53. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  54. package/dist/types/src/styles/markdown.d.ts +12 -10
  55. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  56. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  57. package/package.json +22 -32
  58. package/src/components/TextEditor/MarkdownEditor.stories.tsx +275 -0
  59. package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
  60. package/src/components/TextEditor/TextEditor.tsx +79 -44
  61. package/src/components/TextEditor/extensions/autocomplete.ts +74 -0
  62. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
  63. package/src/components/TextEditor/extensions/comments.ts +190 -0
  64. package/src/components/TextEditor/extensions/demo.ts +66 -0
  65. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +18 -2
  66. package/src/components/TextEditor/extensions/index.ts +7 -2
  67. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +76 -31
  68. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +4 -3
  69. package/src/components/TextEditor/extensions/markdown/bundle.ts +21 -25
  70. package/src/components/TextEditor/extensions/markdown/highlight.ts +167 -0
  71. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  72. package/src/components/TextEditor/extensions/tasklist.ts +107 -0
  73. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
  74. package/src/components/TextEditor/index.ts +3 -0
  75. package/src/components/TextEditor/themes/default.ts +185 -0
  76. package/src/components/TextEditor/themes/index.ts +5 -0
  77. package/src/components/TextEditor/yjs.stories.tsx +1 -0
  78. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
  79. package/src/hooks/index.ts +0 -1
  80. package/src/hooks/useTextModel.ts +52 -39
  81. package/src/styles/markdown.ts +26 -24
  82. package/src/testing/replicator.ts +6 -6
  83. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  84. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  85. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  86. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  87. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  88. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  89. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  91. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  92. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  93. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  94. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  95. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  96. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  97. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  98. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  99. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  100. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  101. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  102. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  103. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  104. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  105. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  106. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  107. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  108. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  109. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  110. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  111. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  112. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  113. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  114. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  115. package/src/hooks/useCollaboration.ts +0 -45
  116. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +0 -0
  117. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.d.ts +0 -0
  118. /package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
  119. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.d.ts +0 -0
  120. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  121. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +0 -0
  122. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  123. /package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +0 -0
  124. /package/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.ts +0 -0
  125. /package/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.ts +0 -0
  126. /package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  127. /package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +0 -0
  128. /package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +0 -0
@@ -4,7 +4,14 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
- import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
7
+ import {
8
+ type EditorState,
9
+ type Extension,
10
+ type RangeSet,
11
+ RangeSetBuilder,
12
+ StateField,
13
+ type Transaction,
14
+ } from '@codemirror/state';
8
15
  import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
16
 
10
17
  // Adapted from:
@@ -19,13 +26,33 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
19
26
  // https://codemirror.net/5/demo/simplemode.html
20
27
  // https://codemirror.net/5/demo/search.html (Search dialog).
21
28
 
22
- class Link extends WidgetType {
23
- constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
29
+ class LinkButton extends WidgetType {
30
+ constructor(
31
+ private readonly _pos: number,
32
+ private readonly _url: string,
33
+ private readonly _onAttach: LinkOptions['onRender'],
34
+ ) {
24
35
  super();
25
36
  }
26
37
 
27
- override eq(other: any) {
28
- return this._text === other.text && this._url === other.url;
38
+ override eq(other: LinkButton) {
39
+ return this._pos === other._pos && this._url === other._url;
40
+ }
41
+
42
+ toDOM(view: EditorView) {
43
+ const el = document.createElement('span');
44
+ this._onAttach!(el, this._url);
45
+ return el;
46
+ }
47
+ }
48
+
49
+ class LinkText extends WidgetType {
50
+ constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
51
+ super();
52
+ }
53
+
54
+ override eq(other: LinkText) {
55
+ return this._pos === other._pos && this._url === other._url;
29
56
  }
30
57
 
31
58
  toDOM(view: EditorView) {
@@ -33,13 +60,14 @@ class Link extends WidgetType {
33
60
  link.setAttribute('class', 'cm-link');
34
61
  link.textContent = this._text;
35
62
  if (this._url) {
63
+ link.setAttribute('rel', 'noreferrer');
36
64
  link.setAttribute('target', '_blank');
37
65
  link.href = this._url;
38
66
  } else {
39
67
  link.onclick = () => {
40
68
  view.dispatch({
41
69
  selection: {
42
- anchor: this._anchor,
70
+ anchor: this._pos,
43
71
  },
44
72
  });
45
73
  };
@@ -49,51 +77,68 @@ class Link extends WidgetType {
49
77
  }
50
78
  }
51
79
 
52
- type HyperlinkDecorationConfig = {
53
- link?: boolean;
54
- };
55
-
56
80
  /**
57
- * Creates a state field to replace AST elements with a hyperlink widget.
58
- * https://codemirror.net/docs/ref/#state.StateField
81
+ * Range sets provide a data structure that can hold a collection of tagged,
82
+ * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
83
+ * https://codemirror.net/docs/ref/#state
59
84
  */
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) => {
85
+ const update = (state: EditorState, options: LinkOptions) => {
69
86
  const builder = new RangeSetBuilder();
70
87
  const cursor = state.selection.main.head;
71
88
  syntaxTree(state).iterate({
72
89
  enter: (node) => {
73
90
  // 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) : '';
91
+ if (node.name === 'Link') {
92
+ const marks = node.node.getChildren('LinkMark');
93
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
78
94
 
79
- const urlNode = node.node.getChild('URL');
80
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
95
+ const urlNode = node.node.getChild('URL');
96
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
97
+ if (!url) {
98
+ return false;
99
+ }
81
100
 
101
+ if (cursor < node.from || cursor > node.to) {
82
102
  builder.add(
83
103
  node.from,
84
104
  node.to,
85
105
  Decoration.replace({
86
- widget: new Link(node.from, text, link ? url : undefined),
106
+ widget: new LinkText(node.from, text, options.link ? url : undefined),
87
107
  }),
88
108
  );
109
+ }
89
110
 
90
- return false;
111
+ if (options.onRender) {
112
+ builder.add(
113
+ node.to,
114
+ node.to,
115
+ Decoration.replace({
116
+ widget: new LinkButton(node.from, url, options.onRender),
117
+ }),
118
+ );
91
119
  }
92
- }
93
120
 
94
- return true;
121
+ return false;
122
+ }
95
123
  },
96
124
  });
97
125
 
98
126
  return builder.finish();
99
127
  };
128
+
129
+ export type LinkOptions = {
130
+ link?: boolean;
131
+ onRender?: (el: Element, url: string) => void;
132
+ };
133
+
134
+ /**
135
+ * Creates a state field to replace AST elements with a hyperlink widget.
136
+ * https://codemirror.net/docs/ref/#state.StateField
137
+ */
138
+ export const link = (options: LinkOptions = {}): Extension => {
139
+ return StateField.define<RangeSet<any>>({
140
+ create: (state) => update(state, options),
141
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
142
+ provide: (field) => EditorView.decorations.from(field),
143
+ });
144
+ };
@@ -4,12 +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) =>
12
+ // TODO(burdon): Expose options.
13
+ export const listener = ({ onChange }: ListenerOptions) =>
13
14
  StateField.define({
14
15
  create: () => null,
15
16
  update: (_value, transaction) => {
@@ -3,17 +3,10 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
6
+ import { defaultKeymap, history, 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';
@@ -29,12 +22,10 @@ import {
29
22
  placeholder,
30
23
  rectangularSelection,
31
24
  } from '@codemirror/view';
32
- // import { GFM } from '@lezer/markdown';
33
25
 
34
26
  import type { ThemeMode } from '@dxos/react-ui';
35
27
 
36
- import { markdownTagsExtension } from './tags';
37
- import { markdownHighlightStyle } from './theme';
28
+ import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
38
29
 
39
30
  export type MarkdownBundleOptions = {
40
31
  themeMode?: ThemeMode;
@@ -52,7 +43,6 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
52
43
  EditorState.allowMultipleSelections.of(true),
53
44
  EditorView.lineWrapping,
54
45
 
55
- // autocompletion(),
56
46
  crosshairCursor(),
57
47
  dropCursor(),
58
48
  drawSelection(),
@@ -64,36 +54,42 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
64
54
  indentOnInput(),
65
55
  rectangularSelection(),
66
56
 
57
+ // TODO(burdon): Review.
67
58
  keymap.of([
68
59
  ...closeBracketsKeymap,
69
- // ...completionKeymap,
70
60
  ...defaultKeymap,
71
- ...foldKeymap,
72
- ...historyKeymap,
73
- ...lintKeymap,
61
+ // ...foldKeymap,
62
+ // ...historyKeymap,
63
+ // ...lintKeymap,
74
64
  ...searchKeymap,
75
65
  indentWithTab,
76
66
  ]),
77
67
 
78
68
  // Main extension.
79
69
  // https://github.com/codemirror/lang-markdown
70
+ // https://codemirror.net/5/mode/markdown/index.html (demo).
80
71
  markdown({
72
+ // GRM by default (vs strict CommonMark):
73
+ // Table, TaskList, Strikethrough, and Autolink.
74
+ // NOTE: This extends the parser; it doesn't affect rendering.
75
+ // https://github.github.com/gfm
76
+ // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
81
77
  base: markdownLanguage,
78
+
79
+ // Languages for syntax highlighting fenced code blocks.
82
80
  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
81
 
90
- // Custom styling.
82
+ // Parser extensions.
83
+ extensions: [
84
+ // GFM, // TODO(burdon): Provided by default?
91
85
  markdownTagsExtension,
92
86
  ],
93
87
  }),
94
88
 
95
- // TODO(thure): All but one rule here apply to both themes; rename or refactor.
89
+ // Custom styles.
96
90
  syntaxHighlighting(markdownHighlightStyle),
91
+
92
+ // TODO(thure): All but one rule here apply to both themes; rename or refactor.
97
93
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
98
94
  ].filter(Boolean) as Extension[];
99
95
  };
@@ -0,0 +1,167 @@
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 } from '@lezer/markdown';
9
+ import get from 'lodash.get';
10
+
11
+ import {
12
+ blockquote,
13
+ bold,
14
+ code,
15
+ codeMark,
16
+ heading,
17
+ horizontalRule,
18
+ inlineUrl,
19
+ italic,
20
+ mark,
21
+ strikethrough,
22
+ tokens,
23
+ } from '../../../../styles';
24
+
25
+ /**
26
+ * Custom tags defined and processed by the GFM lezer extension.
27
+ * https://github.com/lezer-parser/markdown
28
+ * https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
29
+ */
30
+ export const markdownTags = {
31
+ Blockquote: Tag.define(),
32
+ CodeMark: Tag.define(),
33
+ CodeText: Tag.define(),
34
+ EmphasisMark: Tag.define(),
35
+ HeaderMark: Tag.define(),
36
+ HorizontalRule: Tag.define(),
37
+ InlineCode: Tag.define(),
38
+ LinkLabel: Tag.define(),
39
+ LinkReference: Tag.define(),
40
+ ListMark: Tag.define(),
41
+ QuoteMark: Tag.define(),
42
+ URL: Tag.define(),
43
+ };
44
+
45
+ export const markdownTagsExtension: MarkdownConfig = {
46
+ props: [styleTags(markdownTags)],
47
+ };
48
+
49
+ /**
50
+ * Styling based on `lezer` parser tags.
51
+ * https://codemirror.net/examples/styling
52
+ * https://github.com/lezer-parser/highlight
53
+ * https://lezer.codemirror.net/docs/ref/#highlight (list of tags)
54
+ */
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
+ },
95
+
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
+ },
111
+
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] },
119
+
120
+ // Emphasis.
121
+ { tag: tags.emphasis, class: italic },
122
+ { tag: tags.strong, class: bold },
123
+ { tag: tags.strikethrough, class: strikethrough },
124
+
125
+ // Naked URLs.
126
+ {
127
+ tag: [markdownTags.URL],
128
+ class: inlineUrl,
129
+ },
130
+
131
+ // E.g., code block language (after ```).
132
+ {
133
+ tag: [tags.function(tags.variableName), tags.labelName],
134
+ class: codeMark,
135
+ },
136
+
137
+ // NOTE: The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
138
+ // However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
139
+ // when a language is specified. In this case, the syntax highlighting extensions will colorize
140
+ // the code, but all other CSS properties will be inherited.
141
+ // IMPORTANT: Therefore, the fenced code block will use the base editor font.
142
+ {
143
+ tag: [markdownTags.CodeText, markdownTags.InlineCode],
144
+ class: code,
145
+ },
146
+
147
+ {
148
+ tag: [markdownTags.QuoteMark],
149
+ class: blockquote,
150
+ },
151
+
152
+ {
153
+ tag: [markdownTags.HorizontalRule],
154
+ class: horizontalRule,
155
+ },
156
+
157
+ // Main content, paragraphs, etc.
158
+ // {
159
+ // tag: [tags.content],
160
+ // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
161
+ // },
162
+ ],
163
+ {
164
+ scope: markdownLanguage,
165
+ all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
166
+ },
167
+ );
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './bundle';
6
- export * from './theme';
6
+ export * from './highlight';
@@ -0,0 +1,107 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import {
6
+ EditorView,
7
+ Decoration,
8
+ WidgetType,
9
+ MatchDecorator,
10
+ ViewPlugin,
11
+ type DecorationSet,
12
+ type ViewUpdate,
13
+ } from '@codemirror/view';
14
+
15
+ class CheckboxWidget extends WidgetType {
16
+ constructor(
17
+ private readonly _pos: number,
18
+ private _checked: boolean,
19
+ private readonly _indent: number,
20
+ private readonly _onCheck: (check: boolean) => void,
21
+ ) {
22
+ super();
23
+ }
24
+
25
+ override eq(other: CheckboxWidget) {
26
+ return this._pos === other._pos && this._checked === other._checked && this._indent === other._indent;
27
+ }
28
+
29
+ toDOM(view: EditorView) {
30
+ const wrap = document.createElement('span');
31
+ wrap.className = 'cm-task-item';
32
+ wrap.setAttribute('aria-hidden', 'true');
33
+ wrap.style.setProperty('margin-left', this._indent * 24 + 'px');
34
+
35
+ const input = wrap.appendChild(document.createElement('input'));
36
+ input.type = 'checkbox';
37
+ input.checked = this._checked;
38
+ input.onchange = (event: Event) => {
39
+ this._onCheck((event.target as any).checked);
40
+ return true;
41
+ };
42
+
43
+ return wrap;
44
+ }
45
+
46
+ override ignoreEvent() {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ // TODO(burdon): Reconcile with theme.
52
+ const styles = EditorView.baseTheme({
53
+ '& .cm-task-item': {
54
+ paddingLeft: '4px',
55
+ paddingRight: '8px',
56
+ },
57
+ });
58
+
59
+ export const tasklist = () => {
60
+ // TODO(burdon): Matcher isn't as precise as syntax tree.
61
+ // Allows for indents to be greater than AST would allow.
62
+ const taskMatcher = new MatchDecorator({
63
+ regexp: /^(\s*)- \[([ xX])\]\s/g,
64
+ decoration: (match, view, pos) => {
65
+ const indent = Math.floor(match[1].length / 2);
66
+ const checked = match[2] === 'x' || match[2] === 'X';
67
+ return Decoration.replace({
68
+ widget: new CheckboxWidget(pos, checked, indent, (checked) => {
69
+ const idx = pos + match[0].indexOf('[') + 1;
70
+ view.dispatch({
71
+ changes: {
72
+ from: idx,
73
+ to: idx + 1,
74
+ insert: checked ? 'x' : ' ',
75
+ },
76
+ // TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
77
+ selection: {
78
+ anchor: idx + 3,
79
+ },
80
+ });
81
+ }),
82
+ });
83
+ },
84
+ });
85
+
86
+ const tasks = ViewPlugin.fromClass(
87
+ class {
88
+ tasks: DecorationSet;
89
+ constructor(view: EditorView) {
90
+ this.tasks = taskMatcher.createDeco(view);
91
+ }
92
+
93
+ update(update: ViewUpdate) {
94
+ this.tasks = taskMatcher.updateDeco(update, this.tasks);
95
+ }
96
+ },
97
+ {
98
+ decorations: (instance) => instance.tasks,
99
+ provide: (plugin) =>
100
+ EditorView.atomicRanges.of((view) => {
101
+ return view.plugin(plugin)?.tasks || Decoration.none;
102
+ }),
103
+ },
104
+ );
105
+
106
+ return [tasks, styles];
107
+ };
@@ -2,21 +2,24 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { type Extension } from '@codemirror/state';
5
6
  import { hoverTooltip } from '@codemirror/view';
6
7
 
7
8
  import { tooltipContent } from '@dxos/react-ui-theme';
8
9
 
9
10
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
10
11
 
11
- export type OnHyperlinkHover = (el: Element, url: string) => void;
12
+ export type TooltipOptions = {
13
+ onHover: (el: Element, url: string) => void;
14
+ regexp?: RegExp;
15
+ };
12
16
 
13
17
  /**
14
18
  * https://codemirror.net/examples/tooltip
15
19
  * https://codemirror.net/docs/ref/#view.hoverTooltip
16
20
  * https://github.com/codemirror/view/blob/main/src/tooltip.ts
17
21
  */
18
- // TODO(burdon): Create config object.
19
- export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
22
+ export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions): Extension =>
20
23
  hoverTooltip((view, pos) => {
21
24
  const { from, text } = view.state.doc.lineAt(pos);
22
25
  const p = pos - from;
@@ -49,13 +52,10 @@ export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markd
49
52
  end: idx + from + match[0].length,
50
53
  above: true,
51
54
  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);
55
+ const el = document.createElement('div');
56
+ el.className = tooltipContent({}, 'pli-2 plb-1');
57
57
  onHover(el, url);
58
- return { dom: el };
58
+ return { dom: el, offset: { x: 0, y: 12 } };
59
59
  },
60
60
  };
61
61
  });
@@ -3,6 +3,9 @@
3
3
  //
4
4
 
5
5
  export { tags } from '@lezer/highlight';
6
+ export { type Extension } from '@codemirror/state';
6
7
 
7
8
  export * from './extensions';
9
+ export * from './themes';
10
+
8
11
  export * from './TextEditor';