@@ -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,78 @@ 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 ( )
48+ let markdown = plaintext
49+ let markdownIgnoreBeforeIndex = 0
50+ let index = 0
51+ const NODE_LIMIT = 10000
52+
53+ // Walk through the DOM tree
54+ while ( currentNode && index < NODE_LIMIT ) {
55+ index ++
56+ const text = isLink ( currentNode ) ? currentNode . textContent || '' : ( currentNode . firstChild as Text ) ?. wholeText || ''
57+
58+ // No need to transform whitespace
59+ if ( isEmptyString ( text ) ) {
60+ currentNode = walker . nextNode ( )
61+ continue
62+ }
63+
64+ // Find the index where "text" is found in "markdown" _after_ "markdownIgnoreBeforeIndex"
65+ const markdownFoundIndex = markdown . indexOf ( text , markdownIgnoreBeforeIndex )
66+
67+ if ( markdownFoundIndex >= 0 ) {
68+ if ( isLink ( currentNode ) ) {
69+ const markdownLink = linkify ( currentNode )
70+ // Transform 'example link plus more text' into 'example [link](example link) plus more text'
71+ // Method: 'example [link](example link) plus more text' = 'example ' + '[link](example link)' + ' plus more text'
72+ markdown =
73+ markdown . slice ( 0 , markdownFoundIndex ) + markdownLink + markdown . slice ( markdownFoundIndex + text . length )
74+ markdownIgnoreBeforeIndex = markdownFoundIndex + markdownLink . length
75+ } else {
76+ markdownIgnoreBeforeIndex = markdownFoundIndex + text . length
77+ }
6278 }
79+
80+ currentNode = walker . nextNode ( )
6381 }
64- markdownParts . push ( text )
65- return markdownParts . join ( '' )
66- }
6782
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}
83+ // Unless we hit the node limit, we should have processed all nodes
84+ return index === NODE_LIMIT ? plaintext : markdown
85+ }
7586
76- index += search . length
87+ function isEmptyString ( text : string ) : boolean {
88+ return ! text || text ?. trim ( ) . length === 0
89+ }
7790
78- return {
79- part : text . substring ( 0 , index ) ,
80- index
81- }
91+ function isLink ( node : Node ) : node is HTMLAnchorElement {
92+ return ( node as HTMLElement ) . tagName ?. toLowerCase ( ) === 'a' && ( node as HTMLElement ) . hasAttribute ( 'href' )
8293}
8394
8495function hasHTML ( transfer : DataTransfer ) : boolean {
0 commit comments