@dxos/react-ui-editor 0.6.7 → 0.6.8-main.3be982f

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 (109) hide show
  1. package/dist/lib/browser/index.mjs +1019 -796
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/{hooks/InputMode.stories.d.ts → InputMode.stories.d.ts} +6 -4
  5. package/dist/types/src/InputMode.stories.d.ts.map +1 -0
  6. package/dist/types/src/{hooks/TextEditor.stories.d.ts → TextEditor.stories.d.ts} +43 -26
  7. package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -9
  9. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  10. package/dist/types/src/defaults.d.ts +10 -0
  11. package/dist/types/src/defaults.d.ts.map +1 -0
  12. package/dist/types/src/extensions/autocomplete.d.ts +2 -2
  13. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +6 -4
  15. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  17. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  18. package/dist/types/src/extensions/blast.d.ts.map +1 -1
  19. package/dist/types/src/extensions/command/state.d.ts +1 -1
  20. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  21. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  22. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  23. package/dist/types/src/extensions/folding.d.ts +7 -0
  24. package/dist/types/src/extensions/folding.d.ts.map +1 -0
  25. package/dist/types/src/extensions/index.d.ts +1 -0
  26. package/dist/types/src/extensions/index.d.ts.map +1 -1
  27. package/dist/types/src/extensions/markdown/decorate.d.ts +5 -1
  28. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  29. package/dist/types/src/extensions/markdown/formatting.d.ts +9 -9
  30. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/image.d.ts +1 -1
  32. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/link-paste.d.ts +6 -0
  34. package/dist/types/src/extensions/markdown/link-paste.d.ts.map +1 -0
  35. package/dist/types/src/extensions/markdown/link-paste.test.d.ts +2 -0
  36. package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +1 -0
  37. package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
  38. package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
  39. package/dist/types/src/extensions/state.d.ts.map +1 -1
  40. package/dist/types/src/extensions/util/error.d.ts +2 -0
  41. package/dist/types/src/extensions/util/error.d.ts.map +1 -0
  42. package/dist/types/src/extensions/util/index.d.ts +3 -1
  43. package/dist/types/src/extensions/util/index.d.ts.map +1 -1
  44. package/dist/types/src/extensions/util/overlap.d.ts.map +1 -1
  45. package/dist/types/src/extensions/util/react.d.ts +3 -0
  46. package/dist/types/src/extensions/util/react.d.ts.map +1 -0
  47. package/dist/types/src/hooks/useActionHandler.d.ts +1 -1
  48. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  49. package/dist/types/src/index.d.ts +1 -2
  50. package/dist/types/src/index.d.ts.map +1 -1
  51. package/dist/types/src/styles/index.d.ts +1 -1
  52. package/dist/types/src/styles/index.d.ts.map +1 -1
  53. package/dist/types/src/styles/markdown.d.ts +0 -1
  54. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  55. package/dist/types/src/styles/theme.d.ts +36 -0
  56. package/dist/types/src/styles/theme.d.ts.map +1 -0
  57. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  58. package/dist/types/src/translations.d.ts +2 -0
  59. package/dist/types/src/translations.d.ts.map +1 -1
  60. package/dist/types/src/util.d.ts.map +1 -1
  61. package/package.json +26 -29
  62. package/src/{hooks/InputMode.stories.tsx → InputMode.stories.tsx} +6 -11
  63. package/src/{hooks/TextEditor.stories.tsx → TextEditor.stories.tsx} +139 -92
  64. package/src/components/Toolbar/Toolbar.tsx +17 -9
  65. package/src/defaults.ts +28 -0
  66. package/src/extensions/autocomplete.ts +24 -18
  67. package/src/extensions/automerge/automerge.stories.tsx +4 -6
  68. package/src/extensions/comments.ts +4 -0
  69. package/src/extensions/factories.ts +3 -2
  70. package/src/extensions/folding.tsx +34 -0
  71. package/src/extensions/index.ts +1 -0
  72. package/src/extensions/markdown/bundle.ts +1 -1
  73. package/src/extensions/markdown/decorate.ts +359 -129
  74. package/src/extensions/markdown/formatting.ts +10 -12
  75. package/src/extensions/markdown/image.ts +3 -1
  76. package/src/extensions/markdown/link-paste.test.ts +28 -0
  77. package/src/extensions/markdown/link-paste.ts +104 -0
  78. package/src/extensions/markdown/parser.test.ts +47 -0
  79. package/src/extensions/markdown/table.ts +21 -24
  80. package/src/extensions/util/error.ts +15 -0
  81. package/src/extensions/util/index.ts +3 -1
  82. package/src/extensions/util/overlap.ts +1 -0
  83. package/src/extensions/util/react.tsx +15 -0
  84. package/src/hooks/useTextEditor.ts +1 -1
  85. package/src/index.ts +2 -2
  86. package/src/styles/index.ts +1 -1
  87. package/src/styles/markdown.ts +4 -3
  88. package/src/{themes/default.ts → styles/theme.ts} +51 -43
  89. package/src/styles/tokens.ts +0 -1
  90. package/src/translations.ts +2 -0
  91. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -57
  92. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +0 -1
  93. package/dist/types/src/extensions/markdown/linkPaste.d.ts +0 -16
  94. package/dist/types/src/extensions/markdown/linkPaste.d.ts.map +0 -1
  95. package/dist/types/src/extensions/markdown/linkPaste.test.d.ts +0 -2
  96. package/dist/types/src/extensions/markdown/linkPaste.test.d.ts.map +0 -1
  97. package/dist/types/src/hooks/InputMode.stories.d.ts.map +0 -1
  98. package/dist/types/src/hooks/TextEditor.stories.d.ts.map +0 -1
  99. package/dist/types/src/styles/layout.d.ts +0 -4
  100. package/dist/types/src/styles/layout.d.ts.map +0 -1
  101. package/dist/types/src/themes/default.d.ts +0 -14
  102. package/dist/types/src/themes/default.d.ts.map +0 -1
  103. package/dist/types/src/themes/index.d.ts +0 -2
  104. package/dist/types/src/themes/index.d.ts.map +0 -1
  105. package/src/components/Toolbar/Toolbar.stories.tsx +0 -119
  106. package/src/extensions/markdown/linkPaste.test.ts +0 -45
  107. package/src/extensions/markdown/linkPaste.ts +0 -113
  108. package/src/styles/layout.ts +0 -9
  109. package/src/themes/index.ts +0 -5
