Add Colors to GitLab Flavored Markdown
This commit is contained in:
parent
501d81c523
commit
d90d141c24
14 changed files with 360 additions and 2 deletions
|
@ -461,7 +461,7 @@ class GfmAutoComplete {
|
|||
const accentAChar = decodeURI('%C3%80');
|
||||
const accentYChar = decodeURI('%C3%BF');
|
||||
|
||||
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
|
||||
const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
|
||||
|
||||
return regexp.exec(targetSubtext);
|
||||
}
|
||||
|
|
|
@ -16,3 +16,33 @@
|
|||
background-color: $user-mention-bg-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.gfm-color_chip {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
margin-bottom: 2px;
|
||||
vertical-align: middle;
|
||||
border-radius: 3px;
|
||||
|
||||
$side: 0.9em;
|
||||
$bg-size: $side / 0.9;
|
||||
$bg-pos: $bg-size / 2;
|
||||
$bg-color: $gray-dark;
|
||||
|
||||
width: $side;
|
||||
height: $side;
|
||||
background: $white-light;
|
||||
background-image: linear-gradient(135deg, $bg-color 25%, transparent 0%, transparent 75%, $bg-color 0%),
|
||||
linear-gradient(135deg, $bg-color 25%, transparent 0%, transparent 75%, $bg-color 0%);
|
||||
background-size: $bg-size $bg-size;
|
||||
background-position: 0 0, $bg-pos $bg-pos;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $black-transparent;
|
||||
}
|
||||
}
|
||||
|
|
5
changelogs/unreleased/24167__color_label.yml
Normal file
5
changelogs/unreleased/24167__color_label.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Colors to GitLab Flavored Markdown
|
||||
merge_request: 16095
|
||||
author: Tony Rom <thetonyrom@gmail.com>
|
||||
type: added
|
|
@ -253,7 +253,7 @@ GFM will recognize the following:
|
|||
| `@user_name` | specific user |
|
||||
| `@group_name` | specific group |
|
||||
| `@all` | entire team |
|
||||
| `#123` | issue |
|
||||
| `#12345` | issue |
|
||||
| `!123` | merge request |
|
||||
| `$123` | snippet |
|
||||
| `~123` | label by ID |
|
||||
|
@ -379,6 +379,45 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
|
|||
>**Note:**
|
||||
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
|
||||
|
||||
### Colors
|
||||
|
||||
> If this is not rendered correctly, see
|
||||
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#colors
|
||||
|
||||
It is possible to have color written in HEX, RGB or HSL format rendered with a color indicator.
|
||||
|
||||
Color written inside backticks will be followed by a color "chip".
|
||||
|
||||
Examples:
|
||||
|
||||
`#F00`
|
||||
`#F00A`
|
||||
`#FF0000`
|
||||
`#FF0000AA`
|
||||
`RGB(0,255,0)`
|
||||
`RGB(0%,100%,0%)`
|
||||
`RGBA(0,255,0,0.7)`
|
||||
`HSL(540,70%,50%)`
|
||||
`HSLA(540,70%,50%,0.7)`
|
||||
|
||||
Becomes:
|
||||
|
||||
`#F00`
|
||||
`#F00A`
|
||||
`#FF0000`
|
||||
`#FF0000AA`
|
||||
`RGB(0,255,0)`
|
||||
`RGB(0%,100%,0%)`
|
||||
`RGBA(0,255,0,0.7)`
|
||||
`HSL(540,70%,50%)`
|
||||
`HSLA(540,70%,50%,0.7)`
|
||||
|
||||
#### Supported formats:
|
||||
|
||||
* HEX: `` `#RGB[A]` `` or `` `#RRGGBB[AA]` ``
|
||||
* RGB: `` `RGB[A](R, G, B[, A])` ``
|
||||
* HSL: `` `HSL[A](H, S, L[, A])` ``
|
||||
|
||||
### Mermaid
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107) in
|
||||
|
|
50
lib/banzai/color_parser.rb
Normal file
50
lib/banzai/color_parser.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
module Banzai
|
||||
module ColorParser
|
||||
ALPHA = /0(?:\.\d+)?|\.\d+|1(?:\.0+)?/ # 0.0..1.0
|
||||
PERCENTS = /(?:\d{1,2}|100)%/ # 00%..100%
|
||||
ALPHA_CHANNEL = /(?:,\s*(?:#{ALPHA}|#{PERCENTS}))?/
|
||||
BITS = /\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5])/ # 00..255
|
||||
DEGS = /-?\d+(?:deg)?/i # [-]digits[deg]
|
||||
RADS = /-?(?:\d+(?:\.\d+)?|\.\d+)rad/i # [-](digits[.digits] OR .digits)rad
|
||||
HEX_FORMAT = /\#(?:\h{3}|\h{4}|\h{6}|\h{8})/
|
||||
RGB_FORMAT = /
|
||||
(?:rgba?
|
||||
\(
|
||||
(?:
|
||||
(?:(?:#{BITS},\s*){2}#{BITS})
|
||||
|
|
||||
(?:(?:#{PERCENTS},\s*){2}#{PERCENTS})
|
||||
)
|
||||
#{ALPHA_CHANNEL}
|
||||
\)
|
||||
)
|
||||
/xi
|
||||
HSL_FORMAT = /
|
||||
(?:hsla?
|
||||
\(
|
||||
(?:#{DEGS}|#{RADS}),\s*#{PERCENTS},\s*#{PERCENTS}
|
||||
#{ALPHA_CHANNEL}
|
||||
\)
|
||||
)
|
||||
/xi
|
||||
|
||||
FORMATS = [HEX_FORMAT, RGB_FORMAT, HSL_FORMAT].freeze
|
||||
|
||||
class << self
|
||||
# Public: Analyzes whether the String is a color code.
|
||||
#
|
||||
# text - The String to be parsed.
|
||||
#
|
||||
# Returns the recognized color String or nil if none was found.
|
||||
def parse(text)
|
||||
text if color_format =~ text
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def color_format
|
||||
@color_format ||= /\A(#{Regexp.union(FORMATS)})\z/ix
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
31
lib/banzai/filter/color_filter.rb
Normal file
31
lib/banzai/filter/color_filter.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
module Banzai
|
||||
module Filter
|
||||
# HTML filter that renders `color` followed by a color "chip".
|
||||
#
|
||||
class ColorFilter < HTML::Pipeline::Filter
|
||||
COLOR_CHIP_CLASS = 'gfm-color_chip'.freeze
|
||||
|
||||
def call
|
||||
doc.css('code').each do |node|
|
||||
color = ColorParser.parse(node.content)
|
||||
node << color_chip(color) if color
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def color_chip(color)
|
||||
checkerboard = doc.document.create_element('span', class: COLOR_CHIP_CLASS)
|
||||
chip = doc.document.create_element('span', style: inline_styles(color: color))
|
||||
|
||||
checkerboard << chip
|
||||
end
|
||||
|
||||
def inline_styles(color:)
|
||||
"background-color: #{color};"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,6 +7,7 @@ module Banzai
|
|||
Filter::SanitizationFilter,
|
||||
|
||||
Filter::EmojiFilter,
|
||||
Filter::ColorFilter,
|
||||
Filter::AutolinkFilter,
|
||||
Filter::ExternalLinkFilter
|
||||
]
|
||||
|
|
|
@ -14,6 +14,7 @@ module Banzai
|
|||
Filter::SyntaxHighlightFilter,
|
||||
|
||||
Filter::MathFilter,
|
||||
Filter::ColorFilter,
|
||||
Filter::MermaidFilter,
|
||||
Filter::VideoLinkFilter,
|
||||
Filter::ImageLazyLoadFilter,
|
||||
|
|
|
@ -259,6 +259,10 @@ describe 'GitLab Markdown' do
|
|||
it 'includes VideoLinkFilter' do
|
||||
expect(doc).to parse_video_links
|
||||
end
|
||||
|
||||
it 'includes ColorFilter' do
|
||||
expect(doc).to parse_colors
|
||||
end
|
||||
end
|
||||
|
||||
context 'wiki pipeline' do
|
||||
|
@ -320,6 +324,10 @@ describe 'GitLab Markdown' do
|
|||
it 'includes VideoLinkFilter' do
|
||||
expect(doc).to parse_video_links
|
||||
end
|
||||
|
||||
it 'includes ColorFilter' do
|
||||
expect(doc).to parse_colors
|
||||
end
|
||||
end
|
||||
|
||||
# Fake a `current_user` helper
|
||||
|
|
12
spec/fixtures/markdown.md.erb
vendored
12
spec/fixtures/markdown.md.erb
vendored
|
@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such:
|
|||
|
||||
![My Video](/assets/videos/gitlab-demo.mp4)
|
||||
|
||||
### Colors
|
||||
|
||||
`#F00`
|
||||
`#F00A`
|
||||
`#FF0000`
|
||||
`#FF0000AA`
|
||||
`RGB(0,255,0)`
|
||||
`RGB(0%,100%,0%)`
|
||||
`RGBA(0,255,0,0.7)`
|
||||
`HSL(540,70%,50%)`
|
||||
`HSLA(540,70%,50%,0.7)`
|
||||
|
||||
### Mermaid
|
||||
|
||||
> If this is not rendered correctly, see
|
||||
|
|
|
@ -131,6 +131,7 @@ describe('GfmAutoComplete', function () {
|
|||
|
||||
describe('should not match special sequences', () => {
|
||||
const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
|
||||
const ShouldNotBePrependedBy = ['`'];
|
||||
|
||||
flagsUseDefaultMatcher.forEach((atSign) => {
|
||||
ShouldNotBeFollowedBy.forEach((followedSymbol) => {
|
||||
|
@ -140,6 +141,14 @@ describe('GfmAutoComplete', function () {
|
|||
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
ShouldNotBePrependedBy.forEach((prependedSymbol) => {
|
||||
const seq = prependedSymbol + atSign;
|
||||
|
||||
it(`should not match "${seq}"`, () => {
|
||||
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
90
spec/lib/banzai/color_parser_spec.rb
Normal file
90
spec/lib/banzai/color_parser_spec.rb
Normal file
|
@ -0,0 +1,90 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::ColorParser do
|
||||
describe '.parse' do
|
||||
context 'HEX format' do
|
||||
[
|
||||
'#abc', '#ABC',
|
||||
'#d2d2d2', '#D2D2D2',
|
||||
'#123a', '#123A',
|
||||
'#123456aa', '#123456AA'
|
||||
].each do |color|
|
||||
it "parses the valid hex color #{color}" do
|
||||
expect(subject.parse(color)).to eq(color)
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
'#', '#1', '#12', '#12g', '#12G',
|
||||
'#12345', '#r2r2r2', '#R2R2R2', '#1234567',
|
||||
'# 123', '# 1234', '# 123456', '# 12345678',
|
||||
'#1 2 3', '#123 4', '#12 34 56', '#123456 78'
|
||||
].each do |color|
|
||||
it "does not parse the invalid hex color #{color}" do
|
||||
expect(subject.parse(color)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'RGB format' do
|
||||
[
|
||||
'rgb(0,0,0)', 'rgb(255,255,255)',
|
||||
'rgb(0, 0, 0)', 'RGB(0,0,0)',
|
||||
'rgb(0,0,0,0)', 'rgb(0,0,0,0.0)', 'rgb(0,0,0,.0)',
|
||||
'rgb(0,0,0, 0)', 'rgb(0,0,0, 0.0)', 'rgb(0,0,0, .0)',
|
||||
'rgb(0,0,0,1)', 'rgb(0,0,0,1.0)',
|
||||
'rgba(0,0,0)', 'rgba(0,0,0,0)', 'RGBA(0,0,0)',
|
||||
'rgb(0%,0%,0%)', 'rgba(0%,0%,0%,0%)'
|
||||
].each do |color|
|
||||
it "parses the valid rgb color #{color}" do
|
||||
expect(subject.parse(color)).to eq(color)
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
'FOOrgb(0,0,0)', 'rgb(0,0,0)BAR',
|
||||
'rgb(0,0,-1)', 'rgb(0,0,-0)', 'rgb(0,0,256)',
|
||||
'rgb(0,0,0,-0.1)', 'rgb(0,0,0,-0.0)', 'rgb(0,0,0,-.1)',
|
||||
'rgb(0,0,0,1.1)', 'rgb(0,0,0,2)',
|
||||
'rgba(0,0,0,)', 'rgba(0,0,0,0.)', 'rgba(0,0,0,1.)',
|
||||
'rgb(0,0,0%)', 'rgb(101%,0%,0%)'
|
||||
].each do |color|
|
||||
it "does not parse the invalid rgb color #{color}" do
|
||||
expect(subject.parse(color)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'HSL format' do
|
||||
[
|
||||
'hsl(0,0%,0%)', 'hsl(0,100%,100%)',
|
||||
'hsl(540,0%,0%)', 'hsl(-720,0%,0%)',
|
||||
'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)',
|
||||
'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)',
|
||||
'hsl(0,0%,0%,0)', 'hsl(0,0%,0%,0.0)', 'hsl(0,0%,0%,.0)',
|
||||
'hsl(0,0%,0%, 0)', 'hsl(0,0%,0%, 0.0)', 'hsl(0,0%,0%, .0)',
|
||||
'hsl(0,0%,0%,1)', 'hsl(0,0%,0%,1.0)',
|
||||
'hsla(0,0%,0%)', 'hsla(0,0%,0%,0)', 'HSLA(0,0%,0%)',
|
||||
'hsl(1rad,0%,0%)', 'hsl(1.1rad,0%,0%)', 'hsl(.1rad,0%,0%)',
|
||||
'hsl(-1rad,0%,0%)', 'hsl(1RAD,0%,0%)'
|
||||
].each do |color|
|
||||
it "parses the valid hsl color #{color}" do
|
||||
expect(subject.parse(color)).to eq(color)
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
'hsl(+0,0%,0%)', 'hsl(0,0,0%)', 'hsl(0,0%,0)', 'hsl(0 deg,0%,0%)',
|
||||
'hsl(0,-0%,0%)', 'hsl(0,101%,0%)', 'hsl(0,-1%,0%)',
|
||||
'hsl(0,0%,0%,-0.1)', 'hsl(0,0%,0%,-.1)',
|
||||
'hsl(0,0%,0%,1.1)', 'hsl(0,0%,0%,2)',
|
||||
'hsl(0,0%,0%,)', 'hsl(0,0%,0%,0.)', 'hsl(0,0%,0%,1.)',
|
||||
'hsl(deg,0%,0%)', 'hsl(rad,0%,0%)'
|
||||
].each do |color|
|
||||
it "does not parse the invalid hsl color #{color}" do
|
||||
expect(subject.parse(color)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
61
spec/lib/banzai/filter/color_filter_spec.rb
Normal file
61
spec/lib/banzai/filter/color_filter_spec.rb
Normal file
|
@ -0,0 +1,61 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Banzai::Filter::ColorFilter, lib: true do
|
||||
include FilterSpecHelper
|
||||
|
||||
let(:color) { '#F00' }
|
||||
let(:color_chip_selector) { 'code > span.gfm-color_chip > span' }
|
||||
|
||||
['#123', '#1234', '#123456', '#12345678',
|
||||
'rgb(0,0,0)', 'RGB(0, 0, 0)', 'rgba(0,0,0,1)', 'RGBA(0,0,0,0.7)',
|
||||
'hsl(270,30%,50%)', 'HSLA(270, 30%, 50%, .7)'].each do |color|
|
||||
it "inserts color chip for supported color format #{color}" do
|
||||
content = code_tag(color)
|
||||
doc = filter(content)
|
||||
color_chip = doc.at_css(color_chip_selector)
|
||||
|
||||
expect(color_chip.content).to be_empty
|
||||
expect(color_chip.parent[:class]).to eq 'gfm-color_chip'
|
||||
expect(color_chip[:style]).to eq "background-color: #{color};"
|
||||
end
|
||||
end
|
||||
|
||||
it 'ignores valid color code without backticks(code tags)' do
|
||||
doc = filter(color)
|
||||
|
||||
expect(doc.css('span.gfm-color_chip').size).to be_zero
|
||||
end
|
||||
|
||||
it 'ignores valid color code with prepended space' do
|
||||
content = code_tag(' ' + color)
|
||||
doc = filter(content)
|
||||
|
||||
expect(doc.css(color_chip_selector).size).to be_zero
|
||||
end
|
||||
|
||||
it 'ignores valid color code with appended space' do
|
||||
content = code_tag(color + ' ')
|
||||
doc = filter(content)
|
||||
|
||||
expect(doc.css(color_chip_selector).size).to be_zero
|
||||
end
|
||||
|
||||
it 'ignores valid color code surrounded by spaces' do
|
||||
content = code_tag(' ' + color + ' ')
|
||||
doc = filter(content)
|
||||
|
||||
expect(doc.css(color_chip_selector).size).to be_zero
|
||||
end
|
||||
|
||||
it 'ignores invalid color code' do
|
||||
invalid_color = '#BAR'
|
||||
content = code_tag(invalid_color)
|
||||
doc = filter(content)
|
||||
|
||||
expect(doc.css(color_chip_selector).size).to be_zero
|
||||
end
|
||||
|
||||
def code_tag(string)
|
||||
"<code>#{string}</code>"
|
||||
end
|
||||
end
|
|
@ -190,6 +190,27 @@ module MarkdownMatchers
|
|||
expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4')
|
||||
end
|
||||
end
|
||||
|
||||
# ColorFilter
|
||||
matcher :parse_colors do
|
||||
set_default_markdown_messages
|
||||
|
||||
match do |actual|
|
||||
color_chips = actual.css('code > span.gfm-color_chip > span')
|
||||
|
||||
expect(color_chips.count).to eq(9)
|
||||
|
||||
[
|
||||
'#F00', '#F00A', '#FF0000', '#FF0000AA', 'RGB(0,255,0)',
|
||||
'RGB(0%,100%,0%)', 'RGBA(0,255,0,0.7)', 'HSL(540,70%,50%)',
|
||||
'HSLA(540,70%,50%,0.7)'
|
||||
].each_with_index do |color, i|
|
||||
parsed_color = Banzai::ColorParser.parse(color)
|
||||
expect(color_chips[i]['style']).to match("background-color: #{parsed_color};")
|
||||
expect(color_chips[i].parent.parent.content).to match(color)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
|
||||
|
|
Loading…
Reference in a new issue