@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.
Files changed (93) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +21 -0
  3. package/package.json +121 -0
  4. package/src/defaults.ts +34 -0
  5. package/src/extensions/annotations.ts +55 -0
  6. package/src/extensions/autocomplete/autocomplete.ts +151 -0
  7. package/src/extensions/autocomplete/index.ts +8 -0
  8. package/src/extensions/autocomplete/match.ts +46 -0
  9. package/src/extensions/autocomplete/placeholder.ts +117 -0
  10. package/src/extensions/autocomplete/typeahead.ts +87 -0
  11. package/src/extensions/automerge/automerge.test.tsx +76 -0
  12. package/src/extensions/automerge/automerge.ts +105 -0
  13. package/src/extensions/automerge/cursor.ts +28 -0
  14. package/src/extensions/automerge/defs.ts +31 -0
  15. package/src/extensions/automerge/index.ts +5 -0
  16. package/src/extensions/automerge/sync.ts +79 -0
  17. package/src/extensions/automerge/update-automerge.ts +50 -0
  18. package/src/extensions/automerge/update-codemirror.ts +115 -0
  19. package/src/extensions/autoscroll.ts +165 -0
  20. package/src/extensions/awareness/awareness-provider.ts +127 -0
  21. package/src/extensions/awareness/awareness.ts +315 -0
  22. package/src/extensions/awareness/index.ts +6 -0
  23. package/src/extensions/blast.ts +363 -0
  24. package/src/extensions/blocks.ts +131 -0
  25. package/src/extensions/bookmarks.ts +77 -0
  26. package/src/extensions/comments.ts +579 -0
  27. package/src/extensions/debug.ts +15 -0
  28. package/src/extensions/dnd.ts +39 -0
  29. package/src/extensions/factories.ts +284 -0
  30. package/src/extensions/focus.ts +36 -0
  31. package/src/extensions/folding.ts +63 -0
  32. package/src/extensions/hashtag.ts +68 -0
  33. package/src/extensions/index.ts +34 -0
  34. package/src/extensions/json.ts +57 -0
  35. package/src/extensions/listener.ts +32 -0
  36. package/src/extensions/markdown/action.ts +117 -0
  37. package/src/extensions/markdown/bundle.ts +105 -0
  38. package/src/extensions/markdown/changes.test.ts +26 -0
  39. package/src/extensions/markdown/changes.ts +149 -0
  40. package/src/extensions/markdown/debug.ts +44 -0
  41. package/src/extensions/markdown/decorate.ts +622 -0
  42. package/src/extensions/markdown/formatting.test.ts +498 -0
  43. package/src/extensions/markdown/formatting.ts +1265 -0
  44. package/src/extensions/markdown/highlight.ts +183 -0
  45. package/src/extensions/markdown/image.ts +118 -0
  46. package/src/extensions/markdown/index.ts +13 -0
  47. package/src/extensions/markdown/link.ts +50 -0
  48. package/src/extensions/markdown/parser.test.ts +75 -0
  49. package/src/extensions/markdown/styles.ts +135 -0
  50. package/src/extensions/markdown/table.ts +150 -0
  51. package/src/extensions/mention.ts +41 -0
  52. package/src/extensions/modal.ts +24 -0
  53. package/src/extensions/modes.ts +41 -0
  54. package/src/extensions/outliner/commands.ts +270 -0
  55. package/src/extensions/outliner/editor.test.ts +33 -0
  56. package/src/extensions/outliner/editor.ts +184 -0
  57. package/src/extensions/outliner/index.ts +7 -0
  58. package/src/extensions/outliner/menu.ts +128 -0
  59. package/src/extensions/outliner/outliner.test.ts +100 -0
  60. package/src/extensions/outliner/outliner.ts +167 -0
  61. package/src/extensions/outliner/selection.ts +50 -0
  62. package/src/extensions/outliner/tree.test.ts +168 -0
  63. package/src/extensions/outliner/tree.ts +317 -0
  64. package/src/extensions/preview/index.ts +5 -0
  65. package/src/extensions/preview/preview.ts +193 -0
  66. package/src/extensions/replacer.test.ts +75 -0
  67. package/src/extensions/replacer.ts +93 -0
  68. package/src/extensions/scrolling.ts +189 -0
  69. package/src/extensions/selection.ts +100 -0
  70. package/src/extensions/state.ts +7 -0
  71. package/src/extensions/submit.ts +62 -0
  72. package/src/extensions/tags/extended-markdown.test.ts +263 -0
  73. package/src/extensions/tags/extended-markdown.ts +78 -0
  74. package/src/extensions/tags/index.ts +7 -0
  75. package/src/extensions/tags/streamer.ts +243 -0
  76. package/src/extensions/tags/xml-tags.ts +507 -0
  77. package/src/extensions/tags/xml-util.test.ts +48 -0
  78. package/src/extensions/tags/xml-util.ts +93 -0
  79. package/src/extensions/typewriter.ts +68 -0
  80. package/src/index.ts +14 -0
  81. package/src/styles/index.ts +7 -0
  82. package/src/styles/markdown.ts +26 -0
  83. package/src/styles/theme.ts +293 -0
  84. package/src/styles/tokens.ts +17 -0
  85. package/src/types/index.ts +5 -0
  86. package/src/types/types.ts +32 -0
  87. package/src/util/cursor.ts +56 -0
  88. package/src/util/debug.ts +56 -0
  89. package/src/util/decorations.ts +21 -0
  90. package/src/util/dom.ts +36 -0
  91. package/src/util/facet.ts +13 -0
  92. package/src/util/index.ts +10 -0
  93. 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 `![${label || host}](${url})`;
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
+ };