@dxos/react-ui-editor 0.4.5-main.181e1ed → 0.4.5-main.2a3f55f
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 +334 -537
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +7 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts +2 -0
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +2 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +2 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +5 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -4
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -11
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +24 -24
- 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 +79 -46
- package/src/extensions/markdown/decorate.ts +301 -0
- package/src/extensions/markdown/formatting.ts +1 -64
- package/src/extensions/markdown/index.ts +1 -4
- package/src/extensions/markdown/link.ts +23 -185
- package/src/index.ts +2 -0
- package/src/translations.ts +5 -3
- package/dist/types/src/extensions/markdown/code.d.ts +0 -2
- package/dist/types/src/extensions/markdown/code.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/heading.d.ts +0 -6
- package/dist/types/src/extensions/markdown/heading.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/hr.d.ts +0 -2
- package/dist/types/src/extensions/markdown/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/tasklist.d.ts +0 -3
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +0 -1
- package/src/extensions/markdown/code.ts +0 -162
- package/src/extensions/markdown/heading.ts +0 -59
- package/src/extensions/markdown/hr.ts +0 -71
- package/src/extensions/markdown/tasklist.ts +0 -110
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { RangeSetBuilder, type EditorState } from '@codemirror/state';
|
|
7
|
+
import { EditorView, Decoration, type DecorationSet, WidgetType, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
import { getToken } from '../../styles';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Widget for first/last line of code block (read-only).
|
|
15
|
+
*/
|
|
16
|
+
// TODO(burdon): Add copy-to-clipboard button.
|
|
17
|
+
class FencedCodeBoundary extends WidgetType {
|
|
18
|
+
constructor(private readonly _className: string) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override toDOM() {
|
|
23
|
+
const el = document.createElement('span');
|
|
24
|
+
el.innerHTML = ' ';
|
|
25
|
+
el.className = mx('cm-code cm-codeblock', this._className);
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class HorizontalRuleWidget extends WidgetType {
|
|
31
|
+
override toDOM() {
|
|
32
|
+
const el = document.createElement('span');
|
|
33
|
+
el.className = 'cm-hr';
|
|
34
|
+
return el;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class LinkButton extends WidgetType {
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly url: string,
|
|
41
|
+
private readonly render: (el: Element, url: string) => void,
|
|
42
|
+
) {
|
|
43
|
+
super();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override eq(other: this) {
|
|
47
|
+
return this.url === other.url;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override toDOM(view: EditorView) {
|
|
51
|
+
const el = document.createElement('span');
|
|
52
|
+
this.render(el, this.url);
|
|
53
|
+
return el;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class CheckboxWidget extends WidgetType {
|
|
58
|
+
constructor(private _checked: boolean) {
|
|
59
|
+
super();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override eq(other: this) {
|
|
63
|
+
return this._checked === other._checked;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override toDOM(view: EditorView) {
|
|
67
|
+
const input = document.createElement('input');
|
|
68
|
+
input.className = 'cm-task-checkbox';
|
|
69
|
+
input.type = 'checkbox';
|
|
70
|
+
input.checked = this._checked;
|
|
71
|
+
if (view.state.readOnly) {
|
|
72
|
+
input.setAttribute('disabled', 'true');
|
|
73
|
+
} else {
|
|
74
|
+
input.onmousedown = (event: Event) => {
|
|
75
|
+
const pos = view.posAtDOM(input);
|
|
76
|
+
const text = view.state.sliceDoc(pos, pos + 3);
|
|
77
|
+
if (text === (this._checked ? '[x]' : '[ ]')) {
|
|
78
|
+
view.dispatch({
|
|
79
|
+
changes: { from: pos + 1, to: pos + 2, insert: this._checked ? ' ' : 'x' },
|
|
80
|
+
});
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return input;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override ignoreEvent() {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hide = Decoration.replace({});
|
|
94
|
+
const fencedCodeTop = Decoration.replace({ widget: new FencedCodeBoundary('cm-codeblock-end cm-codeblock-first') });
|
|
95
|
+
const fencedCodeBottom = Decoration.replace({ widget: new FencedCodeBoundary('cm-codeblock-end cm-codeblock-last') });
|
|
96
|
+
const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() });
|
|
97
|
+
const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
|
|
98
|
+
const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
|
|
99
|
+
|
|
100
|
+
const editingRange = (state: EditorState, range: { from: number; to: number }) => {
|
|
101
|
+
const {
|
|
102
|
+
readOnly,
|
|
103
|
+
selection: {
|
|
104
|
+
main: { head },
|
|
105
|
+
},
|
|
106
|
+
} = state;
|
|
107
|
+
return !readOnly && head >= range.from && head <= range.to;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const MarksByParent = new Set(['CodeMark', 'EmphasisMark', 'StrikethroughMark', 'SubscriptMark', 'SuperscriptMark']);
|
|
111
|
+
|
|
112
|
+
const buildDecorations = (view: EditorView, options: DecorateOptions): DecorationSet => {
|
|
113
|
+
const builder = new RangeSetBuilder<Decoration>();
|
|
114
|
+
const { state } = view;
|
|
115
|
+
|
|
116
|
+
for (const { from, to } of view.visibleRanges) {
|
|
117
|
+
syntaxTree(state).iterate({
|
|
118
|
+
from,
|
|
119
|
+
to,
|
|
120
|
+
enter: (node) => {
|
|
121
|
+
if (node.name === 'FencedCode') {
|
|
122
|
+
const editing = editingRange(state, node);
|
|
123
|
+
// FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
|
|
124
|
+
for (const block of view.viewportLineBlocks) {
|
|
125
|
+
if (block.to < node.from) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (block.from > node.to) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
const first = block.from <= node.from;
|
|
132
|
+
const last = block.to >= node.to;
|
|
133
|
+
if (!editing && (first || last)) {
|
|
134
|
+
builder.add(block.from, block.to, first ? fencedCodeTop : fencedCodeBottom);
|
|
135
|
+
} else {
|
|
136
|
+
builder.add(
|
|
137
|
+
block.from,
|
|
138
|
+
block.from,
|
|
139
|
+
Decoration.line({
|
|
140
|
+
class: mx('cm-code cm-codeblock-line', first && 'cm-codeblock-first', last && 'cm-codeblock-last'),
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
} else if (node.name === 'Link') {
|
|
147
|
+
const marks = node.node.getChildren('LinkMark');
|
|
148
|
+
const urlNode = node.node.getChild('URL');
|
|
149
|
+
const editing = editingRange(state, node);
|
|
150
|
+
if (urlNode && marks.length >= 2) {
|
|
151
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
152
|
+
if (!editing) {
|
|
153
|
+
builder.add(node.from, marks[0].to, hide);
|
|
154
|
+
}
|
|
155
|
+
builder.add(
|
|
156
|
+
marks[0].to,
|
|
157
|
+
marks[1].from,
|
|
158
|
+
Decoration.mark({
|
|
159
|
+
tagName: 'a',
|
|
160
|
+
attributes: {
|
|
161
|
+
class: 'cm-link',
|
|
162
|
+
href: url,
|
|
163
|
+
rel: 'noreferrer',
|
|
164
|
+
target: '_blank',
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
if (!editing) {
|
|
169
|
+
builder.add(
|
|
170
|
+
marks[1].from,
|
|
171
|
+
node.to,
|
|
172
|
+
options.renderLinkButton
|
|
173
|
+
? Decoration.replace({
|
|
174
|
+
widget: new LinkButton(url, options.renderLinkButton),
|
|
175
|
+
})
|
|
176
|
+
: hide,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else if (node.name === 'HeaderMark') {
|
|
181
|
+
const parent = node.node.parent!;
|
|
182
|
+
if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from))) {
|
|
183
|
+
const next = state.doc.sliceString(node.to, node.to + 1);
|
|
184
|
+
builder.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
|
|
185
|
+
}
|
|
186
|
+
} else if (node.name === 'HorizontalRule') {
|
|
187
|
+
if (!editingRange(state, node)) {
|
|
188
|
+
builder.add(node.from, node.to, horizontalRule);
|
|
189
|
+
}
|
|
190
|
+
} else if (node.name === 'TaskMarker') {
|
|
191
|
+
// Check if cursor is inside text.
|
|
192
|
+
if (!editingRange(state, node)) {
|
|
193
|
+
const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
|
|
194
|
+
builder.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
|
|
195
|
+
builder.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
|
|
196
|
+
}
|
|
197
|
+
} else if (MarksByParent.has(node.name)) {
|
|
198
|
+
if (!editingRange(state, node.node.parent!)) {
|
|
199
|
+
builder.add(node.from, node.to, hide);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return builder.finish();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export interface DecorateOptions {
|
|
210
|
+
renderLinkButton?: (el: Element, url: string) => void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
214
|
+
return [
|
|
215
|
+
ViewPlugin.fromClass(
|
|
216
|
+
class {
|
|
217
|
+
decorations: DecorationSet;
|
|
218
|
+
constructor(view: EditorView) {
|
|
219
|
+
this.decorations = buildDecorations(view, options);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
update(update: ViewUpdate) {
|
|
223
|
+
if (update.docChanged || update.selectionSet || update.viewportChanged) {
|
|
224
|
+
this.decorations = buildDecorations(update.view, options);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
decorations: (value) => value.decorations,
|
|
230
|
+
},
|
|
231
|
+
),
|
|
232
|
+
formattingStyles,
|
|
233
|
+
];
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const formattingStyles = EditorView.baseTheme({
|
|
237
|
+
'& .cm-code': {
|
|
238
|
+
fontFamily: getToken('fontFamily.mono', []).join(','),
|
|
239
|
+
},
|
|
240
|
+
'& .cm-codeblock-line': {
|
|
241
|
+
paddingInline: '1rem !important',
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
'& .cm-codeblock-end': {
|
|
245
|
+
display: 'inline-block',
|
|
246
|
+
width: '100%',
|
|
247
|
+
position: 'relative',
|
|
248
|
+
'&::after': {
|
|
249
|
+
position: 'absolute',
|
|
250
|
+
inset: 0,
|
|
251
|
+
content: '""',
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
'& .cm-codeblock-first': {
|
|
255
|
+
'&::after': {
|
|
256
|
+
borderTopLeftRadius: '.5rem',
|
|
257
|
+
borderTopRightRadius: '.5rem',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
'& .cm-codeblock-last': {
|
|
261
|
+
'&::after': {
|
|
262
|
+
borderBottomLeftRadius: '.5rem',
|
|
263
|
+
borderBottomRightRadius: '.5rem',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
'&light .cm-codeblock-line, &light .cm-activeLine.cm-codeblock-line': {
|
|
268
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
269
|
+
mixBlendMode: 'darken',
|
|
270
|
+
},
|
|
271
|
+
'&dark .cm-codeblock-line, &dark .cm-activeLine.cm-codeblock-line': {
|
|
272
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
273
|
+
mixBlendMode: 'lighten',
|
|
274
|
+
},
|
|
275
|
+
'&light .cm-codeblock-first, &light .cm-codeblock-last': {
|
|
276
|
+
mixBlendMode: 'darken',
|
|
277
|
+
'&::after': {
|
|
278
|
+
background: getToken('extend.semanticColors.input.light'),
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
'&dark .cm-codeblock-first, &dark .cm-codeblock-last': {
|
|
282
|
+
mixBlendMode: 'lighten',
|
|
283
|
+
'&::after': {
|
|
284
|
+
background: getToken('extend.semanticColors.input.dark'),
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
'& .cm-hr': {
|
|
288
|
+
display: 'inline-block',
|
|
289
|
+
width: '100%',
|
|
290
|
+
height: '0',
|
|
291
|
+
verticalAlign: 'middle',
|
|
292
|
+
borderTop: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
293
|
+
},
|
|
294
|
+
'& .cm-task': {
|
|
295
|
+
color: getToken('extend.colors.blue.500'),
|
|
296
|
+
},
|
|
297
|
+
'& .cm-task-checkbox': {
|
|
298
|
+
marginLeft: '4px',
|
|
299
|
+
marginRight: '4px',
|
|
300
|
+
},
|
|
301
|
+
});
|
|
@@ -7,14 +7,13 @@ import { syntaxTree } from '@codemirror/language';
|
|
|
7
7
|
import {
|
|
8
8
|
type Extension,
|
|
9
9
|
type StateCommand,
|
|
10
|
-
RangeSetBuilder,
|
|
11
10
|
type EditorState,
|
|
12
11
|
type ChangeSpec,
|
|
13
12
|
type Text,
|
|
14
13
|
EditorSelection,
|
|
15
14
|
type Line,
|
|
16
15
|
} from '@codemirror/state';
|
|
17
|
-
import {
|
|
16
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
18
17
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
|
19
18
|
import { useState, useMemo } from 'react';
|
|
20
19
|
|
|
@@ -996,68 +995,6 @@ export const formatting = (options: FormattingOptions = {}): Extension => {
|
|
|
996
995
|
run: toggleStrong,
|
|
997
996
|
},
|
|
998
997
|
]),
|
|
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
|
-
),
|
|
1061
998
|
];
|
|
1062
999
|
};
|
|
1063
1000
|
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './bundle';
|
|
6
|
-
export * from './
|
|
6
|
+
export * from './decorate';
|
|
7
7
|
export * from './formatting';
|
|
8
|
-
export * from './heading';
|
|
9
8
|
export * from './highlight';
|
|
10
|
-
export * from './hr';
|
|
11
9
|
export * from './image';
|
|
12
10
|
export * from './link';
|
|
13
11
|
export * from './table';
|
|
14
|
-
export * from './tasklist';
|
|
@@ -4,196 +4,34 @@
|
|
|
4
4
|
//
|
|
5
5
|
|
|
6
6
|
import { syntaxTree } from '@codemirror/language';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
hoverTooltip,
|
|
10
|
-
Decoration,
|
|
11
|
-
type DecorationSet,
|
|
12
|
-
type EditorView,
|
|
13
|
-
ViewPlugin,
|
|
14
|
-
type ViewUpdate,
|
|
15
|
-
WidgetType,
|
|
16
|
-
} from '@codemirror/view';
|
|
7
|
+
import { hoverTooltip } from '@codemirror/view';
|
|
17
8
|
import { type SyntaxNode } from '@lezer/common';
|
|
18
9
|
|
|
19
10
|
import { tooltipContent } from '@dxos/react-ui-theme';
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
}
|
|
100
|
-
|
|
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
|
-
};
|
|
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;
|
|
140
18
|
}
|
|
141
19
|
|
|
142
|
-
|
|
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
|
-
}
|
|
20
|
+
const url = link && link.getChild('URL');
|
|
21
|
+
if (!url || !link) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
189
24
|
|
|
190
|
-
|
|
191
|
-
|
|
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 } };
|
|
192
35
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return builder.finish();
|
|
199
|
-
};
|
|
36
|
+
};
|
|
37
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
|
+
import translations from './translations';
|
|
4
5
|
|
|
5
6
|
export { type Extension, type EditorState } from '@codemirror/state';
|
|
6
7
|
export { type EditorView, keymap } from '@codemirror/view';
|
|
@@ -20,3 +21,4 @@ export {
|
|
|
20
21
|
} from './styles';
|
|
21
22
|
export * from './themes';
|
|
22
23
|
export * from './util';
|
|
24
|
+
export { translations };
|
package/src/translations.ts
CHANGED
|
@@ -8,10 +8,11 @@ export default [
|
|
|
8
8
|
{
|
|
9
9
|
'en-US': {
|
|
10
10
|
[translationKey]: {
|
|
11
|
-
'strong label': '
|
|
12
|
-
'emphasis label': '
|
|
13
|
-
'strikethrough label': '
|
|
11
|
+
'strong label': 'Bold',
|
|
12
|
+
'emphasis label': 'Italics',
|
|
13
|
+
'strikethrough label': 'Strikethrough',
|
|
14
14
|
'code label': 'Code',
|
|
15
|
+
'link label': 'Link',
|
|
15
16
|
'list-bullet label': 'Bullet list',
|
|
16
17
|
'list-ordered label': 'Numbered list',
|
|
17
18
|
'list-task label': 'Task list',
|
|
@@ -19,6 +20,7 @@ export default [
|
|
|
19
20
|
'codeblock label': 'Code block',
|
|
20
21
|
'comment label': 'Create comment',
|
|
21
22
|
'heading label': 'Heading level',
|
|
23
|
+
'table label': 'Create table',
|
|
22
24
|
},
|
|
23
25
|
},
|
|
24
26
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/code.ts"],"names":[],"mappings":"AA8IA,eAAO,MAAM,IAAI,+CAmBhB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"heading.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/heading.ts"],"names":[],"mappings":"AAMA,OAAO,EAAc,KAAK,aAAa,EAAmB,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAkChH,eAAO,MAAM,OAAO;;mBASG,UAAU;IAShC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hr.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/markdown/hr.ts"],"names":[],"mappings":"AAmDA,eAAO,MAAM,EAAE,+CAmBd,CAAC"}
|