@dxos/react-ui-editor 0.3.10 → 0.3.11-main.0e40963
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 +1971 -1744
- 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 +14 -10
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
- package/dist/types/src/extensions/basic.d.ts.map +1 -0
- package/dist/types/src/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/extensions/code.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +48 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts +2 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/image.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +3 -3
- package/dist/types/src/extensions/index.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
- package/dist/types/src/extensions/link.d.ts.map +1 -0
- package/dist/types/src/extensions/listener.d.ts +10 -0
- package/dist/types/src/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/extensions/mermaid.d.ts +3 -0
- package/dist/types/src/extensions/mermaid.d.ts.map +1 -0
- package/dist/types/src/extensions/table.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions/demo.d.ts → extensions/typewriter.d.ts} +2 -3
- package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/util.d.ts +5 -0
- package/dist/types/src/extensions/util.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts +2 -3
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/{PatchSemaphore.d.ts → semaphore.d.ts} +1 -1
- package/dist/types/src/hooks/automerge/semaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/update-automerge.d.ts +6 -0
- package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts +5 -0
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +14 -6
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs/yjs.test.d.ts +2 -0
- package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +5 -13
- package/dist/types/src/themes/default.d.ts.map +1 -0
- package/dist/types/src/themes/index.d.ts.map +1 -0
- package/package.json +24 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +130 -71
- package/src/components/TextEditor/TextEditor.stories.tsx +2 -2
- package/src/components/TextEditor/TextEditor.tsx +29 -25
- package/src/components/TextEditor/index.ts +0 -3
- package/src/{components/TextEditor/extensions → extensions}/code.ts +13 -13
- package/src/extensions/comments.ts +391 -0
- package/src/extensions/hr.ts +74 -0
- package/src/{components/TextEditor/extensions → extensions}/image.ts +3 -1
- package/src/{components/TextEditor/extensions → extensions}/index.ts +3 -3
- package/src/{components/TextEditor/extensions → extensions}/link.ts +66 -10
- package/src/extensions/listener.ts +35 -0
- package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +6 -9
- package/src/extensions/mermaid.ts +11 -0
- package/src/{components/TextEditor/extensions → extensions}/table.ts +14 -1
- package/src/{components/TextEditor/extensions/demo.ts → extensions/typewriter.ts} +2 -3
- package/src/extensions/util.ts +18 -0
- package/src/hooks/automerge/automerge.stories.tsx +2 -3
- package/src/hooks/automerge/plugin.ts +1 -1
- package/src/hooks/automerge/{PatchSemaphore.ts → semaphore.ts} +4 -3
- package/src/hooks/automerge/{translation/codemirror-to-automerge.ts → update-automerge.ts} +3 -3
- package/src/hooks/automerge/{translation/automerge-to-codemirror.ts → update-codemirror.ts} +2 -7
- package/src/hooks/useTextModel.ts +60 -22
- package/src/hooks/yjs/yjs.test.ts +59 -0
- package/src/index.ts +3 -0
- package/src/testing/replicator.ts +0 -1
- package/src/{components/TextEditor/themes → themes}/default.ts +61 -38
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +0 -18
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +0 -10
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +0 -24
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +0 -5
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +0 -6
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/translation/index.d.ts +0 -3
- package/dist/types/src/hooks/automerge/translation/index.d.ts.map +0 -1
- package/src/components/TextEditor/extensions/comments.ts +0 -173
- package/src/components/TextEditor/extensions/experimental.tsx +0 -114
- package/src/components/TextEditor/extensions/highlight.ts +0 -128
- package/src/components/TextEditor/extensions/listener.ts +0 -23
- package/src/components/TextEditor/extensions/tooltip.tsx +0 -61
- package/src/hooks/automerge/translation/index.ts +0 -6
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/tasklist.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/tasklist.ts +0 -0
- /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
StateField,
|
|
13
13
|
type Transaction,
|
|
14
14
|
} from '@codemirror/state';
|
|
15
|
-
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
15
|
+
import { Decoration, EditorView, WidgetType, hoverTooltip } from '@codemirror/view';
|
|
16
|
+
|
|
17
|
+
import { tooltipContent } from '@dxos/react-ui-theme';
|
|
18
|
+
|
|
19
|
+
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
|
|
16
20
|
|
|
17
21
|
// Adapted from:
|
|
18
22
|
// https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
|
|
@@ -20,6 +24,7 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
|
20
24
|
|
|
21
25
|
export type LinkOptions = {
|
|
22
26
|
link?: boolean;
|
|
27
|
+
onHover?: (el: Element, url: string) => void;
|
|
23
28
|
onRender?: (el: Element, url: string) => void;
|
|
24
29
|
};
|
|
25
30
|
|
|
@@ -28,11 +33,62 @@ export type LinkOptions = {
|
|
|
28
33
|
* https://codemirror.net/docs/ref/#state.StateField
|
|
29
34
|
*/
|
|
30
35
|
export const link = (options: LinkOptions = {}): Extension => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const extensions: Extension[] = [
|
|
37
|
+
StateField.define<RangeSet<any>>({
|
|
38
|
+
create: (state) => update(state, options),
|
|
39
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
|
|
40
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
41
|
+
}),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (options.onHover) {
|
|
45
|
+
extensions.push(
|
|
46
|
+
// https://codemirror.net/examples/tooltip
|
|
47
|
+
// https://codemirror.net/docs/ref/#view.hoverTooltip
|
|
48
|
+
// https://github.com/codemirror/view/blob/main/src/tooltip.ts
|
|
49
|
+
hoverTooltip((view, pos) => {
|
|
50
|
+
const { from, text } = view.state.doc.lineAt(pos);
|
|
51
|
+
const p = pos - from;
|
|
52
|
+
|
|
53
|
+
let idx = 0;
|
|
54
|
+
let match;
|
|
55
|
+
do {
|
|
56
|
+
match = text.substring(idx).match(markdownLinkRegexp);
|
|
57
|
+
if (!match) {
|
|
58
|
+
match = undefined;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
idx = text.indexOf(match[0]);
|
|
63
|
+
if (p >= idx && p < idx + match[0].length) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
idx += match[0].length;
|
|
68
|
+
} while (true);
|
|
69
|
+
|
|
70
|
+
if (!match) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [, , url] = match ?? [];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
pos: idx + from,
|
|
78
|
+
end: idx + from + match[0].length,
|
|
79
|
+
above: true,
|
|
80
|
+
create: () => {
|
|
81
|
+
const el = document.createElement('div');
|
|
82
|
+
el.className = tooltipContent({}, 'pli-2 plb-1');
|
|
83
|
+
options.onHover!(el, url);
|
|
84
|
+
return { dom: el, offset: { x: 0, y: 4 } };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return extensions;
|
|
36
92
|
};
|
|
37
93
|
|
|
38
94
|
class LinkButton extends WidgetType {
|
|
@@ -56,12 +112,12 @@ class LinkButton extends WidgetType {
|
|
|
56
112
|
}
|
|
57
113
|
|
|
58
114
|
class LinkText extends WidgetType {
|
|
59
|
-
constructor(private readonly
|
|
115
|
+
constructor(private readonly _text: string, private readonly _url?: string) {
|
|
60
116
|
super();
|
|
61
117
|
}
|
|
62
118
|
|
|
63
119
|
override eq(other: LinkText) {
|
|
64
|
-
return this.
|
|
120
|
+
return this._url === other._url;
|
|
65
121
|
}
|
|
66
122
|
|
|
67
123
|
toDOM(view: EditorView) {
|
|
@@ -76,7 +132,7 @@ class LinkText extends WidgetType {
|
|
|
76
132
|
link.onclick = () => {
|
|
77
133
|
view.dispatch({
|
|
78
134
|
selection: {
|
|
79
|
-
anchor:
|
|
135
|
+
anchor: view.posAtDOM(link),
|
|
80
136
|
},
|
|
81
137
|
});
|
|
82
138
|
};
|
|
@@ -112,7 +168,7 @@ const update = (state: EditorState, options: LinkOptions) => {
|
|
|
112
168
|
node.from,
|
|
113
169
|
node.to,
|
|
114
170
|
Decoration.replace({
|
|
115
|
-
widget: new LinkText(
|
|
171
|
+
widget: new LinkText(text, options.link ? url : undefined),
|
|
116
172
|
}),
|
|
117
173
|
);
|
|
118
174
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
export type ListenerOptions = {
|
|
9
|
+
onFocus?: (focused: boolean) => void;
|
|
10
|
+
onChange?: (text: string) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Event listener.
|
|
15
|
+
*/
|
|
16
|
+
export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
|
|
17
|
+
const extensions: Extension[] = [];
|
|
18
|
+
|
|
19
|
+
onFocus &&
|
|
20
|
+
extensions.push(
|
|
21
|
+
EditorView.focusChangeEffect.of((_, focused) => {
|
|
22
|
+
onFocus(focused);
|
|
23
|
+
return null;
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
onChange &&
|
|
28
|
+
extensions.push(
|
|
29
|
+
EditorView.updateListener.of((update) => {
|
|
30
|
+
onChange(update.state.doc.toString());
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return extensions;
|
|
35
|
+
};
|
|
@@ -8,21 +8,18 @@ import { tags, styleTags, Tag } from '@lezer/highlight';
|
|
|
8
8
|
import { type MarkdownConfig, Table } from '@lezer/markdown';
|
|
9
9
|
import get from 'lodash.get';
|
|
10
10
|
|
|
11
|
-
import { mx } from '@dxos/react-ui-theme';
|
|
12
|
-
|
|
13
11
|
import {
|
|
14
12
|
blockquote,
|
|
15
13
|
bold,
|
|
16
14
|
code,
|
|
17
15
|
codeMark,
|
|
18
16
|
heading,
|
|
19
|
-
horizontalRule,
|
|
20
17
|
inlineUrl,
|
|
21
18
|
italic,
|
|
22
19
|
mark,
|
|
23
20
|
strikethrough,
|
|
24
21
|
tokens,
|
|
25
|
-
} from '
|
|
22
|
+
} from '../../styles';
|
|
26
23
|
|
|
27
24
|
/**
|
|
28
25
|
* Custom tags defined and processed by the GFM lezer extension.
|
|
@@ -35,7 +32,7 @@ export const markdownTags = {
|
|
|
35
32
|
CodeText: Tag.define(),
|
|
36
33
|
EmphasisMark: Tag.define(),
|
|
37
34
|
HeaderMark: Tag.define(),
|
|
38
|
-
HorizontalRule: Tag.define(),
|
|
35
|
+
// HorizontalRule: Tag.define(),
|
|
39
36
|
InlineCode: Tag.define(),
|
|
40
37
|
LinkLabel: Tag.define(),
|
|
41
38
|
LinkReference: Tag.define(),
|
|
@@ -182,10 +179,10 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
|
|
|
182
179
|
},
|
|
183
180
|
|
|
184
181
|
// TODO(burdon): Replace with extension.
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
},
|
|
182
|
+
// {
|
|
183
|
+
// tag: [markdownTags.HorizontalRule],
|
|
184
|
+
// class: mx(horizontalRule, readonly && '-indent-[100rem]'),
|
|
185
|
+
// },
|
|
189
186
|
|
|
190
187
|
// TODO(burdon): Only if being edited.
|
|
191
188
|
{
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
// TODO(burdon): How will this interact with the code extension?
|
|
8
|
+
// TODO(burdon): Move to mermaid-plugin (which should contribute this extension).
|
|
9
|
+
export const mermaid = (): Extension => {
|
|
10
|
+
return [];
|
|
11
|
+
};
|
|
@@ -9,6 +9,9 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
|
9
9
|
// TODO(burdon): Snippet to create basic table.
|
|
10
10
|
// https://codemirror.net/docs/ref/#autocomplete.snippet
|
|
11
11
|
|
|
12
|
+
// TODO(burdon): Advanced formatting (left/right/center).
|
|
13
|
+
// TODO(burdon): Editor to auto balance columns.
|
|
14
|
+
|
|
12
15
|
export type TableOptions = {};
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -73,7 +76,17 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
73
76
|
|
|
74
77
|
tables.forEach((table) => {
|
|
75
78
|
const hide = state.readOnly || cursor < table.from || cursor > table.to;
|
|
76
|
-
hide &&
|
|
79
|
+
hide &&
|
|
80
|
+
builder.add(
|
|
81
|
+
table.from,
|
|
82
|
+
table.to,
|
|
83
|
+
Decoration.replace({
|
|
84
|
+
block: true,
|
|
85
|
+
widget: new TableWidget(table),
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Add class for styling.
|
|
77
90
|
builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
|
|
78
91
|
});
|
|
79
92
|
|
|
@@ -15,12 +15,11 @@ export type DemoOptions = {
|
|
|
15
15
|
const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Demo script.
|
|
19
18
|
* Configurable plugin that let's user cycle through pre-configured input script.
|
|
20
19
|
*/
|
|
21
|
-
export const
|
|
20
|
+
export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
|
|
22
21
|
let t: any;
|
|
23
|
-
let idx = 0;
|
|
22
|
+
let idx = 0; // TODO(burdon): Make global.
|
|
24
23
|
|
|
25
24
|
return [
|
|
26
25
|
keymap.of([
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { log } from '@dxos/log';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CodeMirror callbacks swallow errors so wrap handlers.
|
|
9
|
+
*/
|
|
10
|
+
// TODO(burdon): Figure out how to systematize this.
|
|
11
|
+
export const callbackWrapper = <T extends Function>(fn: T): T =>
|
|
12
|
+
((...args: any[]) => {
|
|
13
|
+
try {
|
|
14
|
+
return fn(...args);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
log.catch(error);
|
|
17
|
+
}
|
|
18
|
+
}) as unknown as T;
|
|
@@ -17,8 +17,8 @@ import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '
|
|
|
17
17
|
import * as automerge from '@dxos/automerge/automerge';
|
|
18
18
|
import { type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
19
19
|
|
|
20
|
-
import { PatchSemaphore } from './PatchSemaphore';
|
|
21
20
|
import { type IDocHandle } from './handle';
|
|
21
|
+
import { PatchSemaphore } from './semaphore';
|
|
22
22
|
|
|
23
23
|
export type Value = {
|
|
24
24
|
lastHeads: Heads;
|
|
@@ -10,7 +10,8 @@ import { next as automerge } from '@dxos/automerge/automerge';
|
|
|
10
10
|
|
|
11
11
|
import { type IDocHandle } from './handle';
|
|
12
12
|
import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
|
|
13
|
-
import {
|
|
13
|
+
import { updateAutomerge } from './update-automerge';
|
|
14
|
+
import { updateCodeMirror } from './update-codemirror';
|
|
14
15
|
|
|
15
16
|
type Doc<T> = automerge.Doc<T>;
|
|
16
17
|
type Heads = automerge.Heads;
|
|
@@ -55,7 +56,7 @@ export class PatchSemaphore {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Apply the unreconciled transactions to the document.
|
|
58
|
-
let newHeads =
|
|
59
|
+
let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
|
|
59
60
|
|
|
60
61
|
// NOTE: null and undefined each come from automerge and repo respectively.
|
|
61
62
|
if (newHeads === null || newHeads === undefined) {
|
|
@@ -65,7 +66,7 @@ export class PatchSemaphore {
|
|
|
65
66
|
|
|
66
67
|
// Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
|
|
67
68
|
const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.docSync()!, oldHeads, newHeads);
|
|
68
|
-
|
|
69
|
+
updateCodeMirror(view, selection, path, diff);
|
|
69
70
|
|
|
70
71
|
view.dispatch({
|
|
71
72
|
effects: updateHeads(newHeads),
|
|
@@ -8,10 +8,10 @@ import { type EditorState, type Text, type Transaction } from '@codemirror/state
|
|
|
8
8
|
|
|
9
9
|
import { next as am, type Heads } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
|
-
import { type IDocHandle } from '
|
|
12
|
-
import { type Field } from '
|
|
11
|
+
import { type IDocHandle } from './handle';
|
|
12
|
+
import { type Field } from './plugin';
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const updateAutomerge = (
|
|
15
15
|
field: Field,
|
|
16
16
|
handle: IDocHandle,
|
|
17
17
|
transactions: Transaction[],
|
|
@@ -16,14 +16,9 @@ import {
|
|
|
16
16
|
type SpliceTextPatch,
|
|
17
17
|
} from '@dxos/automerge/automerge';
|
|
18
18
|
|
|
19
|
-
import { reconcileAnnotationType } from '
|
|
19
|
+
import { reconcileAnnotationType } from './plugin';
|
|
20
20
|
|
|
21
|
-
export const
|
|
22
|
-
view: EditorView,
|
|
23
|
-
selection: EditorSelection,
|
|
24
|
-
target: Prop[],
|
|
25
|
-
patches: Patch[],
|
|
26
|
-
) => {
|
|
21
|
+
export const updateCodeMirror = (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
|
|
27
22
|
for (const patch of patches) {
|
|
28
23
|
const changeSpec = handlePatch(patch, target, view.state);
|
|
29
24
|
if (changeSpec != null) {
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension } from '@codemirror/state';
|
|
5
|
+
import { type Extension, StateField } from '@codemirror/state';
|
|
6
6
|
import get from 'lodash.get';
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import { yCollab } from 'y-codemirror.next';
|
|
9
9
|
import type * as awarenessProtocol from 'y-protocols/awareness';
|
|
10
|
+
import * as Y from 'yjs';
|
|
10
11
|
|
|
11
12
|
import { invariant } from '@dxos/invariant';
|
|
12
13
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from '@dxos/react-client/echo';
|
|
20
21
|
import { type Identity } from '@dxos/react-client/halo';
|
|
21
22
|
import type { YText, YXmlFragment } from '@dxos/text-model';
|
|
23
|
+
import { arrayToString, stringToArray } from '@dxos/util';
|
|
22
24
|
|
|
23
25
|
import { automergePlugin } from './automerge';
|
|
24
26
|
import { SpaceAwarenessProvider } from './yjs';
|
|
@@ -26,19 +28,34 @@ import { SpaceAwarenessProvider } from './yjs';
|
|
|
26
28
|
// TODO(burdon): Move.
|
|
27
29
|
type Awareness = awarenessProtocol.Awareness;
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* State field makes the model available to other extensions.
|
|
33
|
+
*/
|
|
34
|
+
// TODO(burdon): Use facet?
|
|
35
|
+
export const modelState = StateField.define<EditorModel | undefined>({
|
|
36
|
+
create: () => undefined,
|
|
37
|
+
update: (model) => model,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type Range = {
|
|
31
41
|
from: number;
|
|
32
42
|
to: number;
|
|
33
43
|
};
|
|
34
44
|
|
|
45
|
+
export type CommentRange = {
|
|
46
|
+
id: string;
|
|
47
|
+
// TODO(burdon): Split into begin/end?
|
|
48
|
+
cursor: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
35
51
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
36
52
|
export type EditorModel = {
|
|
37
53
|
id: string;
|
|
38
54
|
// TODO(burdon): Remove.
|
|
39
55
|
content: string | YText | YXmlFragment | DocAccessor;
|
|
40
56
|
text: () => string;
|
|
41
|
-
|
|
57
|
+
getCursorFromRange?: (value: Range) => string;
|
|
58
|
+
getRangeFromCursor?: (cursor: string) => Range | undefined;
|
|
42
59
|
extension?: Extension;
|
|
43
60
|
awareness?: Awareness;
|
|
44
61
|
peer?: {
|
|
@@ -48,7 +65,7 @@ export type EditorModel = {
|
|
|
48
65
|
};
|
|
49
66
|
|
|
50
67
|
// TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
|
|
51
|
-
export type
|
|
68
|
+
export type UseTextModelProps = {
|
|
52
69
|
identity?: Identity | null;
|
|
53
70
|
space?: Space;
|
|
54
71
|
text?: TextObject;
|
|
@@ -56,39 +73,56 @@ export type UseTextModelOptions = {
|
|
|
56
73
|
|
|
57
74
|
// TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
|
|
58
75
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
59
|
-
export const useTextModel = (
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}, [identity, space, text]);
|
|
76
|
+
export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
|
|
77
|
+
const { identity, space, text } = props;
|
|
78
|
+
const [model, setModel] = useState<EditorModel | undefined>(() => createModel(props));
|
|
79
|
+
useEffect(() => setModel(createModel(props)), [identity, space, text]);
|
|
64
80
|
return model;
|
|
65
81
|
};
|
|
66
82
|
|
|
67
|
-
const createModel = (
|
|
68
|
-
const { text } =
|
|
83
|
+
const createModel = (props: UseTextModelProps) => {
|
|
84
|
+
const { text } = props;
|
|
69
85
|
if (isActualAutomergeObject(text)) {
|
|
70
|
-
return createAutomergeModel(
|
|
86
|
+
return createAutomergeModel(props);
|
|
71
87
|
} else {
|
|
72
88
|
if (!text?.doc) {
|
|
73
89
|
return undefined;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
return createYjsModel(
|
|
92
|
+
return createYjsModel(props);
|
|
77
93
|
}
|
|
78
94
|
};
|
|
79
95
|
|
|
80
|
-
const createYjsModel = ({ identity, space, text }:
|
|
96
|
+
const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
|
|
81
97
|
invariant(text?.doc && text?.content);
|
|
82
98
|
const provider = space
|
|
83
99
|
? new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
|
|
84
100
|
: undefined;
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
const model: EditorModel = {
|
|
87
103
|
id: text.doc.guid,
|
|
88
104
|
content: text.content,
|
|
89
105
|
text: () => text.content!.toString(),
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
// https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
107
|
+
getCursorFromRange: (range: Range) => {
|
|
108
|
+
const from = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.from));
|
|
109
|
+
const to = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.to, -1));
|
|
110
|
+
return [arrayToString(from), arrayToString(to)].join(':');
|
|
111
|
+
},
|
|
112
|
+
getRangeFromCursor: (cursor: string) => {
|
|
113
|
+
invariant(text.doc);
|
|
114
|
+
const parts = cursor.split(':');
|
|
115
|
+
const from = Y.createAbsolutePositionFromRelativePosition(
|
|
116
|
+
Y.decodeRelativePosition(stringToArray(parts[0])),
|
|
117
|
+
text.doc,
|
|
118
|
+
);
|
|
119
|
+
const to = Y.createAbsolutePositionFromRelativePosition(
|
|
120
|
+
Y.decodeRelativePosition(stringToArray(parts[1])),
|
|
121
|
+
text.doc,
|
|
122
|
+
);
|
|
123
|
+
return from && to ? { from: from.index, to: to.index } : undefined;
|
|
124
|
+
},
|
|
125
|
+
extension: [yCollab(text.content as YText, provider?.awareness), modelState.init(() => model)],
|
|
92
126
|
awareness: provider?.awareness,
|
|
93
127
|
peer: identity
|
|
94
128
|
? {
|
|
@@ -97,18 +131,20 @@ const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorM
|
|
|
97
131
|
}
|
|
98
132
|
: undefined,
|
|
99
133
|
};
|
|
134
|
+
|
|
135
|
+
return model;
|
|
100
136
|
};
|
|
101
137
|
|
|
102
|
-
const createAutomergeModel = ({ identity, text }:
|
|
138
|
+
const createAutomergeModel = ({ identity, text }: UseTextModelProps): EditorModel => {
|
|
103
139
|
const obj = text as any as AutomergeTextCompat;
|
|
104
140
|
const doc = getRawDoc(obj, [obj.field]);
|
|
105
141
|
|
|
106
|
-
|
|
142
|
+
const model: EditorModel = {
|
|
107
143
|
id: obj.id,
|
|
108
144
|
content: doc,
|
|
109
145
|
text: () => get(doc.handle.docSync(), doc.path),
|
|
110
|
-
|
|
111
|
-
extension: automergePlugin(doc.handle, doc.path),
|
|
146
|
+
// TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
|
|
147
|
+
extension: [automergePlugin(doc.handle, doc.path), modelState.init(() => model)],
|
|
112
148
|
peer: identity
|
|
113
149
|
? {
|
|
114
150
|
id: identity.identityKey.toHex(),
|
|
@@ -116,4 +152,6 @@ const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorMo
|
|
|
116
152
|
}
|
|
117
153
|
: undefined,
|
|
118
154
|
};
|
|
155
|
+
|
|
156
|
+
return model;
|
|
119
157
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { effect } from '@preact/signals-react';
|
|
6
|
+
import { expect } from 'chai';
|
|
7
|
+
import * as Y from 'yjs';
|
|
8
|
+
|
|
9
|
+
import { Trigger } from '@dxos/async';
|
|
10
|
+
import { Expando, setGlobalAutomergePreference, TextObject } from '@dxos/echo-schema';
|
|
11
|
+
import { registerSignalFactory } from '@dxos/echo-signals';
|
|
12
|
+
import { describe, test } from '@dxos/test';
|
|
13
|
+
import { type YText } from '@dxos/text-model';
|
|
14
|
+
|
|
15
|
+
describe('YJS', () => {
|
|
16
|
+
// https://docs.yjs.dev/api/shared-types/y.text
|
|
17
|
+
// https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
18
|
+
test('absolute position', () => {
|
|
19
|
+
const obj = new TextObject('hello world');
|
|
20
|
+
const index = obj.text.indexOf('world');
|
|
21
|
+
const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
|
|
25
|
+
expect(cursor!.index).to.equal(index);
|
|
26
|
+
expect(obj.text.substring(cursor!.index)).to.equal('world');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
(obj.content as YText).insert(index, 'cruel ');
|
|
30
|
+
expect(obj.text).to.equal('hello cruel world');
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
|
|
34
|
+
expect(cursor!.index).not.to.equal(index);
|
|
35
|
+
expect(obj.text.substring(cursor!.index)).to.equal('world');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// TODO(burdon): Test deleting end character.
|
|
40
|
+
|
|
41
|
+
test.skip('effect', async () => {
|
|
42
|
+
setGlobalAutomergePreference(true);
|
|
43
|
+
registerSignalFactory();
|
|
44
|
+
const obj = new Expando({ comments: [] });
|
|
45
|
+
|
|
46
|
+
const trigger = new Trigger();
|
|
47
|
+
const unsubscribe = effect(() => {
|
|
48
|
+
// TODO(burdon): BUG: doesn't work with YJS.
|
|
49
|
+
console.log(obj.comments);
|
|
50
|
+
if (obj.comments.length > 0) {
|
|
51
|
+
trigger.wake();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
obj.comments.push({ message: 'hello' });
|
|
56
|
+
await trigger.wait();
|
|
57
|
+
unsubscribe();
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
|
|
6
6
|
export { Doc, YText, YXmlFragment } from '@dxos/text-model';
|
|
7
|
+
export { type Extension } from '@codemirror/state';
|
|
7
8
|
|
|
8
9
|
export * from './components';
|
|
10
|
+
export * from './extensions';
|
|
9
11
|
export * from './hooks';
|
|
12
|
+
export * from './themes';
|