Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Jérome Perrin
gitlab-ce
Commits
68903277
Commit
68903277
authored
Mar 10, 2017
by
Douwe Maan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Copy code as GFM from diffs, blobs and GFM code blocks
parent
b7166806
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
537 additions
and
343 deletions
+537
-343
app/assets/javascripts/copy_as_gfm.js
app/assets/javascripts/copy_as_gfm.js
+60
-12
changelogs/unreleased/dm-copy-code-as-gfm.yml
changelogs/unreleased/dm-copy-code-as-gfm.yml
+4
-0
lib/banzai/filter/syntax_highlight_filter.rb
lib/banzai/filter/syntax_highlight_filter.rb
+1
-12
lib/gitlab/highlight.rb
lib/gitlab/highlight.rb
+2
-2
lib/rouge/formatters/html_gitlab.rb
lib/rouge/formatters/html_gitlab.rb
+3
-2
spec/features/copy_as_gfm_spec.rb
spec/features/copy_as_gfm_spec.rb
+467
-315
No files found.
app/assets/javascripts/copy_as_gfm.js
View file @
68903277
...
@@ -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
.
trim
Right
();
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
(
);
c
onst
gfm
=
CopyAsGFM
.
nodeToGFM
(
documentFragm
ent
);
c
lipboardData
.
setData
(
'
text/plain
'
,
el
.
textCont
ent
);
clipboardData
.
setData
(
'
text/x-gfm
'
,
gfm
);
clipboardData
.
setData
(
'
text/x-gfm
'
,
CopyAsGFM
.
nodeToGFM
(
el
)
);
}
}
handlePaste
(
e
)
{
pasteGFM
(
e
)
{
const
clipboardData
=
e
.
originalEvent
.
clipboardData
;
const
clipboardData
=
e
.
originalEvent
.
clipboardData
;
if
(
!
clipboardData
)
return
;
if
(
!
clipboardData
)
return
;
...
@@ -306,7 +307,54 @@ class CopyAsGFM {
...
@@ -306,7 +307,54 @@ class CopyAsGFM {
window
.
gl
.
utils
.
insertText
(
e
.
target
,
gfm
);
window
.
gl
.
utils
.
insertText
(
e
.
target
,
gfm
);
}
}
static
transformGFMSelection
(
documentFragment
)
{
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if
(
documentFragment
.
querySelector
(
'
.md, .wiki
'
))
return
null
;
return
documentFragment
;
}
static
transformCodeSelection
(
documentFragment
)
{
const
lineEls
=
documentFragment
.
querySelectorAll
(
'
.line
'
);
let
codeEl
;
if
(
lineEls
.
length
>
1
)
{
codeEl
=
document
.
createElement
(
'
pre
'
);
codeEl
.
className
=
'
code highlight
'
;
const
lang
=
lineEls
[
0
].
getAttribute
(
'
lang
'
);
if
(
lang
)
{
codeEl
.
setAttribute
(
'
lang
'
,
lang
);
}
}
else
{
codeEl
=
document
.
createElement
(
'
code
'
);
}
if
(
lineEls
.
length
>
0
)
{
for
(
let
i
=
0
;
i
<
lineEls
.
length
;
i
+=
1
)
{
const
lineEl
=
lineEls
[
i
];
codeEl
.
appendChild
(
lineEl
);
codeEl
.
appendChild
(
document
.
createTextNode
(
'
\n
'
));
}
}
else
{
codeEl
.
appendChild
(
documentFragment
);
}
return
codeEl
;
}
static
selectionToGFM
(
documentFragment
,
transformer
)
{
const
el
=
transformer
(
documentFragment
.
cloneNode
(
true
));
if
(
!
el
)
return
null
;
return
CopyAsGFM
.
nodeToGFM
(
el
);
}
static
nodeToGFM
(
node
)
{
static
nodeToGFM
(
node
)
{
if
(
node
.
nodeType
===
Node
.
COMMENT_NODE
)
{
return
''
;
}
if
(
node
.
nodeType
===
Node
.
TEXT_NODE
)
{
if
(
node
.
nodeType
===
Node
.
TEXT_NODE
)
{
return
node
.
textContent
;
return
node
.
textContent
;
}
}
...
...
changelogs/unreleased/dm-copy-code-as-gfm.yml
0 → 100644
View file @
68903277
---
title
:
Copy code as GFM from diffs, blobs and GFM code blocks
merge_request
:
author
:
lib/banzai/filter/syntax_highlight_filter.rb
View file @
68903277
...
@@ -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
lib/gitlab/highlight.rb
View file @
68903277
...
@@ -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
...
...
lib/rouge/formatters/html_gitlab.rb
View file @
68903277
...
@@ -6,9 +6,10 @@ module Rouge
...
@@ -6,9 +6,10 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
#
# [+linenostart+] The line number for the first line (default: 1).
# [+linenostart+] The line number for the first line (default: 1).
def
initialize
(
linenostart:
1
)
def
initialize
(
linenostart:
1
,
tag:
nil
)
@linenostart
=
linenostart
@linenostart
=
linenostart
@line_number
=
linenostart
@line_number
=
linenostart
@tag
=
tag
end
end
def
stream
(
tokens
,
&
b
)
def
stream
(
tokens
,
&
b
)
...
@@ -17,7 +18,7 @@ module Rouge
...
@@ -17,7 +18,7 @@ module Rouge
yield
"
\n
"
unless
is_first
yield
"
\n
"
unless
is_first
is_first
=
false
is_first
=
false
yield
%(<span id="LC#{@line_number}" class="line">)
yield
%(<span id="LC#{@line_number}" class="line"
lang="#{@tag}"
>)
line
.
each
{
|
token
,
value
|
yield
span
(
token
,
value
.
chomp
)
}
line
.
each
{
|
token
,
value
|
yield
span
(
token
,
value
.
chomp
)
}
yield
%(</span>)
yield
%(</span>)
...
...
spec/features/copy_as_gfm_spec.rb
View file @
68903277
...
@@ -2,8 +2,14 @@ require 'spec_helper'
...
@@ -2,8 +2,14 @@ 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
login_as
:admin
end
describe
'Copying rendered GFM'
do
before
do
before
do
@feat
=
MarkdownFeature
.
new
@feat
=
MarkdownFeature
.
new
...
@@ -410,17 +416,6 @@ describe 'Copy as GFM', feature: true, js: true do
...
@@ -410,17 +416,6 @@ describe 'Copy as GFM', feature: true, js: true do
alias_method
:gfm_to_html
,
:markdown
alias_method
:gfm_to_html
,
:markdown
def
html_to_gfm
(
html
)
js
=
<<-
JS
.
strip_heredoc
(function(html) {
var node = document.createElement('div');
node.innerHTML = html;
return window.gl.CopyAsGFM.nodeToGFM(node);
})("
#{
escape_javascript
(
html
)
}
")
JS
page
.
evaluate_script
(
js
)
end
def
verify
(
label
,
*
gfms
)
def
verify
(
label
,
*
gfms
)
aggregate_failures
(
label
)
do
aggregate_failures
(
label
)
do
gfms
.
each
do
|
gfm
|
gfms
.
each
do
|
gfm
|
...
@@ -435,4 +430,161 @@ describe 'Copy as GFM', feature: true, js: true do
...
@@ -435,4 +430,161 @@ describe 'Copy as GFM', feature: true, js: true do
def
current_user
def
current_user
@feat
.
user
@feat
.
user
end
end
end
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'
,
'`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(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 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
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment