summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/copy_as_gfm.js72
-rw-r--r--changelogs/unreleased/dm-copy-code-as-gfm.yml4
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb13
-rw-r--r--lib/gitlab/highlight.rb4
-rw-r--r--lib/rouge/formatters/html_gitlab.rb5
-rw-r--r--spec/features/copy_as_gfm_spec.rb782
6 files changed, 537 insertions, 343 deletions
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 <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>)
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
+ login_as :admin
+ end
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
+ describe 'Copying rendered GFM' do
+ before do
+ @feat = MarkdownFeature.new
- visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
- end
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
- # 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.
+ visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+ end
- # These are all in a single `it` for performance reasons.
- it 'works', :aggregate_failures do
- verify(
- 'nesting',
+ # 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.
- '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
- )
+ # These are all in a single `it` for performance reasons.
+ it 'works', :aggregate_failures do
+ verify(
+ 'nesting',
- verify(
- 'a real world example from the gitlab-ce README',
+ '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+ )
- <<-GFM.strip_heredoc
- # GitLab
+ verify(
+ 'a real world example from the gitlab-ce README',
- [![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)
+ <<-GFM.strip_heredoc
+ # GitLab
- ## Canonical source
+ [![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)
- The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+ ## Canonical source
- ## Open source software to collaborate on code
+ The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
- To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
+ ## 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
- - Perform code reviews and enhance collaboration with merge requests
+ - Manage Git repositories with fine grained access controls that keep your code secure
- - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+ - Perform code reviews and enhance collaboration with merge requests
- - Each project can also have an issue tracker, issue board, and a wiki
+ - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
- - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+ - Each project can also have an issue tracker, issue board, and a wiki
- - Completely free and open source (MIT Expat license)
- GFM
- )
+ - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- verify(
- 'InlineDiffFilter',
+ - Completely free and open source (MIT Expat license)
+ GFM
+ )
- '{-Deleted text-}',
- '{+Added text+}'
- )
+ verify(
+ 'InlineDiffFilter',
- verify(
- 'TaskListFilter',
+ '{-Deleted text-}',
+ '{+Added text+}'
+ )
- '- [ ] Unchecked task',
- '- [x] Checked task',
- '1. [ ] Unchecked numbered task',
- '1. [x] Checked numbered task'
- )
+ verify(
+ 'TaskListFilter',
- verify(
- 'ReferenceFilter',
+ '- [ ] Unchecked task',
+ '- [x] Checked task',
+ '1. [ ] Unchecked numbered task',
+ '1. [x] Checked numbered task'
+ )
- # 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(
+ 'ReferenceFilter',
- verify(
- 'AutolinkFilter',
+ # 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')})",
+ )
- 'https://example.com'
- )
+ verify(
+ 'AutolinkFilter',
- verify(
- 'TableOfContentsFilter',
+ 'https://example.com'
+ )
- '[[_TOC_]]'
- )
+ verify(
+ 'TableOfContentsFilter',
- verify(
- 'EmojiFilter',
+ '[[_TOC_]]'
+ )
- ':thumbsup:'
- )
+ verify(
+ 'EmojiFilter',
- verify(
- 'ImageLinkFilter',
-
- '![Image](https://example.com/image.png)'
- )
+ ':thumbsup:'
+ )
- verify(
- 'VideoLinkFilter',
+ verify(
+ 'ImageLinkFilter',
+
+ '![Image](https://example.com/image.png)'
+ )
- '![Video](https://example.com/video.mp4)'
- )
+ verify(
+ 'VideoLinkFilter',
- verify(
- 'MathFilter: math as converted from GFM to HTML',
+ '![Video](https://example.com/video.mp4)'
+ )
- '$`c = \pm\sqrt{a^2 + b^2}`$',
+ verify(
+ 'MathFilter: math as converted from GFM to HTML',
- # math block
- <<-GFM.strip_heredoc
- ```math
- c = \pm\sqrt{a^2 + b^2}
- ```
- 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}`$'
+ # math block
+ <<-GFM.strip_heredoc
+ ```math
+ c = \pm\sqrt{a^2 + b^2}
+ ```
+ GFM
+ )
- html = <<-HTML.strip_heredoc
- <span class="katex">
- <span class="katex-mathml">
- <math>
- <semantics>
- <mrow>
- <mi>c</mi>
- <mo>=</mo>
- <mo>±</mo>
- <msqrt>
- <mrow>
- <msup>
- <mi>a</mi>
- <mn>2</mn>
- </msup>
- <mo>+</mo>
- <msup>
- <mi>b</mi>
- <mn>2</mn>
- </msup>
- </mrow>
- </msqrt>
- </mrow>
- <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
- </semantics>
- </math>
- </span>
- <span class="katex-html" aria-hidden="true">
- <span class="strut" style="height: 0.913389em;"></span>
- <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
- <span class="base textstyle uncramped">
- <span class="mord mathit">c</span>
- <span class="mrel">=</span>
- <span class="mord">±</span>
- <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
- <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
- </span>
- <span class="vlist">
- <span class="" style="top: 0em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
- </span>
- <span class="mord textstyle cramped">
- <span class="mord">
- <span class="mord mathit">a</span>
- <span class="msupsub">
- <span class="vlist">
- <span class="" style="top: -0.289em; margin-right: 0.05em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- <span class="reset-textstyle scriptstyle cramped">
- <span class="mord mathrm">2</span>
+ aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
+ gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+
+ html = <<-HTML.strip_heredoc
+ <span class="katex">
+ <span class="katex-mathml">
+ <math>
+ <semantics>
+ <mrow>
+ <mi>c</mi>
+ <mo>=</mo>
+ <mo>±</mo>
+ <msqrt>
+ <mrow>
+ <msup>
+ <mi>a</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <msup>
+ <mi>b</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </msqrt>
+ </mrow>
+ <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
+ </semantics>
+ </math>
+ </span>
+ <span class="katex-html" aria-hidden="true">
+ <span class="strut" style="height: 0.913389em;"></span>
+ <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
+ <span class="base textstyle uncramped">
+ <span class="mord mathit">c</span>
+ <span class="mrel">=</span>
+ <span class="mord">±</span>
+ <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
+ <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
+ </span>
+ <span class="vlist">
+ <span class="" style="top: 0em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="mord textstyle cramped">
+ <span class="mord">
+ <span class="mord mathit">a</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
</span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- ​</span>
</span>
</span>
- </span>
- <span class="mbin">+</span>
- <span class="mord">
- <span class="mord mathit">b</span>
- <span class="msupsub">
- <span class="vlist">
- <span class="" style="top: -0.289em; margin-right: 0.05em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- <span class="reset-textstyle scriptstyle cramped">
- <span class="mord mathrm">2</span>
+ <span class="mbin">+</span>
+ <span class="mord">
+ <span class="mord mathit">b</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
</span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- ​</span>
</span>
</span>
</span>
</span>
- </span>
- <span class="" style="top: -0.833389em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
+ <span class="" style="top: -0.833389em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
</span>
- <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
- </span>
- ​</span>
</span>
</span>
</span>
</span>
- </span>
- HTML
+ HTML
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
- end
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
- verify(
- 'SanitizationFilter',
+ verify(
+ 'SanitizationFilter',
- <<-GFM.strip_heredoc
- <a name="named-anchor"></a>
+ <<-GFM.strip_heredoc
+ <a name="named-anchor"></a>
- <sub>sub</sub>
+ <sub>sub</sub>
- <dl>
- <dt>dt</dt>
- <dd>dd</dd>
- </dl>
+ <dl>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ </dl>
- <kbd>kbd</kbd>
+ <kbd>kbd</kbd>
- <q>q</q>
+ <q>q</q>
- <samp>samp</samp>
+ <samp>samp</samp>
- <var>var</var>
+ <var>var</var>
- <ruby>ruby</ruby>
+ <ruby>ruby</ruby>
- <rt>rt</rt>
+ <rt>rt</rt>
- <rp>rp</rp>
+ <rp>rp</rp>
- <abbr>abbr</abbr>
+ <abbr>abbr</abbr>
- <summary>summary</summary>
+ <summary>summary</summary>
- <details>details</details>
- GFM
- )
+ <details>details</details>
+ GFM
+ )
- verify(
- 'SanitizationFilter',
+ verify(
+ 'SanitizationFilter',
- <<-GFM.strip_heredoc,
- ```
- Plain text
- ```
- GFM
+ <<-GFM.strip_heredoc,
+ ```
+ Plain text
+ ```
+ GFM
- <<-GFM.strip_heredoc,
- ```ruby
- def foo
- bar
- end
- ```
- 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
- <<-GFM.strip_heredoc
- Foo
- This is an example of GFM
+ 1. Numbered lists
+ GFM
- ```js
- Code goes here
- ```
- GFM
- )
+ '# Heading',
+ '## Heading',
+ '### Heading',
+ '#### Heading',
+ '##### Heading',
+ '###### Heading',
- verify(
- 'MarkdownFilter',
+ '**Bold**',
- "Line with two spaces at the end \nto insert a linebreak",
+ '_Italics_',
- '`code`',
- '`` code with ` ticks ``',
+ '~~Strikethrough~~',
- '> Quote',
+ '2^2',
- # multiline quote
- <<-GFM.strip_heredoc,
- > Multiline
- > Quote
- >
- > With multiple paragraphs
- GFM
+ '-----',
- '![Image](https://example.com/image.png)',
+ # table
+ <<-GFM.strip_heredoc,
+ | Centered | Right | Left |
+ |:--------:|------:|------|
+ | Foo | Bar | **Baz** |
+ | Foo | Bar | **Baz** |
+ GFM
- '# Heading with no anchor link',
+ # table with empty heading
+ <<-GFM.strip_heredoc,
+ | | x | y |
+ |---|---|---|
+ | a | 1 | 0 |
+ | b | 0 | 1 |
+ GFM
+ )
+ end
- '[Link](https://example.com)',
+ alias_method :gfm_to_html, :markdown
- '- List item',
+ 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
- # multiline list item
- <<-GFM.strip_heredoc,
- - Multiline
- List item
- GFM
+ # Fake a `current_user` helper
+ def current_user
+ @feat.user
+ end
+ end
- # nested lists
- <<-GFM.strip_heredoc,
- - Nested
+ 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
- - Lists
- GFM
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
- # list with blockquote
- <<-GFM.strip_heredoc,
- - List
+ '`RuntimeError`'
+ )
+ end
+ end
- > Blockquote
- GFM
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
- '1. Numbered list item',
+ '`raise RuntimeError, "System commands must be given as an array of strings"`'
+ )
+ end
+ end
- # multiline numbered list item
- <<-GFM.strip_heredoc,
- 1. Multiline
- Numbered list item
- GFM
+ 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
- # nested numbered list
- <<-GFM.strip_heredoc,
- 1. Nested
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC9"] .no',
+ '`RuntimeError`'
+ )
+ end
+ end
- 1. Numbered lists
- GFM
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC9"]',
- '# Heading',
- '## Heading',
- '### Heading',
- '#### Heading',
- '##### Heading',
- '###### Heading',
+ '`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
- '**Bold**',
+ 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
- '_Italics_',
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC27"] .s2',
- '~~Strikethrough~~',
+ '`"bio"`'
+ )
+ end
+ end
- '2^2',
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC27"]',
- '-----',
+ '`"bio": null,`'
+ )
+ end
+ end
- # table
- <<-GFM.strip_heredoc,
- | Centered | Right | Left |
- |:--------:|------:|------|
- | Foo | Bar | **Baz** |
- | Foo | Bar | **Baz** |
- GFM
+ 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
- # table with empty heading
- <<-GFM.strip_heredoc,
- | | x | y |
- |---|---|---|
- | a | 1 | 0 |
- | b | 0 | 1 |
- GFM
- )
+ 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
- alias_method :gfm_to_html, :markdown
+ 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)
+ def html_to_gfm(html, transformer = 'transformGFMSelection')
js = <<-JS.strip_heredoc
(function(html) {
var node = document.createElement('div');
node.innerHTML = html;
- return window.gl.CopyAsGFM.nodeToGFM(node);
+ var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
+ return window.gl.CopyAsGFM.selectionToGFM(node, transformer);
})("#{escape_javascript(html)}")
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
- end
end