@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,105 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { completionKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
7
|
+
import { jsonLanguage } from '@codemirror/lang-json';
|
|
8
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|
9
|
+
import { xml } from '@codemirror/lang-xml';
|
|
10
|
+
import { LanguageDescription, syntaxHighlighting } from '@codemirror/language';
|
|
11
|
+
import { languages } from '@codemirror/language-data';
|
|
12
|
+
import { type Extension } from '@codemirror/state';
|
|
13
|
+
import { keymap } from '@codemirror/view';
|
|
14
|
+
import { type MarkdownConfig } from '@lezer/markdown';
|
|
15
|
+
|
|
16
|
+
import { isTruthy } from '@dxos/util';
|
|
17
|
+
|
|
18
|
+
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
19
|
+
|
|
20
|
+
export type MarkdownBundleOptions = {
|
|
21
|
+
extensions?: MarkdownConfig[];
|
|
22
|
+
indentWithTab?: boolean;
|
|
23
|
+
setextHeading?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates markdown extensions.
|
|
28
|
+
* To be used in conjunction with createBasicExtensions.
|
|
29
|
+
*
|
|
30
|
+
* Refs:
|
|
31
|
+
* https://codemirror.net/docs/community
|
|
32
|
+
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
33
|
+
*/
|
|
34
|
+
export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): Extension[] => {
|
|
35
|
+
return [
|
|
36
|
+
// Main extension.
|
|
37
|
+
// https://github.com/codemirror/lang-markdown
|
|
38
|
+
// https://codemirror.net/5/mode/markdown/index.html (demo).
|
|
39
|
+
markdown({
|
|
40
|
+
// GRM by default (vs strict CommonMark):
|
|
41
|
+
// Table, TaskList, Strikethrough, and Autolink.
|
|
42
|
+
// NOTE: This extends the parser; it doesn't affect rendering.
|
|
43
|
+
// https://github.github.com/gfm
|
|
44
|
+
// https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
|
|
45
|
+
base: markdownLanguage,
|
|
46
|
+
|
|
47
|
+
// Languages for syntax highlighting fenced code blocks.
|
|
48
|
+
defaultCodeLanguage: jsonLanguage,
|
|
49
|
+
codeLanguages: languages,
|
|
50
|
+
|
|
51
|
+
// Don't complete HTML tags.
|
|
52
|
+
completeHTMLTags: false,
|
|
53
|
+
|
|
54
|
+
// Parser extensions.
|
|
55
|
+
extensions: [
|
|
56
|
+
// GFM provided by default.
|
|
57
|
+
markdownTagsExtensions,
|
|
58
|
+
...(options.extensions ?? defaultExtensions()),
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
// Custom styles.
|
|
63
|
+
syntaxHighlighting(markdownHighlightStyle()),
|
|
64
|
+
|
|
65
|
+
keymap.of(
|
|
66
|
+
[
|
|
67
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
68
|
+
options.indentWithTab !== false && indentWithTab,
|
|
69
|
+
|
|
70
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
|
71
|
+
...defaultKeymap,
|
|
72
|
+
|
|
73
|
+
// TODO(burdon): Remove?
|
|
74
|
+
...completionKeymap,
|
|
75
|
+
].filter(isTruthy),
|
|
76
|
+
),
|
|
77
|
+
];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const xmlLanguageDesc = LanguageDescription.of({
|
|
81
|
+
name: 'xml',
|
|
82
|
+
alias: ['html', 'xhtml'],
|
|
83
|
+
extensions: ['xml', 'xhtml'],
|
|
84
|
+
load: async () => xml(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Default customizations.
|
|
89
|
+
* https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
|
|
90
|
+
*/
|
|
91
|
+
export const defaultExtensions = (): MarkdownConfig[] => [noSetExtHeading, noHtml];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remove SetextHeading (e.g., headings created from "---").
|
|
95
|
+
*/
|
|
96
|
+
const noSetExtHeading: MarkdownConfig = {
|
|
97
|
+
remove: ['SetextHeading'],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove HTML and XML parsing.
|
|
102
|
+
*/
|
|
103
|
+
const noHtml: MarkdownConfig = {
|
|
104
|
+
// remove: ['HTMLBlock', 'HTMLTag'],
|
|
105
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createLinkLabel } from './changes';
|
|
8
|
+
|
|
9
|
+
const testCases = [
|
|
10
|
+
{ input: 'https://www.example.com', expected: 'example.com' },
|
|
11
|
+
{ input: 'http://example.com', expected: 'example.com' },
|
|
12
|
+
{ input: 'https://example.com/', expected: 'example.com' },
|
|
13
|
+
{ input: 'https://www.example.com/', expected: 'example.com' },
|
|
14
|
+
{ input: 'https://example.com/test', expected: 'example.com/test' },
|
|
15
|
+
{ input: 'https://www.example.com/test', expected: 'example.com/test' },
|
|
16
|
+
{ input: 'https://example.com?name=value', expected: 'example.com' },
|
|
17
|
+
{ input: 'ftp://example.com', expected: 'ftp://example.com' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe('changes', () => {
|
|
21
|
+
test('createLinkLabel', () => {
|
|
22
|
+
testCases.forEach(({ input, expected }) => {
|
|
23
|
+
expect(createLinkLabel(new URL(input))).to.eq(expected);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type ChangeSpec, Transaction } from '@codemirror/state';
|
|
7
|
+
import { type PluginValue, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Monitors and augments changes.
|
|
11
|
+
*/
|
|
12
|
+
// TODO(burdon): Tests.
|
|
13
|
+
export const adjustChanges = () => {
|
|
14
|
+
return ViewPlugin.fromClass(
|
|
15
|
+
class implements PluginValue {
|
|
16
|
+
update(update: ViewUpdate) {
|
|
17
|
+
const tree = syntaxTree(update.state);
|
|
18
|
+
const adjustments: ChangeSpec[] = [];
|
|
19
|
+
|
|
20
|
+
for (const tr of update.transactions) {
|
|
21
|
+
const event = tr.annotation(Transaction.userEvent);
|
|
22
|
+
switch (event) {
|
|
23
|
+
//
|
|
24
|
+
// Enter
|
|
25
|
+
//
|
|
26
|
+
case 'input': {
|
|
27
|
+
const changes = tr.changes;
|
|
28
|
+
if (changes.empty) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
changes.iterChanges((fromA) => {
|
|
33
|
+
const node = tree.resolveInner(fromA, 1);
|
|
34
|
+
if (node?.name === 'BulletList') {
|
|
35
|
+
// Add space to previous line if an empty list item (otherwise it is not interpreted as a Task).
|
|
36
|
+
const { text } = update.state.doc.lineAt(fromA);
|
|
37
|
+
if (text.endsWith(']')) {
|
|
38
|
+
adjustments.push({ from: fromA, to: fromA, insert: ' ' });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//
|
|
47
|
+
// Paste
|
|
48
|
+
//
|
|
49
|
+
case 'input.paste': {
|
|
50
|
+
const changes = tr.changes;
|
|
51
|
+
if (changes.empty) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
56
|
+
// Check for URL.
|
|
57
|
+
const url = getValidUrl(update.view.state.sliceDoc(fromB, toB));
|
|
58
|
+
if (url) {
|
|
59
|
+
// Check if pasting inside existing link.
|
|
60
|
+
const node = tree.resolveInner(fromA, -1);
|
|
61
|
+
const invalidPositions = new Set(['Code', 'CodeText', 'FencedCode', 'Link', 'LinkMark', 'URL']);
|
|
62
|
+
if (!invalidPositions.has(node?.name)) {
|
|
63
|
+
const replacedText = tr.startState.sliceDoc(fromA, toA);
|
|
64
|
+
adjustments.push({ from: fromA, to: toB, insert: createLink(url, replacedText) });
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const node = tree.resolveInner(fromA, 1);
|
|
68
|
+
switch (node?.name) {
|
|
69
|
+
case 'Task': {
|
|
70
|
+
// Remove task marker if pasting into task list.
|
|
71
|
+
const str = text.toString();
|
|
72
|
+
const match = str.match(/\s*- \[[ xX]\]\s*(.+)/);
|
|
73
|
+
if (match) {
|
|
74
|
+
const [, replacement] = match;
|
|
75
|
+
adjustments.push({ from: fromA, to: toB, insert: replacement });
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// TODO(burdon): Is this the right way to augment changes? Alt: EditorState.transactionFilter
|
|
89
|
+
if (adjustments.length) {
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
update.view.dispatch(
|
|
92
|
+
update.view.state.update({
|
|
93
|
+
changes: adjustments,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//
|
|
104
|
+
// Links
|
|
105
|
+
//
|
|
106
|
+
|
|
107
|
+
export const createLink = (url: URL, label: string): string => {
|
|
108
|
+
// Check if image.
|
|
109
|
+
// Example: https://dxos.network/dxos-logotype-blue.png
|
|
110
|
+
const { host, pathname } = url;
|
|
111
|
+
const [, extension] = pathname.split('.');
|
|
112
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
|
113
|
+
if (imageExtensions.includes(extension)) {
|
|
114
|
+
return ``;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!label) {
|
|
118
|
+
label = createLinkLabel(url);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return `[${label}](${url})`;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const createLinkLabel = (url: URL): string => {
|
|
125
|
+
let { protocol, host, pathname } = url;
|
|
126
|
+
if (protocol === 'http:' || protocol === 'https:') {
|
|
127
|
+
protocol = '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// NOTE(Zan): Consult: https://github.com/dxos/dxos/issues/7331 before changing this.
|
|
131
|
+
// Remove 'www.' if at the beginning of the URL
|
|
132
|
+
host = host.replace(/^www\./, '');
|
|
133
|
+
|
|
134
|
+
return [protocol, host].filter(Boolean).join('//') + (pathname !== '/' ? pathname : '');
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const getValidUrl = (str: string): URL | undefined => {
|
|
138
|
+
const validProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
|
|
139
|
+
try {
|
|
140
|
+
const url = new URL(str);
|
|
141
|
+
if (!validProtocols.includes(url.protocol)) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return url;
|
|
146
|
+
} catch (_err) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, type Extension, StateField } from '@codemirror/state';
|
|
7
|
+
import { type TreeCursor } from '@lezer/common';
|
|
8
|
+
|
|
9
|
+
export const debugTree = (cb: (tree: DebugNode) => void): Extension =>
|
|
10
|
+
StateField.define({
|
|
11
|
+
create: (state) => cb(convertTreeToJson(state)),
|
|
12
|
+
update: (value, tr) => cb(convertTreeToJson(tr.state)),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type DebugNode = {
|
|
16
|
+
type: string;
|
|
17
|
+
from: number;
|
|
18
|
+
to: number;
|
|
19
|
+
text: string;
|
|
20
|
+
children: DebugNode[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const convertTreeToJson = (state: EditorState): DebugNode => {
|
|
24
|
+
const treeToJson = (cursor: TreeCursor): DebugNode => {
|
|
25
|
+
const node: DebugNode = {
|
|
26
|
+
type: cursor.type.name,
|
|
27
|
+
from: cursor.from,
|
|
28
|
+
to: cursor.to,
|
|
29
|
+
text: state.doc.slice(cursor.from, cursor.to).toString(),
|
|
30
|
+
children: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (cursor.firstChild()) {
|
|
34
|
+
do {
|
|
35
|
+
node.children.push(treeToJson(cursor));
|
|
36
|
+
} while (cursor.nextSibling());
|
|
37
|
+
cursor.parent();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return node;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return treeToJson(syntaxTree(state).cursor());
|
|
44
|
+
};
|