1
1
import assertNever from "assert-never" ;
2
2
import { NothingSelected , PluginSettings } from "setting" ;
3
3
import fileUrl from "file-url" ;
4
-
5
- interface WordBoundaries {
6
- start : { line : number ; ch : number } ;
7
- end : { line : number ; ch : number } ;
8
- }
4
+ import { Editor , EditorPosition , EditorRange } from "obsidian" ;
9
5
10
6
// https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
11
7
const win32Path = / ^ [ a - z ] : \\ (?: [ ^ \\ / : * ? " < > | \r \n ] + \\ ) * [ ^ \\ / : * ? " < > | \r \n ] * $ / i;
12
8
const unixPath = / ^ (?: \/ [ ^ / ] + ) + \/ ? $ / i;
13
9
const testFilePath = ( url : string ) => win32Path . test ( url ) || unixPath . test ( url ) ;
14
10
15
11
/**
16
- * @param cm CodeMirror Instance
12
+ * @param editor Obsidian Editor Instance
17
13
* @param cbString text on clipboard
18
14
* @param settings plugin settings
19
15
*/
20
- export default function UrlIntoSelection (
21
- cm : CodeMirror . Editor ,
22
- cbString : string ,
23
- settings : PluginSettings
24
- ) : void ;
16
+ export default function UrlIntoSelection ( editor : Editor , cbString : string , settings : PluginSettings ) : void ;
25
17
/**
26
- * @param cm CodeMirror Instance
18
+ * @param editor Obsidian Editor Instance
27
19
* @param cbEvent clipboard event
28
20
* @param settings plugin settings
29
21
*/
30
- export default function UrlIntoSelection (
31
- cm : CodeMirror . Editor ,
32
- cbEvent : ClipboardEvent ,
33
- settings : PluginSettings
34
- ) : void ;
35
- export default function UrlIntoSelection (
36
- cm : CodeMirror . Editor ,
37
- cb : string | ClipboardEvent ,
38
- settings : PluginSettings
39
- ) : void {
22
+ export default function UrlIntoSelection ( editor : Editor , cbEvent : ClipboardEvent , settings : PluginSettings ) : void ;
23
+ export default function UrlIntoSelection ( editor : Editor , cb : string | ClipboardEvent , settings : PluginSettings ) : void {
40
24
// skip all if nothing should be done
41
- if (
42
- ! cm . somethingSelected ( ) &&
43
- settings . nothingSelected === NothingSelected . doNothing
44
- )
25
+ if ( ! editor . somethingSelected ( ) && settings . nothingSelected === NothingSelected . doNothing )
45
26
return ;
46
27
47
28
if ( typeof cb !== "string" && cb . clipboardData === null ) {
@@ -52,41 +33,36 @@ export default function UrlIntoSelection(
52
33
const clipboardText = getCbText ( cb ) ;
53
34
if ( clipboardText === null ) return ;
54
35
55
- const { selectedText, replaceRange } = getSelnRange ( cm , settings ) ;
36
+ const { selectedText, replaceRange } = getSelnRange ( editor , settings ) ;
56
37
const replaceText = getReplaceText ( clipboardText , selectedText , settings ) ;
57
38
if ( replaceText === null ) return ;
58
39
59
40
// apply changes
60
- if ( typeof cb !== "string" ) cb . preventDefault ( ) ; // disable default copy behavior
61
- replace ( cm , replaceText , replaceRange ) ;
62
-
63
- if (
64
- ! cm . somethingSelected ( ) &&
65
- settings . nothingSelected === NothingSelected . insertInline
66
- ) {
67
- cm . setCursor ( {
68
- ch : replaceRange . start . ch + 1 ,
69
- line : replaceRange . start . line ,
70
- } ) ;
41
+ if ( typeof cb !== "string" ) cb . preventDefault ( ) ; // prevent default paste behavior
42
+ replace ( editor , replaceText , replaceRange ) ;
43
+
44
+ // if nothing is selected and the nothing selected behavior is to insert [](url) place the cursor between the square brackets
45
+ if ( ( selectedText === "" ) && settings . nothingSelected === NothingSelected . insertInline ) {
46
+ editor . setCursor ( { ch : replaceRange . from . ch + 1 , line : replaceRange . from . line } ) ;
71
47
}
72
48
}
73
49
74
- function getSelnRange ( cm : CodeMirror . Editor , settings : PluginSettings ) {
50
+ function getSelnRange ( editor : Editor , settings : PluginSettings ) {
75
51
let selectedText : string ;
76
- let replaceRange : WordBoundaries | null ;
52
+ let replaceRange : EditorRange | null ;
77
53
78
- if ( cm . somethingSelected ( ) ) {
79
- selectedText = cm . getSelection ( ) . trim ( ) ;
54
+ if ( editor . somethingSelected ( ) ) {
55
+ selectedText = editor . getSelection ( ) . trim ( ) ;
80
56
replaceRange = null ;
81
57
} else {
82
58
switch ( settings . nothingSelected ) {
83
59
case NothingSelected . autoSelect :
84
- replaceRange = getWordBoundaries ( cm ) ;
85
- selectedText = cm . getRange ( replaceRange . start , replaceRange . end ) ;
60
+ replaceRange = getWordBoundaries ( editor , settings ) ;
61
+ selectedText = editor . getRange ( replaceRange . from , replaceRange . to ) ;
86
62
break ;
87
63
case NothingSelected . insertInline :
88
64
case NothingSelected . insertBare :
89
- replaceRange = getCursor ( cm ) ;
65
+ replaceRange = getCursor ( editor ) ;
90
66
selectedText = "" ;
91
67
break ;
92
68
case NothingSelected . doNothing :
@@ -98,52 +74,57 @@ function getSelnRange(cm: CodeMirror.Editor, settings: PluginSettings) {
98
74
return { selectedText, replaceRange } ;
99
75
}
100
76
101
- function getReplaceText (
102
- clipboardText : string ,
103
- selectedText : string ,
104
- settings : PluginSettings
105
- ) : string | null {
106
- const isUrl = ( text : string ) : boolean => {
107
- if ( text === "" ) return false ;
108
- try {
109
- // throw TypeError: Invalid URL if not valid
110
- new URL ( text ) ;
111
- return true ;
112
- } catch ( error ) {
113
- // settings.regex: fallback test allows url without protocol (http,file...)
114
- return testFilePath ( text ) || new RegExp ( settings . regex ) . test ( text ) ;
115
- }
116
- } ;
117
- const isImgEmbed = ( text : string ) : boolean => {
118
- const rules = settings . listForImgEmbed
119
- . split ( "\n" )
120
- . filter ( ( v ) => v . length > 0 )
121
- . map ( ( v ) => new RegExp ( v ) ) ;
122
- for ( const reg of rules ) {
123
- if ( reg . test ( text ) ) return true ;
124
- }
125
- return false ;
126
- } ;
77
+ function isUrl ( text : string , settings : PluginSettings ) : boolean {
78
+ if ( text === "" ) return false ;
79
+ try {
80
+ // throw TypeError: Invalid URL if not valid
81
+ new URL ( text ) ;
82
+ return true ;
83
+ } catch ( error ) {
84
+ // settings.regex: fallback test allows url without protocol (http,file...)
85
+ return testFilePath ( text ) || new RegExp ( settings . regex ) . test ( text ) ;
86
+ }
87
+ }
88
+
89
+ function isImgEmbed ( text : string , settings : PluginSettings ) : boolean {
90
+ const rules = settings . listForImgEmbed
91
+ . split ( "\n" )
92
+ . filter ( ( v ) => v . length > 0 )
93
+ . map ( ( v ) => new RegExp ( v ) ) ;
94
+ for ( const reg of rules ) {
95
+ if ( reg . test ( text ) ) return true ;
96
+ }
97
+ return false ;
98
+ }
99
+
100
+ /**
101
+ * Validate that either the text on the clipboard or the selected text is a link, and if so return the link as
102
+ * a markdown link with the selected text as the link's text, or, if the value on the clipboard is not a link
103
+ * but the selected text is, the value of the clipboard as the link's text.
104
+ * If the link matches one of the image url regular expressions return a markdown image link.
105
+ * @param clipboardText text on the clipboard.
106
+ * @param selectedText highlighted text
107
+ * @param settings plugin settings
108
+ * @returns a mardown link or image link if the clipboard or selction value is a valid link, else null.
109
+ */
110
+ function getReplaceText ( clipboardText : string , selectedText : string , settings : PluginSettings ) : string | null {
127
111
128
112
let linktext : string ;
129
113
let url : string ;
130
114
131
- if ( isUrl ( clipboardText ) ) {
115
+ if ( isUrl ( clipboardText , settings ) ) {
132
116
linktext = selectedText ;
133
117
url = clipboardText ;
134
- } else if ( isUrl ( selectedText ) ) {
118
+ } else if ( isUrl ( selectedText , settings ) ) {
135
119
linktext = clipboardText ;
136
120
url = selectedText ;
137
121
} else return null ; // if neither of two is an URL, the following code would be skipped.
138
122
139
- const imgEmbedMark = isImgEmbed ( clipboardText ) ? "!" : "" ;
123
+ const imgEmbedMark = isImgEmbed ( clipboardText , settings ) ? "!" : "" ;
140
124
141
125
url = processUrl ( url ) ;
142
126
143
- if (
144
- selectedText === "" &&
145
- settings . nothingSelected === NothingSelected . insertBare
146
- ) {
127
+ if ( selectedText === "" && settings . nothingSelected === NothingSelected . insertBare ) {
147
128
return `<${ url } >` ;
148
129
} else {
149
130
return imgEmbedMark + `[${ linktext } ](${ url } )` ;
@@ -181,32 +162,60 @@ function getCbText(cb: string | ClipboardEvent): string | null {
181
162
return clipboardText . trim ( ) ;
182
163
}
183
164
184
- function getWordBoundaries ( editor : CodeMirror . Editor ) : WordBoundaries {
165
+ function getWordBoundaries ( editor : Editor , settings : PluginSettings ) : EditorRange {
185
166
const cursor = editor . getCursor ( ) ;
186
-
187
- let wordBoundaries : WordBoundaries ;
188
- if ( editor . getTokenTypeAt ( cursor ) === "url" ) {
189
- const { start : startCh , end : endCh } = editor . getTokenAt ( cursor ) ;
190
- const line = cursor . line ;
191
- wordBoundaries = { start : { line, ch : startCh } , end : { line, ch : endCh } } ;
192
- } else {
193
- const { anchor : start , head : end } = editor . findWordAt ( cursor ) ;
194
- wordBoundaries = { start, end } ;
167
+ const line = editor . getLine ( cursor . line ) ;
168
+ let wordBoundaries = findWordAt ( line , cursor ) ; ;
169
+
170
+ // If the token the cursor is on is a url, grab the whole thing instead of just parsing it like a word
171
+ let start = wordBoundaries . from . ch ;
172
+ let end = wordBoundaries . to . ch ;
173
+ while ( start > 0 && ! / \s / . test ( line . charAt ( start - 1 ) ) ) -- start ;
174
+ while ( end < line . length && ! / \s / . test ( line . charAt ( end ) ) ) ++ end ;
175
+ if ( isUrl ( line . slice ( start , end ) , settings ) ) {
176
+ wordBoundaries . from . ch = start ;
177
+ wordBoundaries . to . ch = end ;
195
178
}
196
179
return wordBoundaries ;
197
180
}
198
181
199
- function getCursor ( editor : CodeMirror . Editor ) : WordBoundaries {
200
- return { start : editor . getCursor ( ) , end : editor . getCursor ( ) } ;
182
+ const findWordAt = ( ( ) => {
183
+ const nonASCIISingleCaseWordChar = / [ \u00df \u0587 \u0590 - \u05f4 \u0600 - \u06ff \u3040 - \u309f \u30a0 - \u30ff \u3400 - \u4db5 \u4e00 - \u9fcc \uac00 - \ud7af ] / ;
184
+
185
+ function isWordChar ( char : string ) {
186
+ return / \w / . test ( char ) || char > "\x80" &&
187
+ ( char . toUpperCase ( ) != char . toLowerCase ( ) || nonASCIISingleCaseWordChar . test ( char ) ) ;
188
+ }
189
+
190
+ return ( line : string , pos : EditorPosition ) : EditorRange => {
191
+ let check ;
192
+ let start = pos . ch ;
193
+ let end = pos . ch ;
194
+ ( end === line . length ) ? -- start : ++ end ;
195
+ const startChar = line . charAt ( pos . ch ) ;
196
+ if ( isWordChar ( startChar ) ) {
197
+ check = ( ch : string ) => isWordChar ( ch ) ;
198
+ } else if ( / \s / . test ( startChar ) ) {
199
+ check = ( ch : string ) => / \s / . test ( ch ) ;
200
+ } else {
201
+ check = ( ch : string ) => ( ! / \s / . test ( ch ) && ! isWordChar ( ch ) ) ;
202
+ }
203
+
204
+ while ( start > 0 && check ( line . charAt ( start - 1 ) ) ) -- start ;
205
+ while ( end < line . length && check ( line . charAt ( end ) ) ) ++ end ;
206
+ return { from : { line : pos . line , ch : start } , to : { line : pos . line , ch : end } } ;
207
+ } ;
208
+ } ) ( ) ;
209
+
210
+ function getCursor ( editor : Editor ) : EditorRange {
211
+ return { from : editor . getCursor ( ) , to : editor . getCursor ( ) } ;
201
212
}
202
213
203
- function replace (
204
- cm : CodeMirror . Editor ,
205
- replaceText : string ,
206
- replaceRange : WordBoundaries | null = null
207
- ) : void {
208
- if ( replaceRange && replaceRange . start && replaceRange . end )
209
- cm . replaceRange ( replaceText , replaceRange . start , replaceRange . end ) ;
214
+ function replace ( editor : Editor , replaceText : string , replaceRange : EditorRange | null = null ) : void {
215
+ // replaceRange is only not null when there isn't anything selected.
216
+ if ( replaceRange && replaceRange . from && replaceRange . to ) {
217
+ editor . replaceRange ( replaceText , replaceRange . from , replaceRange . to ) ;
218
+ }
210
219
// if word is null or undefined
211
- else cm . replaceSelection ( replaceText ) ;
220
+ else editor . replaceSelection ( replaceText ) ;
212
221
}
0 commit comments