@dxos/react-ui-editor 0.6.9 → 0.6.10-main.3cfcc89
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 +759 -732
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/InputMode.stories.d.ts +2 -1
- package/dist/types/src/InputMode.stories.d.ts.map +1 -1
- package/dist/types/src/TextEditor.stories.d.ts +20 -13
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +5 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +3 -2
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
- package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
- package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +2 -5
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +26 -27
- package/src/InputMode.stories.tsx +1 -1
- package/src/TextEditor.stories.tsx +125 -77
- package/src/components/Toolbar/Toolbar.tsx +91 -92
- package/src/defaults.ts +16 -11
- package/src/extensions/annotations.ts +2 -2
- package/src/extensions/autocomplete.ts +4 -1
- package/src/extensions/automerge/automerge.stories.tsx +1 -1
- package/src/extensions/awareness/awareness.ts +1 -1
- package/src/extensions/comments.ts +11 -45
- package/src/extensions/dnd.ts +3 -5
- package/src/extensions/factories.ts +8 -8
- package/src/extensions/folding.tsx +3 -4
- package/src/extensions/markdown/action.ts +1 -0
- package/src/extensions/markdown/bundle.ts +3 -1
- package/src/extensions/markdown/{link-paste.test.ts → changes.test.ts} +2 -2
- package/src/extensions/markdown/changes.ts +148 -0
- package/src/extensions/markdown/debug.ts +44 -0
- package/src/extensions/markdown/decorate.ts +35 -108
- package/src/extensions/markdown/formatting.ts +1 -2
- package/src/extensions/markdown/highlight.ts +2 -2
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/markdown/parser.test.ts +29 -0
- package/src/extensions/markdown/styles.ts +103 -0
- package/src/index.ts +0 -2
- package/src/styles/theme.ts +85 -147
- package/src/styles/tokens.ts +6 -6
- package/src/translations.ts +1 -0
- package/dist/types/src/extensions/markdown/link-paste.d.ts +0 -9
- package/dist/types/src/extensions/markdown/link-paste.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts +0 -2
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +0 -1
- package/src/extensions/markdown/link-paste.ts +0 -107
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
autocompletion,
|
|
11
11
|
completionKeymap,
|
|
12
|
+
type CompletionSource,
|
|
12
13
|
type Completion,
|
|
13
14
|
type CompletionContext,
|
|
14
15
|
type CompletionResult,
|
|
@@ -20,13 +21,14 @@ export type AutocompleteResult = Completion;
|
|
|
20
21
|
|
|
21
22
|
export type AutocompleteOptions = {
|
|
22
23
|
activateOnTyping?: boolean;
|
|
24
|
+
override?: CompletionSource[];
|
|
23
25
|
onSearch?: (text: string) => Completion[];
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* Autocomplete extension.
|
|
28
30
|
*/
|
|
29
|
-
export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions = {}) => {
|
|
31
|
+
export const autocomplete = ({ activateOnTyping, override, onSearch }: AutocompleteOptions = {}) => {
|
|
30
32
|
const extentions = [
|
|
31
33
|
// https://codemirror.net/docs/ref/#view.keymap
|
|
32
34
|
// https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
|
|
@@ -37,6 +39,7 @@ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions
|
|
|
37
39
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
38
40
|
autocompletion({
|
|
39
41
|
activateOnTyping,
|
|
42
|
+
override,
|
|
40
43
|
|
|
41
44
|
// closeOnBlur: false,
|
|
42
45
|
// defaultKeymap: false,
|
|
@@ -32,7 +32,6 @@ import { nonNullable } from '@dxos/util';
|
|
|
32
32
|
import { Cursor } from './cursor';
|
|
33
33
|
import { type Comment, type Range } from './types';
|
|
34
34
|
import { overlap } from './util';
|
|
35
|
-
import { getToken } from '../styles';
|
|
36
35
|
import { callbackWrapper } from '../util';
|
|
37
36
|
|
|
38
37
|
//
|
|
@@ -106,53 +105,20 @@ export const commentsState = StateField.define<CommentsState>({
|
|
|
106
105
|
},
|
|
107
106
|
});
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const styles = EditorView.baseTheme({
|
|
108
|
+
/**
|
|
109
|
+
* NOTE: Matches search.
|
|
110
|
+
*/
|
|
111
|
+
const styles = EditorView.theme({
|
|
114
112
|
'.cm-comment, .cm-comment-current': {
|
|
113
|
+
margin: '0 -3px',
|
|
114
|
+
padding: '3px',
|
|
115
|
+
borderRadius: '3px',
|
|
116
|
+
backgroundColor: 'var(--dx-cmCommentSurface)',
|
|
117
|
+
color: 'var(--dx-cmComment)',
|
|
115
118
|
cursor: 'pointer',
|
|
116
|
-
borderWidth: '1px',
|
|
117
|
-
borderStyle: 'solid',
|
|
118
|
-
borderRadius: '2px',
|
|
119
|
-
transition: 'background-color 0.1s ease',
|
|
120
|
-
},
|
|
121
|
-
// Light theme.
|
|
122
|
-
'&light .cm-comment': {
|
|
123
|
-
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
124
|
-
mixBlendMode: 'darken',
|
|
125
|
-
borderColor: getToken('extend.colors.yellow.100'),
|
|
126
|
-
},
|
|
127
|
-
'&light .cm-comment:hover': { backgroundColor: getToken('extend.colors.yellow.100') },
|
|
128
|
-
'&light .cm-comment-current': {
|
|
129
|
-
backgroundColor: getToken('extend.colors.primary.100'),
|
|
130
|
-
borderColor: getToken('extend.colors.primary.200'),
|
|
131
|
-
},
|
|
132
|
-
'&light .cm-comment-current:hover': {
|
|
133
|
-
backgroundColor: getToken('extend.colors.primary.150'),
|
|
134
|
-
borderColor: getToken('extend.colors.primary.250'),
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
// Dark theme.
|
|
138
|
-
'&dark .cm-comment': {
|
|
139
|
-
color: getToken('extend.colors.yellow.50'),
|
|
140
|
-
backgroundColor: getToken('extend.colors.yellow.800'),
|
|
141
|
-
borderColor: getToken('extend.colors.yellow.700'),
|
|
142
|
-
mixBlendMode: 'plus-lighter',
|
|
143
|
-
},
|
|
144
|
-
'&dark .cm-comment:hover': {
|
|
145
|
-
backgroundColor: getToken('extend.colors.yellow.700'),
|
|
146
|
-
borderColor: getToken('extend.colors.yellow.650'),
|
|
147
|
-
},
|
|
148
|
-
'&dark .cm-comment-current': {
|
|
149
|
-
color: getToken('extend.colors.primary.50'),
|
|
150
|
-
backgroundColor: getToken('extend.colors.primary.800'),
|
|
151
|
-
borderColor: getToken('extend.colors.primary.700'),
|
|
152
119
|
},
|
|
153
|
-
'
|
|
154
|
-
|
|
155
|
-
borderColor: getToken('extend.colors.primary.650'),
|
|
120
|
+
'.cm-comment:hover, .cm-comment-current': {
|
|
121
|
+
textDecoration: 'underline',
|
|
156
122
|
},
|
|
157
123
|
});
|
|
158
124
|
|
package/src/extensions/dnd.ts
CHANGED
|
@@ -5,14 +5,12 @@
|
|
|
5
5
|
import type { Extension } from '@codemirror/state';
|
|
6
6
|
import { dropCursor, EditorView } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
-
import { getToken } from '../styles';
|
|
9
|
-
|
|
10
8
|
export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
|
|
11
9
|
|
|
12
|
-
const styles = EditorView.
|
|
10
|
+
const styles = EditorView.theme({
|
|
13
11
|
'.cm-dropCursor': {
|
|
14
|
-
borderLeft:
|
|
15
|
-
color:
|
|
12
|
+
borderLeft: '2px solid var(--dx-accentText)',
|
|
13
|
+
color: 'var(--dx-accentText)',
|
|
16
14
|
padding: '0 4px',
|
|
17
15
|
},
|
|
18
16
|
'.cm-dropCursor:after': {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
-
import { defaultKeymap, history, historyKeymap, standardKeymap } from '@codemirror/commands';
|
|
6
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
7
|
import { bracketMatching } from '@codemirror/language';
|
|
8
8
|
import { searchKeymap } from '@codemirror/search';
|
|
9
9
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
scrollPastEnd,
|
|
20
20
|
} from '@codemirror/view';
|
|
21
21
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
22
|
+
import merge from 'lodash.merge';
|
|
22
23
|
|
|
23
24
|
import { generateName } from '@dxos/display-name';
|
|
24
25
|
import { log } from '@dxos/log';
|
|
@@ -95,7 +96,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
95
96
|
props.bracketMatching && bracketMatching(),
|
|
96
97
|
props.closeBrackets && closeBrackets(),
|
|
97
98
|
props.dropCursor && dropCursor(),
|
|
98
|
-
props.drawSelection && drawSelection(),
|
|
99
|
+
props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
|
|
99
100
|
props.highlightActiveLine && highlightActiveLine(),
|
|
100
101
|
props.history && history(),
|
|
101
102
|
props.lineNumbers && lineNumbers(),
|
|
@@ -109,9 +110,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
109
110
|
keymap.of(
|
|
110
111
|
[
|
|
111
112
|
...((props.keymap && keymaps[props.keymap]) ?? []),
|
|
112
|
-
// NOTE:
|
|
113
|
+
// NOTE: Tabs are also configured by markdown extension.
|
|
113
114
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
114
|
-
|
|
115
|
+
...(props.indentWithTab ? [indentWithTab] : []),
|
|
115
116
|
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
116
117
|
...(props.closeBrackets ? closeBracketsKeymap : []),
|
|
117
118
|
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
@@ -128,8 +129,8 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
128
129
|
//
|
|
129
130
|
|
|
130
131
|
export type ThemeExtensionsOptions = {
|
|
131
|
-
theme?: ThemeStyles;
|
|
132
132
|
themeMode?: ThemeMode;
|
|
133
|
+
styles?: ThemeStyles;
|
|
133
134
|
slots?: {
|
|
134
135
|
editor?: {
|
|
135
136
|
className?: string;
|
|
@@ -148,12 +149,11 @@ const defaultThemeSlots = {
|
|
|
148
149
|
|
|
149
150
|
// TODO(burdon): Should only have one baseTheme?
|
|
150
151
|
// https://codemirror.net/examples/styling
|
|
151
|
-
export const createThemeExtensions = ({
|
|
152
|
+
export const createThemeExtensions = ({ themeMode, styles, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
152
153
|
const slots = defaultsDeep({}, _slots, defaultThemeSlots);
|
|
153
154
|
return [
|
|
154
|
-
EditorView.baseTheme(defaultTheme),
|
|
155
155
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
156
|
-
|
|
156
|
+
EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
|
|
157
157
|
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
158
158
|
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
159
159
|
].filter(isNotFalsy);
|
|
@@ -6,7 +6,8 @@ import { codeFolding, foldGutter } from '@codemirror/language';
|
|
|
6
6
|
import { type Extension } from '@codemirror/state';
|
|
7
7
|
import React from 'react';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { Icon } from '@dxos/react-ui';
|
|
10
|
+
import { getSize } from '@dxos/react-ui-theme';
|
|
10
11
|
|
|
11
12
|
import { renderRoot } from './util';
|
|
12
13
|
|
|
@@ -23,9 +24,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
|
|
|
23
24
|
markerDOM: (open) => {
|
|
24
25
|
return renderRoot(
|
|
25
26
|
document.createElement('div'),
|
|
26
|
-
<
|
|
27
|
-
<use href={'/icons.svg#ph--caret-right--regular'} />
|
|
28
|
-
</svg>,
|
|
27
|
+
<Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'm-2 cursor-pointer', open && 'rotate-90']} />,
|
|
29
28
|
);
|
|
30
29
|
},
|
|
31
30
|
}),
|
|
@@ -44,6 +44,9 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
|
44
44
|
// Languages for syntax highlighting fenced code blocks.
|
|
45
45
|
codeLanguages: languages,
|
|
46
46
|
|
|
47
|
+
// Don't complete HTML tags.
|
|
48
|
+
completeHTMLTags: false,
|
|
49
|
+
|
|
47
50
|
// Parser extensions.
|
|
48
51
|
extensions: [
|
|
49
52
|
// GFM provided by default.
|
|
@@ -58,7 +61,6 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
|
58
61
|
syntaxHighlighting(markdownHighlightStyle()),
|
|
59
62
|
|
|
60
63
|
keymap.of([
|
|
61
|
-
// TODO(burdon): Indent by 4 if in task list.
|
|
62
64
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
63
65
|
indentWithTab,
|
|
64
66
|
|
|
@@ -6,7 +6,7 @@ import { expect } from 'chai';
|
|
|
6
6
|
|
|
7
7
|
import { describe, test } from '@dxos/test';
|
|
8
8
|
|
|
9
|
-
import { createLinkLabel } from './
|
|
9
|
+
import { createLinkLabel } from './changes';
|
|
10
10
|
|
|
11
11
|
const testCases = [
|
|
12
12
|
{ input: 'https://www.example.com', expected: 'example.com' },
|
|
@@ -19,7 +19,7 @@ const testCases = [
|
|
|
19
19
|
{ input: 'ftp://example.com', expected: 'ftp://example.com' },
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
-
describe('
|
|
22
|
+
describe('changes', () => {
|
|
23
23
|
test('createLinkLabel', () => {
|
|
24
24
|
testCases.forEach(({ input, expected }) => {
|
|
25
25
|
expect(createLinkLabel(new URL(input))).to.eq(expected);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type ChangeSpec, Transaction } from '@codemirror/state';
|
|
7
|
+
import { ViewPlugin, type ViewUpdate, type PluginValue } 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
|
+
const node = tree.resolveInner(fromA, -1);
|
|
60
|
+
const invalidPositions = new Set(['Link', 'LinkMark', 'Code', 'CodeText', 'FencedCode', 'URL']);
|
|
61
|
+
if (!invalidPositions.has(node?.name)) {
|
|
62
|
+
const replacedText = tr.startState.sliceDoc(fromA, toA);
|
|
63
|
+
adjustments.push({ from: fromA, to: toB, insert: createLink(url, replacedText) });
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
const node = tree.resolveInner(fromA, 1);
|
|
67
|
+
switch (node?.name) {
|
|
68
|
+
case 'Task': {
|
|
69
|
+
// Remove task marker if pasting into task list.
|
|
70
|
+
const str = text.toString();
|
|
71
|
+
const match = str.match(/\s*- \[[ xX]\]\s*(.+)/);
|
|
72
|
+
if (match) {
|
|
73
|
+
const [, replacement] = match;
|
|
74
|
+
adjustments.push({ from: fromA, to: toB, insert: replacement });
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TODO(burdon): Is this the right way to augment changes?
|
|
88
|
+
if (adjustments.length) {
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
update.view.dispatch(
|
|
91
|
+
update.view.state.update({
|
|
92
|
+
changes: adjustments,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//
|
|
103
|
+
// Links
|
|
104
|
+
//
|
|
105
|
+
|
|
106
|
+
export const createLink = (url: URL, label: string): string => {
|
|
107
|
+
// Check if image.
|
|
108
|
+
// Example: https://dxos.network/dxos-logotype-blue.png
|
|
109
|
+
const { host, pathname } = url;
|
|
110
|
+
const [, extension] = pathname.split('.');
|
|
111
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
|
112
|
+
if (imageExtensions.includes(extension)) {
|
|
113
|
+
return ``;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!label) {
|
|
117
|
+
label = createLinkLabel(url);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `[${label}](${url})`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const createLinkLabel = (url: URL): string => {
|
|
124
|
+
let { protocol, host, pathname } = url;
|
|
125
|
+
if (protocol === 'http:' || protocol === 'https:') {
|
|
126
|
+
protocol = '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// NOTE(Zan): Consult: https://github.com/dxos/dxos/issues/7331 before changing this.
|
|
130
|
+
// Remove 'www.' if at the beginning of the URL
|
|
131
|
+
host = host.replace(/^www\./, '');
|
|
132
|
+
|
|
133
|
+
return [protocol, host].filter(Boolean).join('//') + (pathname !== '/' ? pathname : '');
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getValidUrl = (str: string): URL | undefined => {
|
|
137
|
+
const validProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(str);
|
|
140
|
+
if (!validProtocols.includes(url.protocol)) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return url;
|
|
145
|
+
} catch (_err) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, StateField } from '@codemirror/state';
|
|
7
|
+
import { type TreeCursor } from '@lezer/common';
|
|
8
|
+
|
|
9
|
+
export const debugTree = (cb: (tree: DebugNode) => void) =>
|
|
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
|
+
};
|