Commit d90d141c authored by Tony Rom's avatar Tony Rom

Add Colors to GitLab Flavored Markdown

parent 501d81c5
...@@ -461,7 +461,7 @@ class GfmAutoComplete { ...@@ -461,7 +461,7 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80'); const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF'); 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); return regexp.exec(targetSubtext);
} }
......
...@@ -16,3 +16,33 @@ ...@@ -16,3 +16,33 @@
background-color: $user-mention-bg-hover; 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;
}
}
---
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: ...@@ -253,7 +253,7 @@ GFM will recognize the following:
| `@user_name` | specific user | | `@user_name` | specific user |
| `@group_name` | specific group | | `@group_name` | specific group |
| `@all` | entire team | | `@all` | entire team |
| `#123` | issue | | `#12345` | issue |
| `!123` | merge request | | `!123` | merge request |
| `$123` | snippet | | `$123` | snippet |
| `~123` | label by ID | | `~123` | label by ID |
...@@ -379,6 +379,45 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ ...@@ -379,6 +379,45 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
>**Note:** >**Note:**
This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual]. 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 ### Mermaid
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107) in > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107) in
......
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
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 ...@@ -7,6 +7,7 @@ module Banzai
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
Filter::ColorFilter,
Filter::AutolinkFilter, Filter::AutolinkFilter,
Filter::ExternalLinkFilter Filter::ExternalLinkFilter
] ]
......
...@@ -14,6 +14,7 @@ module Banzai ...@@ -14,6 +14,7 @@ module Banzai
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::MathFilter, Filter::MathFilter,
Filter::ColorFilter,
Filter::MermaidFilter, Filter::MermaidFilter,
Filter::VideoLinkFilter, Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter, Filter::ImageLazyLoadFilter,
......
...@@ -259,6 +259,10 @@ describe 'GitLab Markdown' do ...@@ -259,6 +259,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links expect(doc).to parse_video_links
end end
it 'includes ColorFilter' do
expect(doc).to parse_colors
end
end end
context 'wiki pipeline' do context 'wiki pipeline' do
...@@ -320,6 +324,10 @@ describe 'GitLab Markdown' do ...@@ -320,6 +324,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links expect(doc).to parse_video_links
end end
it 'includes ColorFilter' do
expect(doc).to parse_colors
end
end end
# Fake a `current_user` helper # Fake a `current_user` helper
......
...@@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such: ...@@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4) ![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 ### Mermaid
> If this is not rendered correctly, see > If this is not rendered correctly, see
......
...@@ -131,6 +131,7 @@ describe('GfmAutoComplete', function () { ...@@ -131,6 +131,7 @@ describe('GfmAutoComplete', function () {
describe('should not match special sequences', () => { describe('should not match special sequences', () => {
const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
const ShouldNotBePrependedBy = ['`'];
flagsUseDefaultMatcher.forEach((atSign) => { flagsUseDefaultMatcher.forEach((atSign) => {
ShouldNotBeFollowedBy.forEach((followedSymbol) => { ShouldNotBeFollowedBy.forEach((followedSymbol) => {
...@@ -140,6 +141,14 @@ describe('GfmAutoComplete', function () { ...@@ -140,6 +141,14 @@ describe('GfmAutoComplete', function () {
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); 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);
});
});
}); });
}); });
}); });
......
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
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 ...@@ -190,6 +190,27 @@ module MarkdownMatchers
expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4') expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4')
end end
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 end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
......
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