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 = { ...@@ -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,47 @@ class CopyAsGFM { ...@@ -306,7 +307,47 @@ 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 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;
} }
......
...@@ -172,7 +172,9 @@ module GitlabMarkdownHelper ...@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents # text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false. # and return true. Otherwise return false.
def truncate_if_block(node, truncated) 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 node.inner_html = "#{node.inner_html}..." if node.next_sibling
true true
else else
......
---
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
......
...@@ -5,10 +5,10 @@ module Rouge ...@@ -5,10 +5,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). # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
def initialize(linenostart: 1) def initialize(tag: nil)
@linenostart = linenostart @line_number = 1
@line_number = linenostart @tag = tag
end end
def stream(tokens, &b) def stream(tokens, &b)
...@@ -17,7 +17,7 @@ module Rouge ...@@ -17,7 +17,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,594 @@ require 'spec_helper' ...@@ -2,437 +2,594 @@ 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
<<-GFM.strip_heredoc ```js
Foo Code goes here
```
GFM
)
This is an example of GFM verify(
'MarkdownFilter',
```js "Line with two spaces at the end \nto insert a linebreak",
Code goes here
```
GFM
)
verify( '`code`',
'MarkdownFilter', '`` code with ` ticks ``',
"Line with two spaces at the end \nto insert a linebreak", '> Quote',
'`code`', # multiline quote
'`` code with ` ticks ``', <<-GFM.strip_heredoc,
> Multiline
> Quote
>
> With multiple paragraphs
GFM
'> Quote', '![Image](https://example.com/image.png)',
# multiline quote '# Heading with no anchor link',
<<-GFM.strip_heredoc,
> Multiline
> Quote
>
> With multiple paragraphs
GFM
'![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 - Lists
<<-GFM.strip_heredoc, GFM
- Nested
# list with blockquote
<<-GFM.strip_heredoc,
- List
- Lists > Blockquote
GFM GFM
# list with blockquote '1. Numbered list item',
<<-GFM.strip_heredoc,
- List
> Blockquote # multiline numbered list item
GFM <<-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 1. Numbered lists
<<-GFM.strip_heredoc, GFM
1. Nested
'# Heading',
'## Heading',
'### Heading',
'#### Heading',
'##### Heading',
'###### Heading',
1. Numbered lists '**Bold**',
GFM
'# Heading', '_Italics_',
'## Heading',
'### Heading',
'#### Heading',
'##### Heading',
'###### Heading',
'**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 def verify(label, *gfms)
<<-GFM.strip_heredoc, aggregate_failures(label) do
| Centered | Right | Left | gfms.each do |gfm|
|:--------:|------:|------| html = gfm_to_html(gfm)
| Foo | Bar | **Baz** | output_gfm = html_to_gfm(html)
| Foo | Bar | **Baz** | expect(output_gfm.strip).to eq(gfm.strip)
GFM end
end
end
# table with empty heading # Fake a `current_user` helper
<<-GFM.strip_heredoc, def current_user
| | x | y | @feat.user
|---|---|---| end
| a | 1 | 0 |
| b | 0 | 1 |
GFM
)
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 js = <<-JS.strip_heredoc
(function(html) { (function(html) {
var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
var node = document.createElement('div'); var node = document.createElement('div');
node.innerHTML = html; node.innerHTML = html;
node = transformer(node);
if (!node) return null;
return window.gl.CopyAsGFM.nodeToGFM(node); return window.gl.CopyAsGFM.nodeToGFM(node);
})("#{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
...@@ -19,12 +19,12 @@ describe BlobHelper do ...@@ -19,12 +19,12 @@ describe BlobHelper do
describe '#highlight' do describe '#highlight' do
it 'returns plaintext for unknown lexer context' do it 'returns plaintext for unknown lexer context' do
result = helper.highlight(blob_name, no_context_content) 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 end
it 'highlights single block' do 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> 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"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] <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) expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end end
...@@ -43,10 +43,10 @@ describe BlobHelper do ...@@ -43,10 +43,10 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' } let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do let(:expected) do
%q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span> %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
<span id="LC2" class="line"><span class="gi">+bbb</span></span> <span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
<span id="LC3" class="line"><span class="gd">- ccc</span></span> <span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
<span id="LC4" class="line"> ddd</span></code></pre>) <span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
end end
it 'highlights each line properly' do it 'highlights each line properly' do
......
...@@ -28,7 +28,7 @@ describe EventsHelper do ...@@ -28,7 +28,7 @@ describe EventsHelper do
it 'displays the first line of a code block' do it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```" 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) expect(helper.event_note(input)).to match(expected)
end end
...@@ -55,10 +55,8 @@ describe EventsHelper do ...@@ -55,10 +55,8 @@ describe EventsHelper do
it 'preserves code color scheme' do it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```" input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \ expected = '<pre class="code highlight js-syntax-highlight ruby">' \
"<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
" <span class=\"s1\">\'hello world\'</span>\n" \ "</code></pre>"
"<span class=\"k\">end</span>\n" \
'</code></pre>'
expect(helper.event_note(input)).to eq(expected) expect(helper.event_note(input)).to eq(expected)
end end
......
...@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do ...@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do context "when no language is specified" do
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>') 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
end end
context "when a valid language is specified" do context "when a valid language is specified" do
it "highlights as that language" do it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>') 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
end end
context "when an invalid language is specified" do context "when an invalid language is specified" do
it "highlights as plaintext" do it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>') 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
end end
......
...@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do ...@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do
end end
it 'highlights and marks unchanged lines' do 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) expect(subject[2].text).to eq(code)
end end
it 'highlights and marks removed lines' do 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) expect(subject[4].text).to eq(code)
end end
it 'highlights and marks added lines' do 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) expect(subject[5].text).to eq(code)
end end
......
...@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do ...@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do
end end
it 'highlights all the lines properly' do 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[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"> <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[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"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</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 end
describe 'with CRLF' do describe 'with CRLF' do
...@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do ...@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do
end end
it 'strips extra LFs' do 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 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