diff options
author | Douwe Maan <douwe@selenight.nl> | 2017-01-16 15:27:05 -0500 |
---|---|---|
committer | Douwe Maan <douwe@selenight.nl> | 2017-01-16 16:14:18 -0500 |
commit | dbfa58e2da7f939734eee5a599b4014d6095dde3 (patch) | |
tree | 44b7265dc7a20fcd1d4cc8f5782514837e959f17 /app/assets | |
parent | 142be72a2aa6920fa60cc267737f2e702fdeae12 (diff) | |
download | gitlab-ce-dbfa58e2da7f939734eee5a599b4014d6095dde3.tar.gz |
Copying a rendered issue/comment will paste into GFM textareas as actual GFM
Diffstat (limited to 'app/assets')
-rw-r--r-- | app/assets/javascripts/copy_as_gfm.js.es6 | 319 | ||||
-rw-r--r-- | app/assets/javascripts/shortcuts_issuable.js | 48 |
2 files changed, 345 insertions, 22 deletions
diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 new file mode 100644 index 00000000000..4827f7b7420 --- /dev/null +++ b/app/assets/javascripts/copy_as_gfm.js.es6 @@ -0,0 +1,319 @@ +/* eslint-disable class-methods-use-this */ + +(() => { + const gfmRules = { + // Should have an entry for every filter in lib/banzai/pipeline/gfm_pipeline.rb, + // in reverse order. + // Should have test coverage in spec/features/copy_as_gfm_spec.rb. + "InlineDiffFilter": { + "span.idiff.addition": function(el, text) { + return "{+" + text + "+}"; + }, + "span.idiff.deletion": function(el, text) { + return "{-" + text + "-}"; + }, + }, + "TaskListFilter": { + "input[type=checkbox].task-list-item-checkbox": function(el, text) { + return '[' + (el.checked ? 'x' : ' ') + ']'; + } + }, + "ReferenceFilter": { + "a.gfm:not([data-link=true])": function(el, text) { + return el.getAttribute('data-original') || text; + }, + }, + "AutolinkFilter": { + "a": function(el, text) { + if (text != el.getAttribute("href")) { + // Fall back to handler for MarkdownFilter + return false; + } + + return text; + }, + }, + "TableOfContentsFilter": { + "ul.section-nav": function(el, text) { + return "[[_TOC_]]"; + }, + }, + "EmojiFilter": { + "img.emoji": function(el, text) { + return el.getAttribute("alt"); + }, + }, + "ImageLinkFilter": { + "a.no-attachment-icon": function(el, text) { + return text; + }, + }, + "VideoLinkFilter": { + ".video-container": function(el, text) { + var videoEl = el.querySelector('video'); + if (!videoEl) { + return false; + } + + return CopyAsGFM.nodeToGFM(videoEl); + }, + "video": function(el, text) { + return "![" + el.getAttribute('data-title') + "](" + el.getAttribute("src") + ")"; + }, + }, + "MathFilter": { + "pre.code.math[data-math-style='display']": function(el, text) { + return "```math\n" + text.trim() + "\n```"; + }, + "code.code.math[data-math-style='inline']": function(el, text) { + return "$`" + text + "`$"; + }, + "span.katex-display span.katex-mathml": function(el, text) { + var mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); + if (!mathAnnotation) { + return false; + } + + return "```math\n" + CopyAsGFM.nodeToGFM(mathAnnotation) + "\n```"; + }, + "span.katex-mathml": function(el, text) { + var mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); + if (!mathAnnotation) { + return false; + } + + return "$`" + CopyAsGFM.nodeToGFM(mathAnnotation) + "`$"; + }, + "span.katex-html": function(el, text) { + return ""; + }, + 'annotation[encoding="application/x-tex"]': function(el, text) { + return text.trim(); + } + }, + "SyntaxHighlightFilter": { + "pre.code.highlight": function(el, text) { + var lang = el.getAttribute("lang"); + if (lang == "text") { + lang = ""; + } + return "```" + lang + "\n" + text.trim() + "\n```"; + }, + "pre > code": function(el, text) { + // Don't wrap code blocks in `` + return text; + }, + }, + "MarkdownFilter": { + "code": function(el, text) { + var backtickCount = 1; + var backtickMatch = text.match(/`+/); + if (backtickMatch) { + backtickCount = backtickMatch[0].length + 1; + } + + var backticks = new Array(backtickCount + 1).join('`'); + var spaceOrNoSpace = backtickCount > 1 ? " " : ""; + + return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; + }, + "blockquote": function(el, text) { + return text.trim().split('\n').map(function(s) { return ('> ' + s).trim(); }).join('\n'); + }, + "img": function(el, text) { + return "![" + el.getAttribute("alt") + "](" + el.getAttribute("src") + ")"; + }, + "a.anchor": function(el, text) { + return text; + }, + "a": function(el, text) { + return "[" + text + "](" + el.getAttribute("href") + ")"; + }, + "li": function(el, text) { + var lines = text.trim().split('\n'); + var firstLine = '- ' + lines.shift(); + var nextLines = lines.map(function(s) { return (' ' + s).replace(/\s+$/, ''); }); + + return firstLine + '\n' + nextLines.join('\n'); + }, + "ul": function(el, text) { + return text; + }, + "ol": function(el, text) { + return text.replace(/^- /mg, '1. '); + }, + "h1": function(el, text) { + return '# ' + text.trim(); + }, + "h2": function(el, text) { + return '## ' + text.trim(); + }, + "h3": function(el, text) { + return '### ' + text.trim(); + }, + "h4": function(el, text) { + return '#### ' + text.trim(); + }, + "h5": function(el, text) { + return '##### ' + text.trim(); + }, + "h6": function(el, text) { + return '###### ' + text.trim(); + }, + "strong": function(el, text) { + return '**' + text + '**'; + }, + "em": function(el, text) { + return '_' + text + '_'; + }, + "del": function(el, text) { + return '~~' + text + '~~'; + }, + "sup": function(el, text) { + return '^' + text; + }, + "hr": function(el, text) { + return '-----'; + }, + "table": function(el, text) { + var theadText = CopyAsGFM.nodeToGFM(el.querySelector('thead')); + var tbodyText = CopyAsGFM.nodeToGFM(el.querySelector('tbody')); + + return theadText + tbodyText; + }, + "thead": function(el, text) { + var cells = _.map(el.querySelectorAll('th'), function(cell) { + var chars = CopyAsGFM.nodeToGFM(cell).trim().length; + + var before = ''; + var after = ''; + switch (cell.style.textAlign) { + case 'center': + before = ':'; + after = ':'; + chars -= 2; + break; + case 'right': + after = ':'; + chars -= 1; + break; + } + + chars = Math.max(chars, 0); + + var middle = new Array(chars + 1).join('-'); + + return before + middle + after; + }); + return text + '| ' + cells.join(' | ') + ' |'; + }, + "tr": function(el, text) { + var cells = _.map(el.querySelectorAll('td, th'), function(cell) { + return CopyAsGFM.nodeToGFM(cell).trim(); + }); + 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) { + var clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + if (!window.getSelection) return; + + var selection = window.getSelection(); + if (!selection.rangeCount || selection.rangeCount === 0) return; + + var selectedDocument = selection.getRangeAt(0).cloneContents(); + if (!selectedDocument) return; + + e.preventDefault(); + clipboardData.setData('text/plain', selectedDocument.textContent); + + var gfm = CopyAsGFM.nodeToGFM(selectedDocument); + clipboardData.setData('text/x-gfm', gfm); + } + + handlePaste(e) { + var clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + var gfm = clipboardData.getData('text/x-gfm'); + if (!gfm) return; + + e.preventDefault(); + + this.insertText(e.target, gfm); + } + + insertText(target, text) { + // Firefox doesn't support `document.execCommand('insertText', false, text);` on textareas + var selectionStart = target.selectionStart; + var selectionEnd = target.selectionEnd; + var value = target.value; + var textBefore = value.substring(0, selectionStart); + var textAfter = value.substring(selectionEnd, value.length); + var newText = textBefore + text + textAfter; + target.value = newText; + target.selectionStart = target.selectionEnd = selectionStart + text.length; + } + + static nodeToGFM(node) { + if (node.nodeType == Node.TEXT_NODE) { + return node.textContent; + } + + var text = this.innerGFM(node); + + if (node.nodeType == Node.DOCUMENT_FRAGMENT_NODE) { + return text; + } + + for (var filter in gfmRules) { + var rules = gfmRules[filter]; + + for (var selector in rules) { + var func = rules[selector]; + + if (!node.matches(selector)) continue; + + var result = func(node, text); + if (result === false) continue; + + return result; + } + } + + return text; + } + + static innerGFM(parentNode) { + var nodes = parentNode.childNodes; + + var clonedParentNode = parentNode.cloneNode(true); + var clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); + + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + var clonedNode = clonedNodes[i]; + + var text = this.nodeToGFM(node); + clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); + } + + return clonedParentNode.innerText || clonedParentNode.textContent; + } + } + + window.gl = window.gl || {}; + window.gl.CopyAsGFM = CopyAsGFM; + + new CopyAsGFM(); +})(); diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index b892fbc3393..426821873d7 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -39,29 +39,33 @@ } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, replyField, selected, separator; - if (window.getSelection) { - selected = window.getSelection().toString(); - replyField = $('.js-main-target-form #note_note'); - if (selected.trim() === "") { - return; - } - // Put a '>' character before each non-empty line in the selection - quote = _.map(selected.split("\n"), function(val) { - if (val.trim() !== '') { - return "> " + val + "\n"; - } - }); - // If replyField already has some content, add a newline before our quote - separator = replyField.val().trim() !== "" && "\n" || ''; - replyField.val(function(_, current) { - return current + separator + quote.join('') + "\n"; - }); - // Trigger autosave for the added text - replyField.trigger('input'); - // Focus the input field - return replyField.focus(); + var quote, replyField, selectedDocument, selected, selection, separator; + if (!window.getSelection) return; + + selection = window.getSelection(); + if (!selection.rangeCount || selection.rangeCount === 0) return; + + selectedDocument = selection.getRangeAt(0).cloneContents(); + if (!selectedDocument) return; + + selected = window.gl.CopyAsGFM.nodeToGFM(selectedDocument); + + replyField = $('.js-main-target-form #note_note'); + if (selected.trim() === "") { + return; } + quote = _.map(selected.split("\n"), function(val) { + return "> " + val + "\n"; + }); + // If replyField already has some content, add a newline before our quote + separator = replyField.val().trim() !== "" && "\n" || ''; + replyField.val(function(_, current) { + return current + separator + quote.join('') + "\n"; + }); + // Trigger autosave for the added text + replyField.trigger('input'); + // Focus the input field + return replyField.focus(); }; ShortcutsIssuable.prototype.editIssue = function() { |