@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
@@ -4,34 +4,196 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
- import { hoverTooltip } from '@codemirror/view';
7
+ import { type Extension, RangeSetBuilder } from '@codemirror/state';
8
+ import {
9
+ hoverTooltip,
10
+ Decoration,
11
+ type DecorationSet,
12
+ type EditorView,
13
+ ViewPlugin,
14
+ type ViewUpdate,
15
+ WidgetType,
16
+ } from '@codemirror/view';
8
17
  import { type SyntaxNode } from '@lezer/common';
9
18
 
10
19
  import { tooltipContent } from '@dxos/react-ui-theme';
11
20
 
12
- export const linkTooltip = (render: (el: Element, url: string) => void) =>
13
- hoverTooltip((view, pos, side) => {
14
- const syntax = syntaxTree(view.state).resolveInner(pos, side);
15
- let link = null;
16
- for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
17
- link = node.name === 'Link' ? node : null;
18
- }
21
+ // Adapted from:
22
+ // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
23
+ // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
24
+
25
+ // TODO(burdon): Callback to dynamically lookup title of linked pages.
26
+ export type LinkOptions = {
27
+ link?: boolean;
28
+ onHover?: (el: Element, url: string) => void;
29
+ onRender?: (el: Element, url: string) => void;
30
+ };
31
+
32
+ /**
33
+ * Creates a state field to replace AST elements with a hyperlink widget.
34
+ * https://codemirror.net/docs/ref/#state.StateField
35
+ */
36
+ export const link = (options: LinkOptions = {}): Extension => {
37
+ const extensions: Extension[] = [
38
+ ViewPlugin.fromClass(
39
+ class {
40
+ decorations: DecorationSet;
41
+ constructor(view: EditorView) {
42
+ this.decorations = buildDecorations(view, options);
43
+ }
44
+
45
+ update(update: ViewUpdate) {
46
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
47
+ this.decorations = buildDecorations(update.view, options);
48
+ }
49
+ }
50
+ },
51
+ {
52
+ decorations: (value) => value.decorations,
53
+ },
54
+ ),
55
+ ];
56
+
57
+ if (options.onHover) {
58
+ extensions.push(
59
+ // https://codemirror.net/examples/tooltip
60
+ // https://codemirror.net/docs/ref/#view.hoverTooltip
61
+ // https://github.com/codemirror/view/blob/main/src/tooltip.ts
62
+ hoverTooltip((view, pos, side) => {
63
+ const syntax = syntaxTree(view.state).resolveInner(pos, side);
64
+ let link = null;
65
+ for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
66
+ link = node.name === 'Link' ? node : null;
67
+ }
68
+
69
+ const url = link && link.getChild('URL');
70
+ if (!url || !link) {
71
+ return null;
72
+ }
73
+
74
+ const urlText = view.state.sliceDoc(url.from, url.to);
75
+ return {
76
+ pos: link.from,
77
+ end: link.to,
78
+ above: true,
79
+ create: () => {
80
+ const el = document.createElement('div');
81
+ el.className = tooltipContent({}, 'pli-2 plb-1');
82
+ options.onHover!(el, urlText);
83
+ return { dom: el, offset: { x: 0, y: 4 } };
84
+ },
85
+ };
86
+ }),
87
+ );
88
+ }
89
+
90
+ return extensions;
91
+ };
92
+
93
+ class LinkButton extends WidgetType {
94
+ constructor(
95
+ private readonly _url: string,
96
+ private readonly _onAttach: LinkOptions['onRender'],
97
+ ) {
98
+ super();
99
+ }
19
100
 
20
- const url = link && link.getChild('URL');
21
- if (!url || !link) {
22
- return null;
101
+ override eq(other: this) {
102
+ return this._url === other._url;
103
+ }
104
+
105
+ override toDOM(view: EditorView) {
106
+ const el = document.createElement('span');
107
+ this._onAttach!(el, this._url);
108
+ return el;
109
+ }
110
+ }
111
+
112
+ class LinkText extends WidgetType {
113
+ constructor(
114
+ private readonly _text: string,
115
+ private readonly _url?: string,
116
+ ) {
117
+ super();
118
+ }
119
+
120
+ override eq(other: this) {
121
+ return this._url === other._url;
122
+ }
123
+
124
+ override toDOM(view: EditorView) {
125
+ const link = document.createElement('a');
126
+ link.setAttribute('class', 'cm-link');
127
+ link.textContent = this._text;
128
+ if (this._url) {
129
+ link.setAttribute('rel', 'noreferrer');
130
+ link.setAttribute('target', '_blank');
131
+ link.href = this._url;
132
+ } else {
133
+ link.onclick = () => {
134
+ view.dispatch({
135
+ selection: {
136
+ anchor: view.posAtDOM(link),
137
+ },
138
+ });
139
+ };
23
140
  }
24
141
 
25
- const urlText = view.state.sliceDoc(url.from, url.to);
26
- return {
27
- pos: link.from,
28
- end: link.to,
29
- above: true,
30
- create: () => {
31
- const el = document.createElement('div');
32
- el.className = tooltipContent({}, 'pli-2 plb-1');
33
- render(el, urlText);
34
- return { dom: el, offset: { x: 0, y: 4 } };
142
+ return link;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Range sets provide a data structure that can hold a collection of tagged,
148
+ * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
149
+ * https://codemirror.net/docs/ref/#state
150
+ */
151
+ const buildDecorations = (view: EditorView, options: LinkOptions): DecorationSet => {
152
+ const builder = new RangeSetBuilder<Decoration>();
153
+ const { state } = view;
154
+ const cursor = state.selection.main.head;
155
+
156
+ for (const { from, to } of view.visibleRanges) {
157
+ syntaxTree(state).iterate({
158
+ enter: (node) => {
159
+ // Check if cursor is inside text.
160
+ if (node.name === 'Link') {
161
+ const marks = node.node.getChildren('LinkMark');
162
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
163
+ const urlNode = node.node.getChild('URL');
164
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
165
+ if (!url) {
166
+ return false;
167
+ }
168
+
169
+ // TODO(burdon): Don't expand if moving cursor up/down.
170
+ if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
171
+ builder.add(
172
+ node.from,
173
+ node.to,
174
+ Decoration.replace({
175
+ widget: new LinkText(text, options.link ? url : undefined),
176
+ }),
177
+ );
178
+ }
179
+
180
+ if (options.onRender) {
181
+ builder.add(
182
+ node.to,
183
+ node.to,
184
+ Decoration.replace({
185
+ widget: new LinkButton(url, options.onRender),
186
+ }),
187
+ );
188
+ }
189
+
190
+ return false;
191
+ }
35
192
  },
36
- };
37
- });
193
+ from,
194
+ to,
195
+ });
196
+ }
197
+
198
+ return builder.finish();
199
+ };
@@ -0,0 +1,110 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import { EditorView, Decoration, WidgetType, ViewPlugin, type DecorationSet, type ViewUpdate } from '@codemirror/view';
8
+
9
+ import { getToken } from '../../styles';
10
+
11
+ // TODO(burdon): Reconcile with theme.
12
+ const styles = EditorView.baseTheme({
13
+ '& .cm-task': {
14
+ color: getToken('extend.colors.blue.500'),
15
+ },
16
+ '& .cm-task-checkbox': {
17
+ marginLeft: '4px',
18
+ marginRight: '4px',
19
+ },
20
+ });
21
+
22
+ class CheckboxWidget extends WidgetType {
23
+ constructor(private _checked: boolean) {
24
+ super();
25
+ }
26
+
27
+ override eq(other: this) {
28
+ return this._checked === other._checked;
29
+ }
30
+
31
+ override toDOM(view: EditorView) {
32
+ const input = document.createElement('input');
33
+ input.className = 'cm-task-checkbox';
34
+ input.type = 'checkbox';
35
+ input.checked = this._checked;
36
+ if (view.state.readOnly) {
37
+ input.setAttribute('disabled', 'true');
38
+ } else {
39
+ input.onmousedown = (event: Event) => {
40
+ const pos = view.posAtDOM(input);
41
+ const text = view.state.sliceDoc(pos, pos + 3);
42
+ if (text === (this._checked ? '[x]' : '[ ]')) {
43
+ view.dispatch({
44
+ changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
45
+ });
46
+ event.preventDefault();
47
+ }
48
+ };
49
+ }
50
+
51
+ return input;
52
+ }
53
+
54
+ override ignoreEvent() {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ const checkedDecoration = Decoration.replace({ widget: new CheckboxWidget(true) });
60
+ const uncheckedDecoration = Decoration.replace({ widget: new CheckboxWidget(false) });
61
+
62
+ const buildDecorations = (view: EditorView): DecorationSet => {
63
+ const builder = new RangeSetBuilder<Decoration>();
64
+ const { state } = view;
65
+ const cursor = state.selection.main.head;
66
+
67
+ for (const { from, to } of view.visibleRanges) {
68
+ syntaxTree(state).iterate({
69
+ enter: (node) => {
70
+ if (node.name === 'TaskMarker') {
71
+ // Check if cursor is inside text.
72
+ if (cursor < node.from || cursor > node.to) {
73
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
74
+ builder.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
75
+ builder.add(node.from, node.to, checked ? checkedDecoration : uncheckedDecoration);
76
+ }
77
+ }
78
+ },
79
+ from,
80
+ to,
81
+ });
82
+ }
83
+
84
+ return builder.finish();
85
+ };
86
+
87
+ export type TasklistOptions = {};
88
+
89
+ export const tasklist = (options: TasklistOptions = {}) => {
90
+ return [
91
+ ViewPlugin.fromClass(
92
+ class {
93
+ decorations: DecorationSet;
94
+ constructor(view: EditorView) {
95
+ this.decorations = buildDecorations(view);
96
+ }
97
+
98
+ update(update: ViewUpdate) {
99
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
100
+ this.decorations = buildDecorations(update.view);
101
+ }
102
+ }
103
+ },
104
+ {
105
+ decorations: (value) => value.decorations,
106
+ },
107
+ ),
108
+ styles,
109
+ ];
110
+ };
@@ -1,5 +0,0 @@
1
- export interface DecorateOptions {
2
- renderLinkButton?: (el: Element, url: string) => void;
3
- }
4
- export declare const decorateMarkdown: (options?: DecorateOptions) => import("@codemirror/state").Extension[];
5
- //# sourceMappingURL=decorate.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"decorate.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/decorate.ts"],"names":[],"mappings":"AAgNA,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAED,eAAO,MAAM,gBAAgB,aAAa,eAAe,4CAqBxD,CAAC"}
@@ -1,301 +0,0 @@
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 = '&nbsp;';
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
- });