@dxos/react-ui-editor 0.3.10-main.cbe7692 → 0.3.10-main.cc91b2d

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 (56) hide show
  1. package/dist/lib/browser/index.mjs +654 -412
  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 +6 -3
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -1
  12. package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
  13. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +2 -2
  15. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  16. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
  17. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  18. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
  19. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -0
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  22. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -0
  23. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  24. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +3 -1
  25. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  27. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  28. package/dist/types/src/hooks/useTextModel.d.ts +6 -0
  29. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  30. package/dist/types/src/hooks/yjs/yjs.stories.d.ts +1 -1
  31. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -1
  32. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  33. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  34. package/package.json +22 -20
  35. package/src/components/TextEditor/MarkdownEditor.stories.tsx +51 -32
  36. package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
  37. package/src/components/TextEditor/TextEditor.tsx +21 -23
  38. package/src/components/TextEditor/extensions/basic.ts +4 -3
  39. package/src/components/TextEditor/extensions/blast.ts +27 -29
  40. package/src/components/TextEditor/extensions/code.ts +99 -0
  41. package/src/components/TextEditor/extensions/comments.ts +29 -46
  42. package/src/components/TextEditor/extensions/experimental.tsx +15 -36
  43. package/src/components/TextEditor/extensions/highlight.ts +128 -0
  44. package/src/components/TextEditor/extensions/image.ts +4 -5
  45. package/src/components/TextEditor/extensions/index.ts +2 -0
  46. package/src/components/TextEditor/extensions/link.ts +2 -2
  47. package/src/components/TextEditor/extensions/markdown/bundle.ts +35 -32
  48. package/src/components/TextEditor/extensions/markdown/highlight.ts +37 -11
  49. package/src/components/TextEditor/extensions/table.ts +44 -48
  50. package/src/components/TextEditor/extensions/tasklist.ts +9 -9
  51. package/src/components/TextEditor/themes/default.ts +43 -2
  52. package/src/hooks/automerge/automerge.stories.tsx +1 -0
  53. package/src/hooks/useTextModel.ts +9 -0
  54. package/src/hooks/yjs/yjs.stories.tsx +1 -1
  55. package/src/styles/markdown.ts +5 -4
  56. package/src/testing/replicator.ts +1 -0
@@ -18,25 +18,21 @@ import {
18
18
  import { debounce } from '@dxos/async';
19
19
  import { invariant } from '@dxos/invariant';
20
20
 
21
- // 1. TODO(burdon): Make atomic (for tasklist also).
22
- // - https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
23
- // - https://discuss.codemirror.net/t/creating-atomic-replace-decorations/2961
24
- // - https://codemirror.net/docs/ref/#state.EditorState%5EtransactionFilter (transaction filter: move, delete).
25
- // 2. TODO(burdon): Create/track threads in composer (change global state). Select/follow. Scroll with page.
26
- // - Separate from chat/search.
27
- // 3. TODO(burdon): Support multiple threads in sidebar?
28
- // 4. TODO(burdon): Anchor AI thread when creating section.
29
- // 5. TODO(burdon): Button to resolve thread to close comments.
30
-
31
- // 6. TODO(burdon): Perf: Update existing rangeset.
32
- // https://discuss.codemirror.net/t/rangeset-with-metadata-and-different-decorations/3874
33
-
34
- // TODO(burdon): Import note (performance):
35
- // Easily track & remove content with decorations
36
- // https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
21
+ // TODO(burdon): Reconcile with theme.
22
+ const styles = EditorView.baseTheme({
23
+ '& .cm-bookmark': {
24
+ cursor: 'pointer',
25
+ margin: '4px',
26
+ padding: '4px',
27
+ backgroundColor: 'yellow',
28
+ },
29
+ '& .cm-bookmark-selected': {
30
+ backgroundColor: 'orange',
31
+ },
32
+ });
37
33
 
38
34
  class BookmarkWidget extends WidgetType {
39
- constructor(private readonly _pos: number, private readonly _id: string) {
35
+ constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
40
36
  super();
41
37
  invariant(this._id);
42
38
  }
@@ -48,34 +44,20 @@ class BookmarkWidget extends WidgetType {
48
44
  override toDOM() {
49
45
  const span = document.createElement('span');
50
46
  span.className = 'cm-bookmark';
51
- // TODO(burdon): Call out to react?
52
- // https://emojifinder.com/comment
53
- span.textContent = '💬';
47
+ span.textContent = '§';
48
+ span.onclick = () => this._handleClick();
54
49
  return span;
55
50
  }
56
51
  }
57
52
 
58
- // TODO(burdon): Reconcile with theme.
59
- const styles = EditorView.baseTheme({
60
- '& .cm-bookmark': {
61
- cursor: 'pointer',
62
- margin: '4px',
63
- padding: '4px',
64
- backgroundColor: 'yellow',
65
- },
66
- '& .cm-bookmark-selected': {
67
- backgroundColor: 'orange',
68
- },
69
- });
70
-
71
53
  type CommentsInfo = {
72
54
  active?: string;
73
- items: { id: string; pos: number; location: Rect | null }[];
55
+ items?: { id: string; pos: number; location: Rect | null }[];
74
56
  };
75
57
 
76
58
  export type CommentsOptions = {
77
59
  key?: string;
78
- onCreate?: () => string | void;
60
+ onCreate?: (from: number, to: number) => string | void;
79
61
  onUpdate?: (info: CommentsInfo) => void;
80
62
  };
81
63
 
@@ -91,7 +73,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
91
73
  const id = match[1];
92
74
  return Decoration.replace({
93
75
  id,
94
- widget: new BookmarkWidget(pos, id),
76
+ widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
95
77
  });
96
78
  },
97
79
  });
