diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
index 0fb7bde1fd6..67f7226fe82 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -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;
}
diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml
new file mode 100644
index 00000000000..15ae2da44a3
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-code-as-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Copy code as GFM from diffs, blobs and GFM code blocks
+merge_request:
+author:
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index a447e2b8bff..9f09ca90697 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -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
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 9360afedfcb..d787d5db4a0 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -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
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 4edfd015074..ec95ddf03ea 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -6,9 +6,10 @@ module Rouge
# Creates a new Rouge::Formatter::HTMLGitlab 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 %()
+ yield %()
line.each { |token, value| yield span(token, value.chomp) }
yield %()
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 4638812b2d9..f134d4be154 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -2,437 +2,589 @@ require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper
+ include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
before do
- @feat = MarkdownFeature.new
-
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
-
- visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+ login_as :admin
end
- # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb 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 make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
- # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
+ describe 'Copying rendered GFM' do
+ before do
+ @feat = MarkdownFeature.new
- # These are all in a single `it` for performance reasons.
- it 'works', :aggregate_failures do
- verify(
- 'nesting',
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
- '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
- )
+ visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+ end
- verify(
- 'a real world example from the gitlab-ce README',
+ # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb 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 make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
+ # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
- <<-GFM.strip_heredoc
- # GitLab
+ # These are all in a single `it` for performance reasons.
+ it 'works', :aggregate_failures do
+ verify(
+ 'nesting',
- [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
- [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
- [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+ '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+ )
- ## Canonical source
+ verify(
+ 'a real world example from the gitlab-ce README',
- The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+ <<-GFM.strip_heredoc
+ # GitLab
- ## Open source software to collaborate on code
+ [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+ [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+ [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+ [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
- To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
+ ## Canonical source
+
+ The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+
+ ## Open source software to collaborate on code
+
+ To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
- - Manage Git repositories with fine grained access controls that keep your code secure
+ - Manage Git repositories with fine grained access controls that keep your code secure
- - Perform code reviews and enhance collaboration with merge requests
+ - Perform code reviews and enhance collaboration with merge requests
- - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+ - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
- - Each project can also have an issue tracker, issue board, and a wiki
+ - Each project can also have an issue tracker, issue board, and a wiki
- - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+ - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- - Completely free and open source (MIT Expat license)
- GFM
- )
+ - Completely free and open source (MIT Expat license)
+ GFM
+ )
- verify(
- 'InlineDiffFilter',
+ verify(
+ 'InlineDiffFilter',
- '{-Deleted text-}',
- '{+Added text+}'
- )
+ '{-Deleted text-}',
+ '{+Added text+}'
+ )
- verify(
- 'TaskListFilter',
+ verify(
+ 'TaskListFilter',
- '- [ ] Unchecked task',
- '- [x] Checked task',
- '1. [ ] Unchecked numbered task',
- '1. [x] Checked numbered task'
- )
+ '- [ ] Unchecked task',
+ '- [x] Checked task',
+ '1. [ ] Unchecked numbered task',
+ '1. [x] Checked numbered task'
+ )
- verify(
- 'ReferenceFilter',
+ verify(
+ 'ReferenceFilter',
- # issue reference
- @feat.issue.to_reference,
- # full issue reference
- @feat.issue.to_reference(full: true),
- # issue URL
- namespace_project_issue_url(@project.namespace, @project, @feat.issue),
- # issue URL with note anchor
- namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
- # issue link
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
- # issue link with note anchor
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
- )
+ # issue reference
+ @feat.issue.to_reference,
+ # full issue reference
+ @feat.issue.to_reference(full: true),
+ # issue URL
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue),
+ # issue URL with note anchor
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
+ # issue link
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
+ # issue link with note anchor
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+ )
- verify(
- 'AutolinkFilter',
+ verify(
+ 'AutolinkFilter',
- 'https://example.com'
- )
+ 'https://example.com'
+ )
- verify(
- 'TableOfContentsFilter',
+ verify(
+ 'TableOfContentsFilter',
- '[[_TOC_]]'
- )
+ '[[_TOC_]]'
+ )
- verify(
- 'EmojiFilter',
+ verify(
+ 'EmojiFilter',
- ':thumbsup:'
- )
+ ':thumbsup:'
+ )
- verify(
- 'ImageLinkFilter',
+ verify(
+ 'ImageLinkFilter',
- '![Image](https://example.com/image.png)'
- )
+ '![Image](https://example.com/image.png)'
+ )
- verify(
- 'VideoLinkFilter',
+ verify(
+ 'VideoLinkFilter',
- '![Video](https://example.com/video.mp4)'
- )
+ '![Video](https://example.com/video.mp4)'
+ )
- verify(
- 'MathFilter: math as converted from GFM to HTML',
+ verify(
+ 'MathFilter: math as converted from GFM to HTML',
- '$`c = \pm\sqrt{a^2 + b^2}`$',
+ '$`c = \pm\sqrt{a^2 + b^2}`$',
- # math block
- <<-GFM.strip_heredoc
- ```math
- c = \pm\sqrt{a^2 + b^2}
- ```
- GFM
- )
+ # math block
+ <<-GFM.strip_heredoc
+ ```math
+ c = \pm\sqrt{a^2 + b^2}
+ ```
+ GFM
+ )
- aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
- gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+ aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
+ gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
- html = <<-HTML.strip_heredoc
-
-
-
-
-
-
-
-
- c
- =
- ±
-
- √
-
-
-
-
-
-
-
-
- a
-
-
-
-
-
-
-
- 2
+ html = <<-HTML.strip_heredoc
+
+
+
+
+
+
+
+
+ c
+ =
+ ±
+
+ √
+
+
+
+
+
+
+
+
+ a
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
-
-
-
-
-
-
- +
-
- b
-
-
-
-
-
-
-
- 2
+ +
+
+ b
+
+
+
+
+
+
+
+ 2
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
+
-
-
-
-
-
-
- HTML
+ HTML
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+
+ verify(
+ 'SanitizationFilter',
+
+ <<-GFM.strip_heredoc
+
+
+ sub
+
+
+ - dt
+ - dd
+
+
+ kbd
+
+ q
+
+ samp
+
+ var
+
+ ruby
+
+
+
+
+
+ abbr
+
+ summary
+
+ details
+ GFM
+ )
+
+ verify(
+ 'SanitizationFilter',
+
+ <<-GFM.strip_heredoc,
+ ```
+ Plain text
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ def foo
+ bar
+ end
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc
+ Foo
+
+ This is an example of GFM
+
+ ```js
+ Code goes here
+ ```
+ GFM
+ )
+
+ verify(
+ 'MarkdownFilter',
+
+ "Line with two spaces at the end \nto insert a linebreak",
+
+ '`code`',
+ '`` code with ` ticks ``',
+
+ '> Quote',
+
+ # multiline quote
+ <<-GFM.strip_heredoc,
+ > Multiline
+ > Quote
+ >
+ > With multiple paragraphs
+ GFM
+
+ '![Image](https://example.com/image.png)',
+
+ '# Heading with no anchor link',
+
+ '[Link](https://example.com)',
+
+ '- List item',
+
+ # multiline list item
+ <<-GFM.strip_heredoc,
+ - Multiline
+ List item
+ GFM
+
+ # nested lists
+ <<-GFM.strip_heredoc,
+ - Nested
+
+
+ - Lists
+ GFM
+
+ # list with blockquote
+ <<-GFM.strip_heredoc,
+ - List
+
+ > Blockquote
+ GFM
+
+ '1. Numbered list item',
+
+ # multiline numbered list item
+ <<-GFM.strip_heredoc,
+ 1. Multiline
+ Numbered list item
+ GFM
+
+ # nested numbered list
+ <<-GFM.strip_heredoc,
+ 1. Nested
+
+
+ 1. Numbered lists
+ GFM
+
+ '# Heading',
+ '## Heading',
+ '### Heading',
+ '#### Heading',
+ '##### Heading',
+ '###### Heading',
+
+ '**Bold**',
+
+ '_Italics_',
+
+ '~~Strikethrough~~',
+
+ '2^2',
+
+ '-----',
+
+ # table
+ <<-GFM.strip_heredoc,
+ | Centered | Right | Left |
+ |:--------:|------:|------|
+ | Foo | Bar | **Baz** |
+ | Foo | Bar | **Baz** |
+ GFM
+
+ # table with empty heading
+ <<-GFM.strip_heredoc,
+ | | x | y |
+ |---|---|---|
+ | a | 1 | 0 |
+ | b | 0 | 1 |
+ GFM
+ )
end
- verify(
- 'SanitizationFilter',
+ alias_method :gfm_to_html, :markdown
- <<-GFM.strip_heredoc
-
-
- sub
-
-
- - dt
- - dd
-
-
- kbd
-
- q
-
- samp
-
- var
-
- ruby
-
-
-
-
-
- abbr
-
- summary
-
- details
- GFM
- )
-
- verify(
- 'SanitizationFilter',
-
- <<-GFM.strip_heredoc,
- ```
- Plain text
- ```
- GFM
-
- <<-GFM.strip_heredoc,
- ```ruby
- def foo
- bar
+ def verify(label, *gfms)
+ aggregate_failures(label) do
+ gfms.each do |gfm|
+ html = gfm_to_html(gfm)
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
end
- ```
- GFM
+ end
+ end
- <<-GFM.strip_heredoc
- Foo
-
- This is an example of GFM
-
- ```js
- Code goes here
- ```
- GFM
- )
-
- verify(
- 'MarkdownFilter',
-
- "Line with two spaces at the end \nto insert a linebreak",
-
- '`code`',
- '`` code with ` ticks ``',
-
- '> Quote',
-
- # multiline quote
- <<-GFM.strip_heredoc,
- > Multiline
- > Quote
- >
- > With multiple paragraphs
- GFM
-
- '![Image](https://example.com/image.png)',
-
- '# Heading with no anchor link',
-
- '[Link](https://example.com)',
-
- '- List item',
-
- # multiline list item
- <<-GFM.strip_heredoc,
- - Multiline
- List item
- GFM
-
- # nested lists
- <<-GFM.strip_heredoc,
- - Nested
-
-
- - Lists
- GFM
-
- # list with blockquote
- <<-GFM.strip_heredoc,
- - List
-
- > Blockquote
- GFM
-
- '1. Numbered list item',
-
- # multiline numbered list item
- <<-GFM.strip_heredoc,
- 1. Multiline
- Numbered list item
- GFM
-
- # nested numbered list
- <<-GFM.strip_heredoc,
- 1. Nested
-
-
- 1. Numbered lists
- GFM
-
- '# Heading',
- '## Heading',
- '### Heading',
- '#### Heading',
- '##### Heading',
- '###### Heading',
-
- '**Bold**',
-
- '_Italics_',
-
- '~~Strikethrough~~',
-
- '2^2',
-
- '-----',
-
- # table
- <<-GFM.strip_heredoc,
- | Centered | Right | Left |
- |:--------:|------:|------|
- | Foo | Bar | **Baz** |
- | Foo | Bar | **Baz** |
- GFM
-
- # table with empty heading
- <<-GFM.strip_heredoc,
- | | x | y |
- |---|---|---|
- | a | 1 | 0 |
- | b | 0 | 1 |
- GFM
- )
+ # Fake a `current_user` helper
+ def current_user
+ @feat.user
+ end
end
- alias_method :gfm_to_html, :markdown
+ describe 'Copying code' do
+ let(:project) { create(:project) }
- def html_to_gfm(html)
+ 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(html) {
- var node = document.createElement('div');
- node.innerHTML = html;
- return window.gl.CopyAsGFM.nodeToGFM(node);
- })("#{escape_javascript(html)}")
+ (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 verify(label, *gfms)
- aggregate_failures(label) do
- gfms.each do |gfm|
- html = gfm_to_html(gfm)
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
- end
- end
- end
-
- # Fake a `current_user` helper
- def current_user
- @feat.user
+ 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