@@ -0,0 +1,34 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { codeFolding, foldGutter } from '@codemirror/language';
6
+ import { type Extension } from '@codemirror/state';
7
+ import React from 'react';
8
+
9
+ import { ThemeProvider } from '@dxos/react-ui';
10
+ import { defaultTx, getSize, mx } from '@dxos/react-ui-theme';
11
+
12
+ import { renderRoot } from './util';
13
+
14
+ export type FoldingOptions = {};
15
+
16
+ /**
17
+ * https://codemirror.net/examples/gutter
18
+ */
19
+ export const folding = (_props: FoldingOptions = {}): Extension => [
20
+ codeFolding({
21
+ placeholderDOM: () => document.createElement('div'),
22
+ }),
23
+ foldGutter({
24
+ markerDOM: (open) => {
25
+ return renderRoot(
26
+ <ThemeProvider tx={defaultTx}>
27
+ <svg className={mx(getSize(3), 'm-3 cursor-pointer', open && 'rotate-90')}>
28
+ <use href={'/icons.svg#ph--caret-right--regular'} />
29
+ </svg>
30
+ </ThemeProvider>,
31
+ );
32
+ },
33
+ }),
34
+ ];
@@ -14,6 +14,7 @@ export * from './debug';
14
14
  export * from './doc';
