Commit 68903277 authored by Douwe Maan's avatar Douwe Maan

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

parent b7166806
...@@ -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,54 @@ class CopyAsGFM { ...@@ -306,7 +307,54 @@ 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 selectionToGFM(documentFragment, transformer) {
const el = transformer(documentFragment.cloneNode(true));
if (!el) return null;
return CopyAsGFM.nodeToGFM(el);
}
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;
} }
......
---
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
......
...@@ -6,9 +6,10 @@ module Rouge ...@@ -6,9 +6,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). # [+linenostart+] The line number for the first line (default: 1).
def initialize(linenostart: 1) def initialize(linenostart: 1, tag: nil)
@linenostart = linenostart @linenostart = linenostart
@line_number = linenostart @line_number = linenostart
@tag = tag
end end
def stream(tokens, &b) def stream(tokens, &b)
...@@ -17,7 +18,7 @@ module Rouge ...@@ -17,7 +18,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>)
......
...@@ -2,437 +2,589 @@ require 'spec_helper' ...@@ -2,437 +2,589 @@ require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper include GitlabMarkdownHelper
include RepoHelpers
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
before do before do
@feat = MarkdownFeature.new login_as :admin
end
# `markdown` helper expects a `@project` variable describe 'Copying rendered GFM' do
@project = @feat.project before do
@feat = MarkdownFeature.new
visit namespace_project_issue_path(@project.namespace, @project, @feat.issue) # `markdown` helper expects a `@project` variable
end @project = @feat.project
# The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
# The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM. end
# 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.
# These are all in a single `it` for performance reasons. # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
it 'works', :aggregate_failures do # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM.
verify( # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
'nesting', # 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( '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
'a real world example from the gitlab-ce README', )
<<-GFM.strip_heredoc verify(
# GitLab '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) <<-GFM.strip_heredoc
[![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) # GitLab
[![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)
## 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) - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
GFM
)
verify( - Completely free and open source (MIT Expat license)
'InlineDiffFilter', GFM
)
'{-Deleted text-}', verify(
'{+Added text+}' 'InlineDiffFilter',
)
verify( '{-Deleted text-}',
'TaskListFilter', '{+Added text+}'
)
'- [ ] Unchecked task', verify(
'- [x] Checked task', 'TaskListFilter',
'1. [ ] Unchecked numbered task',
'1. [x] Checked numbered task'
)
verify( '- [ ] Unchecked task',
'ReferenceFilter', '- [x] Checked task',
'1. [ ] Unchecked numbered task',
'1. [x] Checked numbered task'
)
# issue reference verify(
@feat.issue.to_reference, 'ReferenceFilter',
# 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( # issue reference
'AutolinkFilter', @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( 'https://example.com'
'TableOfContentsFilter', )
'[[_TOC_]]' verify(
) 'TableOfContentsFilter',
verify( '[[_TOC_]]'
'EmojiFilter', )
':thumbsup:' verify(
) 'EmojiFilter',
verify( ':thumbsup:'
'ImageLinkFilter', )
'![Image](https://example.com/image.png)'
)
verify( verify(
'VideoLinkFilter', 'ImageLinkFilter',
'![Image](https://example.com/image.png)'
)
'![Video](https://example.com/video.mp4)' verify(
) 'VideoLinkFilter',
verify( '![Video](https://example.com/video.mp4)'
'MathFilter: math as converted from GFM to HTML', )
'$`c = \pm\sqrt{a^2 + b^2}`$', verify(
'MathFilter: math as converted from GFM to HTML',
# math block '$`c = \pm\sqrt{a^2 + b^2}`$',
<<-GFM.strip_heredoc
```math
c = \pm\sqrt{a^2 + b^2}
```
GFM
)
aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do # math block
gfm = '$`c = \pm\sqrt{a^2 + b^2}`$' <<-GFM.strip_heredoc
```math
c = \pm\sqrt{a^2 + b^2}
```
GFM
)
html = <<-HTML.strip_heredoc aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
<span class="katex"> gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
<span class="katex-mathml">
<math> html = <<-HTML.strip_heredoc
<semantics> <span class="katex">
<mrow> <span class="katex-mathml">
<mi>c</mi> <math>
<mo>=</mo> <semantics>
<mo>±</mo> <mrow>
<msqrt> <mi>c</mi>
<mrow> <mo>=</mo>
<msup> <mo>±</mo>
<mi>a</mi> <msqrt>
<mn>2</mn> <mrow>
</msup> <msup>
<mo>+</mo> <mi>a</mi>
<msup> <mn>2</mn>
<mi>b</mi> </msup>
<mn>2</mn> <mo>+</mo>
</msup> <msup>
</mrow> <mi>b</mi>
</msqrt> <mn>2</mn>
</mrow> </msup>
<annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation> </mrow>
</semantics> </msqrt>
</math> </mrow>
</span> <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
<span class="katex-html" aria-hidden="true"> </semantics>
<span class="strut" style="height: 0.913389em;"></span> </math>
<span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span> </span>
<span class="base textstyle uncramped"> <span class="katex-html" aria-hidden="true">
<span class="mord mathit">c</span> <span class="strut" style="height: 0.913389em;"></span>
<span class="mrel">=</span> <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
<span class="mord">±</span> <span class="base textstyle uncramped">
<span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;"> <span class="mord mathit">c</span>
<span class="style-wrap reset-textstyle textstyle uncramped">√</span> <span class="mrel">=</span>
</span> <span class="mord">±</span>
<span class="vlist"> <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
<span class="" style="top: 0em;"> <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
<span class="fontsize-ensurer reset-size5 size5"> </span>
<span class="" style="font-size: 1em;">​</span> <span class="vlist">
</span> <span class="" style="top: 0em;">
<span class="mord textstyle cramped"> <span class="fontsize-ensurer reset-size5 size5">
<span class="mord"> <span class="" style="font-size: 1em;">​</span>
<span class="mord mathit">a</span> </span>
<span class="msupsub"> <span class="mord textstyle cramped">
<span class="vlist"> <span class="mord">
<span class="" style="top: -0.289em; margin-right: 0.05em;"> <span class="mord mathit">a</span>
<span class="fontsize-ensurer reset-size5 size5"> <span class="msupsub">
<span class="" style="font-size: 0em;">​</span> <span class="vlist">
</span> <span class="" style="top: -0.289em; margin-right: 0.05em;">
<span class="reset-textstyle scriptstyle cramped"> <span class="fontsize-ensurer reset-size5 size5">
<span class="mord mathrm">2</span> <span class="" style="font-size: 0em;">​</span>
</span>
<span class="reset-textstyle scriptstyle cramped">
<span class="mord mathrm">2</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 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="mbin">+</span>
<span class="mbin">+</span> <span class="mord">
<span class="mord"> <span class="mord mathit">b</span>
<span class="mord mathit">b</span> <span class="msupsub">
<span class="msupsub"> <span class="vlist">
<span class="vlist"> <span class="" style="top: -0.289em; margin-right: 0.05em;">
<span class="" style="top: -0.289em; margin-right: 0.05em;"> <span class="fontsize-ensurer reset-size5 size5">
<span class="fontsize-ensurer reset-size5 size5"> <span class="" style="font-size: 0em;">​</span>
<span class="" style="font-size: 0em;">​</span> </span>
</span> <span class="reset-textstyle scriptstyle cramped">
<span class="reset-textstyle scriptstyle cramped"> <span class="mord mathrm">2</span>
<span class="mord mathrm">2</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 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>
</span> </span>
</span> <span class="" style="top: -0.833389em;">
<span class="" style="top: -0.833389em;"> <span class="fontsize-ensurer reset-size5 size5">
<span class="fontsize-ensurer reset-size5 size5"> <span class="" style="font-size: 1em;">​</span>
<span class="" style="font-size: 1em;">​</span> </span>
<span class="reset-textstyle textstyle uncramped sqrt-line"></span>
</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>
<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> </span>
</span> </span>
</span> HTML
HTML
output_gfm = html_to_gfm(html) output_gfm = html_to_gfm(html)
expect(output_gfm.strip).to eq(gfm.strip) expect(output_gfm.strip).to eq(gfm.strip)
end end
verify( verify(
'SanitizationFilter', 'SanitizationFilter',
<<-GFM.strip_heredoc <<-GFM.strip_heredoc
<a name="named-anchor"></a> <a name="named-anchor"></a>
<sub>sub</sub> <sub>sub</sub>
<dl> <dl>
<dt>dt</dt> <dt>dt</dt>
<dd>dd</dd> <dd>dd</dd>
</dl> </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> <details>details</details>
GFM GFM
) )
verify( verify(
'SanitizationFilter', 'SanitizationFilter',
<<-GFM.strip_heredoc, <<-GFM.strip_heredoc,
``` ```
Plain text Plain text
``` ```
GFM GFM
<<-GFM.strip_heredoc, <<-GFM.strip_heredoc,
```ruby ```ruby
def foo def foo
bar bar
end end
``` ```
GFM 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 '# Heading',
Code goes here '## Heading',
``` '### Heading',
GFM '#### Heading',
) '##### Heading',
'###### Heading',
verify( '**Bold**',
'MarkdownFilter',
"Line with two spaces at the end \nto insert a linebreak", '_Italics_',
'`code`', '~~Strikethrough~~',
'`` code with ` ticks ``',
'> 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 # Fake a `current_user` helper
<<-GFM.strip_heredoc, def current_user
- Multiline @feat.user
List item end
GFM end
# nested lists describe 'Copying code' do
<<-GFM.strip_heredoc, let(:project) { create(:project) }
- Nested
context 'from a diff' do
before do
visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
end
- Lists context 'selecting one word of text' do
GFM it 'copies as inline code' do
verify(
'[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
# list with blockquote '`RuntimeError`'
<<-GFM.strip_heredoc, )
- List end
end
> Blockquote context 'selecting one line of text' do
GFM 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 context 'selecting multiple lines of text' do
<<-GFM.strip_heredoc, it 'copies as a code block' do
1. Multiline verify(
Numbered list item '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
GFM
<<-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 context 'selecting one word of text' do
<<-GFM.strip_heredoc, it 'copies as inline code' do
1. Nested verify(
'.line[id="LC9"] .no',
'`RuntimeError`'
)
end
end
1. Numbered lists context 'selecting one line of text' do
GFM it 'copies as inline code' do
verify(
'.line[id="LC9"]',
'# Heading', '`raise RuntimeError, "System commands must be given as an array of strings"`'
'## Heading', )
'### Heading', end
'#### Heading', end
'##### Heading',
'###### Heading', 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 context 'selecting multiple lines of text' do
<<-GFM.strip_heredoc, it 'copies as a code block' do
| Centered | Right | Left | verify(
|:--------:|------:|------| '.line[id="LC27"], .line[id="LC28"]',
| Foo | Bar | **Baz** |
| Foo | Bar | **Baz** | <<-GFM.strip_heredoc,
GFM ```json
"bio": null,
"skype": "",
```
GFM
)
end
end
end
# table with empty heading def verify(selector, gfm)
<<-GFM.strip_heredoc, html = html_for_selector(selector)
| | x | y | output_gfm = html_to_gfm(html, 'transformCodeSelection');
|---|---|---| expect(output_gfm.strip).to eq(gfm.strip)
| a | 1 | 0 | end
| b | 0 | 1 |
GFM
)
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 js = <<-JS.strip_heredoc
(function(html) { (function(html) {
var node = document.createElement('div'); var node = document.createElement('div');
node.innerHTML = html; 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)}") })("#{escape_javascript(html)}")
JS JS
page.evaluate_script(js) page.evaluate_script(js)
end 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 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