@@ -8,8 +8,6 @@ export function uninstall(el: HTMLElement): void {
88 el . removeEventListener ( 'paste' , onPaste )
99}
1010
11- type MarkdownTransformer = ( element : HTMLElement | HTMLAnchorElement , args : string [ ] ) => string
12-
1311function onPaste ( event : ClipboardEvent ) {
1412 const transfer = event . clipboardData
1513 // if there is no clipboard data, or
@@ -20,65 +18,80 @@ function onPaste(event: ClipboardEvent) {
2018 if ( ! ( field instanceof HTMLTextAreaElement ) ) return
2119
2220 // Get the plaintext and html version of clipboard contents
23- let text = transfer . getData ( 'text/plain' )
21+ let plaintext = transfer . getData ( 'text/plain' )
2422 const textHTML = transfer . getData ( 'text/html' )
2523 // Replace Unicode equivalent of " " with a space
26- const textHTMLClean = textHTML . replace ( / \u00A0 / g, ' ' )
24+ const textHTMLClean = textHTML . replace ( / \u00A0 / g, ' ' ) . replace ( / \uC2A0 / g , ' ' )
2725 if ( ! textHTML ) return
2826
29- text = text . trim ( )
30- if ( ! text ) return
27+ plaintext = plaintext . trim ( )
28+ if ( ! plaintext ) return
3129
3230 // Generate DOM tree from HTML string
3331 const parser = new DOMParser ( )
3432 const doc = parser . parseFromString ( textHTMLClean , 'text/html' )
33+ const walker = doc . createTreeWalker ( doc . body , NodeFilter . SHOW_ELEMENT )
3534
36- const a = doc . getElementsByTagName ( 'a' )
37- const markdown = transform ( a , text , linkify as MarkdownTransformer )
35+ const markdown = convertToMarkdown ( plaintext , walker )
3836
3937 // If no changes made by transforming
40- if ( markdown === text ) return
38+ if ( markdown === plaintext ) return
4139
4240 event . stopPropagation ( )
4341 event . preventDefault ( )
4442
4543 insertText ( field , markdown )
4644}
4745
48- // Build a markdown string from a DOM tree and plaintext
49- function transform (
50- elements : HTMLCollectionOf < HTMLElement > ,
51- text : string ,
52- transformer : MarkdownTransformer ,
53- ...args : string [ ]
54- ) : string {
55- const markdownParts = [ ]
56- for ( const element of elements ) {
57- const textContent = element . textContent || ''
58- const { part, index} = trimAfter ( text , textContent )
59- if ( index >= 0 ) {
60- markdownParts . push ( part . replace ( textContent , transformer ( element , args ) ) )
61- text = text . slice ( index )
46+ function convertToMarkdown ( plaintext : string , walker : TreeWalker ) : string {
47+ let currentNode = walker . firstChild ( ) as HTMLAnchorElement | HTMLElement | null
48+ let markdown = plaintext
49+ let markdownIgnoreBeforeIndex = 0
50+ let index = 0
51+ const NODE_LIMIT = 100000
52+
53+ // Walk through the DOM tree
54+ while ( currentNode && index < NODE_LIMIT ) {
55+ index ++
56+ const text = isLink ( currentNode )
57+ ? ( currentNode as HTMLAnchorElement ) . textContent || ''
58+ : ( currentNode . firstChild as Text ) ?. wholeText || ''
59+
60+ // No need to transform whitespace
61+ if ( isEmptyString ( text ) ) {
62+ currentNode = walker . nextNode ( ) as HTMLAnchorElement | HTMLElement | null
63+ continue
64+ }
65+
66+ // Find the index where "text" is found in "markdown" _after_ "markdownIgnoreBeforeIndex"
67+ const markdownFoundIndex = markdown . indexOf ( text , markdownIgnoreBeforeIndex )
68+
69+ if ( markdownFoundIndex >= 0 ) {
70+ if ( isLink ( currentNode ) ) {
71+ const markdownLink = linkify ( currentNode as HTMLAnchorElement )
72+ // Transform 'example link plus more text' into 'example [link](example link) plus more text'
73+ // Method: 'example [link](example link) plus more text' = 'example ' + '[link](example link)' + ' plus more text'
74+ markdown =
75+ markdown . slice ( 0 , markdownFoundIndex ) + markdownLink + markdown . slice ( markdownFoundIndex + text . length )
76+ markdownIgnoreBeforeIndex = markdownFoundIndex + markdownLink . length
77+ } else {
78+ markdownIgnoreBeforeIndex = markdownFoundIndex + text . length
79+ }
6280 }
81+
82+ currentNode = walker . nextNode ( ) as HTMLAnchorElement | HTMLElement | null
6383 }
64- markdownParts . push ( text )
65- return markdownParts . join ( '' )
66- }
6784
68- // Trim text at index of last character of the first occurrence of "search" and
69- // return a new string with the substring until the index
70- // Example: trimAfter('Hello world', 'world') => {part: 'Hello world', index: 11}
71- // Example: trimAfter('Hello world', 'bananas') => {part: '', index: -1}
72- function trimAfter ( text : string , search = '' ) : { part : string ; index : number } {
73- let index = text . indexOf ( search )
74- if ( index === - 1 ) return { part : '' , index}
85+ // Unless we hit the node limit, we should have processed all nodes
86+ return index === NODE_LIMIT ? plaintext : markdown
87+ }
7588
76- index += search . length
89+ function isEmptyString ( text : string ) : boolean {
90+ return ! text || text ?. trim ( ) . length === 0
91+ }
7792
78- return {
79- part : text . substring ( 0 , index ) ,
80- index
81- }
93+ function isLink ( node : HTMLElement ) : boolean {
94+ return node . tagName . toLowerCase ( ) === 'a' && node . hasAttribute ( 'href' )
8295}
8396
8497function hasHTML ( transfer : DataTransfer ) : boolean {
0 commit comments