Commit ef447a62 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'dm-copy-code-as-gfm' into 'master'

Copy code as GFM from diffs, blobs and GFM code blocks

See merge request !9874
parents b29aee2f 9a0a4f17
...@@ -118,10 +118,10 @@ const gfmRules = { ...@@ -118,10 +118,10 @@ const gfmRules = {
}, },
SyntaxHighlightFilter: { SyntaxHighlightFilter: {
'pre.code.highlight'(el, t) { 'pre.code.highlight'(el, t) {
const text = t.trim(); const text = t.trimRight();
let lang = el.getAttribute('lang'); let lang = el.getAttribute('lang');
if (lang === 'plaintext') { if (!lang || lang === 'plaintext') {
lang = ''; lang = '';
} }
...@@ -157,7 +157,7 @@ const gfmRules = { ...@@ -157,7 +157,7 @@ const gfmRules = {
const backticks = Array(backtickCount + 1).join('`'); const backticks = Array(backtickCount + 1).join('`');
const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
}, },
'blockquote'(el, text) { 'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
...@@ -273,28 +273,29 @@ const gfmRules = { ...@@ -273,28 +273,29 @@ const gfmRules = {
class CopyAsGFM { class CopyAsGFM {
constructor() { constructor() {
$(document).on('copy', '.md, .wiki', this.handleCopy); $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('paste', '.js-gfm-input', this.handlePaste); $(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));
} }
handleCopy(e) { copyAsGFM(e, transformer) {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
const documentFragment = window.gl.utils.getSelectedFragment(); const documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) return;
// If the documentFragment contains more than just Markdown, don't copy as GFM. const el = transformer(documentFragment.cloneNode(true));
if (documentFragment.querySelector('.md, .wiki')) return; if (!el) return;
e.preventDefault(); e.preventDefault();
clipboardData.setData('text/plain', documentFragment.textContent); e.stopPropagation();
const gfm = CopyAsGFM.nodeToGFM(documentFragment); clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', gfm); clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el));
} }
handlePaste(e) { pasteGFM(e) {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
...@@ -306,7 +307,47 @@ class CopyAsGFM { ...@@ -306,7 +307,47 @@ class CopyAsGFM {
window.gl.utils.insertText(e.target, gfm); window.gl.utils.insertText(e.target, gfm);
} }
static transformGFMSelection(documentFragment) {
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return null;
return documentFragment;
}
static transformCodeSelection(documentFragment) {
const lineEls = documentFragment.querySelectorAll('.line');
let codeEl;
if (lineEls.length > 1) {
codeEl = document.createElement('pre');
codeEl.className = 'code highlight';
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) { static nodeToGFM(node) {
if (node.nodeType === Node.COMMENT_NODE) {
return '';
}
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
return node.textContent; return node.textContent;
} }
......
...@@ -172,7 +172,9 @@ module GitlabMarkdownHelper ...@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents # text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false. # and return true. Otherwise return false.
def truncate_if_block(node, truncated) def truncate_if_block(node, truncated)
if node.element? && node.description&.block? && !truncated return true if truncated
if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
node.inner_html = "#{node.inner_html}..." if node.next_sibling node.inner_html = "#{node.inner_html}..." if node.next_sibling
true true
else else
......
---
title: Copy code as GFM from diffs, blobs and GFM code blocks
merge_request:
author:
...@@ -5,8 +5,6 @@ module Banzai ...@@ -5,8 +5,6 @@ module Banzai
# HTML Filter to highlight fenced code blocks # HTML Filter to highlight fenced code blocks
# #
class SyntaxHighlightFilter < HTML::Pipeline::Filter class SyntaxHighlightFilter < HTML::Pipeline::Filter
include Rouge::Plugins::Redcarpet
def call def call
doc.search('pre > code').each do |node| doc.search('pre > code').each do |node|
highlight_node(node) highlight_node(node)
...@@ -23,7 +21,7 @@ module Banzai ...@@ -23,7 +21,7 @@ module Banzai
lang = lexer.tag lang = lexer.tag
begin begin
code = format(lex(lexer, code)) code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
css_classes << " js-syntax-highlight #{lang}" css_classes << " js-syntax-highlight #{lang}"
rescue rescue
...@@ -45,10 +43,6 @@ module Banzai ...@@ -45,10 +43,6 @@ module Banzai
lexer.lex(code) lexer.lex(code)
end end
def format(tokens)
rouge_formatter.format(tokens)
end
def lexer_for(language) def lexer_for(language)
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end end
...@@ -57,11 +51,6 @@ module Banzai ...@@ -57,11 +51,6 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block # Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted) node.parent.replace(highlighted)
end end
# Override Rouge::Plugins::Redcarpet#rouge_formatter
def rouge_formatter(lexer = nil)
@rouge_formatter ||= Rouge::Formatters::HTML.new
end
end end
end end
end end
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
end end
def initialize(blob_name, blob_content, repository: nil) def initialize(blob_name, blob_content, repository: nil)
@formatter = Rouge::Formatters::HTMLGitlab.new @formatter = Rouge::Formatters::HTMLGitlab
@repository = repository @repository = repository
@blob_name = blob_name @blob_name = blob_name
@blob_content = blob_content @blob_content = blob_content
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
hl_lexer = self.lexer hl_lexer = self.lexer
end end
@formatter.format(hl_lexer.lex(text, continue: continue)).html_safe @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
rescue rescue
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
end end
......
...@@ -5,10 +5,10 @@ module Rouge ...@@ -5,10 +5,10 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance. # Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
# #
# [+linenostart+] The line number for the first line (default: 1). # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
def initialize(linenostart: 1) def initialize(tag: nil)
@linenostart = linenostart @line_number = 1
@line_number = linenostart @tag = tag
end end
def stream(tokens, &b) def stream(tokens, &b)
...@@ -17,7 +17,7 @@ module Rouge ...@@ -17,7 +17,7 @@ module Rouge
yield "\n" unless is_first yield "\n" unless is_first
is_first = false is_first = false
yield %(<span id="LC#{@line_number}" class="line">) yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
line.each { |token, value| yield span(token, value.chomp) } line.each { |token, value| yield span(token, value.chomp) }
yield %(</span>) yield %(</span>)
......
This diff is collapsed.
...@@ -19,12 +19,12 @@ describe BlobHelper do ...@@ -19,12 +19,12 @@ describe BlobHelper do
describe '#highlight' do describe '#highlight' do
it 'returns plaintext for unknown lexer context' do it 'returns plaintext for unknown lexer context' do
result = helper.highlight(blob_name, no_context_content) result = helper.highlight(blob_name, no_context_content)
expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>]) expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
end end
it 'highlights single block' do it 'highlights single block' do
expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] <span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
expect(helper.highlight(blob_name, blob_content)).to eq(expected) expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end end
...@@ -43,10 +43,10 @@ describe BlobHelper do ...@@ -43,10 +43,10 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' } let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do let(:expected) do
%q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span> %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
<span id="LC2" class="line"><span class="gi">+bbb</span></span> <span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
<span id="LC3" class="line"><span class="gd">- ccc</span></span> <span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
<span id="LC4" class="line"> ddd</span></code></pre>) <span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
end end
it 'highlights each line properly' do it 'highlights each line properly' do
......
...@@ -28,7 +28,7 @@ describe EventsHelper do ...@@ -28,7 +28,7 @@ describe EventsHelper do
it 'displays the first line of a code block' do it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```" input = "```\nCode block\nwith two lines\n```"
expected = %r{<pre.+><code>Code block\.\.\.</code></pre>} expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
expect(helper.event_note(input)).to match(expected) expect(helper.event_note(input)).to match(expected)
end end
...@@ -55,10 +55,8 @@ describe EventsHelper do ...@@ -55,10 +55,8 @@ describe EventsHelper do
it 'preserves code color scheme' do it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```" input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \ expected = '<pre class="code highlight js-syntax-highlight ruby">' \
"<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
" <span class=\"s1\">\'hello world\'</span>\n" \ "</code></pre>"
"<span class=\"k\">end</span>\n" \
'</code></pre>'
expect(helper.event_note(input)).to eq(expected) expect(helper.event_note(input)).to eq(expected)
end end
......
...@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do ...@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do context "when no language is specified" do
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>') result = filter('<pre><code>def fun end</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
end end
end end
context "when a valid language is specified" do context "when a valid language is specified" do
it "highlights as that language" do it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>') result = filter('<pre><code class="ruby">def fun end</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
end end
end end
context "when an invalid language is specified" do context "when an invalid language is specified" do
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>') result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end end
end end
......
...@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do ...@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do
end end
it 'highlights and marks unchanged lines' do it 'highlights and marks unchanged lines' do
code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n} code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
expect(subject[2].text).to eq(code) expect(subject[2].text).to eq(code)
end end
it 'highlights and marks removed lines' do it 'highlights and marks removed lines' do
code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].text).to eq(code) expect(subject[4].text).to eq(code)
end end
it 'highlights and marks added lines' do it 'highlights and marks added lines' do
code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code) expect(subject[5].text).to eq(code)
end end
......
...@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do ...@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do
end end
it 'highlights all the lines properly' do it 'highlights all the lines properly' do
expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
end end
describe 'with CRLF' do describe 'with CRLF' do
...@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do ...@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do
end end
it 'strips extra LFs' do it 'strips extra LFs' do
expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>") expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>")
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment