Copy code as GFM from diffs, blobs and GFM code blocks
This commit is contained in:
parent
b716680692
commit
6890327762
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
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,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
|
||||
<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>
|
||||
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)
|
||||
output_gfm = html_to_gfm(html)
|
||||
expect(output_gfm.strip).to eq(gfm.strip)
|
||||
end
|
||||
|
||||
verify(
|
||||
'SanitizationFilter',
|
||||
|
||||
<<-GFM.strip_heredoc
|
||||
<a name="named-anchor"></a>
|
||||
|
||||
<sub>sub</sub>
|
||||
|
||||
<dl>
|
||||
<dt>dt</dt>
|
||||
<dd>dd</dd>
|
||||
</dl>
|
||||
|
||||
<kbd>kbd</kbd>
|
||||
|
||||
<q>q</q>
|
||||
|
||||
<samp>samp</samp>
|
||||
|
||||
<var>var</var>
|
||||
|
||||
<ruby>ruby</ruby>
|
||||
|
||||
<rt>rt</rt>
|
||||
|
||||
<rp>rp</rp>
|
||||
|
||||
<abbr>abbr</abbr>
|
||||
|
||||
<summary>summary</summary>
|
||||
|
||||
<details>details</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
|
||||
<a name="named-anchor"></a>
|
||||
|
||||
<sub>sub</sub>
|
||||
|
||||
<dl>
|
||||
<dt>dt</dt>
|
||||
<dd>dd</dd>
|
||||
</dl>
|
||||
|
||||
<kbd>kbd</kbd>
|
||||
|
||||
<q>q</q>
|
||||
|
||||
<samp>samp</samp>
|
||||
|
||||
<var>var</var>
|
||||
|
||||
<ruby>ruby</ruby>
|
||||
|
||||
<rt>rt</rt>
|
||||
|
||||
<rp>rp</rp>
|
||||
|
||||
<abbr>abbr</abbr>
|
||||
|
||||
<summary>summary</summary>
|
||||
|
||||
<details>details</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
|
||||
|
|
Loading…
Reference in New Issue