@dxos/react-ui-editor 0.3.9 → 0.3.10-main.3c0616c
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 +722 -701
- 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 +35 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +4 -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 +9 -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/{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 +5 -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 +7 -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 +5 -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 +17 -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/useTextModel.d.ts +1 -0
- 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 +21 -32
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +242 -0
- package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
- package/src/components/TextEditor/TextEditor.tsx +62 -45
- package/src/components/TextEditor/extensions/autocomplete.ts +72 -0
- package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +17 -2
- package/src/components/TextEditor/extensions/index.ts +5 -2
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +68 -31
- package/src/components/TextEditor/extensions/{change.ts → listener.ts} +3 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +15 -11
- package/src/components/TextEditor/extensions/markdown/highlight.ts +166 -0
- package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
- package/src/components/TextEditor/extensions/tasklist.ts +113 -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 +170 -0
- package/src/components/TextEditor/themes/index.ts +5 -0
- package/src/components/TextEditor/yjs.stories.tsx +1 -0
- package/src/hooks/useCollaboration.ts +4 -4
- package/src/hooks/useTextModel.ts +10 -5
- package/src/styles/markdown.ts +26 -24
- package/src/testing/replicator.ts +1 -0
- 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/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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// TODO(burdon): Automcomplete: https://codemirror.net/5/doc/manual.html#addon_runmode
|
|
6
|
+
// TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
|
|
7
|
+
// TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
|
|
8
|
+
// https://codemirror.net/5/doc/manual.html#addon_runmode
|
|
9
|
+
// TODO(burdon): Add-on: dialog.
|
|
10
|
+
// TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
|
|
11
|
+
// TODO(burdon): Split view: https://codemirror.net/examples/split
|
|
12
|
+
|
|
13
|
+
// https://codemirror.net/examples/autocompletion
|
|
14
|
+
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
15
|
+
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
autocompletion,
|
|
19
|
+
type Completion,
|
|
20
|
+
type CompletionContext,
|
|
21
|
+
completionKeymap,
|
|
22
|
+
type CompletionResult,
|
|
23
|
+
} from '@codemirror/autocomplete';
|
|
24
|
+
import { keymap } from '@codemirror/view';
|
|
25
|
+
|
|
26
|
+
export type AutocompleteOptions = {
|
|
27
|
+
getOptions: (text: string) => Completion[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Autocomplete extension.
|
|
32
|
+
*/
|
|
33
|
+
export const autocomplete = ({ getOptions }: AutocompleteOptions) => {
|
|
34
|
+
return [
|
|
35
|
+
// https://codemirror.net/docs/ref/#view.keymap
|
|
36
|
+
// https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
|
|
37
|
+
keymap.of(completionKeymap),
|
|
38
|
+
|
|
39
|
+
// https://codemirror.net/examples/autocompletion
|
|
40
|
+
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
41
|
+
autocompletion({
|
|
42
|
+
// TODO(burdon): Set custom keymap.
|
|
43
|
+
// defaultKeymap: false,
|
|
44
|
+
|
|
45
|
+
// Don't start unless key press.
|
|
46
|
+
activateOnTyping: false,
|
|
47
|
+
|
|
48
|
+
// TODO(burdon): Option to create new page?
|
|
49
|
+
// TODO(burdon): Optional decoration via addToOptions
|
|
50
|
+
override: [
|
|
51
|
+
(context: CompletionContext): CompletionResult | null => {
|
|
52
|
+
const word = context.matchBefore(/\w*/);
|
|
53
|
+
if (!word || (word.from === word.to && !context.explicit)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO(burdon): Option to convert to links?
|
|
58
|
+
return {
|
|
59
|
+
from: word.from,
|
|
60
|
+
options: getOptions(word.text.toLowerCase()),
|
|
61
|
+
// options: [
|
|
62
|
+
// { label: 'apple', type: 'keyword' },
|
|
63
|
+
// { label: 'amazon', type: 'keyword' },
|
|
64
|
+
// { label: 'hello', type: 'variable', info: '(World)' },
|
|
65
|
+
// { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
|
66
|
+
// ],
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
71
|
+
];
|
|
72
|
+
};
|
|
@@ -17,12 +17,14 @@ export type BasicBundleOptions = {
|
|
|
17
17
|
|
|
18
18
|
export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
|
|
19
19
|
[
|
|
20
|
+
// TODO(burdon): Compare with minimalSetup.
|
|
21
|
+
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
20
22
|
bracketMatching(),
|
|
21
23
|
closeBrackets(),
|
|
22
|
-
_placeholder && placeholder(_placeholder),
|
|
23
|
-
|
|
24
24
|
EditorView.lineWrapping,
|
|
25
25
|
|
|
26
|
+
_placeholder && placeholder(_placeholder),
|
|
27
|
+
|
|
26
28
|
// TODO(burdon): Is this required?
|
|
27
29
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
28
30
|
].filter(Boolean) as Extension[];
|
package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx}
RENAMED
|
@@ -13,12 +13,27 @@ import {
|
|
|
13
13
|
type ViewUpdate,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
15
|
|
|
16
|
+
// TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
|
|
17
|
+
// TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
|
|
18
|
+
// https://codemirror.net/5/doc/manual.html#addon_runmode
|
|
19
|
+
// TODO(burdon): Add-on: dialog.
|
|
20
|
+
// TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
|
|
21
|
+
// TODO(burdon): Split view: https://codemirror.net/examples/split
|
|
22
|
+
|
|
23
|
+
// https://codemirror.net/examples/autocompletion
|
|
24
|
+
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
25
|
+
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
|
26
|
+
|
|
27
|
+
// TODO(burdon): Hint to customize?
|
|
28
|
+
// https://codemirror.net/examples/autocompletion
|
|
29
|
+
|
|
16
30
|
// Notes: Can't edit widget content (or cursor nav through them).
|
|
17
31
|
// - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
|
|
18
32
|
// Widgets intentionally always get set to contenteditable=false,
|
|
19
33
|
// or they would become part of CodeMirror’s editable content element.
|
|
20
34
|
// You should be able to introduce new contenteditable=true child elements inside of them.
|
|
21
35
|
|
|
36
|
+
// TODO(burdon): URL match?
|
|
22
37
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
|
|
23
38
|
|
|
24
39
|
/**
|
|
@@ -102,7 +117,7 @@ export function hyperLinkExtension() {
|
|
|
102
117
|
);
|
|
103
118
|
}
|
|
104
119
|
|
|
105
|
-
export const
|
|
120
|
+
export const styles = EditorView.baseTheme({
|
|
106
121
|
'.cm-hyperlink-label': {
|
|
107
122
|
cursor: 'pointer',
|
|
108
123
|
textDecoration: 'underline',
|
|
@@ -116,4 +131,4 @@ export const hyperlinkStyle = EditorView.baseTheme({
|
|
|
116
131
|
},
|
|
117
132
|
});
|
|
118
133
|
|
|
119
|
-
export const hyperlinkWidget: Extension = [hyperLinkExtension(),
|
|
134
|
+
export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export * from './autocomplete';
|
|
5
6
|
export * from './basic';
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
7
|
+
export * from './link';
|
|
8
|
+
export * from './listener';
|
|
8
9
|
export * from './markdown';
|
|
10
|
+
export * from './tasklist';
|
|
11
|
+
export * from './tooltip';
|
|
@@ -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,45 +77,37 @@ class Link extends WidgetType {
|
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
79
|
|
|
52
|
-
|
|
53
|
-
link?: boolean;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
58
|
-
* https://codemirror.net/docs/ref/#state.StateField
|
|
59
|
-
*/
|
|
60
|
-
export const hyperlinkDecoration = ({ link }: HyperlinkDecorationConfig = {}) => {
|
|
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) => {
|
|
80
|
+
const update = (state: EditorState, options: LinkOptions) => {
|
|
69
81
|
const builder = new RangeSetBuilder();
|
|
70
82
|
const cursor = state.selection.main.head;
|
|
71
83
|
syntaxTree(state).iterate({
|
|
72
84
|
enter: (node) => {
|
|
73
85
|
// Check if cursor is inside text.
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
86
|
+
if (node.name === 'Link') {
|
|
87
|
+
const marks = node.node.getChildren('LinkMark');
|
|
88
|
+
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
const urlNode = node.node.getChild('URL');
|
|
91
|
+
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
81
92
|
|
|
93
|
+
if (cursor < node.from || cursor > node.to) {
|
|
82
94
|
builder.add(
|
|
83
95
|
node.from,
|
|
84
96
|
node.to,
|
|
85
97
|
Decoration.replace({
|
|
86
|
-
widget: new
|
|
98
|
+
widget: new LinkText(node.from, text, options.link ? url : undefined),
|
|
87
99
|
}),
|
|
88
100
|
);
|
|
101
|
+
}
|
|
89
102
|
|
|
90
|
-
|
|
103
|
+
if (options.onRender) {
|
|
104
|
+
builder.add(
|
|
105
|
+
node.to,
|
|
106
|
+
node.to,
|
|
107
|
+
Decoration.replace({
|
|
108
|
+
widget: new LinkButton(node.from, url, options.onRender),
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
91
111
|
}
|
|
92
112
|
}
|
|
93
113
|
|
|
@@ -97,3 +117,20 @@ const update = (state: EditorState, link?: boolean) => {
|
|
|
97
117
|
|
|
98
118
|
return builder.finish();
|
|
99
119
|
};
|
|
120
|
+
|
|
121
|
+
export type LinkOptions = {
|
|
122
|
+
link?: boolean;
|
|
123
|
+
onRender?: (el: Element, url: string) => void;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
128
|
+
* https://codemirror.net/docs/ref/#state.StateField
|
|
129
|
+
*/
|
|
130
|
+
export const link = (options: LinkOptions = {}): Extension => {
|
|
131
|
+
return StateField.define<RangeSet<any>>({
|
|
132
|
+
create: (state) => update(state, options),
|
|
133
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
134
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
135
|
+
});
|
|
136
|
+
};
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { StateField } from '@codemirror/state';
|
|
6
6
|
|
|
7
|
+
export type TextListener = (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
|
-
|
|
12
|
-
export const onChangeExtension = (onChange: (text: string) => void) =>
|
|
12
|
+
export const listener = (onChange: TextListener) =>
|
|
13
13
|
StateField.define({
|
|
14
14
|
create: () => null,
|
|
15
15
|
update: (_value, transaction) => {
|
|
@@ -29,12 +29,10 @@ import {
|
|
|
29
29
|
placeholder,
|
|
30
30
|
rectangularSelection,
|
|
31
31
|
} from '@codemirror/view';
|
|
32
|
-
// import { GFM } from '@lezer/markdown';
|
|
33
32
|
|
|
34
33
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
35
34
|
|
|
36
|
-
import { markdownTagsExtension } from './
|
|
37
|
-
import { markdownHighlightStyle } from './theme';
|
|
35
|
+
import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
|
|
38
36
|
|
|
39
37
|
export type MarkdownBundleOptions = {
|
|
40
38
|
themeMode?: ThemeMode;
|
|
@@ -77,23 +75,29 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
77
75
|
|
|
78
76
|
// Main extension.
|
|
79
77
|
// https://github.com/codemirror/lang-markdown
|
|
78
|
+
// https://codemirror.net/5/mode/markdown/index.html (demo).
|
|
80
79
|
markdown({
|
|
80
|
+
// GRM by default (vs strict CommonMark):
|
|
81
|
+
// Table, TaskList, Strikethrough, and Autolink.
|
|
82
|
+
// NOTE: This extends the parser; it doesn't affect rendering.
|
|
83
|
+
// https://github.github.com/gfm
|
|
84
|
+
// https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
|
|
81
85
|
base: markdownLanguage,
|
|
86
|
+
|
|
87
|
+
// Languages for syntax highlighting fenced code blocks.
|
|
82
88
|
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
89
|
|
|
90
|
-
|
|
90
|
+
// Parser extensions.
|
|
91
|
+
extensions: [
|
|
92
|
+
// GFM, // TODO(burdon): Provided by default?
|
|
91
93
|
markdownTagsExtension,
|
|
92
94
|
],
|
|
93
95
|
}),
|
|
94
96
|
|
|
95
|
-
//
|
|
97
|
+
// Custom styles.
|
|
96
98
|
syntaxHighlighting(markdownHighlightStyle),
|
|
99
|
+
|
|
100
|
+
// TODO(thure): All but one rule here apply to both themes; rename or refactor.
|
|
97
101
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
98
102
|
].filter(Boolean) as Extension[];
|
|
99
103
|
};
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
// 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
|
+
{
|
|
142
|
+
tag: [markdownTags.CodeText, markdownTags.InlineCode],
|
|
143
|
+
class: code,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
tag: [markdownTags.QuoteMark],
|
|
148
|
+
class: blockquote,
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
tag: [markdownTags.HorizontalRule],
|
|
153
|
+
class: horizontalRule,
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// Main content, paragraphs, etc.
|
|
157
|
+
// {
|
|
158
|
+
// tag: [tags.content],
|
|
159
|
+
// fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
160
|
+
// },
|
|
161
|
+
],
|
|
162
|
+
{
|
|
163
|
+
scope: markdownLanguage,
|
|
164
|
+
all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
|
|
165
|
+
},
|
|
166
|
+
);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
type RangeSet,
|
|
10
|
+
RangeSetBuilder,
|
|
11
|
+
StateField,
|
|
12
|
+
type Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
14
|
+
import { EditorView, Decoration, WidgetType } from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
class CheckboxWidget extends WidgetType {
|
|
17
|
+
constructor(private readonly _pos: number, private readonly _getCursor: () => number, private _checked: boolean) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override eq(other: CheckboxWidget) {
|
|
22
|
+
// Uniqueness based on position; also checks state.
|
|
23
|
+
// TODO(burdon): Is this efficient? Check life-cycle.
|
|
24
|
+
return this._pos === other._pos && this._checked === other._checked;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toDOM(view: EditorView) {
|
|
28
|
+
// console.log('toDom', this._pos);
|
|
29
|
+
const wrap = document.createElement('span');
|
|
30
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
31
|
+
wrap.className = 'cm-task-item';
|
|
32
|
+
|
|
33
|
+
const box = wrap.appendChild(document.createElement('input'));
|
|
34
|
+
box.type = 'checkbox';
|
|
35
|
+
box.checked = this._checked;
|
|
36
|
+
box.onclick = (event) => {
|
|
37
|
+
this._checked = !isChecked(view.state, this._pos);
|
|
38
|
+
view.dispatch({
|
|
39
|
+
changes: {
|
|
40
|
+
from: this._pos + 1,
|
|
41
|
+
to: this._pos + 2,
|
|
42
|
+
insert: this._checked ? 'x' : ' ',
|
|
43
|
+
},
|
|
44
|
+
// TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
|
|
45
|
+
selection: {
|
|
46
|
+
anchor: this._pos + 4,
|
|
47
|
+
// anchor: this._getCursor(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return wrap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO(burdon): Remove listener?
|
|
57
|
+
// override destroy() {
|
|
58
|
+
// console.log('destroy', this._pos);
|
|
59
|
+
// }
|
|
60
|
+
|
|
61
|
+
override ignoreEvent() {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isChecked = (state: EditorState, pos: number) => {
|
|
67
|
+
return state.sliceDoc(pos, pos + 3).toLowerCase() === '[x]';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const statefield = (): Extension => {
|
|
71
|
+
let lastPosition: number = 0;
|
|
72
|
+
const listener = EditorView.updateListener.of((update) => {
|
|
73
|
+
lastPosition = update.startState.selection.main.head;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const checkbox = (state: EditorState) => {
|
|
77
|
+
const builder = new RangeSetBuilder();
|
|
78
|
+
syntaxTree(state).iterate({
|
|
79
|
+
enter: (node) => {
|
|
80
|
+
// console.log(node.name, node.from, node.to);
|
|
81
|
+
if (node.name === 'TaskMarker') {
|
|
82
|
+
builder.add(
|
|
83
|
+
node.from,
|
|
84
|
+
node.to,
|
|
85
|
+
Decoration.replace({
|
|
86
|
+
widget: new CheckboxWidget(node.from, () => lastPosition, isChecked(state, node.from)),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return builder.finish();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
listener,
|
|
98
|
+
StateField.define<RangeSet<any>>({
|
|
99
|
+
create: (state) => checkbox(state),
|
|
100
|
+
update: (_: RangeSet<any>, tr: Transaction) => checkbox(tr.state),
|
|
101
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
102
|
+
}),
|
|
103
|
+
];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// TODO(burdon): Reconcile with theme.
|
|
107
|
+
export const styles = EditorView.baseTheme({
|
|
108
|
+
'& .cm-task-item': {
|
|
109
|
+
padding: '0 4px',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const tasklist = () => [statefield(), styles];
|