2017-01-16 15:27:05 -05:00
|
|
|
/* eslint-disable class-methods-use-this */
|
2017-01-16 16:44:46 -05:00
|
|
|
/*jshint esversion: 6 */
|
2017-01-16 15:27:05 -05:00
|
|
|
|
|
|
|
(() => {
|
|
|
|
const gfmRules = {
|
2017-01-16 18:11:15 -05:00
|
|
|
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
|
|
|
|
// These handlers consequently convert that same HTML to GFM to be copied to the clipboard.
|
|
|
|
// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML from GFM should have a handler here, in reverse order.
|
|
|
|
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
|
2017-01-16 16:44:46 -05:00
|
|
|
InlineDiffFilter: {
|
|
|
|
'span.idiff.addition'(el, text) {
|
|
|
|
return `{+${text}+}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'span.idiff.deletion'(el, text) {
|
|
|
|
return `{-${text}-}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
TaskListFilter: {
|
|
|
|
'input[type=checkbox].task-list-item-checkbox'(el, text) {
|
|
|
|
return `[${el.checked ? 'x' : ' '}]`;
|
2017-01-16 15:27:05 -05:00
|
|
|
}
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
ReferenceFilter: {
|
|
|
|
'a.gfm:not([data-link=true])'(el, text) {
|
|
|
|
return el.dataset.original || text;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
AutolinkFilter: {
|
|
|
|
'a'(el, text) {
|
|
|
|
// Fallback on the regular MarkdownFilter's `a` handler.
|
|
|
|
if (text !== el.getAttribute('href')) return false;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
|
|
|
return text;
|
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
TableOfContentsFilter: {
|
|
|
|
'ul.section-nav'(el, text) {
|
|
|
|
return '[[_TOC_]]';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
EmojiFilter: {
|
|
|
|
'img.emoji'(el, text) {
|
|
|
|
return el.getAttribute('alt');
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
ImageLinkFilter: {
|
|
|
|
'a.no-attachment-icon'(el, text) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return text;
|
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
VideoLinkFilter: {
|
|
|
|
'.video-container'(el, text) {
|
|
|
|
let videoEl = el.querySelector('video');
|
|
|
|
if (!videoEl) return false;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
|
|
|
return CopyAsGFM.nodeToGFM(videoEl);
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'video'(el, text) {
|
|
|
|
return `![${el.dataset.title}](${el.getAttribute('src')})`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
MathFilter: {
|
|
|
|
'pre.code.math[data-math-style=display]'(el, text) {
|
|
|
|
return '```math\n' + text.trim() + '\n```';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'code.code.math[data-math-style=inline]'(el, text) {
|
|
|
|
return '$`' + text + '`$';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'span.katex-display span.katex-mathml'(el, text) {
|
|
|
|
let mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
|
|
|
if (!mathAnnotation) return false;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
return '```math\n' + CopyAsGFM.nodeToGFM(mathAnnotation) + '\n```';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'span.katex-mathml'(el, text) {
|
|
|
|
let mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
|
|
|
|
if (!mathAnnotation) return false;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
return '$`' + CopyAsGFM.nodeToGFM(mathAnnotation) + '`$';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'span.katex-html'(el, text) {
|
|
|
|
// We don't want to include the content of this element in the copied text.
|
|
|
|
return '';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'annotation[encoding="application/x-tex"]'(el, text) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return text.trim();
|
|
|
|
}
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
SyntaxHighlightFilter: {
|
|
|
|
'pre.code.highlight'(el, text) {
|
|
|
|
let lang = el.getAttribute('lang');
|
2017-01-17 13:06:12 -05:00
|
|
|
if (lang === 'plaintext') {
|
2017-01-16 16:44:46 -05:00
|
|
|
lang = '';
|
2017-01-16 15:27:05 -05:00
|
|
|
}
|
2017-01-16 16:44:46 -05:00
|
|
|
return '```' + lang + '\n' + text.trim() + '\n```';
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'pre > code'(el, text) {
|
2017-01-16 15:27:05 -05:00
|
|
|
// Don't wrap code blocks in ``
|
|
|
|
return text;
|
|
|
|
},
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
MarkdownFilter: {
|
|
|
|
'code'(el, text) {
|
|
|
|
let backtickCount = 1;
|
|
|
|
let backtickMatch = text.match(/`+/);
|
2017-01-16 15:27:05 -05:00
|
|
|
if (backtickMatch) {
|
|
|
|
backtickCount = backtickMatch[0].length + 1;
|
|
|
|
}
|
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let backticks = new Array(backtickCount + 1).join('`');
|
|
|
|
let spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
|
2017-01-16 15:27:05 -05:00
|
|
|
|
|
|
|
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'blockquote'(el, text) {
|
|
|
|
return text.trim().split('\n').map((s) => (`> ${s}`).trim()).join('\n');
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'img'(el, text) {
|
|
|
|
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'a.anchor'(el, text) {
|
|
|
|
// Don't render a Markdown link for the anchor link inside a heading
|
2017-01-16 15:27:05 -05:00
|
|
|
return text;
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'a'(el, text) {
|
|
|
|
return `[${text}](${el.getAttribute('href')})`;
|
|
|
|
},
|
|
|
|
'li'(el, text) {
|
|
|
|
let lines = text.trim().split('\n');
|
|
|
|
let firstLine = '- ' + lines.shift();
|
|
|
|
// Add two spaces to the front of subsequent list items lines, or leave the line entirely blank.
|
|
|
|
let nextLines = lines.map(function(s) {
|
|
|
|
if (s.trim().length === 0) {
|
|
|
|
return '';
|
|
|
|
} else {
|
|
|
|
return ` ${s}`;
|
|
|
|
}
|
|
|
|
});
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
return `${firstLine}\n${nextLines.join('\n')}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'ul'(el, text) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return text;
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'ol'(el, text) {
|
|
|
|
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
|
2017-01-16 15:27:05 -05:00
|
|
|
return text.replace(/^- /mg, '1. ');
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'h1'(el, text) {
|
|
|
|
return `# ${text.trim()}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'h2'(el, text) {
|
|
|
|
return `## ${text.trim()}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'h3'(el, text) {
|
|
|
|
return `### ${text.trim()}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'h4'(el, text) {
|
|
|
|
return `#### ${text.trim()}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'h5'(el, text) {
|
|
|
|
return `##### ${text.trim()}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'h6'(el, text) {
|
|
|
|
return `###### ${text.trim()}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'strong'(el, text) {
|
|
|
|
return `**${text}**`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'em'(el, text) {
|
|
|
|
return `_${text}_`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'del'(el, text) {
|
|
|
|
return `~~${text}~~`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'sup'(el, text) {
|
|
|
|
return `^${text}`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'hr'(el, text) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return '-----';
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'table'(el, text) {
|
|
|
|
let theadText = CopyAsGFM.nodeToGFM(el.querySelector('thead'));
|
|
|
|
let tbodyText = CopyAsGFM.nodeToGFM(el.querySelector('tbody'));
|
2017-01-16 15:27:05 -05:00
|
|
|
|
|
|
|
return theadText + tbodyText;
|
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'thead'(el, text) {
|
|
|
|
let cells = _.map(el.querySelectorAll('th'), function(cell) {
|
|
|
|
let chars = CopyAsGFM.nodeToGFM(cell).trim().length;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let before = '';
|
|
|
|
let after = '';
|
2017-01-16 15:27:05 -05:00
|
|
|
switch (cell.style.textAlign) {
|
|
|
|
case 'center':
|
|
|
|
before = ':';
|
|
|
|
after = ':';
|
|
|
|
chars -= 2;
|
|
|
|
break;
|
|
|
|
case 'right':
|
|
|
|
after = ':';
|
|
|
|
chars -= 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
chars = Math.max(chars, 0);
|
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let middle = new Array(chars + 1).join('-');
|
2017-01-16 15:27:05 -05:00
|
|
|
|
|
|
|
return before + middle + after;
|
|
|
|
});
|
2017-01-16 16:44:46 -05:00
|
|
|
|
|
|
|
return text + `| ${cells.join(' | ')} |`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
2017-01-16 16:44:46 -05:00
|
|
|
'tr'(el, text) {
|
|
|
|
let cells = _.map(el.querySelectorAll('td, th'), function(cell) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return CopyAsGFM.nodeToGFM(cell).trim();
|
|
|
|
});
|
2017-01-16 16:44:46 -05:00
|
|
|
return `| ${cells.join(' | ')} |`;
|
2017-01-16 15:27:05 -05:00
|
|
|
},
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
class CopyAsGFM {
|
|
|
|
constructor() {
|
|
|
|
$(document).on('copy', '.md, .wiki', this.handleCopy.bind(this));
|
|
|
|
$(document).on('paste', '.js-gfm-input', this.handlePaste.bind(this));
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCopy(e) {
|
2017-01-16 16:44:46 -05:00
|
|
|
let clipboardData = e.originalEvent.clipboardData;
|
2017-01-16 15:27:05 -05:00
|
|
|
if (!clipboardData) return;
|
|
|
|
|
2017-01-17 13:07:20 -05:00
|
|
|
let documentFragment = CopyAsGFM.getSelectedFragment();
|
|
|
|
if (!documentFragment) return;
|
2017-01-16 18:12:42 -05:00
|
|
|
|
2017-01-16 15:27:05 -05:00
|
|
|
e.preventDefault();
|
2017-01-17 13:07:20 -05:00
|
|
|
clipboardData.setData('text/plain', documentFragment.textContent);
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-17 13:07:20 -05:00
|
|
|
let gfm = CopyAsGFM.nodeToGFM(documentFragment);
|
2017-01-16 15:27:05 -05:00
|
|
|
clipboardData.setData('text/x-gfm', gfm);
|
|
|
|
}
|
|
|
|
|
|
|
|
handlePaste(e) {
|
2017-01-16 16:44:46 -05:00
|
|
|
let clipboardData = e.originalEvent.clipboardData;
|
2017-01-16 15:27:05 -05:00
|
|
|
if (!clipboardData) return;
|
2017-01-16 16:44:46 -05:00
|
|
|
|
|
|
|
let gfm = clipboardData.getData('text/x-gfm');
|
2017-01-16 15:27:05 -05:00
|
|
|
if (!gfm) return;
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
2017-01-17 13:07:20 -05:00
|
|
|
CopyAsGFM.insertText(e.target, gfm);
|
2017-01-16 15:27:05 -05:00
|
|
|
}
|
|
|
|
|
2017-01-17 13:07:20 -05:00
|
|
|
static getSelectedFragment() {
|
|
|
|
if (!window.getSelection) return null;
|
|
|
|
|
|
|
|
let selection = window.getSelection();
|
|
|
|
if (!selection.rangeCount || selection.rangeCount === 0) return null;
|
|
|
|
|
|
|
|
let documentFragment = selection.getRangeAt(0).cloneContents();
|
|
|
|
if (!documentFragment) return null;
|
|
|
|
|
|
|
|
if (documentFragment.textContent.length === 0) return null;
|
|
|
|
|
|
|
|
return documentFragment;
|
|
|
|
}
|
|
|
|
|
|
|
|
static insertText(target, text) {
|
|
|
|
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
|
2017-01-16 16:44:46 -05:00
|
|
|
|
|
|
|
let selectionStart = target.selectionStart;
|
|
|
|
let selectionEnd = target.selectionEnd;
|
|
|
|
let value = target.value;
|
|
|
|
|
|
|
|
let textBefore = value.substring(0, selectionStart);
|
|
|
|
let textAfter = value.substring(selectionEnd, value.length);
|
|
|
|
let newText = textBefore + text + textAfter;
|
|
|
|
|
2017-01-16 15:27:05 -05:00
|
|
|
target.value = newText;
|
|
|
|
target.selectionStart = target.selectionEnd = selectionStart + text.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
static nodeToGFM(node) {
|
2017-01-16 16:44:46 -05:00
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return node.textContent;
|
|
|
|
}
|
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let text = this.innerGFM(node);
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
2017-01-16 15:27:05 -05:00
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
for (let filter in gfmRules) {
|
|
|
|
let rules = gfmRules[filter];
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
for (let selector in rules) {
|
|
|
|
let func = rules[selector];
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-17 13:07:20 -05:00
|
|
|
if (!CopyAsGFM.nodeMatchesSelector(node, selector)) continue;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let result = func(node, text);
|
2017-01-16 15:27:05 -05:00
|
|
|
if (result === false) continue;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
static innerGFM(parentNode) {
|
2017-01-16 16:44:46 -05:00
|
|
|
let nodes = parentNode.childNodes;
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let clonedParentNode = parentNode.cloneNode(true);
|
|
|
|
let clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
|
|
let node = nodes[i];
|
|
|
|
let clonedNode = clonedNodes[i];
|
2017-01-16 15:27:05 -05:00
|
|
|
|
2017-01-16 16:44:46 -05:00
|
|
|
let text = this.nodeToGFM(node);
|
2017-01-17 13:07:20 -05:00
|
|
|
|
|
|
|
// `clonedNode.replaceWith(text)` is not yet widely supported
|
2017-01-16 15:27:05 -05:00
|
|
|
clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
|
|
|
|
}
|
|
|
|
|
|
|
|
return clonedParentNode.innerText || clonedParentNode.textContent;
|
|
|
|
}
|
2017-01-17 13:07:20 -05:00
|
|
|
|
|
|
|
static nodeMatchesSelector(node, selector) {
|
|
|
|
let matches = Element.prototype.matches ||
|
|
|
|
Element.prototype.matchesSelector ||
|
|
|
|
Element.prototype.mozMatchesSelector ||
|
|
|
|
Element.prototype.msMatchesSelector ||
|
|
|
|
Element.prototype.oMatchesSelector ||
|
|
|
|
Element.prototype.webkitMatchesSelector;
|
|
|
|
|
|
|
|
if (matches) {
|
|
|
|
return matches.call(node, selector);
|
|
|
|
}
|
|
|
|
|
|
|
|
// IE11 doesn't support `node.matches(selector)`
|
|
|
|
|
|
|
|
let parentNode = node.parentNode;
|
|
|
|
if (!parentNode) {
|
|
|
|
parentNode = document.createElement('div');
|
|
|
|
node = node.cloneNode(true);
|
|
|
|
parentNode.appendChild(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
let matchingNodes = parentNode.querySelectorAll(selector);
|
|
|
|
return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
|
|
|
|
}
|
2017-01-16 15:27:05 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
window.gl = window.gl || {};
|
|
|
|
window.gl.CopyAsGFM = CopyAsGFM;
|
|
|
|
|
|
|
|
new CopyAsGFM();
|
|
|
|
})();
|