@dxos/react-ui-editor 0.4.5-main.ab29abe → 0.4.5-main.b567f21
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 +358 -530
- 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 +5 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- 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.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/package.json +28 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +29 -37
- package/src/components/TextEditor/hooks.stories.tsx +2 -10
- package/src/components/Toolbar/Toolbar.stories.tsx +2 -6
- package/src/components/Toolbar/Toolbar.tsx +0 -1
- 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/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
|
@@ -19,19 +19,15 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
19
19
|
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
20
20
|
import {
|
|
21
21
|
type CommentsOptions,
|
|
22
|
-
type LinkOptions,
|
|
23
22
|
autocomplete,
|
|
24
23
|
blast,
|
|
25
|
-
|
|
24
|
+
decorateMarkdown,
|
|
26
25
|
comments,
|
|
27
26
|
defaultOptions,
|
|
28
|
-
heading,
|
|
29
|
-
hr,
|
|
30
27
|
image,
|
|
31
|
-
|
|
28
|
+
linkTooltip,
|
|
32
29
|
mention,
|
|
33
30
|
table,
|
|
34
|
-
tasklist,
|
|
35
31
|
typewriter,
|
|
36
32
|
useComments,
|
|
37
33
|
formatting,
|
|
@@ -47,9 +43,11 @@ const str = (...lines: string[]) => lines.join('\n');
|
|
|
47
43
|
|
|
48
44
|
const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
const img = '';
|
|
47
|
+
|
|
51
48
|
const text = {
|
|
52
49
|
tasks: str(
|
|
50
|
+
//
|
|
53
51
|
'## Tasks',
|
|
54
52
|
'',
|
|
55
53
|
'- [x] decorator',
|
|
@@ -61,6 +59,7 @@ const text = {
|
|
|
61
59
|
),
|
|
62
60
|
|
|
63
61
|
list: str(
|
|
62
|
+
//
|
|
64
63
|
'## List',
|
|
65
64
|
'',
|
|
66
65
|
'- new york',
|
|
@@ -70,12 +69,13 @@ const text = {
|
|
|
70
69
|
),
|
|
71
70
|
|
|
72
71
|
numbered: str(
|
|
72
|
+
//
|
|
73
73
|
'## Numbered',
|
|
74
74
|
'',
|
|
75
75
|
'1. one',
|
|
76
76
|
'2. two',
|
|
77
77
|
'3. three',
|
|
78
|
-
''
|
|
78
|
+
'',
|
|
79
79
|
),
|
|
80
80
|
|
|
81
81
|
code: str(
|
|
@@ -115,7 +115,7 @@ const text = {
|
|
|
115
115
|
'',
|
|
116
116
|
),
|
|
117
117
|
|
|
118
|
-
image: str('# Image', '',
|
|
118
|
+
image: str('# Image', '', img),
|
|
119
119
|
|
|
120
120
|
headings: str(
|
|
121
121
|
...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
|
|
@@ -123,7 +123,7 @@ const text = {
|
|
|
123
123
|
|
|
124
124
|
paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
|
|
125
125
|
|
|
126
|
-
footer: str('', '', '', '', '')
|
|
126
|
+
footer: str('', '', '', '', ''),
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
const document = str(
|
|
@@ -173,7 +173,7 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
|
|
|
173
173
|
const hover =
|
|
174
174
|
'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
|
|
175
175
|
|
|
176
|
-
const
|
|
176
|
+
const renderLinkTooltip = (el: Element, url: string) => {
|
|
177
177
|
const web = new URL(url);
|
|
178
178
|
createRoot(el).render(
|
|
179
179
|
<StrictMode>
|
|
@@ -206,7 +206,7 @@ const onCommentsHover: CommentsOptions['onHover'] = (el, shortcut) => {
|
|
|
206
206
|
);
|
|
207
207
|
};
|
|
208
208
|
|
|
209
|
-
const
|
|
209
|
+
const renderLinkButton = (el: Element, url: string) => {
|
|
210
210
|
createRoot(el).render(
|
|
211
211
|
<StrictMode>
|
|
212
212
|
<a href={url} target='_blank' rel='noreferrer' className={hover}>
|
|
@@ -257,14 +257,11 @@ const defaults = [
|
|
|
257
257
|
autocomplete({
|
|
258
258
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
259
259
|
}),
|
|
260
|
-
|
|
260
|
+
decorateMarkdown({ renderLinkButton }),
|
|
261
261
|
formatting(),
|
|
262
|
-
heading(),
|
|
263
|
-
hr(),
|
|
264
262
|
image(),
|
|
265
|
-
|
|
263
|
+
linkTooltip(renderLinkTooltip),
|
|
266
264
|
table(),
|
|
267
|
-
tasklist(),
|
|
268
265
|
];
|
|
269
266
|
|
|
270
267
|
export const Default = {
|
|
@@ -281,6 +278,11 @@ export const NoExtensions = {
|
|
|
281
278
|
|
|
282
279
|
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
283
280
|
|
|
281
|
+
const largeWithImages = faker.helpers
|
|
282
|
+
.multiple(() => [faker.lorem.paragraph({ min: 12, max: 16 }), img], { count: 20 })
|
|
283
|
+
.flatMap((x) => x)
|
|
284
|
+
.join('\n\n');
|
|
285
|
+
|
|
284
286
|
export const Empty = {
|
|
285
287
|
render: () => <Story />,
|
|
286
288
|
};
|
|
@@ -289,26 +291,25 @@ export const Scrolling = {
|
|
|
289
291
|
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
290
292
|
};
|
|
291
293
|
|
|
292
|
-
export const
|
|
293
|
-
render: () => (
|
|
294
|
-
<Story
|
|
295
|
-
text={str(text.links, text.footer)}
|
|
296
|
-
extensions={[link({ onHover: onHoverLinkTooltip, onRender: onRenderLink })]}
|
|
297
|
-
/>
|
|
298
|
-
),
|
|
294
|
+
export const ScrollingWithImages = {
|
|
295
|
+
render: () => <Story text={str('# Large Document', '', largeWithImages)} extensions={[image()]} />,
|
|
299
296
|
};
|
|
300
297
|
|
|
301
|
-
export const
|
|
302
|
-
render: () => <Story text={str(text.
|
|
298
|
+
export const Links = {
|
|
299
|
+
render: () => <Story text={str(text.links, text.footer)} extensions={[linkTooltip(renderLinkTooltip)]} />,
|
|
303
300
|
};
|
|
304
301
|
|
|
305
302
|
export const Image = {
|
|
306
303
|
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
307
304
|
};
|
|
308
305
|
|
|
306
|
+
export const Code = {
|
|
307
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[decorateMarkdown()]} />,
|
|
308
|
+
};
|
|
309
|
+
|
|
309
310
|
export const Lists = {
|
|
310
311
|
render: () => (
|
|
311
|
-
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[
|
|
312
|
+
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[decorateMarkdown()]} />
|
|
312
313
|
),
|
|
313
314
|
};
|
|
314
315
|
|
|
@@ -330,7 +331,7 @@ export const Autocomplete = {
|
|
|
330
331
|
<Story
|
|
331
332
|
text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
|
|
332
333
|
extensions={[
|
|
333
|
-
|
|
334
|
+
decorateMarkdown({ renderLinkButton }),
|
|
334
335
|
autocomplete({
|
|
335
336
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
336
337
|
}),
|
|
@@ -388,15 +389,6 @@ export const Comments = {
|
|
|
388
389
|
},
|
|
389
390
|
};
|
|
390
391
|
|
|
391
|
-
export const HorizontalRule = {
|
|
392
|
-
render: () => (
|
|
393
|
-
<Story
|
|
394
|
-
text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
|
|
395
|
-
extensions={[hr()]}
|
|
396
|
-
/>
|
|
397
|
-
),
|
|
398
|
-
};
|
|
399
|
-
|
|
400
392
|
export const Vim = {
|
|
401
393
|
render: () => (
|
|
402
394
|
<Story
|
|
@@ -14,15 +14,11 @@ import { TextEditor } from './TextEditor';
|
|
|
14
14
|
import {
|
|
15
15
|
type EditorMode,
|
|
16
16
|
EditorModes,
|
|
17
|
-
|
|
17
|
+
decorateMarkdown,
|
|
18
18
|
createMarkdownExtensions,
|
|
19
19
|
formatting,
|
|
20
|
-
heading,
|
|
21
|
-
hr,
|
|
22
20
|
image,
|
|
23
|
-
link,
|
|
24
21
|
table,
|
|
25
|
-
tasklist,
|
|
26
22
|
useFormattingState,
|
|
27
23
|
} from '../../extensions';
|
|
28
24
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
@@ -62,14 +58,10 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
62
58
|
// TODO(burdon): Move lineWrapping.
|
|
63
59
|
createMarkdownExtensions({ placeholder, lineWrapping: true }),
|
|
64
60
|
// TODO(burdon): Move into markdown bundle (with React callbacks).
|
|
65
|
-
|
|
61
|
+
decorateMarkdown(),
|
|
66
62
|
formatting(),
|
|
67
|
-
heading(),
|
|
68
|
-
hr(),
|
|
69
63
|
image(),
|
|
70
|
-
link(),
|
|
71
64
|
table(),
|
|
72
|
-
tasklist(),
|
|
73
65
|
trackFormatting,
|
|
74
66
|
],
|
|
75
67
|
[editorMode, themeMode, placeholder, trackFormatting, readonly],
|
|
@@ -14,13 +14,11 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
14
14
|
|
|
15
15
|
import { Toolbar } from './Toolbar';
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
decorateMarkdown,
|
|
18
18
|
comments,
|
|
19
19
|
formatting,
|
|
20
|
-
heading,
|
|
21
20
|
image,
|
|
22
21
|
table,
|
|
23
|
-
tasklist,
|
|
24
22
|
useComments,
|
|
25
23
|
useFormattingState,
|
|
26
24
|
} from '../../extensions';
|
|
@@ -39,7 +37,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
39
37
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
40
38
|
const extensions = useMemo(
|
|
41
39
|
() => [
|
|
42
|
-
|
|
40
|
+
decorateMarkdown(),
|
|
43
41
|
comments({
|
|
44
42
|
onCreate: ({ cursor }) => {
|
|
45
43
|
const id = PublicKey.random().toHex();
|
|
@@ -48,10 +46,8 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
48
46
|
},
|
|
49
47
|
}),
|
|
50
48
|
formatting(),
|
|
51
|
-
heading(),
|
|
52
49
|
image(),
|
|
53
50
|
table(),
|
|
54
|
-
tasklist(),
|
|
55
51
|
formattingObserver,
|
|
56
52
|
],
|
|
57
53
|
[],
|
|
@@ -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', () => {
|