Commit ef447a62 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'dm-copy-code-as-gfm' into 'master'

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

See merge request !9874
parents b29aee2f 9a0a4f17
......@@ -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,47 @@ 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 nodeToGFM(node) {
if (node.nodeType === Node.COMMENT_NODE) {
return '';
}
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
......
......@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false.
def truncate_if_block(node, truncated)
if node.element? && node.description&.block? && !truncated
return true if truncated
if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
node.inner_html = "#{node.inner_html}..." if node.next_sibling
true
else
......
---
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
......
......@@ -5,10 +5,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)
@linenostart = linenostart
@line_number = linenostart
# [+tag+] The tag (language) of the lexer used to generate the formatted tokens
def initialize(tag: nil)
@line_number = 1
@tag = tag
end
def stream(tokens, &b)
......@@ -17,7 +17,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,594 @@ 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
<<-GFM.strip_heredoc
Foo
```js
Code goes here
```
GFM
)
This is an example of GFM
verify(
'MarkdownFilter',
```js
Code goes here
```
GFM
)
"Line with two spaces at the end \nto insert a linebreak",
verify(
'MarkdownFilter',
'`code`',
'`` code with ` ticks ``',
"Line with two spaces at the end \nto insert a linebreak",
'> Quote',
'`code`',
'`` code with ` ticks ``',
# multiline quote
<<-GFM.strip_heredoc,
> Multiline
> Quote
>
> With multiple paragraphs
GFM
'> Quote',
'![Image](https://example.com/image.png)',
# multiline quote
<<-GFM.strip_heredoc,
> Multiline
> Quote
>
> With multiple paragraphs
GFM
'# Heading with no anchor link',
'![Image](https://example.com/image.png)',
'[Link](https://example.com)',
'# Heading with no anchor link',
'- List item',
'[Link](https://example.com)',
# multiline list item
<<-GFM.strip_heredoc,
- Multiline
List item
GFM
'- List item',
# nested lists
<<-GFM.strip_heredoc,
- Nested
# 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
- Lists
GFM
> Blockquote
GFM
# list with blockquote
<<-GFM.strip_heredoc,
- List
'1. Numbered list item',
> Blockquote
GFM
# multiline numbered list item
<<-GFM.strip_heredoc,
1. Multiline
Numbered list item
GFM
'1. Numbered list item',
# nested numbered list
<<-GFM.strip_heredoc,
1. Nested
# 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',
1. Numbered lists
GFM
'**Bold**',
'# Heading',
'## Heading',
'### Heading',
'#### Heading',
'##### Heading',
'###### Heading',
'_Italics_',
'**Bold**',
'~~Strikethrough~~',
'_Italics_',
'2^2',
'~~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
alias_method :gfm_to_html, :markdown
# table
<<-GFM.strip_heredoc,
| Centered | Right | Left |
|:--------:|------:|------|
| Foo | Bar | **Baz** |
| Foo | Bar | **Baz** |
GFM
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
# 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) }
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',
def html_to_gfm(html)
'`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 with the correct language' 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(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, transformer = 'transformGFMSelection')
js = <<-JS.strip_heredoc
(function(html) {
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div');
node.innerHTML = html;
node = transformer(node);
if (!node) return null;
return window.gl.CopyAsGFM.nodeToGFM(node);
})("#{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
......@@ -19,12 +19,12 @@ describe BlobHelper do
describe '#highlight' do
it 'returns plaintext for unknown lexer context' do
result = helper.highlight(blob_name, no_context_content)
expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
end
it 'highlights single block' do
expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end
......@@ -43,10 +43,10 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do
%q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span>
<span id="LC2" class="line"><span class="gi">+bbb</span></span>
<span id="LC3" class="line"><span class="gd">- ccc</span></span>
<span id="LC4" class="line"> ddd</span></code></pre>)
%q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
end
it 'highlights each line properly' do
......
......@@ -28,7 +28,7 @@ describe EventsHelper do
it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```"
expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
expect(helper.event_note(input)).to match(expected)
end
......@@ -55,10 +55,8 @@ describe EventsHelper do
it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \
"<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
" <span class=\"s1\">\'hello world\'</span>\n" \
"<span class=\"k\">end</span>\n" \
'</code></pre>'
"<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
"</code></pre>"
expect(helper.event_note(input)).to eq(expected)
end
......
......@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
end
end
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
end
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end
end
......
......@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks unchanged lines' do
code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
expect(subject[2].text).to eq(code)
end
it 'highlights and marks removed lines' do
code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].text).to eq(code)
end
it 'highlights and marks added lines' do
code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code)
end
......
......@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do
end
it 'highlights all the lines properly' do
expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
end
describe 'with CRLF' do
......@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do
end
it 'strips extra LFs' do
expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>")
expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>")
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