BigW Consortium Gitlab

Commit 68903277 by Douwe Maan

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

parent b7166806
......@@ -118,10 +118,10 @@ const gfmRules = {
},
SyntaxHighlightFilter: {
'pre.code.highlight'(el, t) {
const text = t.trim();
const text = t.trimRight();
let lang = el.getAttribute('lang');
if (lang === 'plaintext') {
if (!lang || lang === 'plaintext') {
lang = '';
}
......@@ -157,7 +157,7 @@ const gfmRules = {
const backticks = Array(backtickCount + 1).join('`');
const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
},
'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
......@@ -273,28 +273,29 @@ const gfmRules = {
class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', this.handleCopy);
$(document).on('paste', '.js-gfm-input', this.handlePaste);
$(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));
}
handleCopy(e) {
copyAsGFM(e, transformer) {
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;
const el = transformer(documentFragment.cloneNode(true));
if (!el) return;
e.preventDefault();
clipboardData.setData('text/plain', documentFragment.textContent);
e.stopPropagation();
const gfm = CopyAsGFM.nodeToGFM(documentFragment);
clipboardData.setData('text/x-gfm', gfm);
clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el));
}
handlePaste(e) {
pasteGFM(e) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
......@@ -306,7 +307,54 @@ class CopyAsGFM {
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 selectionToGFM(documentFragment, transformer) {
const el = transformer(documentFragment.cloneNode(true));
if (!el) return null;
return CopyAsGFM.nodeToGFM(el);
}
static nodeToGFM(node) {
if (node.nodeType === Node.COMMENT_NODE) {
return '';
}
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
......
---
title: Copy code as GFM from diffs, blobs and GFM code blocks
merge_request:
author:
......@@ -5,8 +5,6 @@ module Banzai
# HTML Filter to highlight fenced code blocks
#
class SyntaxHighlightFilter < HTML::Pipeline::Filter
include Rouge::Plugins::Redcarpet
def call
doc.search('pre > code').each do |node|
highlight_node(node)
......@@ -23,7 +21,7 @@ module Banzai
lang = lexer.tag
begin
code = format(lex(lexer, code))
code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
css_classes << " js-syntax-highlight #{lang}"
rescue
......@@ -45,10 +43,6 @@ module Banzai
lexer.lex(code)
end
def format(tokens)
rouge_formatter.format(tokens)
end
def lexer_for(language)
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end
......@@ -57,11 +51,6 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
# Override Rouge::Plugins::Redcarpet#rouge_formatter
def rouge_formatter(lexer = nil)
@rouge_formatter ||= Rouge::Formatters::HTML.new
end
end
end
end
......@@ -14,7 +14,7 @@ module Gitlab
end
def initialize(blob_name, blob_content, repository: nil)
@formatter = Rouge::Formatters::HTMLGitlab.new
@formatter = Rouge::Formatters::HTMLGitlab
@repository = repository
@blob_name = blob_name
@blob_content = blob_content
......@@ -28,7 +28,7 @@ module Gitlab
hl_lexer = self.lexer
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
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
end
......
......@@ -6,9 +6,10 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
# [+linenostart+] The line number for the first line (default: 1).
def initialize(linenostart: 1)
def initialize(linenostart: 1, tag: nil)
@linenostart = linenostart
@line_number = linenostart
@tag = tag
end
def stream(tokens, &b)
......@@ -17,7 +18,7 @@ module Rouge
yield "\n" unless is_first
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) }
yield %(</span>)
......
......@@ -2,9 +2,15 @@ require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
before do
login_as :admin
end
describe 'Copying rendered GFM' do
before do
@feat = MarkdownFeature.new
# `markdown` helper expects a `@project` variable
......@@ -410,17 +416,6 @@ describe 'Copy as GFM', feature: true, js: true do
alias_method :gfm_to_html, :markdown
def html_to_gfm(html)
js = <<-JS.strip_heredoc
(function(html) {
var node = document.createElement('div');
node.innerHTML = html;
return window.gl.CopyAsGFM.nodeToGFM(node);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
end
def verify(label, *gfms)
aggregate_failures(label) do
gfms.each do |gfm|
......@@ -435,4 +430,161 @@ describe 'Copy as GFM', feature: true, js: true do
def current_user
@feat.user
end
end
describe 'Copying code' do
let(:project) { create(:project) }
context 'from a diff' do
before do
visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
end
context 'selecting one word of text' do
it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
'`RuntimeError`'
)
end
end
context 'selecting one line of text' do
it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
'`raise RuntimeError, "System commands must be given as an array of strings"`'
)
end
end
context 'selecting multiple lines of text' do
it 'copies as a code block' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
<<-GFM.strip_heredoc,
```ruby
raise RuntimeError, "System commands must be given as an array of strings"
end
```
GFM
)
end
end
end
context 'from a blob' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
end
context 'selecting one word of text' do
it 'copies as inline code' do
verify(
'.line[id="LC9"] .no',
'`RuntimeError`'
)
end
end
context 'selecting one line of text' do
it 'copies as inline code' do
verify(
'.line[id="LC9"]',
'`raise RuntimeError, "System commands must be given as an array of strings"`'
)
end
end
context 'selecting multiple lines of text' do
it 'copies as a code block' do
verify(
'.line[id="LC9"], .line[id="LC10"]',
<<-GFM.strip_heredoc,
```ruby
raise RuntimeError, "System commands must be given as an array of strings"
end
```
GFM
)
end
end
end
context 'from a GFM code block' do
before do
visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
end
context 'selecting one word of text' do
it 'copies as inline code' do
verify(
'.line[id="LC27"] .s2',
'`"bio"`'
)
end
end
context 'selecting one line of text' do
it 'copies as inline code' do
verify(
'.line[id="LC27"]',
'`"bio": null,`'
)
end
end
context 'selecting multiple lines of text' do
it 'copies as a code block' do
verify(
'.line[id="LC27"], .line[id="LC28"]',
<<-GFM.strip_heredoc,
```json
"bio": null,
"skype": "",
```
GFM
)
end
end
end
def verify(selector, gfm)
html = html_for_selector(selector)
output_gfm = html_to_gfm(html, 'transformCodeSelection');
expect(output_gfm.strip).to eq(gfm.strip)
end
end
def html_for_selector(selector)
js = <<-JS.strip_heredoc
(function(selector) {
var els = document.querySelectorAll(selector);
var htmls = _.map(els, function(el) { return el.outerHTML; });
return htmls.join("\\n");
})("#{escape_javascript(selector)}")
JS
page.evaluate_script(js)
end
def html_to_gfm(html, transformer = 'transformGFMSelection')
js = <<-JS.strip_heredoc
(function(html) {
var node = document.createElement('div');
node.innerHTML = html;
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
return window.gl.CopyAsGFM.selectionToGFM(node, transformer);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
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