Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Paste Markdown objects

- Paste spreadsheet cells and HTML tables as a Markdown tables.
- Paste URLs on selected text as Markdown links.
- Paste image URLs as Markdown image links.
- Paste markdown as markdown. See [`@github/quote-selection`/Preserving markdown syntax](https://github.com/github/quote-selection/tree/9ae5f88f5bc3021f51d2dc9981eca83ce7cfe04f#preserving-markdown-syntax) for details.

Expand All @@ -13,7 +14,7 @@ $ npm install @github/paste-markdown
## Usage

```js
import subscribe from '@github/paste-markdown'
import {subscribe} from '@github/paste-markdown'

// Subscribe the behavior to the textarea.
subscribe(document.querySelector('textarea[data-paste-markdown]'))
Expand All @@ -26,7 +27,7 @@ be applied to any element matching a selector.

```js
import {observe} from 'selector-observer'
import subscribe from '@github/paste-markdown'
import {subscribe} from '@github/paste-markdown'

// Subscribe the behavior to all matching textareas.
observe('textarea[data-paste-markdown]', {subscribe})
Expand Down
9 changes: 6 additions & 3 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@

<img src="https://github.com/hubot.png" width="100" alt="hubot">

<textarea cols="50" rows="10"></textarea>
<p>Test by copying this page's URL and then selecting <i>here</i> in the textarea and pasting the URL.</p>

<textarea cols="50" rows="10">The examples can be found here.</textarea>

<script type="module">
// import subscribe from '../dist/index.esm.js'
import subscribe from 'https://unpkg.com/@github/paste-markdown/dist/index.esm.js'
// import {subscribe} from '../dist/index.esm.js'
import {subscribe} from 'https://unpkg.com/@github/paste-markdown/dist/index.esm.js'
subscribe(document.querySelector('textarea'))
installLink(document.querySelector('textarea'))
</script>
</body>
</html>
19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import {install as installLink, uninstall as uninstallLink} from './paste-markdown-image-link'
import {install as installImageLink, uninstall as uninstallImageLink} from './paste-markdown-image-link'
import {install as installLink, uninstall as uninstallLink} from './paste-markdown-link'
import {install as installTable, uninstall as uninstallTable} from './paste-markdown-table'
import {install as installText, uninstall as uninstallText} from './paste-markdown-text'

interface Subscription {
unsubscribe: () => void
}

export default function subscribe(el: HTMLElement): Subscription {
function subscribe(el: HTMLElement): Subscription {
installTable(el)
installImageLink(el)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having all the install* functions in here so that consumers that use subscribe will get all the features, but then if they want to conditionally apply features they can import only the installers they plan on using?

Copy link
Contributor Author

@steffen steffen Sep 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koddsson I've thought about that as well. Do you know if this line is the only place where we consume the library at GitHub?

Since subscribe is now a named export, users of the library have to adjust the usage of the library either way, so yeah, I agree with adding the new feature to subscribe right away. We then only need to adjust this line as well to use the add and remove hooks as I do in the lines below so that the new Link feature is not loaded by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koddsson I just saw your proposal on using the add and remove hooks for all functions. I missed that earlier. I'll go with that approach. 👍 Thank you! 🙇‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koddsson I have applied your proposal here: 1c6b0cd Thank you! 🙇‍♂️

installLink(el)
installText(el)

return {
unsubscribe: () => {
uninstallTable(el)
uninstallImageLink(el)
uninstallLink(el)
uninstallText(el)
}
}
}

export {
subscribe,
installImageLink,
installLink,
installTable,
installText,
uninstallImageLink,
uninstallTable,
uninstallLink,
uninstallText
}
52 changes: 52 additions & 0 deletions src/paste-markdown-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {insertText} from './text'

export function install(el: HTMLElement): void {
el.addEventListener('paste', onPaste)
}

export function uninstall(el: HTMLElement): void {
el.removeEventListener('paste', onPaste)
}

function onPaste(event: ClipboardEvent) {
const transfer = event.clipboardData
if (!transfer || !hasPlainText(transfer)) return

const field = event.currentTarget
if (!(field instanceof HTMLTextAreaElement)) return

const text = transfer.getData('text/plain')
if (!text) return

if (isWithinLink(field)) return

event.stopPropagation()
event.preventDefault()

const selectedText = field.value.substring(field.selectionStart, field.selectionEnd)

insertText(field, linkify(selectedText, text), {addNewline: false})
}

function hasPlainText(transfer: DataTransfer): boolean {
return Array.from(transfer.types).includes('text/plain')
}

function isWithinLink(textarea: HTMLTextAreaElement): boolean {
const selectionStart = textarea.selectionStart || 0

if (selectionStart > 1) {
const previousChars = textarea.value.substring(selectionStart - 2, selectionStart)
return previousChars === ']('
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about checking for HTML links as well? Since markdown generally supports HTML as well, should we check if we're in a <a> tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about some other edge cases as well, for example let's say you have this issue template:

Repository: [URL of repository]

If you select [URL of repository] and paste an URL on top of it, with the new feature it would now convert it to [[URL of repository]](url).

Though, I'm not sure we should account for all edge cases or if users will learn quickly that they need to delete the selected text first and then paste the URL in order to have the URL as is.

Regarding detecting if the selected text is within an <a> tag:
Do you think of the use case where a user would have an -link in their Markdown, such as <a href="https://some-url">Docs</a>, and where they would want to change the href by selecting the https://some-url part and replace it with another URL?

} else {
return false
}
}

function linkify(selectedText: string, text: string): string {
return selectedText.length && isURL(text) ? `[${selectedText}](${text})` : text
}

function isURL(url: string): boolean {
return /^https?:\/\//i.test(url)
}
8 changes: 6 additions & 2 deletions src/text.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
export function insertText(textarea: HTMLInputElement | HTMLTextAreaElement, text: string): void {
export function insertText(
textarea: HTMLInputElement | HTMLTextAreaElement,
text: string,
options = {addNewline: true}
): void {
const beginning = textarea.value.substring(0, textarea.selectionStart || 0)
const remaining = textarea.value.substring(textarea.selectionEnd || 0)

const newline = beginning.length === 0 || beginning.match(/\n$/) ? '' : '\n'
const newline = !options.addNewline || beginning.length === 0 || beginning.match(/\n$/) ? '' : '\n'
const textBeforeCursor = beginning + newline + text

textarea.value = textBeforeCursor + remaining
Expand Down
10 changes: 9 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import subscribe from '../dist/index.esm.js'
import {subscribe} from '../dist/index.esm.js'

describe('paste-markdown', function () {
describe('installed on textarea', function () {
Expand All @@ -23,6 +23,14 @@ describe('paste-markdown', function () {
assert.include(textarea.value, '![](https://github.com/github.png)\n\n![](https://github.com/hubot.png)')
})

it('turns pasted urls on selected text into markdown links', function () {
// eslint-disable-next-line i18n-text/no-en
textarea.value = 'The examples can be found here.'
textarea.setSelectionRange(26, 30)
paste(textarea, {'text/plain': 'https://github.com'})
assert.equal(textarea.value, 'The examples can be found [here](https://github.com).')
})

it('turns html tables into markdown', function () {
const data = {
'text/html': `
Expand Down