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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"linkify-urls": "^1.0.2",
"select-dom": "^4.1.0",
"shorten-repo-url": "^1.1.0",
"to-markdown": "^3.0.4",
"to-markdown": "^3.1.0",
"to-semver": "^1.1.0",
"webext-dynamic-content-scripts": "^2.0.1",
"webext-options-sync": "^0.11.0"
Expand All @@ -30,6 +30,7 @@
"babel-cli": "^6.24.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
"chrome-webstore-upload-cli": "^1.0.0",
"common-tags": "^1.4.0",
"dot-json": "^1.0.3",
"npm-run-all": "^4.0.2",
"webext": "^1.9.1-with-submit.1",
Expand Down
65 changes: 60 additions & 5 deletions src/libs/copy-markdown.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,77 @@
import toMarkdown from 'to-markdown';
import copyToClipboard from 'copy-text-to-clipboard';

const unwrapContent = content => content;
const unshortenRegex = /^https:[/][/](www[.])?|[/]$/g;

const converters = [
// Drop unnecessary elements
// <g-emoji> is GH's emoji wrapper
// input and .handle appear in "- [ ] lists", let's not copy tasks
{
filter: node => node.matches('g-emoji,.handle,input.task-list-item-checkbox'),
replacement: unwrapContent
},

// Unwrap commit/issue autolinks
{
filter: node => node.matches('.commit-link,.issue-link') || // GH autolinks
(node.href && node.href.replace(unshortenRegex, '') === node.textContent), // Some of bfred-it/shorten-repo-url
replacement: (content, element) => element.href
},

// Unwrap images
{
filter: node => node.tagName === 'A' && // It's a link
node.childNodes.length === 1 && // It has one child
node.firstChild.tagName === 'IMG' && // Its child is an image
node.firstChild.src === node.href, // It links to its own image
replacement: unwrapContent
},

// Keep <img> if it's customized
{
filter: node => node.matches('img[width],img[height],img[align]'),
replacement: (content, element) => element.outerHTML
}
];

export const getSmarterMarkdown = html => toMarkdown(html, {
converters,
gfm: true
});

export default event => {
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const container = range.commonAncestorContainer;
const containerEl = container.closest ? container : container.parentNode;

if (containerEl.closest('pre')) {
// Exclude pure code selections and selections across markdown elements:
// https://github.com/sindresorhus/refined-github/issues/522#issuecomment-311271274
if (containerEl.closest('pre') || containerEl.querySelector('.markdown-body')) {
return;
}

event.stopImmediatePropagation();
event.preventDefault();

const holder = document.createElement('div');
holder.append(range.cloneContents());
const markdown = toMarkdown(holder.innerHTML, {gfm: true});

// Wrap orphaned <li>s in their original parent
// And keep the their original number
if (holder.firstChild.tagName === 'LI') {
const list = document.createElement(containerEl.tagName);
try {
const originalLi = range.startContainer.parentNode.closest('li');
list.start = containerEl.start + [...containerEl.children].indexOf(originalLi);
} catch (err) {}
list.append(...holder.childNodes);
holder.appendChild(list);
}

const markdown = getSmarterMarkdown(holder.innerHTML);

copyToClipboard(markdown);

event.stopImmediatePropagation();
event.preventDefault();
};
101 changes: 101 additions & 0 deletions test/copy-markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import test from 'ava';
import {stripIndent} from 'common-tags';
import {getSmarterMarkdown} from '../src/libs/copy-markdown';

test('base markdown', t => {
t.is(
getSmarterMarkdown('<a href="url">this</a> is <strong>markdown</strong>'),
'[this](url) is **markdown**'
);
});

test('drop <g-emoji>', t => {
t.is(
getSmarterMarkdown('<g-emoji alias="fire" fallback-src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f525.png" ios-version="6.0" title=":fire:">🔥</g-emoji>'),
'🔥'
);
});

test('drop tasks from lists', t => {
t.is(
getSmarterMarkdown(stripIndent`
<ul class="contains-task-list">
<li class="task-list-item enabled"><span class="handle"><svg class="drag-handle" aria-hidden="true" width="16" height="15" version="1.1" viewBox="0 0 16 15"><path d="M12,4V5H4V4h8ZM4,8h8V7H4V8Zm0,3h8V10H4v1Z"></path></svg></span><input type="checkbox" class="task-list-item-checkbox"> try me out</li>
<li class="task-list-item enabled"><span class="handle"><svg class="drag-handle" aria-hidden="true" width="16" height="15" version="1.1" viewBox="0 0 16 15"><path d="M12,4V5H4V4h8ZM4,8h8V7H4V8Zm0,3h8V10H4v1Z"></path></svg></span><input type="checkbox" class="task-list-item-checkbox" checked=""> test across lines</li>
</ul>
`),
stripIndent`
* try me out
* test across lines
`
);
});

test('drop autolinks around images', t => {
t.is(
getSmarterMarkdown(stripIndent`
<a href="https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67" target="_blank"><img src="https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67" alt="" style="max-width:100%;"></a>
`),
stripIndent`
![](https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67)
`
);
});

test('keep img tags if they have width, height or align', t => {
t.is(
getSmarterMarkdown(stripIndent`
<a href="https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67" target="_blank"><img align="center" width="32" alt="copy" src="https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67" style="max-width:100%;"></a>
`),
stripIndent`
<img align="center" width="32" alt="copy" src="https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67" style="max-width:100%;">
`
);
});

test('drop autolinks from issue links and commit links', t => {
t.is(
getSmarterMarkdown(stripIndent`
<a href="https://github.com/sindresorhus/refined-github/issues/522" class="issue-link js-issue-link" data-id="237988387" data-error-text="Failed to load issue title" data-permission-text="Issue title is private" title="'Copy to Markdown' improvements">#522</a>
`),
'https://github.com/sindresorhus/refined-github/issues/522'
);
t.is(
getSmarterMarkdown(stripIndent`
<a href="https://github.com/sindresorhus/refined-github/commit/833d5984fffb18a44b83d965b397f82e0ff3085e" class="commit-link"><tt>833d598</tt></a>
`),
'https://github.com/sindresorhus/refined-github/commit/833d5984fffb18a44b83d965b397f82e0ff3085e'
);
});

test('drop autolinks around some shortened links', t => {
t.is(
getSmarterMarkdown(stripIndent`
<p><a href="https://www.npmjs.com">npmjs.com</a></p>
<p><a href="https://twitter.com/bfred_it">twitter.com/bfred_it</a></p>
<p><a href="https://github.com/">github.com</a></p>
`),
stripIndent`
https://www.npmjs.com/

https://twitter.com/bfred_it

https://github.com/
`
);
});

test('wrap orphaned li in their original parent', t => {
t.is(
getSmarterMarkdown(stripIndent`
<ol start="99">
<li>big lists</li>
<li>deserve big numbers</li>
</ol>
`),
stripIndent`
99. big lists
100. deserve big numbers
`
);
});