@dxos/react-ui-editor 0.4.5-main.f8d6c4e → 0.4.5
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 +464 -572
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +7 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts +2 -0
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +2 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +2 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +8 -0
- package/dist/types/src/extensions/cursor-line-margin.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 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -4
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -11
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/layout.d.ts +2 -2
- package/dist/types/src/styles/layout.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/package.json +29 -26
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +29 -37
- package/src/components/TextEditor/hooks.stories.tsx +5 -17
- package/src/components/Toolbar/Toolbar.stories.tsx +4 -7
- package/src/components/Toolbar/Toolbar.tsx +79 -47
- package/src/extensions/cursor-line-margin.ts +92 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/decorate.ts +301 -0
- package/src/extensions/markdown/formatting.test.ts +17 -0
- package/src/extensions/markdown/formatting.ts +67 -76
- package/src/extensions/markdown/index.ts +1 -4
- package/src/extensions/markdown/link.ts +23 -185
- package/src/index.ts +2 -0
- package/src/styles/layout.ts +3 -2
- package/src/translations.ts +5 -3
- package/dist/types/src/extensions/markdown/code.d.ts +0 -2
- package/dist/types/src/extensions/markdown/code.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/heading.d.ts +0 -6
- package/dist/types/src/extensions/markdown/heading.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/hr.d.ts +0 -2
- package/dist/types/src/extensions/markdown/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/tasklist.d.ts +0 -3
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +0 -1
- package/src/extensions/markdown/code.ts +0 -162
- package/src/extensions/markdown/heading.ts +0 -59
- package/src/extensions/markdown/hr.ts +0 -71
- package/src/extensions/markdown/tasklist.ts +0 -110
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { RangeSetBuilder, type EditorState } from '@codemirror/state';
|
|
7
|
+
import { EditorView, Decoration, type DecorationSet, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
import { getToken } from '../../styles';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Widget for first/last line of code block (read-only).
|
|
15
|
+
*/
|
|
16
|
+
// TODO(burdon): Add copy-to-clipboard button.
|
|
17
|
+
class FencedCodeBoundary extends WidgetType {
|
|
18
|
+
constructor(private readonly _className: string) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override toDOM() {
|
|
23
|
+
const el = document.createElement('span');
|
|
24
|
+
el.innerHTML = ' ';
|
|
25
|
+
el.className = mx('cm-code cm-codeblock', this._className);
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class HorizontalRuleWidget extends WidgetType {
|
|
31
|
+
override toDOM() {
|
|
32
|
+
const el = document.createElement('span');
|
|
33
|
+
el.className = 'cm-hr';
|
|
34
|
+
return el;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class LinkButton extends WidgetType {
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly url: string,
|
|
41
|
+
private readonly render: (el: Element, url: string) => void,
|
|
42
|
+
) {
|
|
43
|
+
super();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override eq(other: this) {
|
|
47
|
+
return this.url === other.url;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override toDOM(view: EditorView) {
|
|
51
|
+
const el = document.createElement('span');
|
|
52
|
+
this.render(el, this.url);
|
|
53
|
+
return el;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class CheckboxWidget extends WidgetType {
|
|
58
|
+
constructor(private _checked: boolean) {
|
|
59
|
+
super();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override eq(other: this) {
|
|
63
|
+
return this._checked === other._checked;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override toDOM(view: EditorView) {
|
|
67
|
+
const input = document.createElement('input');
|
|
68
|
+
input.className = 'cm-task-checkbox';
|
|
69
|
+
input.type = 'checkbox';
|
|
70
|
+
input.checked = this._checked;
|
|
71
|
+
if (view.state.readOnly) {
|
|
72
|
+
input.setAttribute('disabled', 'true');
|
|
73
|
+
} else {
|
|
74
|
+
input.onmousedown = (event: Event) => {
|
|
75
|
+
const pos = view.posAtDOM(input);
|
|
76
|
+
const text = view.state.sliceDoc(pos, pos + 3);
|
|
77
|
+
if (text === (this._checked ? '[x]' : '[ ]')) {
|
|
78
|
+
view.dispatch({
|
|
79
|
+
changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
|
|
80
|
+
});
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return input;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override ignoreEvent() {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hide = Decoration.replace({});
|
|
94
|
+
const fencedCodeTop = Decoration.replace({ widget: new FencedCodeBoundary('cm-codeblock-end cm-codeblock-first') });
|
|
95
|
+
const fencedCodeBottom = Decoration.replace({ widget: new FencedCodeBoundary('cm-codeblock-end cm-codeblock-last') });
|
|
96
|
+
const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() });
|
|
97
|
+
const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
|
|
98
|
+
const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
|
|
99
|
+
|
|
100
|
+
const editingRange = (state: EditorState, range: { from: number; to: number }) => {
|
|
101
|
+
const {
|
|
102
|
+
readOnly,
|
|
103
|
+
selection: {
|
|
104
|
+
main: { head },
|
|
105
|
+
},
|
|
106
|
+
} = state;
|
|
107
|
+
return !readOnly && head >= range.from && head <= range.to;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const MarksByParent = new Set(['CodeMark', 'EmphasisMark', 'StrikethroughMark', 'SubscriptMark', 'SuperscriptMark']);
|
|
111
|
+
|
|
112
|
+
const buildDecorations = (view: EditorView, options: DecorateOptions): DecorationSet => {
|
|
113
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
114
|
+
const { state } = view;
|
|
115
|
+
|
|
116
|
+
for (const { from, to } of view.visibleRanges) {
|
|
117
|
+
syntaxTree(state).iterate({
|
|
118
|
+
from,
|
|
119
|
+
to,
|
|
120
|
+
enter: (node) => {
|
|
121
|
+
if (node.name === 'FencedCode') {
|
|
122
|
+
const editing = editingRange(state, node);
|
|
123
|
+
// FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
|
|
124
|
+
for (const block of view.viewportLineBlocks) {
|
|
125
|
+
if (block.to < node.from) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (block.from > node.to) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
const first = block.from <= node.from;
|
|
132
|
+
const last = block.to >= node.to;
|
|
133
|
+
if (!editing && (first || last)) {
|
|
134
|
+
builder.add(block.from, block.to, first ? fencedCodeTop : fencedCodeBottom);
|
|
135
|
+
} else {
|
|
136
|
+
builder.add(
|
|
137
|
+
block.from,
|
|
138
|
+
block.from,
|
|
139
|
+
Decoration.line({
|
|
140
|
+
class: mx('cm-code cm-codeblock-line', first && 'cm-codeblock-first', last && 'cm-codeblock-last'),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
} else if (node.name === 'Link') {
|
|
147
|
+
const marks = node.node.getChildren('LinkMark');
|
|
148
|
+
const urlNode = node.node.getChild('URL');
|
|
149
|
+
const editing = editingRange(state, node);
|
|
150
|
+
if (urlNode && marks.length >= 2) {
|
|
151
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
152
|
+
if (!editing) {
|
|
153
|
+
builder.add(node.from, marks[0].to, hide);
|
|
154
|
+
}
|
|
155
|
+
builder.add(
|
|
156
|
+
marks[0].to,
|
|
157
|
+
marks[1].from,
|
|
158
|
+
Decoration.mark({
|
|
159
|
+
tagName: 'a',
|
|
160
|
+
attributes: {
|
|
161
|
+
class: 'cm-link',
|
|
162
|
+
href: url,
|
|
163
|
+
rel: 'noreferrer',
|
|
164
|
+
target: '_blank',
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
if (!editing) {
|
|
169
|
+
builder.add(
|
|
170
|
+
marks[1].from,
|
|
171
|
+
node.to,
|
|
172
|
+
options.renderLinkButton
|
|
173
|
+
? Decoration.replace({
|
|
174
|
+
widget: new LinkButton(url, options.renderLinkButton),
|
|
175
|
+
})
|
|
176
|
+
: hide,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else if (node.name === 'HeaderMark') {
|
|
181
|
+
const parent = node.node.parent!;
|
|
182
|
+
if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from))) {
|
|
183
|
+
const next = state.doc.sliceString(node.to, node.to + 1);
|
|
184
|
+
builder.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
|
|
185
|
+
}
|
|
186
|
+
} else if (node.name === 'HorizontalRule') {
|
|
187
|
+
if (!editingRange(state, node)) {
|
|
188
|
+
builder.add(node.from, node.to, horizontalRule);
|
|
189
|
+
}
|
|
190
|
+
} else if (node.name === 'TaskMarker') {
|
|
191
|
+
// Check if cursor is inside text.
|
|
192
|
+
if (!editingRange(state, node)) {
|
|
193
|
+
const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
|
|
194
|
+
builder.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
|
|
195
|
+
builder.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
|
|
196
|
+
}
|
|
197
|
+
} else if (MarksByParent.has(node.name)) {
|
|
198
|
+
if (!editingRange(state, node.node.parent!)) {
|
|
199
|
+
builder.add(node.from, node.to, hide);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return builder.finish();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export interface DecorateOptions {
|
|
210
|
+
renderLinkButton?: (el: Element, url: string) => void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
214
|
+
return [
|
|
215
|
+
ViewPlugin.fromClass(
|
|
216
|
+
class {
|
|
217
|
+
decorations: DecorationSet;
|
|
218
|
+
constructor(view: EditorView) {
|
|
219
|
+
this.decorations = buildDecorations(view, options);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
update(update: ViewUpdate) {
|
|
223
|
+
if (update.docChanged || update.selectionSet || update.viewportChanged) {
|
|
224
|
+
this.decorations = buildDecorations(update.view, options);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
decorations: (value) => value.decorations,
|
|
230
|
+
},
|
|
231
|
+
),
|
|
232
|
+
formattingStyles,
|
|
233
|
+
];
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const formattingStyles = EditorView.baseTheme({
|
|
237
|
+
'& .cm-code': {
|
|
238
|
+
fontFamily: getToken('fontFamily.mono', []).join(','),
|
|
239
|
+
},
|
|
240
|
+
'& .cm-codeblock-line': {
|
|
241
|
+
paddingInline: '1rem !important',
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
'& .cm-codeblock-end': {
|
|
245
|
+
display: 'inline-block',
|
|
246
|
+
width: '100%',
|
|
247
|
+
position: 'relative',
|
|
248
|
+
'&::after': {
|
|
249
|
+
position: 'absolute',
|
|
250
|
+
inset: 0,
|
|
251
|
+
content: '""',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
'& .cm-codeblock-first': {
|
|
255
|
+
'&::after': {
|
|
256
|
+
borderTopLeftRadius: '.5rem',
|
|
257
|
+
borderTopRightRadius: '.5rem',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
'& .cm-codeblock-last': {
|
|
261
|
+
'&::after': {
|
|
262
|
+
borderBottomLeftRadius: '.5rem',
|
|
263
|
+
borderBottomRightRadius: '.5rem',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
'&light .cm-codeblock-line, &light .cm-activeLine.cm-codeblock-line': {
|
|
268
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
269
|
+
mixBlendMode: 'darken',
|
|
270
|
+
},
|
|
271
|
+
'&dark .cm-codeblock-line, &dark .cm-activeLine.cm-codeblock-line': {
|
|
272
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
273
|
+
mixBlendMode: 'lighten',
|
|
274
|
+
},
|
|
275
|
+
'&light .cm-codeblock-first, &light .cm-codeblock-last': {
|
|
276
|
+
mixBlendMode: 'darken',
|
|
277
|
+
'&::after': {
|
|
278
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
'&dark .cm-codeblock-first, &dark .cm-codeblock-last': {
|
|
282
|
+
mixBlendMode: 'lighten',
|
|
283
|
+
'&::after': {
|
|
284
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
'& .cm-hr': {
|
|
288
|
+
display: 'inline-block',
|
|
289
|
+
width: '100%',
|
|
290
|
+
height: '0',
|
|
291
|
+
verticalAlign: 'middle',
|
|
292
|
+
borderTop: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
293
|
+
},
|
|
294
|
+
'& .cm-task': {
|
|
295
|
+
color: getToken('extend.colors.blue.500'),
|
|
296
|
+
},
|
|
297
|
+
'& .cm-task-checkbox': {
|
|
298
|
+
marginLeft: '4px',
|
|
299
|
+
marginRight: '4px',
|
|
300
|
+
},
|
|
301
|
+
});
|
|
@@ -101,6 +101,8 @@ describe('setHeading', () => {
|
|
|
101
101
|
testCommand('can add a heading inside block markup', '> - {one\n> - two}\n', setHeading(1), '> - # one\n> - # two\n');
|
|
102
102
|
|
|
103
103
|
testCommand('can remove a heading inside block markup', '1. # one{}', setHeading(0), '1. one');
|
|
104
|
+
|
|
105
|
+
testCommand('can add a heading to a blank line', 'one\n\n{}', setHeading(1), 'one\n\n# {}');
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
describe('addStyle', () => {
|
|
@@ -158,6 +160,8 @@ describe('addStyle', () => {
|
|
|
158
160
|
em,
|
|
159
161
|
'*[one {two](x) * five',
|
|
160
162
|
);
|
|
163
|
+
|
|
164
|
+
testCommand('can add a style to an empty line', '{}', em, '*{}*');
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
describe('removeStyle', () => {
|
|
@@ -188,6 +192,15 @@ describe('removeStyle', () => {
|
|
|
188
192
|
);
|
|
189
193
|
|
|
190
194
|
testCommand('can shrink existing styles', '*one {two three} four*', em, '*one* {two three} *four*');
|
|
195
|
+
|
|
196
|
+
testCommand('can remove strong from a partially emphasized text', '**{one*two*}**', str, '{one*two*}');
|
|
197
|
+
|
|
198
|
+
testCommand(
|
|
199
|
+
'can remove strong from a partially emphasized text with markers outside',
|
|
200
|
+
'***{o*ne*two}***',
|
|
201
|
+
str,
|
|
202
|
+
'*{o*ne*two}*',
|
|
203
|
+
);
|
|
191
204
|
});
|
|
192
205
|
|
|
193
206
|
describe('addLink', () => {
|
|
@@ -284,6 +297,8 @@ describe('addList', () => {
|
|
|
284
297
|
);
|
|
285
298
|
|
|
286
299
|
testCommand("doesn't renumber lists with a different parent", '> one{}\n\n1. two', ordered, '> 1. one\n\n1. two');
|
|
300
|
+
|
|
301
|
+
testCommand('can add a list to an empty line', '{}', ordered, '1. {}');
|
|
287
302
|
});
|
|
288
303
|
|
|
289
304
|
describe('removeList', () => {
|
|
@@ -345,6 +360,8 @@ describe('addBlockquote', () => {
|
|
|
345
360
|
addBlockquote,
|
|
346
361
|
'> one\n> two\n>\n> # three\n\nfour',
|
|
347
362
|
);
|
|
363
|
+
|
|
364
|
+
testCommand('can add a blockquote to an empty line', '{}', addBlockquote, '>{}');
|
|
348
365
|
});
|
|
349
366
|
|
|
350
367
|
describe('removeBlockquote', () => {
|
|
@@ -7,14 +7,13 @@ import { syntaxTree } from '@codemirror/language';
|
|
|
7
7
|
import {
|
|
8
8
|
type Extension,
|
|
9
9
|
type StateCommand,
|
|
10
|
-
RangeSetBuilder,
|
|
11
10
|
type EditorState,
|
|
12
11
|
type ChangeSpec,
|
|
13
12
|
type Text,
|
|
14
13
|
EditorSelection,
|
|
15
14
|
type Line,
|
|
16
15
|
} from '@codemirror/state';
|
|
17
|
-
import {
|
|
16
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
18
17
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
|
19
18
|
import { useState, useMemo } from 'react';
|
|
20
19
|
|
|
@@ -94,6 +93,7 @@ export const setHeading =
|
|
|
94
93
|
const changes: ChangeSpec[] = [];
|
|
95
94
|
let prevBlock = -1;
|
|
96
95
|
for (const range of ranges) {
|
|
96
|
+
let sawBlock = false;
|
|
97
97
|
syntaxTree(state).iterate({
|
|
98
98
|
from: range.from,
|
|
99
99
|
to: range.to,
|
|
@@ -101,6 +101,7 @@ export const setHeading =
|
|
|
101
101
|
if (!Object.hasOwn(Textblocks, node.name) || prevBlock === node.from) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
+
sawBlock = true;
|
|
104
105
|
prevBlock = node.from;
|
|
105
106
|
const blockType = Textblocks[node.name];
|
|
106
107
|
const isHeading = /heading(\d)/.exec(blockType);
|
|
@@ -129,13 +130,25 @@ export const setHeading =
|
|
|
129
130
|
}
|
|
130
131
|
},
|
|
131
132
|
});
|
|
133
|
+
let line;
|
|
134
|
+
if (!sawBlock && range.empty && level > 0 && !/\S/.test((line = state.doc.lineAt(range.from)).text)) {
|
|
135
|
+
changes.push({ from: line.from, to: line.to, insert: '#'.repeat(level) + ' ' });
|
|
136
|
+
}
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
if (!changes.length) {
|
|
135
140
|
return false;
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
|
|
143
|
+
const changeSet = state.changes(changes);
|
|
144
|
+
dispatch(
|
|
145
|
+
state.update({
|
|
146
|
+
changes: changeSet,
|
|
147
|
+
selection: state.selection.map(changeSet, 1),
|
|
148
|
+
userEvent: 'format.setHeading',
|
|
149
|
+
scrollIntoView: true,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
139
152
|
return true;
|
|
140
153
|
};
|
|
141
154
|
|
|
@@ -174,8 +187,8 @@ export const setStyle =
|
|
|
174
187
|
const changesAtEnd: ChangeSpec[] = [];
|
|
175
188
|
let blockStart = -1;
|
|
176
189
|
let blockEnd = -1;
|
|
177
|
-
let startCovered: boolean |
|
|
178
|
-
let endCovered: boolean |
|
|
190
|
+
let startCovered: boolean | { from: number; to: number } = false;
|
|
191
|
+
let endCovered: boolean | { from: number; to: number } = false;
|
|
179
192
|
let { from, to } = range;
|
|
180
193
|
// Iterate the selected range. For each textblock, determine a
|
|
181
194
|
// start and end position, the overlap of the selected range and
|
|
@@ -218,10 +231,16 @@ export const setStyle =
|
|
|
218
231
|
// by this.
|
|
219
232
|
if (markType === type) {
|
|
220
233
|
if (openEnd <= from && closeStart >= from) {
|
|
221
|
-
startCovered =
|
|
234
|
+
startCovered =
|
|
235
|
+
!enable && openEnd === skipMarkers(from, node.node, -1, openEnd)
|
|
236
|
+
? { from: node.from, to: openEnd }
|
|
237
|
+
: true;
|
|
222
238
|
}
|
|
223
239
|
if (openEnd <= to && closeStart >= to) {
|
|
224
|
-
endCovered =
|
|
240
|
+
endCovered =
|
|
241
|
+
!enable && closeStart === skipMarkers(to, node.node, 1, closeStart)
|
|
242
|
+
? { from: closeStart, to: node.to }
|
|
243
|
+
: true;
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
// Marks of the same type in range, or any mark if we're
|
|
@@ -263,13 +282,13 @@ export const setStyle =
|
|
|
263
282
|
changes.push({ from: rangeEnd, insert: marker });
|
|
264
283
|
}
|
|
265
284
|
} else {
|
|
266
|
-
if (startCovered === '
|
|
267
|
-
changes.push(
|
|
285
|
+
if (typeof startCovered === 'object') {
|
|
286
|
+
changes.push(startCovered);
|
|
268
287
|
} else if (startCovered) {
|
|
269
288
|
changes.push({ from: skipSpaces(rangeStart, state.doc, -1, blockStart), insert: marker });
|
|
270
289
|
}
|
|
271
|
-
if (endCovered === '
|
|
272
|
-
changes.push(
|
|
290
|
+
if (typeof endCovered === 'object') {
|
|
291
|
+
changes.push(endCovered);
|
|
273
292
|
} else if (endCovered) {
|
|
274
293
|
changes.push({ from: skipSpaces(rangeEnd, state.doc, 1, blockEnd), insert: marker });
|
|
275
294
|
}
|
|
@@ -277,6 +296,12 @@ export const setStyle =
|
|
|
277
296
|
}
|
|
278
297
|
},
|
|
279
298
|
});
|
|
299
|
+
if (blockStart < 0 && range.empty && enable && !/\S/.test(state.doc.lineAt(range.from).text)) {
|
|
300
|
+
return {
|
|
301
|
+
changes: { from: range.head, insert: marker + marker },
|
|
302
|
+
range: EditorSelection.cursor(range.head + marker.length),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
280
305
|
const changeSet = state.changes(changes.concat(changesAtEnd));
|
|
281
306
|
return {
|
|
282
307
|
changes: changeSet,
|
|
@@ -350,6 +375,20 @@ const skipSpaces = (pos: number, doc: Text, dir: -1 | 1, limit?: number) => {
|
|
|
350
375
|
return pos;
|
|
351
376
|
};
|
|
352
377
|
|
|
378
|
+
const skipMarkers = (pos: number, tree: SyntaxNode, dir: -1 | 1, limit?: number) => {
|
|
379
|
+
for (;;) {
|
|
380
|
+
const next = tree.resolve(pos, dir);
|
|
381
|
+
if (!IgnoreInline.has(next.name)) {
|
|
382
|
+
return pos;
|
|
383
|
+
}
|
|
384
|
+
const moveTo = dir < 0 ? next.from : next.to;
|
|
385
|
+
if (limit != null && (dir < 0 ? moveTo < limit : moveTo > limit)) {
|
|
386
|
+
return pos;
|
|
387
|
+
}
|
|
388
|
+
pos = moveTo;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
353
392
|
// TODO(burdon): Define and trigger snippets for codeblock, table, etc.
|
|
354
393
|
const snippets = {
|
|
355
394
|
codeblock: snippet(
|
|
@@ -515,7 +554,6 @@ export const addLink: StateCommand = ({ state, dispatch }) => {
|
|
|
515
554
|
// Lists
|
|
516
555
|
//
|
|
517
556
|
|
|
518
|
-
// TODO(burdon): Cursor is positioned incorrectly.
|
|
519
557
|
export const addList =
|
|
520
558
|
(type: List): StateCommand =>
|
|
521
559
|
({ state, dispatch }) => {
|
|
@@ -654,7 +692,15 @@ export const addList =
|
|
|
654
692
|
}
|
|
655
693
|
}
|
|
656
694
|
|
|
657
|
-
|
|
695
|
+
const changeSet = state.changes(changes);
|
|
696
|
+
dispatch(
|
|
697
|
+
state.update({
|
|
698
|
+
changes: changeSet,
|
|
699
|
+
selection: state.selection.map(changeSet, 1),
|
|
700
|
+
userEvent: 'format.list.add',
|
|
701
|
+
scrollIntoView: true,
|
|
702
|
+
}),
|
|
703
|
+
);
|
|
658
704
|
return true;
|
|
659
705
|
};
|
|
660
706
|
|
|
@@ -751,6 +797,7 @@ export const setBlockquote =
|
|
|
751
797
|
const lines: Line[] = [];
|
|
752
798
|
let lastBlock = -1;
|
|
753
799
|
for (const { from, to } of state.selection.ranges) {
|
|
800
|
+
const sawBlock = false;
|
|
754
801
|
syntaxTree(state).iterate({
|
|
755
802
|
from,
|
|
756
803
|
to,
|
|
@@ -786,6 +833,10 @@ export const setBlockquote =
|
|
|
786
833
|
}
|
|
787
834
|
},
|
|
788
835
|
});
|
|
836
|
+
let line;
|
|
837
|
+
if (!sawBlock && enable && from === to && !/\S/.test((line = state.doc.lineAt(from)).text)) {
|
|
838
|
+
lines.push(line);
|
|
839
|
+
}
|
|
789
840
|
}
|
|
790
841
|
|
|
791
842
|
const changes: ChangeSpec[] = [];
|
|
@@ -803,9 +854,11 @@ export const setBlockquote =
|
|
|
803
854
|
return false;
|
|
804
855
|
}
|
|
805
856
|
|
|
857
|
+
const changeSet = state.changes(changes);
|
|
806
858
|
dispatch(
|
|
807
859
|
state.update({
|
|
808
|
-
changes,
|
|
860
|
+
changes: changeSet,
|
|
861
|
+
selection: state.selection.map(changeSet, 1),
|
|
809
862
|
userEvent: enable ? 'format.blockquote.add' : 'format.blockquote.remove',
|
|
810
863
|
scrollIntoView: true,
|
|
811
864
|
}),
|
|
@@ -942,68 +995,6 @@ export const formatting = (options: FormattingOptions = {}): Extension => {
|
|
|
942
995
|
run: toggleStrong,
|
|
943
996
|
},
|
|
944
997
|
]),
|
|
945
|
-
styling(),
|
|
946
|
-
];
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
const styling = (): Extension => {
|
|
950
|
-
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
951
|
-
const builder = new RangeSetBuilder<Decoration>();
|
|
952
|
-
const { state } = view;
|
|
953
|
-
const cursor = state.selection.main.head;
|
|
954
|
-
|
|
955
|
-
// TODO(burdon): Bug if '***foo***' (since StrongEmphasis is nested inside EmphasisMark).
|
|
956
|
-
// Ranges must be added sorted by `from` position and `startSide`.
|
|
957
|
-
const replace = (node: SyntaxNodeRef, marks: SyntaxNode[]) => {
|
|
958
|
-
if (cursor <= node.from || cursor >= node.to) {
|
|
959
|
-
for (const mark of marks) {
|
|
960
|
-
builder.add(mark.from, mark.to, Decoration.replace({}));
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
|
|
965
|
-
for (const { from, to } of view.visibleRanges) {
|
|
966
|
-
syntaxTree(state).iterate({
|
|
967
|
-
enter: (node) => {
|
|
968
|
-
switch (node.name) {
|
|
969
|
-
case 'Emphasis':
|
|
970
|
-
case 'StrongEmphasis': {
|
|
971
|
-
const marks = node.node.getChildren('EmphasisMark');
|
|
972
|
-
replace(node, marks);
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
case 'Strikethrough': {
|
|
977
|
-
const marks = node.node.getChildren('StrikethroughMark');
|
|
978
|
-
replace(node, marks);
|
|
979
|
-
break;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
},
|
|
983
|
-
from,
|
|
984
|
-
to,
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return builder.finish();
|
|
989
|
-
};
|
|
990
|
-
|
|
991
|
-
return [
|
|
992
|
-
ViewPlugin.fromClass(
|
|
993
|
-
class {
|
|
994
|
-
decorations: DecorationSet;
|
|
995
|
-
constructor(view: EditorView) {
|
|
996
|
-
this.decorations = buildDecorations(view);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
update(update: ViewUpdate) {
|
|
1000
|
-
this.decorations = buildDecorations(update.view);
|
|
1001
|
-
}
|
|
1002
|
-
},
|
|
1003
|
-
{
|
|
1004
|
-
decorations: (value) => value.decorations,
|
|
1005
|
-
},
|
|
1006
|
-
),
|
|
1007
998
|
];
|
|
1008
999
|
};
|
|
1009
1000
|
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './bundle';
|
|
6
|
-
export * from './
|
|
6
|
+
export * from './decorate';
|
|
7
7
|
export * from './formatting';
|
|
8
|
-
export * from './heading';
|
|
9
8
|
export * from './highlight';
|
|
10
|
-
export * from './hr';
|
|
11
9
|
export * from './image';
|
|
12
10
|
export * from './link';
|
|
13
11
|
export * from './table';
|
|
14
|
-
export * from './tasklist';
|