@dxos/react-ui-editor 0.5.9-main.72c50cd → 0.5.9-main.7be09a9

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.
@@ -0,0 +1,104 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+ import { syntaxTree } from '@codemirror/language';
5
+ import { type EditorState, Transaction } from '@codemirror/state';
6
+ import { type EditorView, ViewPlugin, type ViewUpdate, type PluginValue } from '@codemirror/view';
7
+ import { type SyntaxNode } from '@lezer/common';
8
+
9
+ const VALID_PROTOCOLS = ['http:', 'https:', 'mailto:', 'tel:'];
10
+
11
+ const createTextLink = (text: string, url: string): string => `[${text}](${url})`;
12
+
13
+ const createUrlLink = (url: string): string => {
14
+ const displayUrl = formatUrlForDisplay(url);
15
+ return `[${displayUrl}](${url})`;
16
+ };
17
+
18
+ const formatUrlForDisplay = (url: string): string => {
19
+ const withoutProtocol = url.replace(/^https?:\/\//, '');
20
+ return truncateQueryParams(withoutProtocol);
21
+ };
22
+
23
+ const truncateQueryParams = (url: string, maxQueryLength: number = 15): string => {
24
+ const [urlBase, queryString] = url.split('?');
25
+ if (!queryString) {
26
+ return urlBase;
27
+ }
28
+ if (queryString.length > maxQueryLength) {
29
+ const truncatedQuery = queryString.slice(0, maxQueryLength) + '...';
30
+ return `${urlBase}?${truncatedQuery}`;
31
+ } else {
32
+ return `${urlBase}?${queryString}`;
33
+ }
34
+ };
35
+
36
+ const isValidUrl = (str: string) => {
37
+ try {
38
+ const url = new URL(str);
39
+ return VALID_PROTOCOLS.includes(url.protocol);
40
+ } catch (e) {
41
+ return false;
42
+ }
43
+ };
44
+
45
+ const onNextUpdate = (callback: () => void) => setTimeout(callback, 0);
46
+
47
+ export const linkPastePlugin = ViewPlugin.fromClass(
48
+ class implements PluginValue {
49
+ view: EditorView;
50
+
51
+ constructor(view: EditorView) {
52
+ this.view = view;
53
+ }
54
+
55
+ update(update: ViewUpdate) {
56
+ for (const tr of update.transactions) {
57
+ const event = tr.annotation(Transaction.userEvent);
58
+ if (event === 'input.paste') {
59
+ this.handleInputRead(this.view, tr);
60
+ }
61
+ }
62
+ }
63
+
64
+ handleInputRead(view: EditorView, tr: Transaction) {
65
+ const changes = tr.changes;
66
+ if (changes.empty) {
67
+ return;
68
+ }
69
+ changes.iterChangedRanges((fromA, toA, fromB, toB) => {
70
+ const insertedText = view.state.sliceDoc(fromB, toB);
71
+ if (isValidUrl(insertedText) && !this.isInCodeBlock(view.state, fromB)) {
72
+ const replacedText = tr.startState.sliceDoc(fromA, toA);
73
+ onNextUpdate(() => {
74
+ view.dispatch(this.createLinkTransaction(view.state, fromA, toB, insertedText, replacedText));
75
+ });
76
+ }
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Determines if a given position is within a code block.
82
+ * Traverses the syntax tree upwards from the position,
83
+ * checking for CodeBlock or FencedCode nodes.
84
+ */
85
+ isInCodeBlock(state: EditorState, pos: number): boolean {
86
+ const tree = syntaxTree(state);
87
+ let node: SyntaxNode | null = tree.resolveInner(pos, -1);
88
+ while (node) {
89
+ if (node.name.includes('Code') || node.name.includes('FencedCode')) {
90
+ return true;
91
+ }
92
+ node = node.parent;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ createLinkTransaction(state: EditorState, from: number, to: number, url: string, text: string): Transaction {
98
+ const linkText = text.trim() ? createTextLink(text, url) : createUrlLink(url);
99
+ return state.update({
100
+ changes: { from, to, insert: linkText },
101
+ });
102
+ }
103
+ },
104
+ );