@dxos/ui-editor 0.0.0
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/LICENSE +8 -0
- package/README.md +21 -0
- package/package.json +121 -0
- package/src/defaults.ts +34 -0
- package/src/extensions/annotations.ts +55 -0
- package/src/extensions/autocomplete/autocomplete.ts +151 -0
- package/src/extensions/autocomplete/index.ts +8 -0
- package/src/extensions/autocomplete/match.ts +46 -0
- package/src/extensions/autocomplete/placeholder.ts +117 -0
- package/src/extensions/autocomplete/typeahead.ts +87 -0
- package/src/extensions/automerge/automerge.test.tsx +76 -0
- package/src/extensions/automerge/automerge.ts +105 -0
- package/src/extensions/automerge/cursor.ts +28 -0
- package/src/extensions/automerge/defs.ts +31 -0
- package/src/extensions/automerge/index.ts +5 -0
- package/src/extensions/automerge/sync.ts +79 -0
- package/src/extensions/automerge/update-automerge.ts +50 -0
- package/src/extensions/automerge/update-codemirror.ts +115 -0
- package/src/extensions/autoscroll.ts +165 -0
- package/src/extensions/awareness/awareness-provider.ts +127 -0
- package/src/extensions/awareness/awareness.ts +315 -0
- package/src/extensions/awareness/index.ts +6 -0
- package/src/extensions/blast.ts +363 -0
- package/src/extensions/blocks.ts +131 -0
- package/src/extensions/bookmarks.ts +77 -0
- package/src/extensions/comments.ts +579 -0
- package/src/extensions/debug.ts +15 -0
- package/src/extensions/dnd.ts +39 -0
- package/src/extensions/factories.ts +284 -0
- package/src/extensions/focus.ts +36 -0
- package/src/extensions/folding.ts +63 -0
- package/src/extensions/hashtag.ts +68 -0
- package/src/extensions/index.ts +34 -0
- package/src/extensions/json.ts +57 -0
- package/src/extensions/listener.ts +32 -0
- package/src/extensions/markdown/action.ts +117 -0
- package/src/extensions/markdown/bundle.ts +105 -0
- package/src/extensions/markdown/changes.test.ts +26 -0
- package/src/extensions/markdown/changes.ts +149 -0
- package/src/extensions/markdown/debug.ts +44 -0
- package/src/extensions/markdown/decorate.ts +622 -0
- package/src/extensions/markdown/formatting.test.ts +498 -0
- package/src/extensions/markdown/formatting.ts +1265 -0
- package/src/extensions/markdown/highlight.ts +183 -0
- package/src/extensions/markdown/image.ts +118 -0
- package/src/extensions/markdown/index.ts +13 -0
- package/src/extensions/markdown/link.ts +50 -0
- package/src/extensions/markdown/parser.test.ts +75 -0
- package/src/extensions/markdown/styles.ts +135 -0
- package/src/extensions/markdown/table.ts +150 -0
- package/src/extensions/mention.ts +41 -0
- package/src/extensions/modal.ts +24 -0
- package/src/extensions/modes.ts +41 -0
- package/src/extensions/outliner/commands.ts +270 -0
- package/src/extensions/outliner/editor.test.ts +33 -0
- package/src/extensions/outliner/editor.ts +184 -0
- package/src/extensions/outliner/index.ts +7 -0
- package/src/extensions/outliner/menu.ts +128 -0
- package/src/extensions/outliner/outliner.test.ts +100 -0
- package/src/extensions/outliner/outliner.ts +167 -0
- package/src/extensions/outliner/selection.ts +50 -0
- package/src/extensions/outliner/tree.test.ts +168 -0
- package/src/extensions/outliner/tree.ts +317 -0
- package/src/extensions/preview/index.ts +5 -0
- package/src/extensions/preview/preview.ts +193 -0
- package/src/extensions/replacer.test.ts +75 -0
- package/src/extensions/replacer.ts +93 -0
- package/src/extensions/scrolling.ts +189 -0
- package/src/extensions/selection.ts +100 -0
- package/src/extensions/state.ts +7 -0
- package/src/extensions/submit.ts +62 -0
- package/src/extensions/tags/extended-markdown.test.ts +263 -0
- package/src/extensions/tags/extended-markdown.ts +78 -0
- package/src/extensions/tags/index.ts +7 -0
- package/src/extensions/tags/streamer.ts +243 -0
- package/src/extensions/tags/xml-tags.ts +507 -0
- package/src/extensions/tags/xml-util.test.ts +48 -0
- package/src/extensions/tags/xml-util.ts +93 -0
- package/src/extensions/typewriter.ts +68 -0
- package/src/index.ts +14 -0
- package/src/styles/index.ts +7 -0
- package/src/styles/markdown.ts +26 -0
- package/src/styles/theme.ts +293 -0
- package/src/styles/tokens.ts +17 -0
- package/src/types/index.ts +5 -0
- package/src/types/types.ts +32 -0
- package/src/util/cursor.ts +56 -0
- package/src/util/debug.ts +56 -0
- package/src/util/decorations.ts +21 -0
- package/src/util/dom.ts +36 -0
- package/src/util/facet.ts +13 -0
- package/src/util/index.ts +10 -0
- package/src/util/util.ts +29 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
|
+
import { HighlightStyle, bracketMatching, syntaxHighlighting } from '@codemirror/language';
|
|
8
|
+
import { searchKeymap } from '@codemirror/search';
|
|
9
|
+
import { type ChangeSpec, EditorState, type Extension, type TransactionSpec } from '@codemirror/state';
|
|
10
|
+
import {
|
|
11
|
+
EditorView,
|
|
12
|
+
type KeyBinding,
|
|
13
|
+
ViewPlugin,
|
|
14
|
+
drawSelection,
|
|
15
|
+
dropCursor,
|
|
16
|
+
highlightActiveLine,
|
|
17
|
+
keymap,
|
|
18
|
+
lineNumbers,
|
|
19
|
+
placeholder,
|
|
20
|
+
scrollPastEnd,
|
|
21
|
+
} from '@codemirror/view';
|
|
22
|
+
import { vscodeDarkStyle, vscodeLightStyle } from '@uiw/codemirror-theme-vscode';
|
|
23
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
24
|
+
|
|
25
|
+
import { generateName } from '@dxos/display-name';
|
|
26
|
+
import { type DocAccessor } from '@dxos/echo-db';
|
|
27
|
+
import { log } from '@dxos/log';
|
|
28
|
+
import { type Messenger } from '@dxos/protocols';
|
|
29
|
+
import { type Identity } from '@dxos/protocols/proto/dxos/client/services';
|
|
30
|
+
import { type HuePalette } from '@dxos/ui-theme';
|
|
31
|
+
import { type ThemeMode } from '@dxos/ui-types';
|
|
32
|
+
import { hexToHue, isTruthy } from '@dxos/util';
|
|
33
|
+
|
|
34
|
+
import { baseTheme, createFontTheme, editorGutter } from '../styles';
|
|
35
|
+
|
|
36
|
+
import { automerge } from './automerge';
|
|
37
|
+
import { SpaceAwarenessProvider, awareness } from './awareness';
|
|
38
|
+
import { focus } from './focus';
|
|
39
|
+
|
|
40
|
+
//
|
|
41
|
+
// Basic
|
|
42
|
+
//
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Enable tabbing into editor (required for tabster to work).
|
|
46
|
+
*/
|
|
47
|
+
export const tabbable = EditorView.contentAttributes.of({ tabindex: '0' });
|
|
48
|
+
|
|
49
|
+
export const filterChars = (chars: RegExp) => {
|
|
50
|
+
return EditorState.transactionFilter.of((transaction) => {
|
|
51
|
+
if (!transaction.docChanged) {
|
|
52
|
+
return transaction;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const changes: ChangeSpec[] = [];
|
|
56
|
+
transaction.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
57
|
+
const inserted = text.toString();
|
|
58
|
+
const filtered = inserted.replace(chars, '');
|
|
59
|
+
if (inserted !== filtered) {
|
|
60
|
+
changes.push({
|
|
61
|
+
from: fromB,
|
|
62
|
+
to: toB,
|
|
63
|
+
insert: filtered,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (changes.length) {
|
|
69
|
+
return [transaction, { changes, sequential: true } as TransactionSpec];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return transaction;
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* https://codemirror.net/docs/extensions
|
|
78
|
+
* https://github.com/codemirror/basic-setup
|
|
79
|
+
* https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
|
80
|
+
* https://github.com/codemirror/theme-one-dark
|
|
81
|
+
*/
|
|
82
|
+
export type BasicExtensionsOptions = {
|
|
83
|
+
allowMultipleSelections?: boolean;
|
|
84
|
+
bracketMatching?: boolean;
|
|
85
|
+
closeBrackets?: boolean;
|
|
86
|
+
dropCursor?: boolean;
|
|
87
|
+
drawSelection?: boolean;
|
|
88
|
+
editable?: boolean;
|
|
89
|
+
focus?: boolean;
|
|
90
|
+
highlightActiveLine?: boolean;
|
|
91
|
+
history?: boolean;
|
|
92
|
+
indentWithTab?: boolean;
|
|
93
|
+
keymap?: null | 'default' | 'standard';
|
|
94
|
+
lineNumbers?: boolean;
|
|
95
|
+
/** If false then do not set a max-width or side margin on the editor. */
|
|
96
|
+
lineWrapping?: boolean;
|
|
97
|
+
placeholder?: string;
|
|
98
|
+
/** If true user cannot edit the text, but they can still select and copy it. */
|
|
99
|
+
readOnly?: boolean;
|
|
100
|
+
search?: boolean;
|
|
101
|
+
/** NOTE: Do not use with stack sections. */
|
|
102
|
+
scrollPastEnd?: boolean;
|
|
103
|
+
standardKeymap?: boolean;
|
|
104
|
+
tabbable?: boolean;
|
|
105
|
+
tabSize?: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const defaultBasicOptions: BasicExtensionsOptions = {
|
|
109
|
+
allowMultipleSelections: true,
|
|
110
|
+
bracketMatching: true,
|
|
111
|
+
closeBrackets: true,
|
|
112
|
+
drawSelection: true,
|
|
113
|
+
focus: true,
|
|
114
|
+
history: true,
|
|
115
|
+
keymap: 'standard',
|
|
116
|
+
lineWrapping: true,
|
|
117
|
+
search: false,
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
const keymaps: { [key: string]: readonly KeyBinding[] } = {
|
|
121
|
+
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
122
|
+
standard: standardKeymap,
|
|
123
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
|
124
|
+
default: defaultKeymap,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const createBasicExtensions = (propsProp?: BasicExtensionsOptions): Extension => {
|
|
128
|
+
const props = defaultsDeep({}, propsProp, defaultBasicOptions);
|
|
129
|
+
return [
|
|
130
|
+
// NOTE: Doesn't catch errors in keymap functions.
|
|
131
|
+
EditorView.exceptionSink.of((err) => {
|
|
132
|
+
log.catch(err);
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
|
|
136
|
+
props.bracketMatching && bracketMatching(),
|
|
137
|
+
props.closeBrackets && closeBrackets(),
|
|
138
|
+
props.dropCursor && dropCursor(),
|
|
139
|
+
props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
|
|
140
|
+
props.editable !== undefined && EditorView.editable.of(props.editable),
|
|
141
|
+
props.focus && focus,
|
|
142
|
+
props.highlightActiveLine && highlightActiveLine(),
|
|
143
|
+
props.history && history(),
|
|
144
|
+
props.lineNumbers && [lineNumbers(), editorGutter],
|
|
145
|
+
props.lineWrapping && EditorView.lineWrapping,
|
|
146
|
+
props.placeholder && placeholder(props.placeholder),
|
|
147
|
+
props.readOnly !== undefined && EditorState.readOnly.of(props.readOnly),
|
|
148
|
+
props.scrollPastEnd && scrollPastEnd(),
|
|
149
|
+
props.tabbable && tabbable,
|
|
150
|
+
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
|
151
|
+
|
|
152
|
+
// https://codemirror.net/docs/ref/#view.KeyBinding
|
|
153
|
+
keymap.of(
|
|
154
|
+
[
|
|
155
|
+
...((props.keymap && keymaps[props.keymap]) ?? []),
|
|
156
|
+
// NOTE: Tabs are also configured by markdown extension.
|
|
157
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
158
|
+
...(props.indentWithTab ? [indentWithTab] : []),
|
|
159
|
+
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
160
|
+
...(props.closeBrackets ? closeBracketsKeymap : []),
|
|
161
|
+
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
162
|
+
...(props.history ? historyKeymap : []),
|
|
163
|
+
// https://codemirror.net/docs/ref/#search.searchKeymap
|
|
164
|
+
...(props.search ? searchKeymap : []),
|
|
165
|
+
// Disable bindings that conflict with system shortcuts.
|
|
166
|
+
// TODO(burdon): Catalog global shortcuts.
|
|
167
|
+
{
|
|
168
|
+
key: 'Mod-Shift-k',
|
|
169
|
+
preventDefault: true,
|
|
170
|
+
run: () => true,
|
|
171
|
+
},
|
|
172
|
+
].filter(isTruthy),
|
|
173
|
+
),
|
|
174
|
+
].filter(isTruthy);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
//
|
|
178
|
+
// Theme
|
|
179
|
+
//
|
|
180
|
+
|
|
181
|
+
export type ThemeExtensionsOptions = {
|
|
182
|
+
monospace?: boolean;
|
|
183
|
+
themeMode?: ThemeMode;
|
|
184
|
+
slots?: {
|
|
185
|
+
editor?: {
|
|
186
|
+
className?: string;
|
|
187
|
+
};
|
|
188
|
+
scroll?: {
|
|
189
|
+
// NOTE: Do not apply vertical padding to scroll container.
|
|
190
|
+
className?: string;
|
|
191
|
+
};
|
|
192
|
+
content?: {
|
|
193
|
+
className?: string;
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
syntaxHighlighting?: boolean;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const grow: ThemeExtensionsOptions['slots'] = {
|
|
200
|
+
editor: {
|
|
201
|
+
className: 'bs-full is-full',
|
|
202
|
+
},
|
|
203
|
+
} as const;
|
|
204
|
+
|
|
205
|
+
export const fullWidth: ThemeExtensionsOptions['slots'] = {
|
|
206
|
+
editor: {
|
|
207
|
+
className: 'is-full',
|
|
208
|
+
},
|
|
209
|
+
} as const;
|
|
210
|
+
|
|
211
|
+
export const defaultThemeSlots = grow;
|
|
212
|
+
|
|
213
|
+
export const defaultStyles = {
|
|
214
|
+
dark: vscodeDarkStyle,
|
|
215
|
+
light: vscodeLightStyle,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* https://codemirror.net/examples/styling
|
|
220
|
+
*/
|
|
221
|
+
export const createThemeExtensions = ({
|
|
222
|
+
monospace,
|
|
223
|
+
themeMode,
|
|
224
|
+
slots: slotsProp,
|
|
225
|
+
syntaxHighlighting: syntaxHighlightingProp,
|
|
226
|
+
}: ThemeExtensionsOptions = {}): Extension => {
|
|
227
|
+
const slots = defaultsDeep({}, slotsProp, defaultThemeSlots);
|
|
228
|
+
return [
|
|
229
|
+
baseTheme,
|
|
230
|
+
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
231
|
+
createFontTheme({ monospace }),
|
|
232
|
+
syntaxHighlightingProp &&
|
|
233
|
+
syntaxHighlighting(HighlightStyle.define(themeMode === 'dark' ? defaultStyles.dark : defaultStyles.light)),
|
|
234
|
+
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
235
|
+
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
236
|
+
slots.scroll?.className &&
|
|
237
|
+
ViewPlugin.fromClass(
|
|
238
|
+
class {
|
|
239
|
+
constructor(view: EditorView) {
|
|
240
|
+
view.scrollDOM.classList.add(...slots.scroll.className.split(/\s+/));
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
),
|
|
244
|
+
].filter(isTruthy);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
//
|
|
248
|
+
// Data
|
|
249
|
+
//
|
|
250
|
+
|
|
251
|
+
export type DataExtensionsProps<T> = {
|
|
252
|
+
id: string;
|
|
253
|
+
text?: DocAccessor<T>;
|
|
254
|
+
messenger?: Messenger;
|
|
255
|
+
identity?: Identity | null;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const createDataExtensions = <T>({ id, text, messenger, identity }: DataExtensionsProps<T>): Extension[] => {
|
|
259
|
+
const extensions: Extension[] = [];
|
|
260
|
+
if (text) {
|
|
261
|
+
extensions.push(automerge(text));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (messenger && identity) {
|
|
265
|
+
const peerId = identity?.identityKey.toHex();
|
|
266
|
+
const hue = (identity?.profile?.data?.hue as HuePalette | undefined) ?? hexToHue(peerId ?? '0');
|
|
267
|
+
extensions.push(
|
|
268
|
+
awareness(
|
|
269
|
+
new SpaceAwarenessProvider({
|
|
270
|
+
messenger,
|
|
271
|
+
channel: `awareness.${id}`,
|
|
272
|
+
peerId: identity.identityKey.toHex(),
|
|
273
|
+
info: {
|
|
274
|
+
darkColor: `var(--dx-${hue}Cursor)`,
|
|
275
|
+
lightColor: `var(--dx-${hue}Cursor)`,
|
|
276
|
+
displayName: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return extensions;
|
|
284
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
const focusEffect = StateEffect.define<boolean>();
|
|
9
|
+
|
|
10
|
+
export const focusField = StateField.define<boolean>({
|
|
11
|
+
create: () => false,
|
|
12
|
+
update: (value, tr) => {
|
|
13
|
+
for (const effect of tr.effects) {
|
|
14
|
+
if (effect.is(focusEffect)) {
|
|
15
|
+
return effect.value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manage focus.
|
|
25
|
+
*/
|
|
26
|
+
export const focus = [
|
|
27
|
+
focusField,
|
|
28
|
+
EditorView.domEventHandlers({
|
|
29
|
+
focus: (_event, view) => {
|
|
30
|
+
requestAnimationFrame(() => view.dispatch({ effects: focusEffect.of(true) }));
|
|
31
|
+
},
|
|
32
|
+
blur: (_event, view) => {
|
|
33
|
+
requestAnimationFrame(() => view.dispatch({ effects: focusEffect.of(false) }));
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { codeFolding, foldGutter } from '@codemirror/language';
|
|
6
|
+
import { type Extension } from '@codemirror/state';
|
|
7
|
+
import { EditorView } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
import { Domino, mx } from '@dxos/ui';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* https://codemirror.net/examples/gutter
|
|
13
|
+
*/
|
|
14
|
+
export const folding = (): Extension => {
|
|
15
|
+
return [
|
|
16
|
+
codeFolding({
|
|
17
|
+
placeholderDOM: () => Domino.of('span').root,
|
|
18
|
+
}),
|
|
19
|
+
foldGutter({
|
|
20
|
+
markerDOM: (open) => {
|
|
21
|
+
return Domino.of('div')
|
|
22
|
+
.classNames('flex bs-full justify-center items-center')
|
|
23
|
+
.children(
|
|
24
|
+
Domino.of('svg', Domino.SVG)
|
|
25
|
+
.classNames(mx('is-4 bs-4 cursor-pointer', open && 'rotate-90'))
|
|
26
|
+
.children(
|
|
27
|
+
Domino.of('use', Domino.SVG).attributes({
|
|
28
|
+
href: Domino.icon('ph--caret-right--regular'),
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
).root;
|
|
32
|
+
},
|
|
33
|
+
// TODO(burdon): markerDOM is called either way, defeating the animation: transition-transform duration-200
|
|
34
|
+
// domEventHandlers: {
|
|
35
|
+
// click: (view, line: BlockInfo, event) => {
|
|
36
|
+
// event.preventDefault();
|
|
37
|
+
// event.stopPropagation();
|
|
38
|
+
// const range = foldable(view.state, line.from, line.to);
|
|
39
|
+
// if (range) {
|
|
40
|
+
// view.dispatch({ effects: foldEffect.of(range) });
|
|
41
|
+
// (event.target as HTMLElement)?.classList.add('rotate-90');
|
|
42
|
+
// } else {
|
|
43
|
+
// foldedRanges(view.state).between(line.from, line.to, (from, to) => {
|
|
44
|
+
// view.dispatch({ effects: unfoldEffect.of({ from, to }) });
|
|
45
|
+
// (event.target as HTMLElement)?.classList.remove('rotate-90');
|
|
46
|
+
// });
|
|
47
|
+
// }
|
|
48
|
+
// return true;
|
|
49
|
+
// },
|
|
50
|
+
// },
|
|
51
|
+
}),
|
|
52
|
+
EditorView.theme({
|
|
53
|
+
'.cm-foldGutter': {
|
|
54
|
+
opacity: 0.3,
|
|
55
|
+
transition: 'opacity 0.3s',
|
|
56
|
+
width: '32px',
|
|
57
|
+
},
|
|
58
|
+
'.cm-foldGutter:hover': {
|
|
59
|
+
opacity: 1,
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
Decoration,
|
|
8
|
+
type DecorationSet,
|
|
9
|
+
EditorView,
|
|
10
|
+
MatchDecorator,
|
|
11
|
+
ViewPlugin,
|
|
12
|
+
type ViewUpdate,
|
|
13
|
+
WidgetType,
|
|
14
|
+
} from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
import { getHashStyles, mx } from '@dxos/ui-theme';
|
|
17
|
+
|
|
18
|
+
class TagWidget extends WidgetType {
|
|
19
|
+
constructor(private _text: string) {
|
|
20
|
+
super();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toDOM(): HTMLSpanElement {
|
|
24
|
+
const span = document.createElement('span');
|
|
25
|
+
span.className = mx('cm-tag', getHashStyles(this._text).surface);
|
|
26
|
+
span.textContent = this._text;
|
|
27
|
+
return span;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tagMatcher = new MatchDecorator({
|
|
32
|
+
regexp: /#(\w+)\W/g,
|
|
33
|
+
decoration: (match) =>
|
|
34
|
+
Decoration.replace({
|
|
35
|
+
widget: new TagWidget(match[1]),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// TODO(burdon): Autocomplete from existing tags?
|
|
40
|
+
export const hashtag = (): Extension => [
|
|
41
|
+
ViewPlugin.fromClass(
|
|
42
|
+
class {
|
|
43
|
+
tags: DecorationSet;
|
|
44
|
+
constructor(view: EditorView) {
|
|
45
|
+
this.tags = tagMatcher.createDeco(view);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
update(update: ViewUpdate) {
|
|
49
|
+
this.tags = tagMatcher.updateDeco(update, this.tags);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
decorations: (instance) => instance.tags,
|
|
54
|
+
provide: (plugin) =>
|
|
55
|
+
EditorView.atomicRanges.of((view) => {
|
|
56
|
+
return view.plugin(plugin)?.tags || Decoration.none;
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
EditorView.theme({
|
|
62
|
+
'.cm-tag': {
|
|
63
|
+
borderRadius: '4px',
|
|
64
|
+
marginRight: '6px',
|
|
65
|
+
padding: '2px 6px',
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export * from './annotations';
|
|
6
|
+
export * from './autocomplete';
|
|
7
|
+
export * from './autoscroll';
|
|
8
|
+
export * from './automerge';
|
|
9
|
+
export * from './awareness';
|
|
10
|
+
export * from './blast';
|
|
11
|
+
export * from './blocks';
|
|
12
|
+
export * from './bookmarks';
|
|
13
|
+
export * from './comments';
|
|
14
|
+
export * from './debug';
|
|
15
|
+
export * from './dnd';
|
|
16
|
+
export * from './factories';
|
|
17
|
+
export * from './focus';
|
|
18
|
+
export * from './folding';
|
|
19
|
+
export * from './hashtag';
|
|
20
|
+
export * from './json';
|
|
21
|
+
export * from './listener';
|
|
22
|
+
export * from './markdown';
|
|
23
|
+
export * from './mention';
|
|
24
|
+
export * from './modal';
|
|
25
|
+
export * from './modes';
|
|
26
|
+
export * from './outliner';
|
|
27
|
+
export * from './preview';
|
|
28
|
+
export * from './replacer';
|
|
29
|
+
export * from './selection';
|
|
30
|
+
export * from './scrolling';
|
|
31
|
+
export * from './state';
|
|
32
|
+
export * from './submit';
|
|
33
|
+
export * from './tags';
|
|
34
|
+
export * from './typewriter';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
|
6
|
+
import { type LintSource, linter } from '@codemirror/lint';
|
|
7
|
+
import { type Extension } from '@codemirror/state';
|
|
8
|
+
import Ajv, { type ValidateFunction } from 'ajv';
|
|
9
|
+
|
|
10
|
+
import { type JsonSchemaType } from '@dxos/echo/internal';
|
|
11
|
+
|
|
12
|
+
export type JsonExtensionsOptions = {
|
|
13
|
+
schema?: JsonSchemaType;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const createJsonExtensions = ({ schema }: JsonExtensionsOptions = {}): Extension => {
|
|
17
|
+
let lintSource: LintSource = jsonParseLinter();
|
|
18
|
+
if (schema) {
|
|
19
|
+
// NOTE: Relaxing strict mode to allow additional custom schema properties.
|
|
20
|
+
const ajv = new Ajv({ allErrors: false, strict: false });
|
|
21
|
+
const validate = ajv.compile(schema);
|
|
22
|
+
lintSource = schemaLinter(validate);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return [json(), linter(lintSource)];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const schemaLinter =
|
|
29
|
+
(validate: ValidateFunction): LintSource =>
|
|
30
|
+
(view) => {
|
|
31
|
+
try {
|
|
32
|
+
const jsonText = view.state.doc.toString();
|
|
33
|
+
const jsonData = JSON.parse(jsonText);
|
|
34
|
+
const valid = validate(jsonData);
|
|
35
|
+
if (valid) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
validate.errors?.map((err: any) => ({
|
|
41
|
+
from: 0,
|
|
42
|
+
to: jsonText.length,
|
|
43
|
+
severity: 'error',
|
|
44
|
+
message: `${err.instancePath || '(root)'} ${err.message}`,
|
|
45
|
+
})) ?? []
|
|
46
|
+
);
|
|
47
|
+
} catch (err: unknown) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
from: 0,
|
|
51
|
+
to: view.state.doc.length,
|
|
52
|
+
severity: 'error',
|
|
53
|
+
message: 'Invalid JSON: ' + (err as Error).message,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { isNonNullable } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import { documentId } from './selection';
|
|
11
|
+
|
|
12
|
+
export type ListenerOptions = {
|
|
13
|
+
onFocus?: (event: { id: string; focusing: boolean }) => void;
|
|
14
|
+
onChange?: (event: { id: string; text: string }) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
|
|
18
|
+
return [
|
|
19
|
+
onFocus &&
|
|
20
|
+
EditorView.focusChangeEffect.of((state, focusing) => {
|
|
21
|
+
onFocus({ id: state.facet(documentId), focusing });
|
|
22
|
+
return null;
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
onChange &&
|
|
26
|
+
EditorView.updateListener.of(({ state, docChanged }) => {
|
|
27
|
+
if (docChanged) {
|
|
28
|
+
onChange({ id: state.facet(documentId), text: state.doc.toString() });
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
].filter(isNonNullable);
|
|
32
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
import { type Node } from '@dxos/app-graph';
|
|
8
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
9
|
+
|
|
10
|
+
import { createComment } from '../comments';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Inline,
|
|
14
|
+
List,
|
|
15
|
+
addBlockquote,
|
|
16
|
+
addCodeblock,
|
|
17
|
+
addLink,
|
|
18
|
+
addList,
|
|
19
|
+
insertTable,
|
|
20
|
+
removeBlockquote,
|
|
21
|
+
removeCodeblock,
|
|
22
|
+
removeLink,
|
|
23
|
+
removeList,
|
|
24
|
+
setHeading,
|
|
25
|
+
setStyle,
|
|
26
|
+
toggleBlockquote,
|
|
27
|
+
toggleList,
|
|
28
|
+
toggleStyle,
|
|
29
|
+
} from './formatting';
|
|
30
|
+
|
|
31
|
+
export type PayloadType =
|
|
32
|
+
| 'view-mode'
|
|
33
|
+
| 'blockquote'
|
|
34
|
+
| 'strong'
|
|
35
|
+
| 'codeblock'
|
|
36
|
+
| 'comment'
|
|
37
|
+
| 'heading'
|
|
38
|
+
| 'image'
|
|
39
|
+
| 'emphasis'
|
|
40
|
+
| 'code'
|
|
41
|
+
| 'link'
|
|
42
|
+
| 'list-bullet'
|
|
43
|
+
| 'list-ordered'
|
|
44
|
+
| 'list-task'
|
|
45
|
+
| 'mention'
|
|
46
|
+
| 'prompt'
|
|
47
|
+
| 'search'
|
|
48
|
+
| 'strikethrough'
|
|
49
|
+
| 'table';
|
|
50
|
+
|
|
51
|
+
export type EditorActionPayload = {
|
|
52
|
+
type: PayloadType;
|
|
53
|
+
data?: any;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type EditorAction = Node.Action<MenuActionProperties & EditorActionPayload>;
|
|
57
|
+
|
|
58
|
+
export type EditorPayloadHandler = (view: EditorView, payload: EditorActionPayload) => void;
|
|
59
|
+
|
|
60
|
+
export const processEditorPayload: EditorPayloadHandler = (view, { type, data }) => {
|
|
61
|
+
let inlineType, listType;
|
|
62
|
+
switch (type) {
|
|
63
|
+
case 'heading':
|
|
64
|
+
setHeading(parseInt(data))(view);
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'strong':
|
|
68
|
+
case 'emphasis':
|
|
69
|
+
case 'strikethrough':
|
|
70
|
+
case 'code':
|
|
71
|
+
inlineType =
|
|
72
|
+
type === 'strong'
|
|
73
|
+
? Inline.Strong
|
|
74
|
+
: type === 'emphasis'
|
|
75
|
+
? Inline.Emphasis
|
|
76
|
+
: type === 'strikethrough'
|
|
77
|
+
? Inline.Strikethrough
|
|
78
|
+
: Inline.Code;
|
|
79
|
+
(typeof data === 'boolean' ? setStyle(inlineType, data) : toggleStyle(inlineType))(view);
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case 'list-ordered':
|
|
83
|
+
case 'list-bullet':
|
|
84
|
+
case 'list-task':
|
|
85
|
+
listType = type === 'list-ordered' ? List.Ordered : type === 'list-bullet' ? List.Bullet : List.Task;
|
|
86
|
+
(data === false ? removeList(listType) : data === true ? addList(listType) : toggleList(listType))(view);
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'blockquote':
|
|
90
|
+
(data === false ? removeBlockquote : data === true ? addBlockquote : toggleBlockquote)(view);
|
|
91
|
+
break;
|
|
92
|
+
case 'codeblock':
|
|
93
|
+
(data === false ? removeCodeblock : addCodeblock)(view);
|
|
94
|
+
break;
|
|
95
|
+
case 'table':
|
|
96
|
+
insertTable(view);
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'link':
|
|
100
|
+
(data === false ? removeLink : addLink())(view);
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'image':
|
|
104
|
+
addLink({ url: data, image: true })(view);
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'comment':
|
|
108
|
+
createComment(view);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
if (!view.hasFocus) {
|
|
114
|
+
view.focus();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
};
|