@dxos/react-ui-editor 0.4.5-main.519e45d → 0.4.5-main.5fcdf8d
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 +505 -291
- 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.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts +2 -0
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/hr.d.ts +2 -0
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +4 -1
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +11 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/package.json +24 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +37 -29
- package/src/components/TextEditor/hooks.stories.tsx +10 -2
- package/src/components/Toolbar/Toolbar.stories.tsx +6 -2
- package/src/extensions/markdown/code.ts +162 -0
- package/src/extensions/markdown/formatting.ts +64 -1
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/index.ts +4 -1
- package/src/extensions/markdown/link.ts +185 -23
- package/src/extensions/markdown/tasklist.ts +110 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts +0 -5
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +0 -1
- package/src/extensions/markdown/decorate.ts +0 -301
|
@@ -19,15 +19,19 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
19
19
|
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
20
20
|
import {
|
|
21
21
|
type CommentsOptions,
|
|
22
|
+
type LinkOptions,
|
|
22
23
|
autocomplete,
|
|
23
24
|
blast,
|
|
24
|
-
|
|
25
|
+
code,
|
|
25
26
|
comments,
|
|
26
27
|
defaultOptions,
|
|
28
|
+
heading,
|
|
29
|
+
hr,
|
|
27
30
|
image,
|
|
28
|
-
|
|
31
|
+
link,
|
|
29
32
|
mention,
|
|
30
33
|
table,
|
|
34
|
+
tasklist,
|
|
31
35
|
typewriter,
|
|
32
36
|
useComments,
|
|
33
37
|
formatting,
|
|
@@ -43,11 +47,9 @@ const str = (...lines: string[]) => lines.join('\n');
|
|
|
43
47
|
|
|
44
48
|
const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// prettier-ignore
|
|
48
51
|
const text = {
|
|
49
52
|
tasks: str(
|
|
50
|
-
//
|
|
51
53
|
'## Tasks',
|
|
52
54
|
'',
|
|
53
55
|
'- [x] decorator',
|
|
@@ -59,7 +61,6 @@ const text = {
|
|
|
59
61
|
),
|
|
60
62
|
|
|
61
63
|
list: str(
|
|
62
|
-
//
|
|
63
64
|
'## List',
|
|
64
65
|
'',
|
|
65
66
|
'- new york',
|
|
@@ -69,13 +70,12 @@ const text = {
|
|
|
69
70
|
),
|
|
70
71
|
|
|
71
72
|
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', '', ''),
|
|
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 onHoverLinkTooltip: LinkOptions['onHover'] = (el, url) => {
|
|
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 onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
210
210
|
createRoot(el).render(
|
|
211
211
|
<StrictMode>
|
|
212
212
|
<a href={url} target='_blank' rel='noreferrer' className={hover}>
|
|
@@ -257,11 +257,14 @@ const defaults = [
|
|
|
257
257
|
autocomplete({
|
|
258
258
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
259
259
|
}),
|
|
260
|
-
|
|
260
|
+
code(),
|
|
261
261
|
formatting(),
|
|
262
|
+
heading(),
|
|
263
|
+
hr(),
|
|
262
264
|
image(),
|
|
263
|
-
|
|
265
|
+
link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
|
|
264
266
|
table(),
|
|
267
|
+
tasklist(),
|
|
265
268
|
];
|
|
266
269
|
|
|
267
270
|
export const Default = {
|
|
@@ -278,11 +281,6 @@ export const NoExtensions = {
|
|
|
278
281
|
|
|
279
282
|
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
280
283
|
|
|
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
|
-
|
|
286
284
|
export const Empty = {
|
|
287
285
|
render: () => <Story />,
|
|
288
286
|
};
|
|
@@ -291,25 +289,26 @@ export const Scrolling = {
|
|
|
291
289
|
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
292
290
|
};
|
|
293
291
|
|
|
294
|
-
export const
|
|
295
|
-
render: () =>
|
|
292
|
+
export const Links = {
|
|
293
|
+
render: () => (
|
|
294
|
+
<Story
|
|
295
|
+
text={str(text.links, text.footer)}
|
|
296
|
+
extensions={[link({ onHover: onHoverLinkTooltip, onRender: onRenderLink })]}
|
|
297
|
+
/>
|
|
298
|
+
),
|
|
296
299
|
};
|
|
297
300
|
|
|
298
|
-
export const
|
|
299
|
-
render: () => <Story text={str(text.
|
|
301
|
+
export const Code = {
|
|
302
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
|
|
300
303
|
};
|
|
301
304
|
|
|
302
305
|
export const Image = {
|
|
303
306
|
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
304
307
|
};
|
|
305
308
|
|
|
306
|
-
export const Code = {
|
|
307
|
-
render: () => <Story text={str(text.code, text.footer)} extensions={[decorateMarkdown()]} />,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
309
|
export const Lists = {
|
|
311
310
|
render: () => (
|
|
312
|
-
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[
|
|
311
|
+
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
|
|
313
312
|
),
|
|
314
313
|
};
|
|
315
314
|
|
|
@@ -331,7 +330,7 @@ export const Autocomplete = {
|
|
|
331
330
|
<Story
|
|
332
331
|
text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
|
|
333
332
|
extensions={[
|
|
334
|
-
|
|
333
|
+
link({ onRender: onRenderLink }),
|
|
335
334
|
autocomplete({
|
|
336
335
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
337
336
|
}),
|
|
@@ -389,6 +388,15 @@ export const Comments = {
|
|
|
389
388
|
},
|
|
390
389
|
};
|
|
391
390
|
|
|
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
|
+
|
|
392
400
|
export const Vim = {
|
|
393
401
|
render: () => (
|
|
394
402
|
<Story
|
|
@@ -14,11 +14,15 @@ import { TextEditor } from './TextEditor';
|
|
|
14
14
|
import {
|
|
15
15
|
type EditorMode,
|
|
16
16
|
EditorModes,
|
|
17
|
-
|
|
17
|
+
code,
|
|
18
18
|
createMarkdownExtensions,
|
|
19
19
|
formatting,
|
|
20
|
+
heading,
|
|
21
|
+
hr,
|
|
20
22
|
image,
|
|
23
|
+
link,
|
|
21
24
|
table,
|
|
25
|
+
tasklist,
|
|
22
26
|
useFormattingState,
|
|
23
27
|
} from '../../extensions';
|
|
24
28
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
@@ -58,10 +62,14 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
58
62
|
// TODO(burdon): Move lineWrapping.
|
|
59
63
|
createMarkdownExtensions({ placeholder, lineWrapping: true }),
|
|
60
64
|
// TODO(burdon): Move into markdown bundle (with React callbacks).
|
|
61
|
-
|
|
65
|
+
code(),
|
|
62
66
|
formatting(),
|
|
67
|
+
heading(),
|
|
68
|
+
hr(),
|
|
63
69
|
image(),
|
|
70
|
+
link(),
|
|
64
71
|
table(),
|
|
72
|
+
tasklist(),
|
|
65
73
|
trackFormatting,
|
|
66
74
|
],
|
|
67
75
|
[editorMode, themeMode, placeholder, trackFormatting, readonly],
|
|
@@ -14,11 +14,13 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
14
14
|
|
|
15
15
|
import { Toolbar } from './Toolbar';
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
code,
|
|
18
18
|
comments,
|
|
19
19
|
formatting,
|
|
20
|
+
heading,
|
|
20
21
|
image,
|
|
21
22
|
table,
|
|
23
|
+
tasklist,
|
|
22
24
|
useComments,
|
|
23
25
|
useFormattingState,
|
|
24
26
|
} from '../../extensions';
|
|
@@ -37,7 +39,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
37
39
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
38
40
|
const extensions = useMemo(
|
|
39
41
|
() => [
|
|
40
|
-
|
|
42
|
+
code(),
|
|
41
43
|
comments({
|
|
42
44
|
onCreate: ({ cursor }) => {
|
|
43
45
|
const id = PublicKey.random().toHex();
|
|
@@ -46,8 +48,10 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
46
48
|
},
|
|
47
49
|
}),
|
|
48
50
|
formatting(),
|
|
51
|
+
heading(),
|
|
49
52
|
image(),
|
|
50
53
|
table(),
|
|
54
|
+
tasklist(),
|
|
51
55
|
formattingObserver,
|
|
52
56
|
],
|
|
53
57
|
[],
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
7
|
+
import {
|
|
8
|
+
ViewPlugin,
|
|
9
|
+
type DecorationSet,
|
|
10
|
+
EditorView,
|
|
11
|
+
type ViewUpdate,
|
|
12
|
+
Decoration,
|
|
13
|
+
type BlockInfo,
|
|
14
|
+
WidgetType,
|
|
15
|
+
} from '@codemirror/view';
|
|
16
|
+
|
|
17
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
18
|
+
|
|
19
|
+
import { getToken } from '../../styles';
|
|
20
|
+
|
|
21
|
+
// TODO(burdon): Reconcile with theme.
|
|
22
|
+
const styles = EditorView.baseTheme({
|
|
23
|
+
'& .cm-code': {
|
|
24
|
+
fontFamily: getToken('fontFamily.mono', []).join(','),
|
|
25
|
+
},
|
|
26
|
+
'& .cm-codeblock-line': {
|
|
27
|
+
paddingInline: '1rem !important',
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
'& .cm-codeblock-end': {
|
|
31
|
+
display: 'inline-block',
|
|
32
|
+
width: '100%',
|
|
33
|
+
position: 'relative',
|
|
34
|
+
'&::after': {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
inset: 0,
|
|
37
|
+
content: '""',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
'& .cm-codeblock-first': {
|
|
41
|
+
'&::after': {
|
|
42
|
+
borderTopLeftRadius: '.5rem',
|
|
43
|
+
borderTopRightRadius: '.5rem',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
'& .cm-codeblock-last': {
|
|
47
|
+
'&::after': {
|
|
48
|
+
borderBottomLeftRadius: '.5rem',
|
|
49
|
+
borderBottomRightRadius: '.5rem',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
'&light .cm-codeblock-line, &light .cm-activeLine.cm-codeblock-line': {
|
|
54
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
55
|
+
mixBlendMode: 'darken',
|
|
56
|
+
},
|
|
57
|
+
'&dark .cm-codeblock-line, &dark .cm-activeLine.cm-codeblock-line': {
|
|
58
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
59
|
+
mixBlendMode: 'lighten',
|
|
60
|
+
},
|
|
61
|
+
'&light .cm-codeblock-first, &light .cm-codeblock-last': {
|
|
62
|
+
mixBlendMode: 'darken',
|
|
63
|
+
'&::after': {
|
|
64
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
'&dark .cm-codeblock-first, &dark .cm-codeblock-last': {
|
|
68
|
+
mixBlendMode: 'lighten',
|
|
69
|
+
'&::after': {
|
|
70
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// TODO(burdon): Start from EditorView.lineBlockAt(pos).
|
|
76
|
+
const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
77
|
+
const start = lines.findIndex((line) => line.from >= from);
|
|
78
|
+
const end = lines.findIndex((line) => line.to >= to);
|
|
79
|
+
return [start, end];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Widget for first/last line of code block (read-only).
|
|
84
|
+
*/
|
|
85
|
+
// TODO(burdon): Add copy-to-clipboard button.
|
|
86
|
+
class LineWidget extends WidgetType {
|
|
87
|
+
constructor(private readonly _className: string) {
|
|
88
|
+
super();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override toDOM() {
|
|
92
|
+
const el = document.createElement('span');
|
|
93
|
+
el.innerHTML = ' ';
|
|
94
|
+
el.className = mx('cm-code cm-codeblock', this._className);
|
|
95
|
+
return el;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const top = new LineWidget('cm-codeblock-end cm-codeblock-first');
|
|
100
|
+
const bottom = new LineWidget('cm-codeblock-end cm-codeblock-last');
|
|
101
|
+
|
|
102
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
103
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
104
|
+
const { state } = view;
|
|
105
|
+
const blocks = view.viewportLineBlocks;
|
|
106
|
+
const cursor = state.selection.main.head;
|
|
107
|
+
|
|
108
|
+
for (const { from, to } of view.visibleRanges) {
|
|
109
|
+
syntaxTree(state).iterate({
|
|
110
|
+
enter: (node) => {
|
|
111
|
+
if (node.name === 'FencedCode') {
|
|
112
|
+
// FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
|
|
113
|
+
const editing = !view.state.readOnly && cursor >= node.from && cursor <= node.to;
|
|
114
|
+
const range = getLineRange(blocks, node.from, node.to);
|
|
115
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
116
|
+
const block = blocks[i];
|
|
117
|
+
if (!editing && (i === range[0] || i === range[1])) {
|
|
118
|
+
builder.add(block.from, block.to, Decoration.replace({ widget: i === range[0] ? top : bottom }));
|
|
119
|
+
} else {
|
|
120
|
+
builder.add(
|
|
121
|
+
block.from,
|
|
122
|
+
block.from,
|
|
123
|
+
Decoration.line({
|
|
124
|
+
class: mx(
|
|
125
|
+
'cm-code cm-codeblock-line',
|
|
126
|
+
i === range[0] && 'cm-codeblock-first',
|
|
127
|
+
i === range[1] && 'cm-codeblock-last',
|
|
128
|
+
),
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
from,
|
|
136
|
+
to,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return builder.finish();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const code = () => {
|
|
144
|
+
return [
|
|
145
|
+
ViewPlugin.fromClass(
|
|
146
|
+
class {
|
|
147
|
+
decorations: DecorationSet;
|
|
148
|
+
constructor(view: EditorView) {
|
|
149
|
+
this.decorations = buildDecorations(view);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
update(update: ViewUpdate) {
|
|
153
|
+
this.decorations = buildDecorations(update.view);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
decorations: (value) => value.decorations,
|
|
158
|
+
},
|
|
159
|
+
),
|
|
160
|
+
styles,
|
|
161
|
+
];
|
|
162
|
+
};
|
|
@@ -7,13 +7,14 @@ import { syntaxTree } from '@codemirror/language';
|
|
|
7
7
|
import {
|
|
8
8
|
type Extension,
|
|
9
9
|
type StateCommand,
|
|
10
|
+
RangeSetBuilder,
|
|
10
11
|
type EditorState,
|
|
11
12
|
type ChangeSpec,
|
|
12
13
|
type Text,
|
|
13
14
|
EditorSelection,
|
|
14
15
|
type Line,
|
|
15
16
|
} from '@codemirror/state';
|
|
16
|
-
import { EditorView, keymap } from '@codemirror/view';
|
|
17
|
+
import { Decoration, type DecorationSet, EditorView, keymap, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
17
18
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
|
18
19
|
import { useState, useMemo } from 'react';
|
|
19
20
|
|
|
@@ -995,6 +996,68 @@ export const formatting = (options: FormattingOptions = {}): Extension => {
|
|
|
995
996
|
run: toggleStrong,
|
|
996
997
|
},
|
|
997
998
|
]),
|
|
999
|
+
styling(),
|
|
1000
|
+
];
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
const styling = (): Extension => {
|
|
1004
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
1005
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
1006
|
+
const { state } = view;
|
|
1007
|
+
const cursor = state.selection.main.head;
|
|
1008
|
+
|
|
1009
|
+
// TODO(burdon): Bug if '***foo***' (since StrongEmphasis is nested inside EmphasisMark).
|
|
1010
|
+
// Ranges must be added sorted by `from` position and `startSide`.
|
|
1011
|
+
const replace = (node: SyntaxNodeRef, marks: SyntaxNode[]) => {
|
|
1012
|
+
if (cursor <= node.from || cursor >= node.to) {
|
|
1013
|
+
for (const mark of marks) {
|
|
1014
|
+
builder.add(mark.from, mark.to, Decoration.replace({}));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
for (const { from, to } of view.visibleRanges) {
|
|
1020
|
+
syntaxTree(state).iterate({
|
|
1021
|
+
enter: (node) => {
|
|
1022
|
+
switch (node.name) {
|
|
1023
|
+
case 'Emphasis':
|
|
1024
|
+
case 'StrongEmphasis': {
|
|
1025
|
+
const marks = node.node.getChildren('EmphasisMark');
|
|
1026
|
+
replace(node, marks);
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
case 'Strikethrough': {
|
|
1031
|
+
const marks = node.node.getChildren('StrikethroughMark');
|
|
1032
|
+
replace(node, marks);
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
from,
|
|
1038
|
+
to,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return builder.finish();
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
return [
|
|
1046
|
+
ViewPlugin.fromClass(
|
|
1047
|
+
class {
|
|
1048
|
+
decorations: DecorationSet;
|
|
1049
|
+
constructor(view: EditorView) {
|
|
1050
|
+
this.decorations = buildDecorations(view);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
update(update: ViewUpdate) {
|
|
1054
|
+
this.decorations = buildDecorations(update.view);
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
decorations: (value) => value.decorations,
|
|
1059
|
+
},
|
|
1060
|
+
),
|
|
998
1061
|
];
|
|
999
1062
|
};
|
|
1000
1063
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
10
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
11
|
+
const { state } = view;
|
|
12
|
+
const cursor = state.selection.main.head;
|
|
13
|
+
|
|
14
|
+
for (const { from, to } of view.visibleRanges) {
|
|
15
|
+
syntaxTree(state).iterate({
|
|
16
|
+
enter: (node) => {
|
|
17
|
+
switch (node.name) {
|
|
18
|
+
case 'ATXHeading1':
|
|
19
|
+
case 'ATXHeading2':
|
|
20
|
+
case 'ATXHeading3':
|
|
21
|
+
case 'ATXHeading4':
|
|
22
|
+
case 'ATXHeading5':
|
|
23
|
+
case 'ATXHeading6': {
|
|
24
|
+
const mark = node.node.getChild('HeaderMark');
|
|
25
|
+
if (mark) {
|
|
26
|
+
if (!view.hasFocus || view.state.readOnly || cursor < node.from || cursor > node.to) {
|
|
27
|
+
builder.add(mark.from, mark.to + 1, Decoration.replace({}));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
from,
|
|
34
|
+
to,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return builder.finish();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const heading = () => {
|
|
42
|
+
return [
|
|
43
|
+
ViewPlugin.fromClass(
|
|
44
|
+
class {
|
|
45
|
+
decorations: DecorationSet;
|
|
46
|
+
constructor(view: EditorView) {
|
|
47
|
+
this.decorations = buildDecorations(view);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
update(update: ViewUpdate) {
|
|
51
|
+
this.decorations = buildDecorations(update.view);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
decorations: (value) => value.decorations,
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
];
|
|
59
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { RangeSetBuilder } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
import { getToken } from '../../styles';
|
|
10
|
+
|
|
11
|
+
// TODO(burdon): Reconcile with theme.
|
|
12
|
+
const styles = EditorView.baseTheme({
|
|
13
|
+
'& .cm-hr': {
|
|
14
|
+
// Note that block-level decorations should not have vertical margins,
|
|
15
|
+
borderTop: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
class HorizontalRuleWidget extends WidgetType {
|
|
20
|
+
override toDOM() {
|
|
21
|
+
const el = document.createElement('div');
|
|
22
|
+
el.className = 'cm-hr';
|
|
23
|
+
return el;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const decoration = Decoration.replace({ widget: new HorizontalRuleWidget() });
|
|
28
|
+
|
|
29
|
+
const buildDecorations = (view: EditorView): DecorationSet => {
|
|
30
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
31
|
+
const { state } = view;
|
|
32
|
+
const cursor = state.selection.main.head;
|
|
33
|
+
|
|
34
|
+
for (const { from, to } of view.visibleRanges) {
|
|
35
|
+
syntaxTree(state).iterate({
|
|
36
|
+
enter: (node) => {
|
|
37
|
+
if (node.name === 'HorizontalRule') {
|
|
38
|
+
// Check if cursor is inside text.
|
|
39
|
+
if (cursor <= node.from || cursor >= node.to) {
|
|
40
|
+
builder.add(node.from, node.to, decoration);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
from,
|
|
45
|
+
to,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return builder.finish();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const hr = () => {
|
|
53
|
+
return [
|
|
54
|
+
ViewPlugin.fromClass(
|
|
55
|
+
class {
|
|
56
|
+
decorations: DecorationSet;
|
|
57
|
+
constructor(view: EditorView) {
|
|
58
|
+
this.decorations = buildDecorations(view);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
update(update: ViewUpdate) {
|
|
62
|
+
this.decorations = buildDecorations(update.view);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
decorations: (value) => value.decorations,
|
|
67
|
+
},
|
|
68
|
+
),
|
|
69
|
+
styles,
|
|
70
|
+
];
|
|
71
|
+
};
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './bundle';
|
|
6
|
-
export * from './
|
|
6
|
+
export * from './code';
|
|
7
7
|
export * from './formatting';
|
|
8
|
+
export * from './heading';
|
|
8
9
|
export * from './highlight';
|
|
10
|
+
export * from './hr';
|
|
9
11
|
export * from './image';
|
|
10
12
|
export * from './link';
|
|
11
13
|
export * from './table';
|
|
14
|
+
export * from './tasklist';
|