Browse Source

Markdown copy improvements (#546)

* Skip selections across elements

* Drop `g-emoji` elements

* Drop tasks from lists

* Drop autolinks around images

* Keep <img> if it has width, height or align

* Unwrap commit/issue autolinks

* Unwrap some shortened links

* Wrap orphaned <li>s in their original parent

This allows partial list selections. Without a parent, <li>s will be
rendered inline elements by to-markdown

* Add support for <ol> lists that start after 1

* Keep the original number of orphaned <li>s in numbered lists

* It should only prevent the original copy if it succeeds

* Add tests ️

* Update to-markdown@3.1.0

Include ol[start] support
master
Federico Brigante 7 years ago
committed by Sindre Sorhus
parent
commit
37335e35b0
  1. 3
      package.json
  2. 65
      src/libs/copy-markdown.js
  3. 101
      test/copy-markdown.js

3
package.json

@ -21,7 +21,7 @@
"linkify-urls": "^1.3.0",
"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"
@ -35,6 +35,7 @@
"babel-plugin-transform-es2015-modules-commonjs": "^6.24.1",
"babel-plugin-transform-react-jsx": "^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",

65
src/libs/copy-markdown.js

@ -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
test/copy-markdown.js

@ -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
`
);
});
Loading…
Cancel
Save