gitlab-org--gitlab-foss/app/assets/javascripts/copy_as_gfm.js.es6

315 lines
8.7 KiB
JavaScript
Raw Normal View History

/* eslint-disable class-methods-use-this */
2017-01-16 21:44:46 +00:00
/*jshint esversion: 6 */
2017-01-18 22:19:51 +00:00
/*= require lib/utils/common_utils */
(() => {
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
// GitLab Flavored Markdown (GFM) to HTML.
2017-01-16 23:11:15 +00:00
// 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.
2017-01-16 23:11:15 +00:00
// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
2017-01-16 21:44:46 +00:00
InlineDiffFilter: {
'span.idiff.addition'(el, text) {
return `{+${text}+}`;
},
2017-01-16 21:44:46 +00:00
'span.idiff.deletion'(el, text) {
return `{-${text}-}`;
},
},
2017-01-16 21:44:46 +00:00
TaskListFilter: {
'input[type=checkbox].task-list-item-checkbox'(el, text) {
return `[${el.checked ? 'x' : ' '}]`;
}
},
2017-01-16 21:44:46 +00:00
ReferenceFilter: {
'a.gfm:not([data-link=true])'(el, text) {
return el.dataset.original || text;
},
},
2017-01-16 21:44:46 +00:00
AutolinkFilter: {
'a'(el, text) {
// Fallback on the regular MarkdownFilter's `a` handler.
if (text !== el.getAttribute('href')) return false;
return text;
},
},
2017-01-16 21:44:46 +00:00
TableOfContentsFilter: {
'ul.section-nav'(el, text) {
return '[[_TOC_]]';
},
},
2017-01-16 21:44:46 +00:00
EmojiFilter: {
'img.emoji'(el, text) {
return el.getAttribute('alt');
},
},
2017-01-16 21:44:46 +00:00
ImageLinkFilter: {
'a.no-attachment-icon'(el, text) {
return text;
},
},
2017-01-16 21:44:46 +00:00
VideoLinkFilter: {
'.video-container'(el, text) {
let videoEl = el.querySelector('video');
if (!videoEl) return false;
return CopyAsGFM.nodeToGFM(videoEl);
},
2017-01-16 21:44:46 +00:00
'video'(el, text) {
return `![${el.dataset.title}](${el.getAttribute('src')})`;
},
},
2017-01-16 21:44:46 +00:00
MathFilter: {
'pre.code.math[data-math-style=display]'(el, text) {
return '```math\n' + text.trim() + '\n```';
},
2017-01-16 21:44:46 +00:00
'code.code.math[data-math-style=inline]'(el, text) {
return '$`' + text + '`$';
},
2017-01-16 21:44:46 +00: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 21:44:46 +00:00
return '```math\n' + CopyAsGFM.nodeToGFM(mathAnnotation) + '\n```';
},
2017-01-16 21:44:46 +00:00
'span.katex-mathml'(el, text) {
let mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
if (!mathAnnotation) return false;
2017-01-16 21:44:46 +00:00
return '$`' + CopyAsGFM.nodeToGFM(mathAnnotation) + '`$';
},
2017-01-16 21:44:46 +00: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 21:44:46 +00:00
'annotation[encoding="application/x-tex"]'(el, text) {
return text.trim();
}
},
2017-01-16 21:44:46 +00:00
SyntaxHighlightFilter: {
'pre.code.highlight'(el, text) {
let lang = el.getAttribute('lang');
2017-01-17 18:06:12 +00:00
if (lang === 'plaintext') {
2017-01-16 21:44:46 +00:00
lang = '';
}
2017-01-16 21:44:46 +00:00
return '```' + lang + '\n' + text.trim() + '\n```';
},
2017-01-16 21:44:46 +00:00
'pre > code'(el, text) {
// Don't wrap code blocks in ``
return text;
},
},
2017-01-16 21:44:46 +00:00
MarkdownFilter: {
'code'(el, text) {
let backtickCount = 1;
let backtickMatch = text.match(/`+/);
if (backtickMatch) {
backtickCount = backtickMatch[0].length + 1;
}
2017-01-16 21:44:46 +00:00
let backticks = new Array(backtickCount + 1).join('`');
let spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
},
2017-01-16 21:44:46 +00:00
'blockquote'(el, text) {
return text.trim().split('\n').map((s) => `> ${s}`.trim()).join('\n');
},
2017-01-16 21:44:46 +00:00
'img'(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
2017-01-16 21:44:46 +00:00
'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading
return text;
},
2017-01-16 21:44:46 +00: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 21:44:46 +00:00
return `${firstLine}\n${nextLines.join('\n')}`;
},
2017-01-16 21:44:46 +00:00
'ul'(el, text) {
return text;
},
2017-01-16 21:44:46 +00:00
'ol'(el, text) {
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
return text.replace(/^- /mg, '1. ');
},
2017-01-16 21:44:46 +00:00
'h1'(el, text) {
return `# ${text.trim()}`;
},
2017-01-16 21:44:46 +00:00
'h2'(el, text) {
return `## ${text.trim()}`;
},
2017-01-16 21:44:46 +00:00
'h3'(el, text) {
return `### ${text.trim()}`;
},
2017-01-16 21:44:46 +00:00
'h4'(el, text) {
return `#### ${text.trim()}`;
},
2017-01-16 21:44:46 +00:00
'h5'(el, text) {
return `##### ${text.trim()}`;
},
2017-01-16 21:44:46 +00:00
'h6'(el, text) {
return `###### ${text.trim()}`;
},
2017-01-16 21:44:46 +00:00
'strong'(el, text) {
return `**${text}**`;
},
2017-01-16 21:44:46 +00:00
'em'(el, text) {
return `_${text}_`;
},
2017-01-16 21:44:46 +00:00
'del'(el, text) {
return `~~${text}~~`;
},
2017-01-16 21:44:46 +00:00
'sup'(el, text) {
return `^${text}`;
},
2017-01-16 21:44:46 +00:00
'hr'(el, text) {
return '-----';
},
2017-01-16 21:44:46 +00:00
'table'(el, text) {
let theadText = CopyAsGFM.nodeToGFM(el.querySelector('thead'));
let tbodyText = CopyAsGFM.nodeToGFM(el.querySelector('tbody'));
return theadText + tbodyText;
},
2017-01-16 21:44:46 +00:00
'thead'(el, text) {
let cells = _.map(el.querySelectorAll('th'), function(cell) {
let chars = CopyAsGFM.nodeToGFM(cell).trim().length;
2017-01-16 21:44:46 +00:00
let before = '';
let after = '';
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 21:44:46 +00:00
let middle = new Array(chars + 1).join('-');
return before + middle + after;
});
2017-01-16 21:44:46 +00:00
return text + `| ${cells.join(' | ')} |`;
},
2017-01-16 21:44:46 +00:00
'tr'(el, text) {
let cells = _.map(el.querySelectorAll('td, th'), function(cell) {
return CopyAsGFM.nodeToGFM(cell).trim();
});
2017-01-16 21:44:46 +00:00
return `| ${cells.join(' | ')} |`;
},
}
};
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 21:44:46 +00:00
let clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
2017-01-18 22:19:51 +00:00
let documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return;
2017-01-16 23:12:42 +00:00
e.preventDefault();
clipboardData.setData('text/plain', documentFragment.textContent);
let gfm = CopyAsGFM.nodeToGFM(documentFragment);
clipboardData.setData('text/x-gfm', gfm);
}
handlePaste(e) {
2017-01-16 21:44:46 +00:00
let clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
2017-01-16 21:44:46 +00:00
let gfm = clipboardData.getData('text/x-gfm');
if (!gfm) return;
e.preventDefault();
2017-01-18 22:19:51 +00:00
window.gl.utils.insertText(e.target, gfm);
}
static nodeToGFM(node) {
2017-01-16 21:44:46 +00:00
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
2017-01-16 21:44:46 +00:00
let text = this.innerGFM(node);
2017-01-16 21:44:46 +00:00
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
return text;
}
2017-01-16 21:44:46 +00:00
for (let filter in gfmRules) {
let rules = gfmRules[filter];
2017-01-16 21:44:46 +00:00
for (let selector in rules) {
let func = rules[selector];
2017-01-18 22:19:51 +00:00
if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
2017-01-16 21:44:46 +00:00
let result = func(node, text);
if (result === false) continue;
return result;
}
}
return text;
}
static innerGFM(parentNode) {
2017-01-16 21:44:46 +00:00
let nodes = parentNode.childNodes;
2017-01-16 21:44:46 +00:00
let clonedParentNode = parentNode.cloneNode(true);
let clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
2017-01-16 21:44:46 +00:00
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
let clonedNode = clonedNodes[i];
2017-01-16 21:44:46 +00:00
let text = this.nodeToGFM(node);
2017-01-18 22:19:51 +00:00
// `clonedNode.replaceWith(text)` is not yet widely supported
clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode);
}
return clonedParentNode.innerText || clonedParentNode.textContent;
}
}
window.gl = window.gl || {};
window.gl.CopyAsGFM = CopyAsGFM;
new CopyAsGFM();
})();