Skip to content

Commit ae282ef

Browse files
authored
Merge pull request #35 from imjohnbo/imjohnbo/markdown-links
Paste multiple links into markdown via comment box
2 parents 909ae9d + 50e84e0 commit ae282ef

File tree

5 files changed

+122
-1
lines changed

5 files changed

+122
-1
lines changed

examples/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848

4949
<p>Test by copying this page's URL and then selecting <i>here</i> in the textarea and pasting the URL.</p>
5050

51+
<p>Or copy and paste a <a href="https://github.com">link</a> and <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">another link</a> and maybe <a href="https://google.com">one more</a> into the textarea.</p>
52+
5153
<textarea cols="50" rows="10">The examples can be found here.</textarea>
5254

5355
<script type="module">

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {install as installHTML, uninstall as uninstallHTML} from './paste-markdown-html'
12
import {install as installImageLink, uninstall as uninstallImageLink} from './paste-markdown-image-link'
23
import {install as installLink, uninstall as uninstallLink} from './paste-markdown-link'
34
import {install as installTable, uninstall as uninstallTable} from './paste-markdown-table'
@@ -12,10 +13,12 @@ function subscribe(el: HTMLElement): Subscription {
1213
installImageLink(el)
1314
installLink(el)
1415
installText(el)
16+
installHTML(el)
1517

1618
return {
1719
unsubscribe: () => {
1820
uninstallTable(el)
21+
uninstallHTML(el)
1922
uninstallImageLink(el)
2023
uninstallLink(el)
2124
uninstallText(el)
@@ -25,10 +28,12 @@ function subscribe(el: HTMLElement): Subscription {
2528

2629
export {
2730
subscribe,
31+
installHTML,
2832
installImageLink,
2933
installLink,
3034
installTable,
3135
installText,
36+
uninstallHTML,
3237
uninstallImageLink,
3338
uninstallTable,
3439
uninstallLink,

src/paste-markdown-html.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {insertText} from './text'
2+
3+
export function install(el: HTMLElement): void {
4+
el.addEventListener('paste', onPaste)
5+
}
6+
7+
export function uninstall(el: HTMLElement): void {
8+
el.removeEventListener('paste', onPaste)
9+
}
10+
11+
type MarkdownTransformer = (element: HTMLElement | HTMLAnchorElement, args: string[]) => string
12+
13+
function onPaste(event: ClipboardEvent) {
14+
const transfer = event.clipboardData
15+
if (!transfer || !hasHTML(transfer)) return
16+
17+
const field = event.currentTarget
18+
if (!(field instanceof HTMLTextAreaElement)) return
19+
20+
// Get the plaintext and html version of clipboard contents
21+
let text = transfer.getData('text/plain')
22+
const textHTML = transfer.getData('text/html')
23+
if (!textHTML) return
24+
25+
text = text.trim()
26+
if (!text) return
27+
28+
// Generate DOM tree from HTML string
29+
const parser = new DOMParser()
30+
const doc = parser.parseFromString(textHTML, 'text/html')
31+
32+
const a = doc.getElementsByTagName('a')
33+
const markdown = transform(a, text, linkify as MarkdownTransformer)
34+
35+
// If no changes made by transforming
36+
if (markdown === text) return
37+
38+
event.stopPropagation()
39+
event.preventDefault()
40+
41+
insertText(field, markdown)
42+
}
43+
44+
// Build a markdown string from a DOM tree and plaintext
45+
function transform(
46+
elements: HTMLCollectionOf<HTMLElement>,
47+
text: string,
48+
transformer: MarkdownTransformer,
49+
...args: string[]
50+
): string {
51+
const markdownParts = []
52+
for (const element of elements) {
53+
const textContent = element.textContent || ''
54+
const {part, index} = trimAfter(text, textContent)
55+
markdownParts.push(part.replace(textContent, transformer(element, args)))
56+
text = text.slice(index)
57+
}
58+
markdownParts.push(text)
59+
return markdownParts.join('')
60+
}
61+
62+
// Trim text at index of last character of the first occurrence of "search" and
63+
// return a new string with the substring until the index
64+
// Example: trimAfter('Hello world', 'world') => {part: 'Hello world', index: 11}
65+
// Example: trimAfter('Hello world', 'bananas') => {part: '', index: -1}
66+
function trimAfter(text: string, search = ''): {part: string; index: number} {
67+
let index = text.indexOf(search)
68+
if (index === -1) return {part: '', index}
69+
70+
index += search.length
71+
72+
return {
73+
part: text.substring(0, index),
74+
index
75+
}
76+
}
77+
78+
function hasHTML(transfer: DataTransfer): boolean {
79+
return transfer.types.includes('text/html')
80+
}
81+
82+
function linkify(element: HTMLAnchorElement): string {
83+
return `[${element.textContent}](${element.href})`
84+
}

test/test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,35 @@ describe('paste-markdown', function () {
118118
paste(textarea, {'text/plain': 'hello', 'text/x-gfm': '# hello'})
119119
assert.include(textarea.value, '# hello')
120120
})
121+
122+
it('turns one html link into a markdown link', function () {
123+
// eslint-disable-next-line github/unescaped-html-literal
124+
const link = `<meta charset='utf-8'><meta charset="utf-8">
125+
<b><a href="https://github.com/" style="text-decoration:none;"><span>link</span></a></b>`
126+
const plaintextLink = 'link'
127+
const markdownLink = '[link](https://github.com/)'
128+
129+
paste(textarea, {'text/html': link, 'text/plain': plaintextLink})
130+
assert.equal(textarea.value, markdownLink)
131+
})
132+
133+
it('turns mixed html content containing several links into appropriate markdown', function () {
134+
// eslint-disable-next-line github/unescaped-html-literal
135+
const sentence = `<meta charset='utf-8'><meta charset="utf-8">
136+
<b style="font-weight:normal;"><p dir="ltr"><span>This is a </span>
137+
<a href="https://github.com/"><span>link</span></a><span> and </span>
138+
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"><span>another link</span></a></p>
139+
<br /><a href="https://github.com/"><span>Link</span></a><span> at the beginning, link at the </span>
140+
<a href="https://github.com/"><span>end</span></a></b>`
141+
// eslint-disable-next-line i18n-text/no-en
142+
const plaintextSentence = 'This is a link and another link\n\nLink at the beginning, link at the end'
143+
const markdownSentence =
144+
'This is a [link](https://github.com/) and [another link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)\n\n' +
145+
'[Link](https://github.com/) at the beginning, link at the [end](https://github.com/)'
146+
147+
paste(textarea, {'text/html': sentence, 'text/plain': plaintextSentence})
148+
assert.equal(textarea.value, markdownSentence)
149+
})
121150
})
122151
})
123152

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"target": "es2017",
55
"lib": [
66
"es2018",
7-
"dom"
7+
"dom",
8+
"dom.iterable"
89
],
910
"strict": true,
1011
"declaration": true,

0 commit comments

Comments
 (0)