@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.
Files changed (34) hide show
  1. package/dist/lib/browser/index.mjs +505 -291
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +5 -5
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
  7. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
  8. package/dist/types/src/extensions/markdown/code.d.ts +2 -0
  9. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  10. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  11. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  12. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  13. package/dist/types/src/extensions/markdown/hr.d.ts +2 -0
  14. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  15. package/dist/types/src/extensions/markdown/index.d.ts +4 -1
  16. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  17. package/dist/types/src/extensions/markdown/link.d.ts +11 -1
  18. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  19. package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
  20. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  21. package/package.json +24 -24
  22. package/src/components/TextEditor/MarkdownEditor.stories.tsx +37 -29
  23. package/src/components/TextEditor/hooks.stories.tsx +10 -2
  24. package/src/components/Toolbar/Toolbar.stories.tsx +6 -2
  25. package/src/extensions/markdown/code.ts +162 -0
  26. package/src/extensions/markdown/formatting.ts +64 -1
  27. package/src/extensions/markdown/heading.ts +59 -0
  28. package/src/extensions/markdown/hr.ts +71 -0
  29. package/src/extensions/markdown/index.ts +4 -1
  30. package/src/extensions/markdown/link.ts +185 -23
  31. package/src/extensions/markdown/tasklist.ts +110 -0
  32. package/dist/types/src/extensions/markdown/decorate.d.ts +0 -5
  33. package/dist/types/src/extensions/markdown/decorate.d.ts.map +0 -1
  34. 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
- decorateMarkdown,
25
+ code,
25
26
  comments,
26
27
  defaultOptions,
28
+ heading,
29
+ hr,
27
30
  image,
28
- linkTooltip,
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
- const img = '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)';
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', '', img),
118
+ image: str('# Image', '', '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)'),
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 renderLinkTooltip = (el: Element, url: string) => {
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 renderLinkButton = (el: Element, url: string) => {
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
- decorateMarkdown({ renderLinkButton }),
260
+ code(),
261
261
  formatting(),
262
+ heading(),
263
+ hr(),
262
264
  image(),
263
- linkTooltip(renderLinkTooltip),
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 ScrollingWithImages = {
295
- render: () => <Story text={str('# Large Document', '', largeWithImages)} extensions={[image()]} />,
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 Links = {
299
- render: () => <Story text={str(text.links, text.footer)} extensions={[linkTooltip(renderLinkTooltip)]} />,
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={[decorateMarkdown()]} />
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
- decorateMarkdown({ renderLinkButton }),
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
- decorateMarkdown,
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
- decorateMarkdown(),
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
- decorateMarkdown,
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
- decorateMarkdown(),
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 = '&nbsp;';
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 './decorate';
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';