@dxos/react-ui-editor 0.3.9 → 0.3.10-main.04816d3
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 +952 -736
- 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 +38 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +5 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +10 -0
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts +10 -0
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +7 -2
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +31 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +20 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +4 -4
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +12 -10
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +22 -32
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +275 -0
- package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
- package/src/components/TextEditor/TextEditor.tsx +79 -44
- package/src/components/TextEditor/extensions/autocomplete.ts +74 -0
- package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
- package/src/components/TextEditor/extensions/comments.ts +190 -0
- package/src/components/TextEditor/extensions/demo.ts +66 -0
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +18 -2
- package/src/components/TextEditor/extensions/index.ts +7 -2
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +76 -31
- package/src/components/TextEditor/extensions/{change.ts → listener.ts} +4 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +21 -25
- package/src/components/TextEditor/extensions/markdown/highlight.ts +167 -0
- package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
- package/src/components/TextEditor/extensions/tasklist.ts +107 -0
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
- package/src/components/TextEditor/index.ts +3 -0
- package/src/components/TextEditor/themes/default.ts +185 -0
- package/src/components/TextEditor/themes/index.ts +5 -0
- package/src/components/TextEditor/yjs.stories.tsx +1 -0
- package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextModel.ts +52 -39
- package/src/styles/markdown.ts +26 -24
- package/src/testing/replicator.ts +6 -6
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
- package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
- package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
- package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
- package/src/components/TextEditor/extensions/basic/index.ts +0 -6
- package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
- package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
- package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
- package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
- package/src/hooks/useCollaboration.ts +0 -45
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.d.ts +0 -0
- /package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +0 -0
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
|
|
6
6
|
import { syntaxTree } from '@codemirror/language';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type EditorState,
|
|
9
|
+
type Extension,
|
|
10
|
+
type RangeSet,
|
|
11
|
+
RangeSetBuilder,
|
|
12
|
+
StateField,
|
|
13
|
+
type Transaction,
|
|
14
|
+
} from '@codemirror/state';
|
|
8
15
|
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
9
16
|
|
|
10
17
|
// Adapted from:
|
|
@@ -19,13 +26,33 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
|
19
26
|
// https://codemirror.net/5/demo/simplemode.html
|
|
20
27
|
// https://codemirror.net/5/demo/search.html (Search dialog).
|
|
21
28
|
|
|
22
|
-
class
|
|
23
|
-
constructor(
|
|
29
|
+
class LinkButton extends WidgetType {
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly _pos: number,
|
|
32
|
+
private readonly _url: string,
|
|
33
|
+
private readonly _onAttach: LinkOptions['onRender'],
|
|
34
|
+
) {
|
|
24
35
|
super();
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
override eq(other:
|
|
28
|
-
return this.
|
|
38
|
+
override eq(other: LinkButton) {
|
|
39
|
+
return this._pos === other._pos && this._url === other._url;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
toDOM(view: EditorView) {
|
|
43
|
+
const el = document.createElement('span');
|
|
44
|
+
this._onAttach!(el, this._url);
|
|
45
|
+
return el;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class LinkText extends WidgetType {
|
|
50
|
+
constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
|
|
51
|
+
super();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override eq(other: LinkText) {
|
|
55
|
+
return this._pos === other._pos && this._url === other._url;
|
|
29
56
|
}
|
|
30
57
|
|
|
31
58
|
toDOM(view: EditorView) {
|
|
@@ -33,13 +60,14 @@ class Link extends WidgetType {
|
|
|
33
60
|
link.setAttribute('class', 'cm-link');
|
|
34
61
|
link.textContent = this._text;
|
|
35
62
|
if (this._url) {
|
|
63
|
+
link.setAttribute('rel', 'noreferrer');
|
|
36
64
|
link.setAttribute('target', '_blank');
|
|
37
65
|
link.href = this._url;
|
|
38
66
|
} else {
|
|
39
67
|
link.onclick = () => {
|
|
40
68
|
view.dispatch({
|
|
41
69
|
selection: {
|
|
42
|
-
anchor: this.
|
|
70
|
+
anchor: this._pos,
|
|
43
71
|
},
|
|
44
72
|
});
|
|
45
73
|
};
|
|
@@ -49,51 +77,68 @@ class Link extends WidgetType {
|
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
type HyperlinkDecorationConfig = {
|
|
53
|
-
link?: boolean;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
80
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
81
|
+
* Range sets provide a data structure that can hold a collection of tagged,
|
|
82
|
+
* possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
|
|
83
|
+
* https://codemirror.net/docs/ref/#state
|
|
59
84
|
*/
|
|
60
|
-
|
|
61
|
-
return StateField.define<RangeSet<any>>({
|
|
62
|
-
create: (state) => update(state, link),
|
|
63
|
-
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, link),
|
|
64
|
-
provide: (field) => EditorView.decorations.from(field),
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const update = (state: EditorState, link?: boolean) => {
|
|
85
|
+
const update = (state: EditorState, options: LinkOptions) => {
|
|
69
86
|
const builder = new RangeSetBuilder();
|
|
70
87
|
const cursor = state.selection.main.head;
|
|
71
88
|
syntaxTree(state).iterate({
|
|
72
89
|
enter: (node) => {
|
|
73
90
|
// Check if cursor is inside text.
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
91
|
+
if (node.name === 'Link') {
|
|
92
|
+
const marks = node.node.getChildren('LinkMark');
|
|
93
|
+
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
78
94
|
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
const urlNode = node.node.getChild('URL');
|
|
96
|
+
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
97
|
+
if (!url) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
81
100
|
|
|
101
|
+
if (cursor < node.from || cursor > node.to) {
|
|
82
102
|
builder.add(
|
|
83
103
|
node.from,
|
|
84
104
|
node.to,
|
|
85
105
|
Decoration.replace({
|
|
86
|
-
widget: new
|
|
106
|
+
widget: new LinkText(node.from, text, options.link ? url : undefined),
|
|
87
107
|
}),
|
|
88
108
|
);
|
|
109
|
+
}
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
if (options.onRender) {
|
|
112
|
+
builder.add(
|
|
113
|
+
node.to,
|
|
114
|
+
node.to,
|
|
115
|
+
Decoration.replace({
|
|
116
|
+
widget: new LinkButton(node.from, url, options.onRender),
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
91
119
|
}
|
|
92
|
-
}
|
|
93
120
|
|
|
94
|
-
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
95
123
|
},
|
|
96
124
|
});
|
|
97
125
|
|
|
98
126
|
return builder.finish();
|
|
99
127
|
};
|
|
128
|
+
|
|
129
|
+
export type LinkOptions = {
|
|
130
|
+
link?: boolean;
|
|
131
|
+
onRender?: (el: Element, url: string) => void;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
136
|
+
* https://codemirror.net/docs/ref/#state.StateField
|
|
137
|
+
*/
|
|
138
|
+
export const link = (options: LinkOptions = {}): Extension => {
|
|
139
|
+
return StateField.define<RangeSet<any>>({
|
|
140
|
+
create: (state) => update(state, options),
|
|
141
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
142
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { StateField } from '@codemirror/state';
|
|
6
6
|
|
|
7
|
+
export type ListenerOptions = { onChange: (text: string) => void };
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
|
|
9
|
-
* @param onChange
|
|
10
11
|
*/
|
|
11
|
-
// TODO(burdon):
|
|
12
|
-
export const
|
|
12
|
+
// TODO(burdon): Expose options.
|
|
13
|
+
export const listener = ({ onChange }: ListenerOptions) =>
|
|
13
14
|
StateField.define({
|
|
14
15
|
create: () => null,
|
|
15
16
|
update: (_value, transaction) => {
|
|
@@ -3,17 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
-
import { defaultKeymap, history,
|
|
6
|
+
import { defaultKeymap, history, indentWithTab } from '@codemirror/commands';
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
|
-
import {
|
|
9
|
-
bracketMatching,
|
|
10
|
-
defaultHighlightStyle,
|
|
11
|
-
foldKeymap,
|
|
12
|
-
indentOnInput,
|
|
13
|
-
syntaxHighlighting,
|
|
14
|
-
} from '@codemirror/language';
|
|
8
|
+
import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
|
15
9
|
import { languages } from '@codemirror/language-data';
|
|
16
|
-
import { lintKeymap } from '@codemirror/lint';
|
|
17
10
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
|
18
11
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
19
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
@@ -29,12 +22,10 @@ import {
|
|
|
29
22
|
placeholder,
|
|
30
23
|
rectangularSelection,
|
|
31
24
|
} from '@codemirror/view';
|
|
32
|
-
// import { GFM } from '@lezer/markdown';
|
|
33
25
|
|
|
34
26
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
35
27
|
|
|
36
|
-
import { markdownTagsExtension } from './
|
|
37
|
-
import { markdownHighlightStyle } from './theme';
|
|
28
|
+
import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
|
|
38
29
|
|
|
39
30
|
export type MarkdownBundleOptions = {
|
|
40
31
|
themeMode?: ThemeMode;
|
|
@@ -52,7 +43,6 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
52
43
|
EditorState.allowMultipleSelections.of(true),
|
|
53
44
|
EditorView.lineWrapping,
|
|
54
45
|
|
|
55
|
-
// autocompletion(),
|
|
56
46
|
crosshairCursor(),
|
|
57
47
|
dropCursor(),
|
|
58
48
|
drawSelection(),
|
|
@@ -64,36 +54,42 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
64
54
|
indentOnInput(),
|
|
65
55
|
rectangularSelection(),
|
|
66
56
|
|
|
57
|
+
// TODO(burdon): Review.
|
|
67
58
|
keymap.of([
|
|
68
59
|
...closeBracketsKeymap,
|
|
69
|
-
// ...completionKeymap,
|
|
70
60
|
...defaultKeymap,
|
|
71
|
-
...foldKeymap,
|
|
72
|
-
...historyKeymap,
|
|
73
|
-
...lintKeymap,
|
|
61
|
+
// ...foldKeymap,
|
|
62
|
+
// ...historyKeymap,
|
|
63
|
+
// ...lintKeymap,
|
|
74
64
|
...searchKeymap,
|
|
75
65
|
indentWithTab,
|
|
76
66
|
]),
|
|
77
67
|
|
|
78
68
|
// Main extension.
|
|
79
69
|
// https://github.com/codemirror/lang-markdown
|
|
70
|
+
// https://codemirror.net/5/mode/markdown/index.html (demo).
|
|
80
71
|
markdown({
|
|
72
|
+
// GRM by default (vs strict CommonMark):
|
|
73
|
+
// Table, TaskList, Strikethrough, and Autolink.
|
|
74
|
+
// NOTE: This extends the parser; it doesn't affect rendering.
|
|
75
|
+
// https://github.github.com/gfm
|
|
76
|
+
// https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
|
|
81
77
|
base: markdownLanguage,
|
|
78
|
+
|
|
79
|
+
// Languages for syntax highlighting fenced code blocks.
|
|
82
80
|
codeLanguages: languages,
|
|
83
|
-
extensions: [
|
|
84
|
-
// TODO(burdon): This seems to upgrade the parser.
|
|
85
|
-
// GitHub flavored markdown bundle: Table, TaskList, Strikethrough, and Autolink.
|
|
86
|
-
// https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
|
|
87
|
-
// https://github.github.com/gfm
|
|
88
|
-
// GFM,
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
// Parser extensions.
|
|
83
|
+
extensions: [
|
|
84
|
+
// GFM, // TODO(burdon): Provided by default?
|
|
91
85
|
markdownTagsExtension,
|
|
92
86
|
],
|
|
93
87
|
}),
|
|
94
88
|
|
|
95
|
-
//
|
|
89
|
+
// Custom styles.
|
|
96
90
|
syntaxHighlighting(markdownHighlightStyle),
|
|
91
|
+
|
|
92
|
+
// TODO(thure): All but one rule here apply to both themes; rename or refactor.
|
|
97
93
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
98
94
|
].filter(Boolean) as Extension[];
|
|
99
95
|
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
|
+
import { HighlightStyle } from '@codemirror/language';
|
|
7
|
+
import { tags, styleTags, Tag } from '@lezer/highlight';
|
|
8
|
+
import { type MarkdownConfig } from '@lezer/markdown';
|
|
9
|
+
import get from 'lodash.get';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
blockquote,
|
|
13
|
+
bold,
|
|
14
|
+
code,
|
|
15
|
+
codeMark,
|
|
16
|
+
heading,
|
|
17
|
+
horizontalRule,
|
|
18
|
+
inlineUrl,
|
|
19
|
+
italic,
|
|
20
|
+
mark,
|
|
21
|
+
strikethrough,
|
|
22
|
+
tokens,
|
|
23
|
+
} from '../../../../styles';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom tags defined and processed by the GFM lezer extension.
|
|
27
|
+
* https://github.com/lezer-parser/markdown
|
|
28
|
+
* https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
|
|
29
|
+
*/
|
|
30
|
+
export const markdownTags = {
|
|
31
|
+
Blockquote: Tag.define(),
|
|
32
|
+
CodeMark: Tag.define(),
|
|
33
|
+
CodeText: Tag.define(),
|
|
34
|
+
EmphasisMark: Tag.define(),
|
|
35
|
+
HeaderMark: Tag.define(),
|
|
36
|
+
HorizontalRule: Tag.define(),
|
|
37
|
+
InlineCode: Tag.define(),
|
|
38
|
+
LinkLabel: Tag.define(),
|
|
39
|
+
LinkReference: Tag.define(),
|
|
40
|
+
ListMark: Tag.define(),
|
|
41
|
+
QuoteMark: Tag.define(),
|
|
42
|
+
URL: Tag.define(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const markdownTagsExtension: MarkdownConfig = {
|
|
46
|
+
props: [styleTags(markdownTags)],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Styling based on `lezer` parser tags.
|
|
51
|
+
* https://codemirror.net/examples/styling
|
|
52
|
+
* https://github.com/lezer-parser/highlight
|
|
53
|
+
* https://lezer.codemirror.net/docs/ref/#highlight (list of tags)
|
|
54
|
+
*/
|
|
55
|
+
export const markdownHighlightStyle = HighlightStyle.define(
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
tag: [
|
|
59
|
+
tags.keyword,
|
|
60
|
+
tags.name,
|
|
61
|
+
tags.deleted,
|
|
62
|
+
tags.character,
|
|
63
|
+
tags.propertyName,
|
|
64
|
+
tags.macroName,
|
|
65
|
+
tags.color,
|
|
66
|
+
tags.constant(tags.name),
|
|
67
|
+
tags.standard(tags.name),
|
|
68
|
+
tags.definition(tags.name),
|
|
69
|
+
tags.separator,
|
|
70
|
+
tags.typeName,
|
|
71
|
+
tags.className,
|
|
72
|
+
tags.number,
|
|
73
|
+
tags.changed,
|
|
74
|
+
tags.annotation,
|
|
75
|
+
tags.modifier,
|
|
76
|
+
tags.self,
|
|
77
|
+
tags.namespace,
|
|
78
|
+
tags.operator,
|
|
79
|
+
tags.operatorKeyword,
|
|
80
|
+
tags.escape,
|
|
81
|
+
tags.regexp,
|
|
82
|
+
tags.special(tags.string),
|
|
83
|
+
tags.meta,
|
|
84
|
+
tags.comment,
|
|
85
|
+
tags.atom,
|
|
86
|
+
tags.bool,
|
|
87
|
+
tags.special(tags.variableName),
|
|
88
|
+
tags.processingInstruction,
|
|
89
|
+
tags.string,
|
|
90
|
+
tags.inserted,
|
|
91
|
+
tags.invalid,
|
|
92
|
+
],
|
|
93
|
+
color: 'inherit !important',
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Markdown marks.
|
|
97
|
+
{
|
|
98
|
+
tag: [
|
|
99
|
+
tags.meta,
|
|
100
|
+
tags.processingInstruction,
|
|
101
|
+
markdownTags.CodeMark,
|
|
102
|
+
markdownTags.EmphasisMark,
|
|
103
|
+
markdownTags.HeaderMark,
|
|
104
|
+
markdownTags.LinkLabel,
|
|
105
|
+
markdownTags.LinkReference,
|
|
106
|
+
markdownTags.ListMark,
|
|
107
|
+
markdownTags.QuoteMark,
|
|
108
|
+
],
|
|
109
|
+
class: mark,
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Headings.
|
|
113
|
+
{ tag: tags.heading1, class: heading[1] },
|
|
114
|
+
{ tag: tags.heading2, class: heading[2] },
|
|
115
|
+
{ tag: tags.heading3, class: heading[3] },
|
|
116
|
+
{ tag: tags.heading4, class: heading[4] },
|
|
117
|
+
{ tag: tags.heading5, class: heading[5] },
|
|
118
|
+
{ tag: tags.heading6, class: heading[6] },
|
|
119
|
+
|
|
120
|
+
// Emphasis.
|
|
121
|
+
{ tag: tags.emphasis, class: italic },
|
|
122
|
+
{ tag: tags.strong, class: bold },
|
|
123
|
+
{ tag: tags.strikethrough, class: strikethrough },
|
|
124
|
+
|
|
125
|
+
// Naked URLs.
|
|
126
|
+
{
|
|
127
|
+
tag: [markdownTags.URL],
|
|
128
|
+
class: inlineUrl,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// E.g., code block language (after ```).
|
|
132
|
+
{
|
|
133
|
+
tag: [tags.function(tags.variableName), tags.labelName],
|
|
134
|
+
class: codeMark,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// NOTE: The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
|
|
138
|
+
// However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
|
|
139
|
+
// when a language is specified. In this case, the syntax highlighting extensions will colorize
|
|
140
|
+
// the code, but all other CSS properties will be inherited.
|
|
141
|
+
// IMPORTANT: Therefore, the fenced code block will use the base editor font.
|
|
142
|
+
{
|
|
143
|
+
tag: [markdownTags.CodeText, markdownTags.InlineCode],
|
|
144
|
+
class: code,
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
tag: [markdownTags.QuoteMark],
|
|
149
|
+
class: blockquote,
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
tag: [markdownTags.HorizontalRule],
|
|
154
|
+
class: horizontalRule,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Main content, paragraphs, etc.
|
|
158
|
+
// {
|
|
159
|
+
// tag: [tags.content],
|
|
160
|
+
// fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
161
|
+
// },
|
|
162
|
+
],
|
|
163
|
+
{
|
|
164
|
+
scope: markdownLanguage,
|
|
165
|
+
all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
|
|
166
|
+
},
|
|
167
|
+
);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
EditorView,
|
|
7
|
+
Decoration,
|
|
8
|
+
WidgetType,
|
|
9
|
+
MatchDecorator,
|
|
10
|
+
ViewPlugin,
|
|
11
|
+
type DecorationSet,
|
|
12
|
+
type ViewUpdate,
|
|
13
|
+
} from '@codemirror/view';
|
|
14
|
+
|
|
15
|
+
class CheckboxWidget extends WidgetType {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly _pos: number,
|
|
18
|
+
private _checked: boolean,
|
|
19
|
+
private readonly _indent: number,
|
|
20
|
+
private readonly _onCheck: (check: boolean) => void,
|
|
21
|
+
) {
|
|
22
|
+
super();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override eq(other: CheckboxWidget) {
|
|
26
|
+
return this._pos === other._pos && this._checked === other._checked && this._indent === other._indent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toDOM(view: EditorView) {
|
|
30
|
+
const wrap = document.createElement('span');
|
|
31
|
+
wrap.className = 'cm-task-item';
|
|
32
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
33
|
+
wrap.style.setProperty('margin-left', this._indent * 24 + 'px');
|
|
34
|
+
|
|
35
|
+
const input = wrap.appendChild(document.createElement('input'));
|
|
36
|
+
input.type = 'checkbox';
|
|
37
|
+
input.checked = this._checked;
|
|
38
|
+
input.onchange = (event: Event) => {
|
|
39
|
+
this._onCheck((event.target as any).checked);
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return wrap;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override ignoreEvent() {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// TODO(burdon): Reconcile with theme.
|
|
52
|
+
const styles = EditorView.baseTheme({
|
|
53
|
+
'& .cm-task-item': {
|
|
54
|
+
paddingLeft: '4px',
|
|
55
|
+
paddingRight: '8px',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const tasklist = () => {
|
|
60
|
+
// TODO(burdon): Matcher isn't as precise as syntax tree.
|
|
61
|
+
// Allows for indents to be greater than AST would allow.
|
|
62
|
+
const taskMatcher = new MatchDecorator({
|
|
63
|
+
regexp: /^(\s*)- \[([ xX])\]\s/g,
|
|
64
|
+
decoration: (match, view, pos) => {
|
|
65
|
+
const indent = Math.floor(match[1].length / 2);
|
|
66
|
+
const checked = match[2] === 'x' || match[2] === 'X';
|
|
67
|
+
return Decoration.replace({
|
|
68
|
+
widget: new CheckboxWidget(pos, checked, indent, (checked) => {
|
|
69
|
+
const idx = pos + match[0].indexOf('[') + 1;
|
|
70
|
+
view.dispatch({
|
|
71
|
+
changes: {
|
|
72
|
+
from: idx,
|
|
73
|
+
to: idx + 1,
|
|
74
|
+
insert: checked ? 'x' : ' ',
|
|
75
|
+
},
|
|
76
|
+
// TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
|
|
77
|
+
selection: {
|
|
78
|
+
anchor: idx + 3,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const tasks = ViewPlugin.fromClass(
|
|
87
|
+
class {
|
|
88
|
+
tasks: DecorationSet;
|
|
89
|
+
constructor(view: EditorView) {
|
|
90
|
+
this.tasks = taskMatcher.createDeco(view);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
update(update: ViewUpdate) {
|
|
94
|
+
this.tasks = taskMatcher.updateDeco(update, this.tasks);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
decorations: (instance) => instance.tasks,
|
|
99
|
+
provide: (plugin) =>
|
|
100
|
+
EditorView.atomicRanges.of((view) => {
|
|
101
|
+
return view.plugin(plugin)?.tasks || Decoration.none;
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return [tasks, styles];
|
|
107
|
+
};
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
5
6
|
import { hoverTooltip } from '@codemirror/view';
|
|
6
7
|
|
|
7
8
|
import { tooltipContent } from '@dxos/react-ui-theme';
|
|
8
9
|
|
|
9
10
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
|
|
10
11
|
|
|
11
|
-
export type
|
|
12
|
+
export type TooltipOptions = {
|
|
13
|
+
onHover: (el: Element, url: string) => void;
|
|
14
|
+
regexp?: RegExp;
|
|
15
|
+
};
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* https://codemirror.net/examples/tooltip
|
|
15
19
|
* https://codemirror.net/docs/ref/#view.hoverTooltip
|
|
16
20
|
* https://github.com/codemirror/view/blob/main/src/tooltip.ts
|
|
17
21
|
*/
|
|
18
|
-
|
|
19
|
-
export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
|
|
22
|
+
export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions): Extension =>
|
|
20
23
|
hoverTooltip((view, pos) => {
|
|
21
24
|
const { from, text } = view.state.doc.lineAt(pos);
|
|
22
25
|
const p = pos - from;
|
|
@@ -49,13 +52,10 @@ export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markd
|
|
|
49
52
|
end: idx + from + match[0].length,
|
|
50
53
|
above: true,
|
|
51
54
|
create: () => {
|
|
52
|
-
const el = document.createElement('
|
|
53
|
-
el.
|
|
54
|
-
el.className = tooltipContent({}, 'mb-2 p-1');
|
|
55
|
-
el.setAttribute('target', '_blank');
|
|
56
|
-
el.setAttribute('href', url);
|
|
55
|
+
const el = document.createElement('div');
|
|
56
|
+
el.className = tooltipContent({}, 'pli-2 plb-1');
|
|
57
57
|
onHover(el, url);
|
|
58
|
-
return { dom: el };
|
|
58
|
+
return { dom: el, offset: { x: 0, y: 12 } };
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
});
|