@@ -116,7 +98,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
116
98
  },
117
99
  );
118
100
 
119
- const doUpdate = debounce((data: CommentsInfo) => {
101
+ const handleUpdate = debounce((data: CommentsInfo) => {
120
102
  options.onUpdate?.(data);
121
103
  }, 200);
122
104
 
@@ -126,13 +108,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
126
108
  key: options?.key ?? 'shift-meta-c',
127
109
  run: (view) => {
128
110
  // Insert footnote.
129
- const id = options.onCreate?.();
111
+ const { from, to, head } = view.state.selection.main;
112
+ const id = options.onCreate?.(from, to);
130
113
  if (id) {
131
- const pos = view.state.selection.main.head;
132
114
  const tag = `[^${id}]`;
115
+ // TODO(burdon): Add selection.
133
116
  view.dispatch({
134
- changes: { from: pos, insert: tag },
135
- selection: { anchor: pos + tag.length },
117
+ changes: { from: head, insert: tag },
118
+ selection: { anchor: head + tag.length },
136
119
  });
137
120
 
138
121
  return true;
@@ -152,14 +135,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
152
135
  return;
153
136
  }
154
137
 
155
- const view = update.view;
156
- const pos = view.state.selection.main.head;
138
+ const { view, state } = update;
139
+ const pos = state.selection.main.head;
157
140
 
158
141
  const decorations: { from: number; to: number; value: Decoration }[] = [];
159
142
  const rangeSet = view.plugin(bookmarks)?.bookmarks;
160
143
  let closest = Infinity;
161
- // TODO(burdon): Always shows entire document?
162
- const { from, to } = view.visibleRanges?.[0] ?? { from: 0, to: view.state.doc.length };
144
+ // TODO(burdon): Handle multiple visible ranges in large documents.
145
+ const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: state.doc.length };
163
146
  rangeSet?.between(from, to, (from, to, value) => {
164
147
  decorations.push({ from, to, value });
165
148
  const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
@@ -171,7 +154,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
171
154
  });
172
155
 
173
156
  if (decorations.length) {
174
- doUpdate({
157
+ handleUpdate({
175
158
  active,
176
159
  items: decorations.map(
177
160
  ({
@@ -13,28 +13,21 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
- // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
17
- // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
18
- // https://codemirror.net/5/doc/manual.html#addon_runmode
19
- // TODO(burdon): Add-on: dialog.
20
- // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
- // TODO(burdon): Split view: https://codemirror.net/examples/split
22
- // TODO(burdon): https://codemirror.net/5/demo/simplemode.html
23
-
24
- // https://codemirror.net/examples/autocompletion
25
- // https://codemirror.net/docs/ref/#autocomplete.autocompletion
26
- // https://codemirror.net/docs/ref/#autocomplete.Completion
27
-
28
- // TODO(burdon): Hint to customize?
29
- // https://codemirror.net/examples/autocompletion
30
-
31
- // Notes: Can't edit widget content (or cursor nav through them).
32
- // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
33
- // Widgets intentionally always get set to contenteditable=false,
34
- // or they would become part of CodeMirror’s editable content element.
35
- // 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
+ });
36
30
 
37
- // TODO(burdon): URL match?
38
31
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
39
32
 
40
33
  /**
@@ -99,7 +92,7 @@ export function hyperLinkExtension() {
99
92
  }
100
93
  },
101
94
  {
102
- decorations: (view) => view.decorations,
95
+ decorations: (value) => value.decorations,
103
96
  eventHandlers: {
104
97
  // TODO(burdon): CLick to expand link.
105
98
  // https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
@@ -118,18 +111,4 @@ export function hyperLinkExtension() {
118
111
  );
119
112
  }
120
113
 
121
- export const styles = EditorView.baseTheme({
122
- '.cm-hyperlink-label': {
123
- cursor: 'pointer',
124
- textDecoration: 'underline',
125
- },
126
- '.cm-hyperlink-bracket': {
127
- opacity: 0.2,
128
- },
129
- '.cm-hyperlink-url': {
130
- opacity: 0.5,
131
- fontFamily: 'monospace',
132
- },
133
- });
134
-
135
114
  export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
@@ -0,0 +1,128 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension, StateEffect, StateField } from '@codemirror/state';
6
+ import { Decoration, EditorView } from '@codemirror/view';
7
+ import sortBy from 'lodash.sortby';
8
+ import { useEffect } from 'react';
9
+
10
+ import { type DocumentRange } from '../../../hooks';
11
+
12
+ // TODO(burdon): Detect click, or cursor proximity.
13
+
14
+ // TODO(burdon): Reconcile with theme.
15
+ const styles = EditorView.baseTheme({
16
+ '& .cm-highlight': {
17
+ backgroundColor: 'yellow', // TODO(burdon): Reconcile with tailwind theme.
18
+ },
19
+ '& .cm-highlight::before': {
20
+ content: ']',
21
+ },
22
+ '& .cm-highlight-active': {
23
+ backgroundColor: 'lime',
24
+ },
25
+ });
26
+
27
+ const marks = {
28
+ highlight: Decoration.mark({ class: 'cm-highlight' }),
29
+ HighlightActive: Decoration.mark({ class: 'cm-highlight-active' }),
30
+ };
31
+
32
+ type HighlightRange = DocumentRange & { active?: boolean };
33
+
34
+ // TODO(burdon): Make configurable to support multiple layers.
35
+ const setHighlights = StateEffect.define<HighlightRange[]>();
36
+
37
+ /**
38
+ * Update field set.
39
+ */
40
+ // TODO(burdon): Reconcile range from data model, vs. in-markdown characters.
41
+ export const useHighlights = (view: EditorView | undefined, ranges: HighlightRange[] = []) => {
42
+ useEffect(() => {
43
+ view?.dispatch({
44
+ effects: setHighlights.of(ranges),
45
+ });
46
+ }, [view, ranges]);
47
+ };
48
+
49
+ /**
50
+ * State field that tracks highlight ranges.
51
+ */
52
+ export const highlightStateField = StateField.define<HighlightRange[]>({
53
+ create: () => [],
54
+ update: (threads, tr) => {
55
+ for (const effect of tr.effects) {
56
+ if (effect.is(setHighlights)) {
57
+ return effect.value;
58
+ }
59
+ }
60
+
61
+ return threads;
62
+ },
63
+ });
64
+
65
+ /**
66
+ * Decorate ranges.
67
+ */
68
+ // TODO(burdon): Pass in field to make reusable (e.g., comments, search).
69
+ export const highlightDecorations = EditorView.decorations.compute([highlightStateField], (state) => {
70
+ const selectionRanges = state.field(highlightStateField);
71
+ const decorations =
72
+ sortBy(selectionRanges ?? [], (selection) => selection.from)?.flatMap((selection) => {
73
+ const range = { from: selection.from, to: selection.to + 1 };
74
+ if (selection.to > selection.from) {
75
+ if (selection.active) {
76
+ return marks.HighlightActive.range(range.from, range.to);
77
+ } else {
78
+ return marks.highlight.range(range.from, range.to);
79
+ }
80
+ } else {
81
+ return [];
82
+ }
83
+ }) ?? [];
84
+
85
+ return Decoration.set(decorations);
86
+ });
87
+
88
+ // TODO(burdon): Use current range to create comment.
89
+ // TODO(burdon): Allow/prevent overlapping?
90
+
91
+ /**
92
+ * Track cursor entering and existing ranges.
93
+ */
94
+ const listener = (options: HighlightOptions) => {
95
+ return EditorView.updateListener.of((update) => {
96
+ const { view, state } = update;
97
+ const { /* from, to, */ head } = state.selection.main;
98
+ const ranges = state.field(highlightStateField);
99
+
100
+ // TODO(burdon): Modify range if editing.
101
+ // console.log('::::', update);
102
+
103
+ // Set active.
104
+ let mod = false;
105
+ const newRanges = ranges.map((range) => {
106
+ const active = head >= range.from && head <= range.to;
107
+ if (active !== range.active) {
108
+ mod = true;
109
+ return { ...range, active };
110
+ } else {
111
+ return range;
112
+ }
113
+ });
114
+
115
+ if (mod) {
116
+ view.dispatch({ effects: setHighlights.of(newRanges) });
117
+ options?.onChange?.(newRanges.find((range) => range.active)?.id);
118
+ }
119
+ });
120
+ };
121
+
122
+ export type HighlightOptions = {
123
+ onChange?: (active?: string) => {};
124
+ };
125
+
126
+ export const highlight = (options: HighlightOptions = {}): Extension => {
127
+ return [highlightStateField, highlightDecorations, listener(options), styles];
128
+ };
@@ -25,19 +25,19 @@ export const image = (options: ImageOptions = {}): Extension => {
25
25
 
26
26
  const update = (state: EditorState, options: ImageOptions) => {
27
27
  const builder = new RangeSetBuilder();
28
+ const cursor = state.selection.main.head;
28
29
 
29
- // TODO(burdon): If not-readonly then show text.
30
- // if (state.readOnly) {
31
30
  syntaxTree(state).iterate({
32
31
  enter: (node) => {
33
32
  if (node.name === 'Image') {
34
33
  const urlNode = node.node.getChild('URL');
35
34
  if (urlNode) {
35
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
36
36
  const url = state.sliceDoc(urlNode.from, urlNode.to);
37
37
  builder.add(
38
- state.readOnly ? node.from : node.to,
38
+ hide ? node.from : node.to,
39
39
  node.to,
40
- Decoration.widget({
40
+ Decoration.replace({
41
41
  widget: new ImageWidget(url),
42
42
  }),
43
43
  );
@@ -45,7 +45,6 @@ const update = (state: EditorState, options: ImageOptions) => {
45
45
  }
46
46
  },
47
47
  });
48
- // }
49
48
 
50
49
  return builder.finish();
51
50
  };
@@ -5,8 +5,10 @@
5
5
  export * from './autocomplete';
6
6
  export * from './basic';
7
7
  export * from './blast';
8
+ export * from './code';
8
9
  export * from './demo';
9
10
  export * from './comments';
11
+ export * from './highlight';
10
12
  export * from './image';
11
13
  export * from './link';
12
14
  export * from './listener';
@@ -94,20 +94,20 @@ class LinkText extends WidgetType {
94
94
  const update = (state: EditorState, options: LinkOptions) => {
95
95
  const builder = new RangeSetBuilder();
96
96
  const cursor = state.selection.main.head;
97
+
97
98
  syntaxTree(state).iterate({
98
99
  enter: (node) => {
99
100
  // Check if cursor is inside text.
100
101
  if (node.name === 'Link') {
101
102
  const marks = node.node.getChildren('LinkMark');
102
103
  const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
103
-
104
104
  const urlNode = node.node.getChild('URL');
105
105
  const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
106
106
  if (!url) {
107
107
  return false;
108
108
  }
109
109
 
110
- if (cursor < node.from || cursor > node.to) {
110
+ if (state.readOnly || cursor < node.from || cursor > node.to) {
111
111
  builder.add(
112
112
  node.from,
113
113
  node.to,
@@ -3,11 +3,11 @@
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
8
  import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
10
- import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
10
+ import { searchKeymap } from '@codemirror/search';
11
11
  import { EditorState, type Extension } from '@codemirror/state';
12
12
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
13
13
  import {
@@ -16,16 +16,13 @@ import {
16
16
  dropCursor,
17
17
  EditorView,
18
18
  highlightActiveLine,
19
- highlightActiveLineGutter,
20
- highlightSpecialChars,
21
19
  keymap,
22
20
  placeholder,
23
- rectangularSelection,
24
21
  } from '@codemirror/view';
25
22
 
26
23
  import type { ThemeMode } from '@dxos/react-ui';
27
24
 
28
- import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
25
+ import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
29
26
 
30
27
  export type MarkdownBundleOptions = {
31
28
  readonly?: boolean;
@@ -33,45 +30,36 @@ export type MarkdownBundleOptions = {
33
30
  placeholder?: string;
34
31
  };
35
32
 
33
+ /**
34
+ * Markdown bundle.
35
+ * Refs:
36
+ * https://github.com/codemirror/basic-setup
37
+ * https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
38
+ * https://codemirror.net/docs/community
39
+ * https://codemirror.net/docs/ref/#codemirror.basicSetup
40
+ */
41
+ // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
36
42
  export const markdownBundle = ({
37
43
  readonly,
38
44
  themeMode,
39
45
  placeholder: _placeholder,
40
46
  }: MarkdownBundleOptions): Extension[] => {
41
- // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
42
- // https://codemirror.net/docs/ref/#codemirror.basicSetup
43
47
  return [
44
- bracketMatching(),
45
- closeBrackets(),
46
- _placeholder && placeholder(_placeholder),
47
-
48
48
  EditorState.allowMultipleSelections.of(true),
49
+ EditorState.tabSize.of(2),
49
50
  EditorView.lineWrapping,
50
51
 
52
+ customKeymap(),
53
+
54
+ bracketMatching(),
55
+ closeBrackets(),
51
56
  crosshairCursor(),
52
57
  dropCursor(),
53
58
  drawSelection(),
54
59
  highlightActiveLine(),
55
- highlightActiveLineGutter(),
56
- highlightSelectionMatches(),
57
- highlightSpecialChars(),
58
60
  history(),
59
61
  indentOnInput(),
60
- rectangularSelection(),
61
-
62
- // https://codemirror.net/docs/ref/#view.keymap
63
- keymap.of([
64
- // https://codemirror.net/docs/ref/#commands.defaultKeymap
65
- ...defaultKeymap,
66
- // https://codemirror.net/docs/ref/#commands.historyKeymap
67
- ...historyKeymap,
68
- // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
69
- ...closeBracketsKeymap,
70
- // https://codemirror.net/docs/ref/#search.searchKeymap
71
- ...searchKeymap,
72
- // https://codemirror.net/docs/ref/#commands.indentWithTab
73
- indentWithTab,
74
- ]),
62
+ _placeholder && placeholder(_placeholder),
75
63
 
76
64
  // Main extension.
77
65
  // https://github.com/codemirror/lang-markdown
@@ -89,8 +77,8 @@ export const markdownBundle = ({
89
77
 
90
78
  // Parser extensions.
91
79
  extensions: [
92
- // GFM, // TODO(burdon): Provided by default?
93
- markdownTagsExtension,
80
+ // GFM provided by default.
81
+ markdownTagsExtensions,
94
82
  ],
95
83
  }),
96
84
 
@@ -101,3 +89,18 @@ export const markdownBundle = ({
101
89
  syntaxHighlighting(markdownHighlightStyle(readonly)),
102
90
  ].filter(Boolean) as Extension[];
103
91
  };
92
+
93
+ // https://codemirror.net/docs/ref/#view.keymap
94
+ const customKeymap = () =>
95
+ keymap.of([
96
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
97
+ indentWithTab,
98
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
99
+ ...standardKeymap,
100
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
101
+ ...historyKeymap,
102
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
103
+ ...closeBracketsKeymap,
104
+ // https://codemirror.net/docs/ref/#search.searchKeymap
105
+ ...searchKeymap,
106
+ ]);
@@ -5,7 +5,7 @@
5
5
  import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags, styleTags, Tag } from '@lezer/highlight';
8
- import { type MarkdownConfig } from '@lezer/markdown';
8
+ import { type MarkdownConfig, Table } from '@lezer/markdown';
9
9
  import get from 'lodash.get';
10
10
 
11
11
  import { mx } from '@dxos/react-ui-theme';
@@ -24,11 +24,6 @@ import {
24
24
  tokens,
25
25
  } from '../../../../styles';
26
26
 
27
- // TODO(burdon): Style table in monospace.
28
- // https://discuss.codemirror.net/t/markdown-table-highlighting/658
29
-
30
- // TODO(burdon): Readonly: use remark rather than hiding markup chars (which cursor can still move into).
31
-
32
27
  /**
33
28
  * Custom tags defined and processed by the GFM lezer extension.
34
29
  * https://github.com/lezer-parser/markdown
@@ -47,24 +42,42 @@ export const markdownTags = {
47
42
  ListMark: Tag.define(),
48
43
  QuoteMark: Tag.define(),
49
44
  URL: Tag.define(),
50
- };
51
45
 
52
- export const markdownTagsExtension: MarkdownConfig = {
53
- props: [styleTags(markdownTags)],
46
+ // Custom.
47
+ TableCell: Tag.define(),
54
48
  };
55
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
+
56
68
  /**
57
69
  * Styling based on `lezer` parser tags.
58
70
  * https://codemirror.net/examples/styling
59
71
  * https://github.com/lezer-parser/highlight
72
+ * https://github.com/lezer-parser/highlight/blob/main/src/highlight.ts#L427
60
73
  * https://lezer.codemirror.net/docs/ref/#highlight.tags (list of tags)
61
74
  *
62
75
  * Examples:
63
76
  * - https://github.com/codemirror/language/blob/main/src/highlight.ts#L194
64
77
  * - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts#L115
65
78
  */
66
- export const markdownHighlightStyle = (readonly?: boolean) =>
67
- HighlightStyle.define(
79
+ export const markdownHighlightStyle = (readonly?: boolean) => {
80
+ return HighlightStyle.define(
68
81
  [
69
82
  {
70
83
  tag: [
@@ -129,6 +142,11 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
129
142
  class: readonly ? 'hidden' : codeMark,
130
143
  },
131
144
 
145
+ {
146
+ tag: [tags.monospace],
147
+ class: 'font-mono',
148
+ },
149
+
132
150
  // Headings.
133
151
  { tag: tags.heading1, class: heading(1, readonly) },
134
152
  { tag: tags.heading2, class: heading(2, readonly) },
@@ -163,11 +181,18 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
163
181
  class: blockquote,
164
182
  },
165
183
 
184
+ // TODO(burdon): Replace with extension.
166
185
  {
167
186
  tag: [markdownTags.HorizontalRule],
168
187
  class: mx(horizontalRule, readonly && '-indent-[100rem]'),
169
188
  },
170
189
 
190
+ // TODO(burdon): Only if being edited.
191
+ {
192
+ tag: [markdownTags.TableCell],
193
+ class: 'font-mono',
194
+ },
195
+
171
196
  // Main content, paragraphs, etc.
172
197
  // {
173
198
  // tag: [tags.content],
@@ -179,3 +204,4 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
179
204
  all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
180
205
  },
181
206
  );
207
+ };