15
15
  export * from './dnd';
16
16
  export * from './factories';
17
+ export * from './folding';
17
18
  export * from './listener';
18
19
  export * from './markdown';
19
20
  export * from './mention';
@@ -15,7 +15,7 @@ import { keymap } from '@codemirror/view';
15
15
  import { type ThemeMode } from '@dxos/react-ui';
16
16
 
17
17
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
18
- import { linkPastePlugin } from './linkPaste';
18
+ import { linkPastePlugin } from './link-paste';
19
19
 
20
20
  export type MarkdownBundleOptions = {
21
21
  themeMode?: ThemeMode;
@@ -5,10 +5,19 @@
5
5
  import { syntaxTree } from '@codemirror/language';
6
6
  import { RangeSetBuilder, type EditorState, StateEffect } from '@codemirror/state';
7
7
  import { EditorView, Decoration, type DecorationSet, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
8
+ import { type SyntaxNodeRef } from '@lezer/common';
8
9
 
10
+ import { invariant } from '@dxos/invariant';
9
11
  import { mx } from '@dxos/react-ui-theme';
10
12
 
11
- import { getToken } from '../../styles';
13
+ import { image } from './image';
14
+ import { table } from './table';
15
+ import { getToken, heading, type HeadingLevel } from '../../styles';
16
+ import { wrapWithCatch } from '../util';
17
+
18
+ //
19
+ // Widgets
20
+ //
12
21
 
13
22
  class HorizontalRuleWidget extends WidgetType {
14
23
  override toDOM() {
@@ -48,14 +57,14 @@ class CheckboxWidget extends WidgetType {
48
57
 
49
58
  override toDOM(view: EditorView) {
50
59
  const input = document.createElement('input');
51
- input.className = 'cm-task-checkbox ch-checkbox ch-focus-ring -mbs-0.5';
60
+ input.className = 'cm-task-checkbox ch-checkbox ch-focus-ring';
52
61
  input.type = 'checkbox';
53
62
  input.checked = this._checked;
54
63
  if (view.state.readOnly) {
55
64
  input.setAttribute('disabled', 'true');
56
65
  } else {
57
66
  input.onmousedown = (event: Event) => {
58
- const pos = view.posAtDOM(input);
67
+ const pos = view.posAtDOM(span);
59
68
  const text = view.state.sliceDoc(pos, pos + 3);
60
69
  if (text === (this._checked ? '[x]' : '[ ]')) {
61
70
  view.dispatch({
@@ -65,7 +74,11 @@ class CheckboxWidget extends WidgetType {
65
74
  }
66
75
  };
67
76
  }
68
- return input;
77
+
78
+ const span = document.createElement('span');
79
+ span.className = 'cm-task';
80
+ span.appendChild(input);
81
+ return span;
69
82
  }
70
83
 
71
84
  override ignoreEvent() {
@@ -73,6 +86,24 @@ class CheckboxWidget extends WidgetType {
73
86
  }
74
87
  }
75
88
 
89
+ class TextWidget extends WidgetType {
90
+ constructor(
91
+ private readonly text: string,
92
+ private readonly className?: string,
93
+ ) {
94
+ super();
95
+ }
96
+
97
+ override toDOM() {
98
+ const el = document.createElement('span');
99
+ if (this.className) {
100
+ el.className = this.className;
101
+ }
102
+ el.innerText = this.text;
103
+ return el;
104
+ }
105
+ }
106
+
76
107
  const hide = Decoration.replace({});
77
108
  const fencedCodeLine = Decoration.line({ class: mx('cm-code cm-codeblock-line') });
78
109
  const fencedCodeLineFirst = Decoration.line({ class: mx('cm-code cm-codeblock-line', 'cm-codeblock-first') });
@@ -84,7 +115,9 @@ const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() }
84
115
  const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
85
116
  const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
86
117
 
87
- // Check if cursor is inside text.
118
+ /**
119
+ * Checks if cursor is inside text.
120
+ */
88
121
  const editingRange = (state: EditorState, range: { from: number; to: number }, focus: boolean) => {
89
122
  const {
90
123
  readOnly,
@@ -95,154 +128,330 @@ const editingRange = (state: EditorState, range: { from: number; to: number }, f
95
128
  return focus && !readOnly && head >= range.from && head <= range.to;
96
129
  };
97
130
 
98
- const MarksByParent = new Set(['CodeMark', 'EmphasisMark', 'StrikethroughMark', 'SubscriptMark', 'SuperscriptMark']);
131
+ const autoHideTags = new Set([
132
+ 'CodeMark',
133
+ 'CodeInfo',
134
+ 'EmphasisMark',
135
+ 'StrikethroughMark',
136
+ 'SubscriptMark',
137
+ 'SuperscriptMark',
138
+ ]);
139
+
140
+ /**
141
+ * Markdown list level.
142
+ */
143
+ type NumberingLevel = { type: string; from: number; to: number; level: number; number: number };
144
+
145
+ const bulletListIndentationWidth = 24;
146
+ const orderedListIndentationWidth = 32; // TODO(burdon): Make variable length based on number of digits.
99
147
 
100
148
  const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boolean) => {
101
149
  const deco = new RangeSetBuilder<Decoration>();
102
150
  const atomicDeco = new RangeSetBuilder<Decoration>();
103
151
  const { state } = view;
104
152
 
105
- for (const { from, to } of view.visibleRanges) {
106
- syntaxTree(state).iterate({
107
- from,
108
- to,
109
- enter: (node) => {
110
- switch (node.name) {
111
- // CommentBlock
112
- case 'CommentBlock': {
113
- const editing = editingRange(state, node, focus);
114
- for (const block of view.viewportLineBlocks) {
115
- if (block.to < node.from) {
116
- continue;
117
- }
118
- if (block.from > node.to) {
119
- break;
120
- }
121
- const first = block.from <= node.from;
122
- const last = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
123
- deco.add(
124
- block.from,
125
- block.from,
126
- first ? commentBlockLineFirst : last ? commentBlockLineLast : commentBlockLine,
127
- );
128
- if (!editing && (first || last)) {
129
- atomicDeco.add(block.from, block.to, hide);
130
- }
131
- }
132
- break;
133
- }
153
+ // Header numbering.
154
+ // TODO(burdon): Pre-parse headers to allow virtualization.
155
+ const headerLevels: (NumberingLevel | null)[] = [];
156
+ const getHeaderLevels = (node: SyntaxNodeRef, level: number): (NumberingLevel | null)[] => {
157
+ invariant(level > 0);
158
+ if (level > headerLevels.length) {
159
+ const len = headerLevels.length;
160
+ headerLevels.length = level;
161
+ headerLevels.fill(null, len);
162
+ headerLevels[level - 1] = { type: node.name, from: node.from, to: node.to, level, number: 0 };
163
+ } else {
164
+ headerLevels.splice(level);
165
+ }
134
166
 
135
- // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
136
- case 'FencedCode': {
137
- const editing = editingRange(state, node, focus);
138
- for (const block of view.viewportLineBlocks) {
139
- if (block.to < node.from) {
140
- continue;
141
- }
142
- if (block.from > node.to) {
143
- break;
144
- }
145
- const first = block.from <= node.from;
146
- const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
147
- deco.add(
148
- block.from,
149
- block.from,
150
- first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine,
151
- );
152
- if (!editing && (first || last)) {
153
- atomicDeco.add(block.from, block.to, hide);
154
- }
155
- }
156
- return false;
157
- }
167
+ return headerLevels.slice(0, level);
168
+ };
169
+
170
+ // List numbering and indentation.
171
+ const listLevels: NumberingLevel[] = [];
172
+ const enterList = (node: SyntaxNodeRef) => {
173
+ listLevels.push({ type: node.name, from: node.from, to: node.to, level: listLevels.length, number: 0 });
174
+ };
175
+ const leaveList = () => {
176
+ listLevels.pop();
177
+ };
178
+ const getCurrentList = (): NumberingLevel => {
179
+ invariant(listLevels.length);
180
+ return listLevels[listLevels.length - 1];
181
+ };
182
+
183
+ const enterNode = (node: SyntaxNodeRef) => {
184
+ // console.log('##', { node: node.name, from: node.from, to: node.to });
185
+ switch (node.name) {
186
+ // ATXHeading > HeaderMark > Paragraph
187
+ // NOTE: Numbering requires processing the entire document since otherwise only the visible range will be
188
+ // processed and the numbering will be incorrect.
189
+ // TODO(burdon): Code folding (via gutter).
190
+ // Modify parser to create foldable sections that can be skipped (or pre-processed).
191
+ case 'ATXHeading1':
192
+ case 'ATXHeading2':
193
+ case 'ATXHeading3':
194
+ case 'ATXHeading4':
195
+ case 'ATXHeading5':
196
+ case 'ATXHeading6': {
197
+ const level = parseInt(node.name['ATXHeading'.length]) as HeadingLevel;
198
+ const headers = getHeaderLevels(node, level);
199
+ if (options.numberedHeadings?.from !== undefined) {
200
+ headers[level - 1]!.number++;
201
+ }
158
202
 
159
- case 'Link': {
160
- const marks = node.node.getChildren('LinkMark');
161
- const urlNode = node.node.getChild('URL');
162
- const editing = editingRange(state, node, focus);
163
- if (urlNode && marks.length >= 2) {
164
- const url = state.sliceDoc(urlNode.from, urlNode.to);
165
- if (!editing) {
166
- atomicDeco.add(node.from, marks[0].to, hide);
167
- }
168
- deco.add(
169
- marks[0].to,
170
- marks[1].from,
171
- Decoration.mark({
172
- tagName: 'a',
173
- attributes: {
174
- class: 'cm-link',
175
- href: url,
176
- rel: 'noreferrer',
177
- target: '_blank',
178
- },
179
- }),
180
- );
181
- if (!editing) {
182
- atomicDeco.add(
183
- marks[1].from,
184
- node.to,
185
- options.renderLinkButton
186
- ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
187
- : hide,
188
- );
203
+ const editing = editingRange(state, node, focus);
204
+ if (!editing) {
205
+ const mark = node.node.firstChild!;
206
+ if (mark?.name === 'HeaderMark') {
207
+ let text = view.state.sliceDoc(mark.to, node.to).trim();
208
+ const { from, to } = options.numberedHeadings ?? {};
209
+ if (from && (!to || level <= to)) {
210
+ const num = headers
211
+ .slice(from - 1)
212
+ .map((level) => level?.number ?? 0)
213
+ .join('.');
214
+ if (num.length) {
215
+ text = `${num} ${text}`;
189
216
  }
190
217
  }
191
- break;
218
+
219
+ deco.add(
220
+ node.from,
221
+ node.to,
222
+ Decoration.replace({
223
+ widget: new TextWidget(text, heading(level)),
224
+ }),
225
+ );
192
226
  }
227
+ }
193
228
 
194
- case 'HeaderMark': {
195
- const parent = node.node.parent!;
196
- if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
197
- const next = state.doc.sliceString(node.to, node.to + 1);
198
- atomicDeco.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
199
- }
229
+ return false;
230
+ }
231
+
232
+ //
233
+ // Lists.
234
+ // [BulletList | OrderedList] > (ListItem > ListMark) > (Task > TaskMarker)?
235
+ //
236
+
237
+ case 'BulletList':
238
+ case 'OrderedList': {
239
+ enterList(node);
240
+ break;
241
+ }
242
+
243
+ case 'ListItem': {
244
+ // Set indentation.
245
+ const list = getCurrentList();
246
+ const width = list.type === 'OrderedList' ? orderedListIndentationWidth : bulletListIndentationWidth;
247
+ const offset = ((list.level ?? 0) + 1) * width;
248
+ const start = state.doc.lineAt(node.from);
249
+
250
+ deco.add(
251
+ start.from,
252
+ start.from,
253
+ Decoration.line({
254
+ class: 'cm-list-item',
255
+ attributes: {
256
+ // Subtract 0.25em to account for the space CM adds to Paragraph nodes following the ListItem.
257
+ // Note: This makes the cursor appear to be left of the margin.
258
+ style: `padding-left: ${offset}px; text-indent: calc(-${width}px - 0.25em);`,
259
+ },
260
+ }),
261
+ );
262
+
263
+ // Remove indentation spaces.
264
+ // TODO(burdon): Replace whitespace with atomic block. Parse ListMark inline.
265
+ const line = state.doc.sliceString(start.from, node.to);
266
+ const whitespace = line.match(/^ */)?.[0].length ?? 0;
267
+ if (whitespace) {
268
+ atomicDeco.add(start.from, start.from + whitespace, hide);
269
+ }
270
+
271
+ // const mark = node.node.firstChild!;
272
+ // console.log(mark?.name);
273
+ // if (mark?.name === 'ListMark') {}
274
+
275
+ break;
276
+ }
277
+
278
+ case 'ListMark': {
279
+ // Look-ahead for task marker.
280
+ const task = tree.resolve(node.to + 1, 1).name === 'TaskMarker';
281
+ if (task) {
282
+ atomicDeco.add(node.from, node.to, hide);
283
+ break;
284
+ }
285
+
286
+ // TODO(burdon): Cursor stops for 1 character when moving back into number (but not dashes).
287
+ // TODO(burdon): Option to make hierarchical; or a, b, c. etc.
288
+ const list = getCurrentList();
289
+ const label = list.type === 'OrderedList' ? `${++list.number}.` : '-';
290
+ atomicDeco.add(
291
+ node.from,
292
+ node.to,
293
+ Decoration.replace({
294
+ widget: new TextWidget(
295
+ label,
296
+ list.type === 'OrderedList' ? 'cm-list-mark cm-list-mark-ordered' : 'cm-list-mark cm-list-mark-bullet',
297
+ ),
298
+ }),
299
+ );
300
+ break;
301
+ }
302
+
303
+ case 'TaskMarker': {
304
+ if (!editingRange(state, node, focus)) {
305
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
306
+ atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task-checkbox' }));
307
+ atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
308
+ }
309
+ break;
310
+ }
311
+
312
+ // CommentBlock
313
+ case 'CommentBlock': {
314
+ const editing = editingRange(state, node, focus);
315
+ for (const block of view.viewportLineBlocks) {
316
+ if (block.to < node.from) {
317
+ continue;
318
+ }
319
+ if (block.from > node.to) {
200
320
  break;
201
321
  }
202
-
203
- case 'HorizontalRule': {
204
- if (!editingRange(state, node, focus)) {
205
- deco.add(node.from, node.to, horizontalRule);
206
- }
322
+ const first = block.from <= node.from;
323
+ const last = block.to >= node.to && /^(\s>)*-->$/.test(state.doc.sliceString(block.from, block.to));
324
+ deco.add(
325
+ block.from,
326
+ block.from,
327
+ first ? commentBlockLineFirst : last ? commentBlockLineLast : commentBlockLine,
328
+ );
329
+ if (!editing && (first || last)) {
330
+ atomicDeco.add(block.from, block.to, hide);
331
+ }
332
+ }
333
+ break;
334
+ }
335
+
336
+ // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
337
+ case 'FencedCode': {
338
+ for (const block of view.viewportLineBlocks) {
339
+ if (block.to < node.from) {
340
+ continue;
341
+ }
342
+ if (block.from > node.to) {
207
343
  break;
208
344
  }
209
345
 
210
- case 'TaskMarker': {
211
- if (!editingRange(state, node, focus)) {
212
- const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
213
- atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
214
- atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
215
- }
216
- break;
346
+ const first = block.from <= node.from;
347
+ const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
348
+ deco.add(block.from, block.from, first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine);
349
+
350
+ const editing = editingRange(state, node, focus);
351
+ if (!editing && (first || last)) {
352
+ atomicDeco.add(block.from, block.to, hide);
353
+ }
354
+ }
355
+ return false;
356
+ }
357
+
358
+ // Link > [LinkMark, URL]
359
+ case 'Link': {
360
+ const marks = node.node.getChildren('LinkMark');
361
+ const urlNode = node.node.getChild('URL');
362
+ const editing = editingRange(state, node, focus);
363
+ if (urlNode && marks.length >= 2) {
364
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
365
+ if (!editing) {
366
+ atomicDeco.add(node.from, marks[0].to, hide);
217
367
  }
218
368
 
219
- case 'ListItem': {
220
- const start = state.doc.lineAt(node.from);
221
- deco.add(start.from, start.from, Decoration.line({ class: 'cm-list-item' }));
222
- break;
369
+ deco.add(
370
+ marks[0].to,
371
+ marks[1].from,
372
+ Decoration.mark({
373
+ tagName: 'a',
374
+ attributes: {
375
+ class: 'cm-link',
376
+ href: url,
377
+ rel: 'noreferrer',
378
+ target: '_blank',
379
+ },
380
+ }),
381
+ );
382
+ if (!editing) {
383
+ atomicDeco.add(
384
+ marks[1].from,
385
+ node.to,
386
+ options.renderLinkButton
387
+ ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
388
+ : hide,
389
+ );
223
390
  }
391
+ }
392
+ break;
393
+ }
224
394
 
225
- default: {
226
- if (MarksByParent.has(node.name)) {
227
- if (!editingRange(state, node.node.parent!, focus)) {
228
- atomicDeco.add(node.from, node.to, hide);
229
- }
230
- }
395
+ // HR
396
+ case 'HorizontalRule': {
397
+ if (!editingRange(state, node, focus)) {
398
+ deco.add(node.from, node.to, horizontalRule);
399
+ }
400
+ break;
401
+ }
402
+
403
+ default: {
404
+ if (autoHideTags.has(node.name)) {
405
+ if (!editingRange(state, node.node.parent!, focus)) {
406
+ atomicDeco.add(node.from, node.to, hide);
231
407
  }
232
408
  }
233
- },
409
+ }
410
+ }
411
+ };
412
+
413
+ const leaveNode = (node: SyntaxNodeRef) => {
414
+ switch (node.name) {
415
+ case 'BulletList':
416
+ case 'OrderedList': {
417
+ leaveList();
418
+ break;
419
+ }
420
+ }
421
+ };
422
+
423
+ const tree = syntaxTree(state);
424
+ if (options.numberedHeadings?.from === undefined) {
425
+ for (const { from, to } of view.visibleRanges) {
426
+ tree.iterate({
427
+ from,
428
+ to,
429
+ enter: wrapWithCatch(enterNode),
430
+ leave: wrapWithCatch(leaveNode),
431
+ });
432
+ }
433
+ } else {
434
+ // NOTE: If line numbering then we must iterate from the start of document.
435
+ // TODO(burdon): Same for lists?
436
+ tree.iterate({
437
+ enter: wrapWithCatch(enterNode),
438
+ leave: wrapWithCatch(leaveNode),
234
439
  });
235
440
  }
236
441
 
237
- return { deco: deco.finish(), atomicDeco: atomicDeco.finish() };
442
+ return {
443
+ deco: deco.finish(),
444
+ atomicDeco: atomicDeco.finish(),
445
+ };
238
446
  };
239
447
 
240
448
  export interface DecorateOptions {
241
- renderLinkButton?: (el: Element, url: string) => void;
242
449
  /**
243
450
  * Prevents triggering decorations as the cursor moves through the document.
244
451
  */
245
452
  selectionChangeDelay?: number;
453
+ numberedHeadings?: { from: number; to?: number };
454
+ renderLinkButton?: (el: Element, url: string) => void;
246
455
  }
247
456
 
248
457
  const forceUpdate = StateEffect.define<null>();
@@ -306,6 +515,8 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
306
515
  },
307
516
  ),
308
517
  formattingStyles,
518
+ image(),
519
+ table(),
309
520
  ];
310
521
  };
311
522
 
@@ -313,6 +524,7 @@ const formattingStyles = EditorView.baseTheme({
313
524
  '& .cm-code': {
314
525
  fontFamily: getToken('fontFamily.mono', []).join(','),
315
526
  },
527
+
316
528
  '& .cm-codeblock-line': {
317
529
  paddingInline: '1rem !important',
318
530
  },
@@ -327,35 +539,53 @@ const formattingStyles = EditorView.baseTheme({
327
539
  },
328
540
  },
329
541
  '& .cm-codeblock-first': {
330
- borderTopLeftRadius: '.5rem',
331
- borderTopRightRadius: '.5rem',
542
+ borderTopLeftRadius: '.25rem',
543
+ borderTopRightRadius: '.25rem',
332
544
  },
333
545
  '& .cm-codeblock-last': {
334
- borderBottomLeftRadius: '.5rem',
335
- borderBottomRightRadius: '.5rem',
546
+ borderBottomLeftRadius: '.25rem',
547
+ borderBottomRightRadius: '.25rem',
336
548
  },
337
549
  '&light .cm-codeblock-line, &light .cm-activeLine.cm-codeblock-line': {
338
550
  background: getToken('extend.semanticColors.input.light'),
339
551
  mixBlendMode: 'darken',
340
552
  },
341
553
  '&dark .cm-codeblock-line, &dark .cm-activeLine.cm-codeblock-line': {
342
- background: getToken('extend.semanticColors.input.dark'),
554
+ background: getToken('extend.semanticColors.input.dark'), // TODO(burdon): Make darker.
343
555
  mixBlendMode: 'lighten',
344
556
  },
557
+
345
558
  '& .cm-hr': {
346
559
  display: 'inline-block',
347
560
  width: '100%',
348
561
  height: '0',
349
562
  verticalAlign: 'middle',
350
- borderTop: `1px solid ${getToken('extend.colors.neutral.200')}`,
563
+ borderTop: `1px solid ${getToken('extend.colors.primary.500')}`,
564
+ opacity: 0.5,
351
565
  },
566
+
352
567
  '& .cm-task': {
568
+ display: 'inline-block',
569
+ width: `calc(${bulletListIndentationWidth}px - 0.25em)`,
353
570
  color: getToken('extend.colors.blue.500'),
354
571
  },
355
572
  '& .cm-task-checkbox': {
356
- marginLeft: '4px',
357
- marginRight: '4px',
573
+ display: 'grid',
574
+ margin: '0',
575
+ transform: 'translateY(2px)',
358
576
  },
359
577
 
360
- // '& .cm-list-item > span:nth-child(2)': {},
578
+ '& .cm-list-item': {},
579
+ '& .cm-list-mark': {
580
+ display: 'inline-block',
581
+ textAlign: 'right',
582
+ color: getToken('extend.colors.neutral.500'),
583
+ fontVariant: 'tabular-nums',
584
+ },
585
+ '& .cm-list-mark-bullet': {
586
+ width: `${bulletListIndentationWidth}px`,
587
+ },
588
+ '& .cm-list-mark-ordered': {
589
+ width: `${orderedListIndentationWidth}px`,
590
+ },
361
591
  });