Commit a24e9a0e authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'copy-as-md' into 'master'

Copying a rendered issue/comment will paste into GFM textareas as actual GFM

See merge request !8597
parents 112f9710 6c2d8f35
This diff is collapsed.
......@@ -160,6 +160,62 @@
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
const documentFragment = selection.getRangeAt(0).cloneContents();
if (documentFragment.textContent.length === 0) return null;
return documentFragment;
};
w.gl.utils.insertText = (target, text) => {
// Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas
const selectionStart = target.selectionStart;
const selectionEnd = target.selectionEnd;
const value = target.value;
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
const newText = textBefore + text + textAfter;
target.value = newText;
target.selectionStart = target.selectionEnd = selectionStart + text.length;
// Trigger autosave
$(target).trigger('input');
// Trigger autosize
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
target.dispatchEvent(event);
};
w.gl.utils.nodeMatchesSelector = (node, selector) => {
const matches = Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector;
if (matches) {
return matches.call(node, selector);
}
// IE11 doesn't support `node.matches(selector)`
let parentNode = node.parentNode;
if (!parentNode) {
parentNode = document.createElement('div');
node = node.cloneNode(true);
parentNode.appendChild(node);
}
const matchingNodes = parentNode.querySelectorAll(selector);
return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
};
/**
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
......
......@@ -39,29 +39,39 @@
}
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, documentFragment, selected, separator;
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;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
replyField = $('.js-main-target-form #note_note');
if (selected.trim() === "") {
return;
}
quote = _.map(selected.split("\n"), function(val) {
return ("> " + val).trim() + "\n";
});
// If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n\n" || '';
replyField.val(function(_, current) {
return current + separator + quote.join('') + "\n";
});
// Trigger autosave
replyField.trigger('input');
// Trigger autosize
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
replyField.get(0).dispatchEvent(event);
// Focus the input field
return replyField.focus();
};
ShortcutsIssuable.prototype.editIssue = function() {
......
---
title: Copying a rendered issue/comment will paste into GFM textareas as actual GFM
merge_request:
author:
......@@ -153,7 +153,7 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
data = data_attributes_for(link_content || match, project, object)
data = data_attributes_for(link_content || match, project, object, link: !!link_content)
if matches.names.include?("url") && matches[:url]
url = matches[:url]
......@@ -172,9 +172,10 @@ module Banzai
end
end
def data_attributes_for(text, project, object)
def data_attributes_for(text, project, object, link: false)
data_attribute(
original: text,
link: link,
project: project.id,
object_sym => object.id
)
......
......@@ -62,7 +62,7 @@ module Banzai
end
end
def data_attributes_for(text, project, object)
def data_attributes_for(text, project, object, link: false)
if object.is_a?(ExternalIssue)
data_attribute(
project: project.id,
......
......@@ -20,17 +20,19 @@ module Banzai
code = node.text
css_classes = "code highlight"
lexer = lexer_for(language)
lang = lexer.tag
begin
code = format(lex(lexer, code))
css_classes << " js-syntax-highlight #{lexer.tag}"
css_classes << " js-syntax-highlight #{lang}"
rescue
lang = nil
# Gracefully handle syntax highlighter bugs/errors to ensure
# users can still access an issue/comment/etc.
end
highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
highlighted = %(<pre class="#{css_classes}" lang="#{lang}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
......
......@@ -35,7 +35,8 @@ module Banzai
src: element['src'],
width: '400',
controls: true,
'data-setup' => '{}')
'data-setup' => '{}',
'data-title' => element['title'] || element['alt'])
link = doc.document.create_element(
'a',
......
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
# These filters convert GitLab Flavored Markdown (GFM) to HTML.
# The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6
# consequently convert that same HTML to GFM to be copied to the clipboard.
# Every filter that generates HTML from GFM should have a handler in
# app/assets/javascripts/copy_as_gfm.js.es6, in reverse order.
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
Filter::SyntaxHighlightFilter,
......
This diff is collapsed.
/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
/* global ShortcutsIssuable */
/*= require copy_as_gfm */
/*= require shortcuts_issuable */
(function() {
......@@ -14,10 +15,12 @@
});
return describe('#replyWithSelectedText', function() {
var stubSelection;
// Stub window.getSelection to return the provided String.
stubSelection = function(text) {
return window.getSelection = function() {
return text;
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
stubSelection = function(html) {
window.gl.utils.getSelectedFragment = function() {
var node = document.createElement('div');
node.innerHTML = html;
return node;
};
};
beforeEach(function() {
......@@ -32,13 +35,13 @@
});
describe('with any selection', function() {
beforeEach(function() {
return stubSelection('Selected text.');
return stubSelection('<p>Selected text.</p>');
});
it('leaves existing input intact', function() {
$(this.selector).val('This text was already here.');
expect($(this.selector).val()).toBe('This text was already here.');
this.shortcut.replyWithSelectedText();
return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n");
return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
});
it('triggers `input`', function() {
var triggered;
......@@ -61,16 +64,16 @@
});
describe('with a one-line selection', function() {
return it('quotes the selection', function() {
stubSelection('This text has been selected.');
stubSelection('<p>This text has been selected.</p>');
this.shortcut.replyWithSelectedText();
return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
});
});
return describe('with a multi-line selection', function() {
return it('quotes the selected lines as a group', function() {
stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n");
stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>");
this.shortcut.replyWithSelectedText();
return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n");
return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
});
});
});
......
......@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight 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>def fun end</code></pre>')
end
end
context "when a valid language is specified" do
it "highlights as that language" do
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" 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 class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
end
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
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" 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>This is a test</code></pre>')
end
end
......@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
it "highlights as plaintext" do
result = filter('<pre><code class="ruby">This is a test</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight" v-pre="true"><code>This is a test</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
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