gitlab-org--gitlab-foss/app/assets/javascripts/copy_as_gfm.js.es6
Mike Greiling 69e4072f89 Merge branch 'master' into go-go-gadget-webpack
* master: (389 commits)
  Document "No gems fetched from git repositories" policy [ci skip]
  Typos
  Small gramatical tweaks
  Typos
  Added PHP & NPM doc
  Use `:empty_project` where possible in request specs
  Add caching of droplab ajax requests
  Use `:empty_project` where possible in model specs
  Revert 3f17f29a
  Remove unused js response from refs controller
  Add MR id to changelog entry
  fixed small mini pipeline graph line glitch
  Prevent form to be submitted twice
  Fix Error 500 when repositories contain annotated tags pointing to blobs
  Fix /explore sorting (trending)
  Simplify wording in "adding an image" docs
  Remove "official merge window" from CONTRIBUTING.md [ci skip]
  Update repository check documentation
  Fixed flexbox and wrap issues
  Update two_factor_authentication.md
  ...
2017-01-27 19:33:58 -06:00

355 lines
10 KiB
JavaScript

/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
/* jshint esversion: 6 */
require('./lib/utils/common_utils');
(() => {
const gfmRules = {
// 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.
InlineDiffFilter: {
'span.idiff.addition'(el, text) {
return `{+${text}+}`;
},
'span.idiff.deletion'(el, text) {
return `{-${text}-}`;
},
},
TaskListFilter: {
'input[type=checkbox].task-list-item-checkbox'(el, text) {
return `[${el.checked ? 'x' : ' '}]`;
},
},
ReferenceFilter: {
'a.gfm:not([data-link=true])'(el, text) {
return el.dataset.original || text;
},
},
AutolinkFilter: {
'a'(el, text) {
// Fallback on the regular MarkdownFilter's `a` handler.
if (text !== el.getAttribute('href')) return false;
return text;
},
},
TableOfContentsFilter: {
'ul.section-nav'(el, text) {
return '[[_TOC_]]';
},
},
EmojiFilter: {
'img.emoji'(el, text) {
return el.getAttribute('alt');
},
},
ImageLinkFilter: {
'a.no-attachment-icon'(el, text) {
return text;
},
},
VideoLinkFilter: {
'.video-container'(el, text) {
const videoEl = el.querySelector('video');
if (!videoEl) return false;
return CopyAsGFM.nodeToGFM(videoEl);
},
'video'(el, text) {
return `![${el.dataset.title}](${el.getAttribute('src')})`;
},
},
MathFilter: {
'pre.code.math[data-math-style=display]'(el, text) {
return `\`\`\`math\n${text.trim()}\n\`\`\``;
},
'code.code.math[data-math-style=inline]'(el, text) {
return `$\`${text}\`$`;
},
'span.katex-display span.katex-mathml'(el, text) {
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
if (!mathAnnotation) return false;
return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``;
},
'span.katex-mathml'(el, text) {
const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]');
if (!mathAnnotation) return false;
return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`;
},
'span.katex-html'(el, text) {
// We don't want to include the content of this element in the copied text.
return '';
},
'annotation[encoding="application/x-tex"]'(el, text) {
return text.trim();
},
},
SanitizationFilter: {
'dl'(el, text) {
let lines = text.trim().split('\n');
// Add two spaces to the front of subsequent list items lines,
// or leave the line entirely blank.
lines = lines.map((l) => {
const line = l.trim();
if (line.length === 0) return '';
return ` ${line}`;
});
return `<dl>\n${lines.join('\n')}\n</dl>`;
},
'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) {
const tag = el.nodeName.toLowerCase();
return `<${tag}>${text}</${tag}>`;
},
},
SyntaxHighlightFilter: {
'pre.code.highlight'(el, t) {
const text = t.trim();
let lang = el.getAttribute('lang');
if (lang === 'plaintext') {
lang = '';
}
// Prefixes lines with 4 spaces if the code contains triple backticks
if (lang === '' && text.match(/^```/gm)) {
return text.split('\n').map((l) => {
const line = l.trim();
if (line.length === 0) return '';
return ` ${line}`;
}).join('\n');
}
return `\`\`\`${lang}\n${text}\n\`\`\``;
},
'pre > code'(el, text) {
// Don't wrap code blocks in ``
return text;
},
},
MarkdownFilter: {
'br'(el, text) {
// Two spaces at the end of a line are turned into a BR
return ' ';
},
'code'(el, text) {
let backtickCount = 1;
const backtickMatch = text.match(/`+/);
if (backtickMatch) {
backtickCount = backtickMatch[0].length + 1;
}
const backticks = Array(backtickCount + 1).join('`');
const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
},
'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
},
'img'(el, text) {
return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
},
'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading
return text;
},
'a'(el, text) {
return `[${text}](${el.getAttribute('href')})`;
},
'li'(el, text) {
const lines = text.trim().split('\n');
const firstLine = `- ${lines.shift()}`;
// Add four spaces to the front of subsequent list items lines,
// or leave the line entirely blank.
const nextLines = lines.map((s) => {
if (s.trim().length === 0) return '';
return ` ${s}`;
});
return `${firstLine}\n${nextLines.join('\n')}`;
},
'ul'(el, text) {
return text;
},
'ol'(el, text) {
// LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists.
return text.replace(/^- /mg, '1. ');
},
'h1'(el, text) {
return `# ${text.trim()}`;
},
'h2'(el, text) {
return `## ${text.trim()}`;
},
'h3'(el, text) {
return `### ${text.trim()}`;
},
'h4'(el, text) {
return `#### ${text.trim()}`;
},
'h5'(el, text) {
return `##### ${text.trim()}`;
},
'h6'(el, text) {
return `###### ${text.trim()}`;
},
'strong'(el, text) {
return `**${text}**`;
},
'em'(el, text) {
return `_${text}_`;
},
'del'(el, text) {
return `~~${text}~~`;
},
'sup'(el, text) {
return `^${text}`;
},
'hr'(el, text) {
return '-----';
},
'table'(el, text) {
const theadEl = el.querySelector('thead');
const tbodyEl = el.querySelector('tbody');
if (!theadEl || !tbodyEl) return false;
const theadText = CopyAsGFM.nodeToGFM(theadEl);
const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl);
return theadText + tbodyText;
},
'thead'(el, text) {
const cells = _.map(el.querySelectorAll('th'), (cell) => {
let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2;
let before = '';
let after = '';
switch (cell.style.textAlign) {
case 'center':
before = ':';
after = ':';
chars -= 2;
break;
case 'right':
after = ':';
chars -= 1;
break;
default:
break;
}
chars = Math.max(chars, 3);
const middle = Array(chars + 1).join('-');
return before + middle + after;
});
return `${text}|${cells.join('|')}|`;
},
'tr'(el, text) {
const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim());
return `| ${cells.join(' | ')} |`;
},
},
};
class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', this.handleCopy);
$(document).on('paste', '.js-gfm-input', this.handlePaste);
}
handleCopy(e) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
const documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return;
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return;
e.preventDefault();
clipboardData.setData('text/plain', documentFragment.textContent);
const gfm = CopyAsGFM.nodeToGFM(documentFragment);
clipboardData.setData('text/x-gfm', gfm);
}
handlePaste(e) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
const gfm = clipboardData.getData('text/x-gfm');
if (!gfm) return;
e.preventDefault();
window.gl.utils.insertText(e.target, gfm);
}
static nodeToGFM(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
const text = this.innerGFM(node);
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
return text;
}
for (const filter in gfmRules) {
const rules = gfmRules[filter];
for (const selector in rules) {
const func = rules[selector];
if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue;
const result = func(node, text);
if (result === false) continue;
return result;
}
}
return text;
}
static innerGFM(parentNode) {
const nodes = parentNode.childNodes;
const clonedParentNode = parentNode.cloneNode(true);
const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0);
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
const clonedNode = clonedNodes[i];
const text = this.nodeToGFM(node);
// `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();
})();