diff options
author | Dimitrie Hoekstra <dimitrie@gitlab.com> | 2017-03-17 17:04:51 +0000 |
---|---|---|
committer | Dimitrie Hoekstra <dimitrie@gitlab.com> | 2017-03-17 17:04:51 +0000 |
commit | b6bab6ce47813c67ea1e2c7d4fde7d9e320da99c (patch) | |
tree | 6c1b7db2aeebc5756c73842cffef22df655cc820 /app/assets/javascripts/copy_as_gfm.js | |
parent | 116efdaf128ddcccc30fb82615cd964b35cacc53 (diff) | |
parent | bb1620aaf712c22c61fda098260f481ad79a05e2 (diff) | |
download | gitlab-ce-b6bab6ce47813c67ea1e2c7d4fde7d9e320da99c.tar.gz |
Merge branch 'master' into 'focus-mode-board'focus-mode-board
# Conflicts:
# app/views/shared/issuable/_filter.html.haml
Diffstat (limited to 'app/assets/javascripts/copy_as_gfm.js')
-rw-r--r-- | app/assets/javascripts/copy_as_gfm.js | 672 |
1 files changed, 355 insertions, 317 deletions
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 8883c339335..570799c030e 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,364 +1,402 @@ /* 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: { - '.tooltip'(el, text) { - return ''; - }, - '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'); - }, - 'gl-emoji'(el, text) { - return `:${el.getAttribute('data-name')}:`; - }, - }, - 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: { - 'a[name]:not([href]):empty'(el, text) { - return el.outerHTML; - }, - '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 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: { + '.tooltip'(el, text) { + return ''; + }, + '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'); + }, + 'gl-emoji'(el, text) { + return `:${el.getAttribute('data-name')}:`; + }, + }, + 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: { + 'a[name]:not([href]):empty'(el, text) { + return el.outerHTML; + }, + '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, summary, details'(el, text) { + const tag = el.nodeName.toLowerCase(); + return `<${tag}>${text}</${tag}>`; + }, + }, + SyntaxHighlightFilter: { + 'pre.code.highlight'(el, t) { + const text = t.trimRight(); + + let lang = el.getAttribute('lang'); + if (!lang || 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}`; - }); + return ` ${line}`; + }).join('\n'); + } - return `<dl>\n${lines.join('\n')}\n</dl>`; - }, - 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}</${tag}>`; - }, + return `\`\`\`${lang}\n${text}\n\`\`\``; + }, + 'pre > code'(el, text) { + // Don't wrap code blocks in `` + return text; }, - SyntaxHighlightFilter: { - 'pre.code.highlight'(el, t) { - const text = t.trim(); + }, + 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; + } - let lang = el.getAttribute('lang'); - if (lang === 'plaintext') { - lang = ''; - } + const backticks = Array(backtickCount + 1).join('`'); + const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; - // 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 backticks + spaceOrNoSpace + text.trim() + 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; - return ` ${line}`; - }).join('\n'); - } + const theadText = CopyAsGFM.nodeToGFM(theadEl); + const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); - 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; + 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; } - 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); - } + chars = Math.max(chars, 3); - handleCopy(e) { - const clipboardData = e.originalEvent.clipboardData; - if (!clipboardData) return; + const middle = Array(chars + 1).join('-'); - const documentFragment = window.gl.utils.getSelectedFragment(); - if (!documentFragment) return; + return before + middle + after; + }); - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; + 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', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); + } - e.preventDefault(); - clipboardData.setData('text/plain', documentFragment.textContent); + copyAsGFM(e, transformer) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; - const gfm = CopyAsGFM.nodeToGFM(documentFragment); - clipboardData.setData('text/x-gfm', gfm); - } + const documentFragment = window.gl.utils.getSelectedFragment(); + if (!documentFragment) return; - handlePaste(e) { - const clipboardData = e.originalEvent.clipboardData; - if (!clipboardData) return; + const el = transformer(documentFragment.cloneNode(true)); + if (!el) return; - const gfm = clipboardData.getData('text/x-gfm'); - if (!gfm) return; + e.preventDefault(); + e.stopPropagation(); - e.preventDefault(); + clipboardData.setData('text/plain', el.textContent); + clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); + } - window.gl.utils.insertText(e.target, gfm); - } + pasteGFM(e) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; - static nodeToGFM(node) { - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent; - } + const gfm = clipboardData.getData('text/x-gfm'); + if (!gfm) return; - const text = this.innerGFM(node); + e.preventDefault(); - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - return text; - } + window.gl.utils.insertText(e.target, gfm); + } - for (const filter in gfmRules) { - const rules = gfmRules[filter]; + static transformGFMSelection(documentFragment) { + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return null; - for (const selector in rules) { - const func = rules[selector]; + return documentFragment; + } - if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + static transformCodeSelection(documentFragment) { + const lineEls = documentFragment.querySelectorAll('.line'); - const result = func(node, text); - if (result === false) continue; + let codeEl; + if (lineEls.length > 1) { + codeEl = document.createElement('pre'); + codeEl.className = 'code highlight'; - return result; - } + const lang = lineEls[0].getAttribute('lang'); + if (lang) { + codeEl.setAttribute('lang', lang); + } + } else { + codeEl = document.createElement('code'); + } + + if (lineEls.length > 0) { + for (let i = 0; i < lineEls.length; i += 1) { + const lineEl = lineEls[i]; + codeEl.appendChild(lineEl); + codeEl.appendChild(document.createTextNode('\n')); } + } else { + codeEl.appendChild(documentFragment); + } + + return codeEl; + } + + static nodeToGFM(node) { + if (node.nodeType === Node.COMMENT_NODE) { + return ''; + } + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent; + } + + const text = this.innerGFM(node); + + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return text; } - static innerGFM(parentNode) { - const nodes = parentNode.childNodes; + for (const filter in gfmRules) { + const rules = gfmRules[filter]; - const clonedParentNode = parentNode.cloneNode(true); - const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); + for (const selector in rules) { + const func = rules[selector]; - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - const clonedNode = clonedNodes[i]; + if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; - const text = this.nodeToGFM(node); + const result = func(node, text); + if (result === false) continue; - // `clonedNode.replaceWith(text)` is not yet widely supported - clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); + return result; } + } + + return text; + } - return clonedParentNode.innerText || clonedParentNode.textContent; + 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; +window.gl = window.gl || {}; +window.gl.CopyAsGFM = CopyAsGFM; - new CopyAsGFM(); -})(); +new CopyAsGFM(); |