@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.
- package/dist/lib/browser/index.mjs +1019 -796
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/{hooks/InputMode.stories.d.ts → InputMode.stories.d.ts} +6 -4
- package/dist/types/src/InputMode.stories.d.ts.map +1 -0
- package/dist/types/src/{hooks/TextEditor.stories.d.ts → TextEditor.stories.d.ts} +43 -26
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -9
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +10 -0
- package/dist/types/src/defaults.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts +2 -2
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +6 -4
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/command/state.d.ts +1 -1
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts +7 -0
- package/dist/types/src/extensions/folding.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +5 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +9 -9
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link-paste.d.ts +6 -0
- package/dist/types/src/extensions/markdown/link-paste.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -1
- package/dist/types/src/extensions/util/error.d.ts +2 -0
- package/dist/types/src/extensions/util/error.d.ts.map +1 -0
- package/dist/types/src/extensions/util/index.d.ts +3 -1
- package/dist/types/src/extensions/util/index.d.ts.map +1 -1
- package/dist/types/src/extensions/util/overlap.d.ts.map +1 -1
- package/dist/types/src/extensions/util/react.d.ts +3 -0
- package/dist/types/src/extensions/util/react.d.ts.map +1 -0
- package/dist/types/src/hooks/useActionHandler.d.ts +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/index.d.ts +1 -1
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +0 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts +36 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +26 -29
- package/src/{hooks/InputMode.stories.tsx → InputMode.stories.tsx} +6 -11
- package/src/{hooks/TextEditor.stories.tsx → TextEditor.stories.tsx} +139 -92
- package/src/components/Toolbar/Toolbar.tsx +17 -9
- package/src/defaults.ts +28 -0
- package/src/extensions/autocomplete.ts +24 -18
- package/src/extensions/automerge/automerge.stories.tsx +4 -6
- package/src/extensions/comments.ts +4 -0
- package/src/extensions/factories.ts +3 -2
- package/src/extensions/folding.tsx +34 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/bundle.ts +1 -1
- package/src/extensions/markdown/decorate.ts +359 -129
- package/src/extensions/markdown/formatting.ts +10 -12
- package/src/extensions/markdown/image.ts +3 -1
- package/src/extensions/markdown/link-paste.test.ts +28 -0
- package/src/extensions/markdown/link-paste.ts +104 -0
- package/src/extensions/markdown/parser.test.ts +47 -0
- package/src/extensions/markdown/table.ts +21 -24
- package/src/extensions/util/error.ts +15 -0
- package/src/extensions/util/index.ts +3 -1
- package/src/extensions/util/overlap.ts +1 -0
- package/src/extensions/util/react.tsx +15 -0
- package/src/hooks/useTextEditor.ts +1 -1
- package/src/index.ts +2 -2
- package/src/styles/index.ts +1 -1
- package/src/styles/markdown.ts +4 -3
- package/src/{themes/default.ts → styles/theme.ts} +51 -43
- package/src/styles/tokens.ts +0 -1
- package/src/translations.ts +2 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -57
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/linkPaste.d.ts +0 -16
- package/dist/types/src/extensions/markdown/linkPaste.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/linkPaste.test.d.ts +0 -2
- package/dist/types/src/extensions/markdown/linkPaste.test.d.ts.map +0 -1
- package/dist/types/src/hooks/InputMode.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/TextEditor.stories.d.ts.map +0 -1
- package/dist/types/src/styles/layout.d.ts +0 -4
- package/dist/types/src/styles/layout.d.ts.map +0 -1
- package/dist/types/src/themes/default.d.ts +0 -14
- package/dist/types/src/themes/default.d.ts.map +0 -1
- package/dist/types/src/themes/index.d.ts +0 -2
- package/dist/types/src/themes/index.d.ts.map +0 -1
- package/src/components/Toolbar/Toolbar.stories.tsx +0 -119
- package/src/extensions/markdown/linkPaste.test.ts +0 -45
- package/src/extensions/markdown/linkPaste.ts +0 -113
- package/src/styles/layout.ts +0 -9
- 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
|
+
];
|
package/src/extensions/index.ts
CHANGED
|
@@ -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 './
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 {
|
|
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: '.
|
|
331
|
-
borderTopRightRadius: '.
|
|
542
|
+
borderTopLeftRadius: '.25rem',
|
|
543
|
+
borderTopRightRadius: '.25rem',
|
|
332
544
|
},
|
|
333
545
|
'& .cm-codeblock-last': {
|
|
334
|
-
borderBottomLeftRadius: '.
|
|
335
|
-
borderBottomRightRadius: '.
|
|
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.
|
|
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
|
-
|
|
357
|
-
|
|
573
|
+
display: 'grid',
|
|
574
|
+
margin: '0',
|
|
575
|
+
transform: 'translateY(2px)',
|
|
358
576
|
},
|
|
359
577
|
|
|
360
|
-
|
|
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
|
});
|