提交 e6fc0207 编写于 作者: E Eric Eastwood

Use native unicode emojis

 - gl_emoji for falling back to image/css-sprite when the browser
   doesn't support an emoji
 - Markdown rendering (Banzai filter)
 - Autocomplete
 - Award emoji menu
    - Perceived perf
    - Immediate response because we now build client-side
 - Update `digests.json` generation in gemojione rake task to be more
   useful and  include `unicodeVersion`

MR: !9437

See issues

 - #26371
 - #27250
 - #22474
上级 f911b948
app/assets/images/emoji.png

1.0 MB | W: | H:

app/assets/images/emoji.png

1.2 MB | W: | H:

app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/emoji@2x.png

2.5 MB | W: | H:

app/assets/images/emoji@2x.png

2.8 MB | W: | H:

app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
  • 2-up
  • Swipe
  • Onion skin
const installCustomElements = require('document-register-element');
const emojiMap = require('emoji-map');
const emojiAliases = require('emoji-aliases');
const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map');
const spreadString = require('./gl_emoji/spread_string');
installCustomElements(window);
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
const name = emojiAliases[inputName] || inputName;
const emojiInfo = emojiMap[name];
const fallbackImageSrc = `${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
>
${contents}
</gl-emoji>
`;
}
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
function isFlagEmoji(emojiUnicode) {
const cp = emojiUnicode.codePointAt(0);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
function isKeycapEmoji(emojiUnicode) {
return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
}
// Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
// Check for `family_*`, `kiss_*`, `couple_*`
// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
const zwj = 8205; // parseInt('200D', 16)
const personStartCodePoint = 128102; // parseInt('1F466', 16)
const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
} else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
hasPersonEmoji = true;
}
});
return hasPersonEmoji && hasZwj;
}
// Helper so we don't have to run `isFlagEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult
);
}
// Helper so we don't have to run `isSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
!isHorseRacingSkinToneResult
);
}
// Helper so we don't have to run `isPersonZwjEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return (
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
}
// Takes in a support map and determines whether
// the given unicode emoji is supported on the platform.
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
}
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
const {
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
this.childNodes,
childNode => childNode.nodeType === 3,
);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if (
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
const emojiName = this.dataset.name;
this.innerHTML = emojiImageTag(emojiName, fallbackSrc);
}
}
};
document.registerElement('gl-emoji', {
prototype: GlEmojiElementProto,
});
module.exports = {
emojiImageTag,
glEmojiTag,
isEmojiUnicodeSupported,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
function knownCharCodeAt(givenString, index) {
const str = `${givenString}`;
const end = str.length;
const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
let idx = index;
while ((surrogatePairs.exec(str)) != null) {
const li = surrogatePairs.lastIndex;
if (li - 2 < idx) {
idx += 1;
} else {
break;
}
}
if (idx >= end || idx < 0) {
return NaN;
}
const code = str.charCodeAt(idx);
let high;
let low;
if (code >= 0xD800 && code <= 0xDBFF) {
high = code;
low = str.charCodeAt(idx + 1);
// Go one further, since one of the "characters" is part of a surrogate pair
return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
return code;
}
// See http://stackoverflow.com/a/38901550/796832
// ES5/PhantomJS compatible version of spreading a string
//
// [...'foo'] -> ['f', 'o', 'o']
// [...'🖐🏿'] -> ['🖐', '🏿']
function spreadString(str) {
const arr = [];
let i = 0;
while (!isNaN(knownCharCodeAt(str, i))) {
const codePoint = knownCharCodeAt(str, i);
arr.push(String.fromCodePoint(codePoint));
i += 1;
}
return arr;
}
module.exports = spreadString;
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
// woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
// family_mwgb
// Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
// horse_racing_tone5
// Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
'\u{1F575}\u{1F3FF}',
// person_with_ball_tone5
'\u{26F9}\u{1F3FF}',
// angel_tone5
'\u{1F47C}\u{1F3FF}',
],
// rofl, http://emojipedia.org/unicode-9.0/
'9.0': '\u{1F923}',
// metal, http://emojipedia.org/unicode-8.0/
'8.0': '\u{1F918}',
// spy, http://emojipedia.org/unicode-7.0/
'7.0': '\u{1F575}',
// expressionless, http://emojipedia.org/unicode-6.1/
6.1: '\u{1F611}',
// japanese_goblin, http://emojipedia.org/unicode-6.0/
'6.0': '\u{1F47A}',
// sailboat, http://emojipedia.org/unicode-5.2/
5.2: '\u{26F5}',
// mahjong, http://emojipedia.org/unicode-5.1/
5.1: '\u{1F004}',
// gear, http://emojipedia.org/unicode-4.1/
4.1: '\u{2699}',
// zap, http://emojipedia.org/unicode-4.0/
'4.0': '\u{26A1}',
// recycle, http://emojipedia.org/unicode-3.2/
3.2: '\u{267B}',
// information_source, http://emojipedia.org/unicode-3.0/
'3.0': '\u{2139}',
// heart, http://emojipedia.org/unicode-1.1/
1.1: '\u{2764}',
};
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
// Check for some sort of color other than black
if (hasColor && isVisible) {
return true;
}
return false;
}
const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
const isChrome = chromeMatches && chromeMatches.length > 0;
const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
function testUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize);
canvas.height = (numTestEntries * fontSize);
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
writeIndex += 1;
});
});
// Read from the canvas
const resultMap = {};
let readIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
const isTestSatisfied = [].concat(testEntry).every(() => {
// Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData(
0,
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
}
}
readIndex += 1;
return isValidEmoji;
});
resultMap[testKey] = isTestSatisfied;
});
resultMap.meta = {
isChrome,
chromeVersion,
};
return resultMap;
}
let unicodeSupportMap;
const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
module.exports = unicodeSupportMap;
......@@ -46,8 +46,8 @@ require('./lib/utils/common_utils');
},
},
EmojiFilter: {
'img.emoji'(el, text) {
return el.getAttribute('alt');
'gl-emoji'(el, text) {
return `:${el.getAttribute('data-name')}:`;
},
},
ImageLinkFilter: {
......
require('string.prototype.codepointat');
require('string.fromcodepoint');
/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
const emojiMap = require('emoji-map');
const emojiAliases = require('emoji-aliases');
const glEmoji = require('./behaviors/gl_emoji');
const glEmojiTag = glEmoji.glEmojiTag;
// Creates the variables for setting up GFM auto-completion
(function() {
if (window.gl == null) {
......@@ -26,7 +32,12 @@
},
// Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
templateFunction: function(name) {
return `<li>
${name} ${glEmojiTag(name)}
</li>
`;
}
},
// Team Members
Members: {
......@@ -113,7 +124,7 @@
$input.atwho({
at: ':',
displayTpl: function(value) {
return value.path != null ? this.Emoji.template : this.Loading.template;
return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
}.bind(this),
insertTpl: ':${name}:',
skipSpecialCharacterTest: true,
......@@ -355,6 +366,8 @@
this.isLoadingData[at] = true;
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (this.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
} else {
$.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
this.loadData($input, at, data);
......
......@@ -3,7 +3,6 @@
/* global Cookies */
/* global Flash */
/* global ConfirmDangerModal */
/* global AwardsHandler */
/* global Aside */
import jQuery from 'jquery';
......@@ -19,6 +18,15 @@ require('mousetrap/plugins/pause/mousetrap-pause');
require('vendor/fuzzaldrin-plus');
require('es6-promise').polyfill();
// extensions
require('./extensions/string');
require('./extensions/array');
require('./extensions/custom_event');
require('./extensions/element');
require('./extensions/jquery');
require('./extensions/object');
require('es6-promise').polyfill();
// expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery;
window.$ = jQuery;
......@@ -61,13 +69,6 @@ require('./templates/issuable_template_selectors');
require('./commit/file.js');
require('./commit/image_file.js');
// extensions
require('./extensions/array');
require('./extensions/custom_event');
require('./extensions/element');
require('./extensions/jquery');
require('./extensions/object');
// lib/utils
require('./lib/utils/animate');
require('./lib/utils/bootstrap_linked_tabs');
......@@ -99,7 +100,7 @@ require('./ajax_loading_spinner');
require('./api');
require('./aside');
require('./autosave');
require('./awards_handler');
const AwardsHandler = require('./awards_handler');
require('./breakpoints');
require('./broadcast_message');
require('./build');
......
......@@ -44,5 +44,6 @@
@import "framework/images.scss";
@import "framework/broadcast-messages";
@import "framework/emojis.scss";
@import "framework/emoji-sprites.scss";
@import "framework/icons.scss";
@import "framework/snippets.scss";
......@@ -7,6 +7,7 @@
.emoji-menu {
position: absolute;
top: 0;
margin-top: 3px;
padding: $gl-padding;
z-index: 9;
......@@ -20,7 +21,7 @@
opacity: 0;
transform: scale(.2);
transform-origin: 0 -45px;
transition: .3s cubic-bezier(.87,-.41,.19,1.44);
transition: .3s cubic-bezier(.67,.06,.19,1.44);
transition-property: transform, opacity;
&.is-aligned-right {
......@@ -47,12 +48,13 @@
}
.emoji-menu-list {
list-style: none;
padding-left: 0;
margin-bottom: 0;
padding-left: 0;
list-style: none;
}
.emoji-menu-list-item {
float: left;
padding: 3px;
margin-left: 1px;
margin-right: 1px;
......
此差异已折叠。
......@@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1;
* Fonts
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
/*
* Dropdowns
......
......@@ -188,6 +188,9 @@ ul.notes {
.note-body {
overflow-x: auto;
overflow-y: hidden;
// Help with emoji cut-off (most noticable in Safari)
// See https://i.imgur.com/0dg87Y9.png
padding-top: 1px;
.note-text {
word-wrap: break-word;
......
class EmojisController < ApplicationController
layout false
def index
end
end
class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:emojis, :members]
def emojis
render json: Gitlab::AwardEmoji.urls
end
before_action :load_autocomplete_service, except: [:members]
def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
......
module EmojiHelper
def emoji_icon(*args)
raw Gitlab::Emoji.gl_emoji_tag(*args)
end
end
......@@ -87,34 +87,6 @@ module IssuesHelper
icon('eye-slash') if issue.confidential?
end
def emoji_icon(name, unicode = nil, aliases = [], sprite: true)
unicode ||= Gitlab::Emoji.emoji_filename(name) rescue ""
data = {
aliases: aliases.join(" "),
emoji: name,
unicode_name: unicode
}
if sprite
# Emoji icons for the emoji menu, these use a spritesheet.
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
title: name,
data: data
else
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
content_tag :img, "",
class: "icon emoji",
title: name,
height: "20px",
width: "20px",
src: url_to_image("#{unicode}.png"),
data: data
end
end
def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
......
......@@ -16,4 +16,4 @@
- else
.empty-state
.text-center
%h4 There are no abuse reports! #{emoji_icon 'tada'}
%h4 There are no abuse reports! #{emoji_icon('tada')}
......@@ -4,7 +4,7 @@
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: (award_state_class(awards, current_user)),
data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
= emoji_icon(emoji)
%span.award-control-text.js-counter
= awards.count
......
.emoji-menu
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji"
.emoji-menu-content
- Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title
= Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
%button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" }
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
......@@ -4,7 +4,6 @@
- if project
:javascript
gl.GfmAutoComplete.dataSources = {
emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
......
---
title: Use native unicode emojis
merge_request:
author:
......@@ -91,7 +91,6 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
......
......@@ -27,9 +27,6 @@ Rails.application.routes.draw do
get '/autocomplete/users/:id' => 'autocomplete#user'
get '/autocomplete/projects' => 'autocomplete#projects'
# Emojis
resources :emojis, only: :index
# Search
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
......
......@@ -13,7 +13,6 @@ constraints(ProjectUrlConstrainer.new) do
resources :autocomplete_sources, only: [] do
collection do
get 'emojis'
get 'members'
get 'issues'
get 'merge_requests'
......
......@@ -132,6 +132,7 @@ var config = {
extensions: ['.js', '.es6', '.js.es6'],
alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'),
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
......
......@@ -90,7 +90,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I see search result for "hand"' do
page.within '.emoji-menu-content' do
expect(page).to have_selector '[data-emoji="raised_hand"]'
expect(page).to have_selector '[data-name="raised_hand"]'
end
end
......
此差异已折叠。
......@@ -17,8 +17,8 @@ module Banzai
next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
html = emoji_name_image_filter(content)
html = emoji_unicode_image_filter(html)
html = emoji_unicode_element_unicode_filter(content)
html = emoji_name_element_unicode_filter(html)
next if html == content
......@@ -27,33 +27,30 @@ module Banzai
doc
end
# Replace :emoji: with corresponding images.
# Replace :emoji: with corresponding gl-emoji unicode.
#
# text - String text to replace :emoji: in.
#
# Returns a String with :emoji: replaced with images.
def emoji_name_image_filter(text)
# Returns a String with :emoji: replaced with gl-emoji unicode.
def emoji_name_element_unicode_filter(text)
text.gsub(emoji_pattern) do |match|
name = $1
emoji_image_tag(name, emoji_url(name))
Gitlab::Emoji.gl_emoji_tag(name)
end
end
# Replace unicode emoji with corresponding images if they exist.
# Replace unicode emoji with corresponding gl-emoji unicode.
#
# text - String text to replace unicode emoji in.
#
# Returns a String with unicode emoji replaced with images.
def emoji_unicode_image_filter(text)
# Returns a String with unicode emoji replaced with gl-emoji unicode.
def emoji_unicode_element_unicode_filter(text)
text.gsub(emoji_unicode_pattern) do |moji|
emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji))
emoji_info = Gitlab::Emoji.emojis_by_moji[moji]
Gitlab::Emoji.gl_emoji_tag(emoji_info['name'])
end
end
def emoji_image_tag(emoji_name, emoji_url)
"<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
end
# Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
......@@ -66,52 +63,13 @@ module Banzai
private
def emoji_url(name)
emoji_path = emoji_filename(name)
if context[:asset_host]
# Asset host is specified.
url_to_image(emoji_path)
elsif context[:asset_root]
# Gitlab url is specified
File.join(context[:asset_root], url_to_image(emoji_path))
else
# All other cases
url_to_image(emoji_path)
end
end
def emoji_unicode_url(moji)
emoji_unicode_path = emoji_unicode_filename(moji)
if context[:asset_host]
url_to_image(emoji_unicode_path)
elsif context[:asset_root]
File.join(context[:asset_root], url_to_image(emoji_unicode_path))
else
url_to_image(emoji_unicode_path)
end
end
def url_to_image(image)
ActionController::Base.helpers.url_to_image(image)
end
def emoji_pattern
self.class.emoji_pattern
end
def emoji_filename(name)
"#{Gitlab::Emoji.emoji_filename(name)}.png"
end
def emoji_unicode_pattern
self.class.emoji_unicode_pattern
end
def emoji_unicode_filename(name)
"#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
end
end
end
end
module Gitlab
class AwardEmoji
CATEGORIES = {
objects: "Objects",
travel: "Travel",
symbols: "Symbols",
nature: "Nature",
people: "People",
activity: "Activity",
flags: "Flags",
food: "Food"
}.with_indifferent_access
def self.normalize_emoji_name(name)
aliases[name] || name
end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
def self.emojis
@emojis ||=
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
Gitlab::Emoji.emojis
end
def self.aliases
@aliases ||=
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
JSON.parse(File.read(json_path))
end
end
# Returns an Array of Emoji names and their asset URLs.
def self.urls
@urls ||= begin
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
# Construct the full asset path ourselves because
# ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
# of entries since it has to do a lot of extra work (e.g. regexps).
prefix = Gitlab::Application.config.assets.prefix
digest = Gitlab::Application.config.assets.digest
base =
if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
Gitlab::Application.config.relative_url_root
else
''
end
JSON.parse(File.read(path)).map do |hash|
fname =
if digest
"#{hash['unicode']}-#{hash['digest']}"
else
hash['unicode']
end
{ name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
end
end
Gitlab::Emoji.emojis_aliases
end
end
end
module Gitlab
module Emoji
extend self
@emoji_unicode_version = JSON.parse(File.read(File.absolute_path(File.dirname(__FILE__) + '/../../node_modules/emoji-unicode-version/emoji-unicode-version-map.json')))
@emoji_aliases = JSON.parse(File.read(File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')))
def emojis
Gemojione.index.instance_variable_get(:@emoji_by_name)
end
......@@ -18,6 +20,10 @@ module Gitlab
emojis.keys
end
def emojis_aliases
@emoji_aliases
end
def emoji_filename(name)
emojis[name]["unicode"]
end
......@@ -25,5 +31,22 @@ module Gitlab
def emoji_unicode_filename(moji)
emojis_by_moji[moji]["unicode"]
end
def emoji_unicode_version(name)
@emoji_unicode_version[name]
end
def emoji_image_tag(name, src)
"<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />"
end
# CSS sprite fallback takes precedence over image fallback
def gl_emoji_tag(name, sprite: false, force_fallback: false)
emoji_name = emojis_aliases[name] || name
emoji_info = emojis[emoji_name]
emoji_fallback_image_source = ActionController::Base.helpers.asset_url("emoji/#{emoji_info['name']}.png")
emoji_fallback_sprite_class = "emoji-#{emoji_name}"
"<gl-emoji #{force_fallback && sprite ? "class='emoji-icon #{emoji_fallback_sprite_class}'" : ""} data-name='#{emoji_name}' data-fallback-src='#{emoji_fallback_image_source}' #{sprite ? "data-fallback-sprite-class='#{emoji_fallback_sprite_class}'" : ""} data-unicode-version='#{emoji_unicode_version(emoji_name)}'>#{force_fallback && sprite === false ? emoji_image_tag(emoji_name, emoji_fallback_image_source) : emoji_info['moji']}</gl-emoji>"
end
end
end
......@@ -7,7 +7,6 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
......
......@@ -5,29 +5,29 @@ namespace :gemojione do
require 'json'
dir = Gemojione.images_path
digests = []
aliases = Hash.new { |hash, key| hash[key] = [] }
aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
JSON.parse(File.read(aliases_path)).each do |alias_name, real_name|
aliases[real_name] << alias_name
end
Gitlab::AwardEmoji.emojis.map do |name, emoji_hash|
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
digest = Digest::SHA256.file(fpath).hexdigest
digests << { name: name, unicode: emoji_hash['unicode'], digest: digest }
resultant_emoji_map = {}
Gitlab::Emoji.emojis.map do |name, emoji_hash|
# Ignore aliases
unless Gitlab::Emoji.emojis_aliases.key?(name)
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
hash_digest = Digest::SHA256.file(fpath).hexdigest
entry = {
category: emoji_hash['category'],
moji: emoji_hash['moji'],
unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
digest: hash_digest,
}
aliases[name].each do |alias_name|
digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest }
resultant_emoji_map[name] = entry
end
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
handle.write(JSON.pretty_generate(digests))
handle.write(JSON.pretty_generate(resultant_emoji_map))
end
end
......@@ -55,21 +55,42 @@ namespace :gemojione do
SPRITESHEET_WIDTH = 860
SPRITESHEET_HEIGHT = 840
# Setup a map to rename image files
emoji_uncicode_string_to_name_map = {}
Gitlab::Emoji.emojis.map do |name, emoji_hash|
# Ignore aliases
unless Gitlab::Emoji.emojis_aliases.key?(name)
emoji_uncicode_string_to_name_map[emoji_hash['unicode']] = name
end
end
# Copy the Gemojione assets to the temporary folder for renaming
emoji_dir = "app/assets/images/emoji"
FileUtils.rm_rf(emoji_dir)
FileUtils.mkdir_p(emoji_dir, mode: 0700)
FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir)
Dir.chdir(emoji_dir) do
Dir["**/*.png"].each do |png|
image_path = File.join(Dir.pwd, png)
rename_to_named_emoji_image!(emoji_uncicode_string_to_name_map, image_path)
end
end
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.images_path, tmpdir)
FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), SIZE)
tmp_image_path = File.join(tmpdir, png)
resize!(tmp_image_path, SIZE)
end
end
style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss))
style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
SpriteFactory.run!(File.join(tmpdir, 'png'), {
SpriteFactory.run!(tmpdir, {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
......@@ -83,7 +104,7 @@ namespace :gemojione do
# let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path}))
system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
# Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f|
......@@ -92,6 +113,8 @@ namespace :gemojione do
.emoji-icon {
background-image: image-url('emoji.png');
background-repeat: no-repeat;
color: transparent;
text-indent: -99em;
height: #{SIZE}px;
width: #{SIZE}px;
......@@ -112,16 +135,17 @@ namespace :gemojione do
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.images_path, tmpdir)
FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), RETINA)
tmp_image_path = File.join(tmpdir, png)
resize!(tmp_image_path, RETINA)
end
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.run!(File.join(tmpdir), {
SpriteFactory.run!(tmpdir, {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
......@@ -155,4 +179,20 @@ namespace :gemojione do
image.write(image_path) { self.quality = 100 }
image.destroy!
end
EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
def rename_to_named_emoji_image!(emoji_uncicode_string_to_name_map, image_path)
# Rename file from unicode to emoji name
matches = EMOJI_IMAGE_PATH_RE.match(image_path)
preceding_path = matches[1]
unicode_string = matches[2]
name = emoji_uncicode_string_to_name_map[unicode_string]
if name
new_png_path = File.join(preceding_path, "#{name}.png")
FileUtils.mv(image_path, new_png_path)
new_png_path
else
puts "Warning: emoji_uncicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}"
end
end
end
......@@ -57,7 +57,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in notes' do
expect(body).to match /Bug confirmed <img[^>]*\/>/
expect(body).to match /Bug confirmed <gl-emoji[^>]*>/
end
it 'has XHTML summaries in merge request descriptions' do
......
......@@ -252,7 +252,7 @@ describe 'Copy as GFM', feature: true, js: true do
<<-GFM.strip_heredoc
<a name="named-anchor"></a>
<sub>sub</sub>
<dl>
......
......@@ -105,7 +105,7 @@ feature 'Group', feature: true do
visit path
expect(page).to have_css('.group-home-desc > p > img')
expect(page).to have_css('.group-home-desc > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
......
......@@ -25,14 +25,14 @@ describe 'Awards Emoji', feature: true do
end
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
find('[data-name="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
find('[data-name="thumbsup"]').click
wait_for_ajax
expect(thumbsup_emoji).to have_text("1")
end
......@@ -44,7 +44,7 @@ describe 'Awards Emoji', feature: true do
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
find('[data-name="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
......@@ -123,9 +123,9 @@ describe 'Awards Emoji', feature: true do
end
unless status
first('[data-emoji="smiley"]').click
first('[data-name="smiley"]').click
else
find('[data-emoji="smiley"]').click
find('[data-name="smiley"]').click
end
wait_for_ajax
......
......@@ -18,7 +18,7 @@ feature 'Project', feature: true do
it 'passes through html-pipeline' do
project.update_attribute(:description, 'This project is the :poop:')
visit path
expect(page).to have_css('.project-home-desc > p > img')
expect(page).to have_css('.project-home-desc > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
......
......@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
to eq '<gl-emoji data-name="book" data-fallback-src="/assets/emoji/book.png" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
end
end
......
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
/* global AwardsHandler */
require('~/awards_handler');
require('./fixtures/emoji_menu');
require('es6-promise').polyfill();
const AwardsHandler = require('~/awards_handler');
(function() {
var awardsHandler, lazyAssert, urlRoot;
var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
awardsHandler = null;
......@@ -13,14 +13,6 @@ require('./fixtures/emoji_menu');
window.gon || (window.gon = {});
gl.emojiAliases = function() {
return {
'+1': 'thumbsup',
'-1': 'thumbsdown'
};
};
gon.award_menu_url = '/emojis';
urlRoot = gon.relative_url_root;
lazyAssert = function(done, assertFn) {
......@@ -32,22 +24,40 @@ require('./fixtures/emoji_menu');
};
describe('AwardsHandler', function() {
preloadFixtures('issues/open-issue.html.raw');
preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function() {
loadFixtures('issues/open-issue.html.raw');
loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
return cb();
};
})(this));
spyOn(jQuery, 'get').and.callFake(function(req, cb) {
return cb(window.emojiMenu);
});
let isEmojiMenuBuilt = false;
openAndWaitForEmojiMenu = function() {
return new Promise((resolve, reject) => {
if (isEmojiMenuBuilt) {
resolve();
} else {
$('.js-add-award').eq(0).click();
const $menu = $('.emoji-menu');
$menu.one('build-emoji-menu-finish', () => {
isEmojiMenuBuilt = true;
resolve();
});
// Fail after 1 second
setTimeout(reject, 1000);
}
});
};
});
afterEach(function() {
// restore original url root value
gon.relative_url_root = urlRoot;
awardsHandler.destroy();
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
......@@ -62,10 +72,9 @@ require('./fixtures/emoji_menu');
});
});
it('should also show emoji menu for the smiley icon in notes', function(done) {
$('.note-action-button').click();
$('.js-add-award.note-action-button').click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
var $emojiMenu = $('.emoji-menu');
return expect($emojiMenu.length).toBe(1);
});
});
......@@ -86,7 +95,7 @@ require('./fixtures/emoji_menu');
var $emojiButton, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
$emojiButton = $votesBlock.find('[data-emoji=heart]');
$emojiButton = $votesBlock.find('[data-name=heart]');
expect($emojiButton.length).toBe(1);
expect($emojiButton.next('.js-counter').text()).toBe('1');
return expect($votesBlock.hasClass('hidden')).toBe(false);
......@@ -96,14 +105,14 @@ require('./fixtures/emoji_menu');
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
$emojiButton = $votesBlock.find('[data-emoji=heart]');
$emojiButton = $votesBlock.find('[data-name=heart]');
return expect($emojiButton.length).toBe(0);
});
return it('should decrement the emoji counter', function() {
var $emojiButton, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
$emojiButton = $votesBlock.find('[data-emoji=heart]');
$emojiButton = $votesBlock.find('[data-name=heart]');
$emojiButton.next('.js-counter').text(5);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
expect($emojiButton.length).toBe(1);
......@@ -120,8 +129,8 @@ require('./fixtures/emoji_menu');
var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent();
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsDownEmoji = $votesBlock.find('[data-name=thumbsdown]').parent();
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
expect($thumbsUpEmoji.hasClass('active')).toBe(true);
expect($thumbsDownEmoji.hasClass('active')).toBe(false);
......@@ -138,9 +147,9 @@ require('./fixtures/emoji_menu');
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAward($votesBlock, awardUrl, 'fire', false);
expect($votesBlock.find('[data-emoji=fire]').length).toBe(1);
awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button'));
return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
expect($votesBlock.find('[data-name=fire]').length).toBe(1);
awardsHandler.removeEmoji($votesBlock.find('[data-name=fire]').closest('button'));
return expect($votesBlock.find('[data-name=fire]').length).toBe(0);
});
});
describe('::addYouToUserList', function() {
......@@ -148,7 +157,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
......@@ -158,7 +167,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
......@@ -170,7 +179,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
......@@ -181,7 +190,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
$thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
$thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You and sam');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
......@@ -190,42 +199,58 @@ require('./fixtures/emoji_menu');
});
});
describe('search', function() {
return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click();
expect($('[data-emoji=angel]').is(':visible')).toBe(true);
expect($('[data-emoji=anger]').is(':visible')).toBe(true);
$('#emoji_search').val('ali').trigger('keyup');
expect($('[data-emoji=angel]').is(':visible')).toBe(false);
expect($('[data-emoji=anger]').is(':visible')).toBe(false);
return expect($('[data-emoji=alien]').is(':visible')).toBe(true);
return it('should filter the emoji', function(done) {
return openAndWaitForEmojiMenu()
.then(() => {
expect($('[data-name=angel]').is(':visible')).toBe(true);
expect($('[data-name=anger]').is(':visible')).toBe(true);
$('#emoji_search').val('ali').trigger('input');
expect($('[data-name=angel]').is(':visible')).toBe(false);
expect($('[data-name=anger]').is(':visible')).toBe(false);
expect($('[data-name=alien]').is(':visible')).toBe(true);
})
.then(done)
.catch(() => {
done.fail('Failed to open and build emoji menu');
});
});
});
return describe('emoji menu', function() {
var openEmojiMenuAndAddEmoji, selector;
selector = '[data-emoji=sunglasses]';
openEmojiMenuAndAddEmoji = function() {
var $block, $emoji, $menu;
$('.js-add-award').eq(0).click();
$menu = $('.emoji-menu');
$block = $('.js-awards-block');
$emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
expect($emoji.length).toBe(1);
expect($block.find(selector).length).toBe(0);
$emoji.click();
expect($menu.hasClass('.is-visible')).toBe(false);
return expect($block.find(selector).length).toBe(1);
describe('emoji menu', function() {
const emojiSelector = '[data-name=sunglasses]';
const openEmojiMenuAndAddEmoji = function() {
return openAndWaitForEmojiMenu()
.then(() => {
const $menu = $('.emoji-menu');
const $block = $('.js-awards-block');
const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
expect($emoji.length).toBe(1);
expect($block.find(emojiSelector).length).toBe(0);
$emoji.click();
expect($menu.hasClass('.is-visible')).toBe(false);
expect($block.find(emojiSelector).length).toBe(1);
});
};
it('should add selected emoji to awards block', function() {
return openEmojiMenuAndAddEmoji();
it('should add selected emoji to awards block', function(done) {
return openEmojiMenuAndAddEmoji()
.then(done)
.catch(() => {
done.fail('Failed to open and build emoji menu');
});
});
return it('should remove already selected emoji', function() {
var $block, $emoji;
openEmojiMenuAndAddEmoji();
$('.js-add-award').eq(0).click();
$block = $('.js-awards-block');
$emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
$emoji.click();
return expect($block.find(selector).length).toBe(0);
it('should remove already selected emoji', function(done) {
return openEmojiMenuAndAddEmoji()
.then(() => {
$('.js-add-award').eq(0).click();
const $block = $('.js-awards-block');
const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`);
$emoji.click();
expect($block.find(emojiSelector).length).toBe(0);
})
.then(done)
.catch((err) => {
done.fail('Failed to open and build emoji menu');
});
});
});
});
......
此差异已折叠。
require('~/extensions/string');
require('~/extensions/array');
const glEmoji = require('~/behaviors/gl_emoji');
const glEmojiTag = glEmoji.glEmojiTag;
const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported;
const isFlagEmoji = glEmoji.isFlagEmoji;
const isKeycapEmoji = glEmoji.isKeycapEmoji;
const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji;
const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji;
const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji;
const emptySupportMap = {
personZwj: false,
horseRacing: false,
flag: false,
skinToneModifier: false,
'9.0': false,
'8.0': false,
'7.0': false,
6.1: false,
'6.0': false,
5.2: false,
5.1: false,
4.1: false,
'4.0': false,
3.2: false,
'3.0': false,
1.1: false,
};
const emojiFixtureMap = {
bomb: {
name: 'bomb',
moji: '💣',
unicodeVersion: '6.0',
},
construction_worker_tone5: {
name: 'construction_worker_tone5',
moji: '👷🏿',
unicodeVersion: '8.0',
},
five: {
name: 'five',
moji: '5️⃣',
unicodeVersion: '3.0',
},
};
function markupToDomElement(markup) {
const div = document.createElement('div');
div.innerHTML = markup;
return div.firstElementChild;
}
function testGlEmojiImageFallback(element, name, src) {
expect(element.tagName.toLowerCase()).toBe('img');
expect(element.getAttribute('src')).toBe(src);
expect(element.getAttribute('title')).toBe(`:${name}:`);
expect(element.getAttribute('alt')).toBe(`:${name}:`);
}
const defaults = {
forceFallback: false,
sprite: false,
};
function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) {
const opts = Object.assign({}, defaults, options);
expect(element.tagName.toLowerCase()).toBe('gl-emoji');
expect(element.dataset.name).toBe(name);
expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0);
expect(element.dataset.unicodeVersion).toBe(unicodeVersion);
const fallbackSpriteClass = `emoji-${name}`;
if (opts.sprite) {
expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass);
}
if (opts.forceFallback && opts.sprite) {
expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
}
if (opts.forceFallback && !opts.sprite) {
// Check for image fallback
testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
} else {
// Otherwise make sure things are still unicode text
expect(element.textContent.trim()).toBe(unicodeMoji);
}
}
describe('gl_emoji', () => {
describe('glEmojiTag', () => {
it('bomb emoji', () => {
const emojiKey = 'bomb';
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
);
});
it('bomb emoji with image fallback', () => {
const emojiKey = 'bomb';
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
forceFallback: true,
});
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
forceFallback: true,
},
);
});
it('bomb emoji with sprite fallback readiness', () => {
const emojiKey = 'bomb';
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
sprite: true,
});
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
sprite: true,
},
);
});
it('bomb emoji with sprite fallback', () => {
const emojiKey = 'bomb';
const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
forceFallback: true,
sprite: true,
});
const glEmojiElement = markupToDomElement(markup);
testGlEmojiElement(
glEmojiElement,
emojiFixtureMap[emojiKey].name,
emojiFixtureMap[emojiKey].unicodeVersion,
emojiFixtureMap[emojiKey].moji,
{
forceFallback: true,
sprite: true,
},
);
});
});
describe('isFlagEmoji', () => {
it('should detect flag_ac', () => {
expect(isFlagEmoji('🇦🇨')).toBeTruthy();
});
it('should detect flag_us', () => {
expect(isFlagEmoji('🇺🇸')).toBeTruthy();
});
it('should detect flag_zw', () => {
expect(isFlagEmoji('🇿🇼')).toBeTruthy();
});
it('should not detect flags', () => {
expect(isFlagEmoji('🎏')).toBeFalsy();
});
it('should not detect triangular_flag_on_post', () => {
expect(isFlagEmoji('🚩')).toBeFalsy();
});
it('should not detect single letter', () => {
expect(isFlagEmoji('🇦')).toBeFalsy();
});
it('should not detect >2 letters', () => {
expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
});
});
describe('isKeycapEmoji', () => {
it('should detect one(keycap)', () => {
expect(isKeycapEmoji('1️⃣')).toBeTruthy();
});
it('should detect nine(keycap)', () => {
expect(isKeycapEmoji('9️⃣')).toBeTruthy();
});
it('should not detect ten(keycap)', () => {
expect(isKeycapEmoji('🔟')).toBeFalsy();
});
it('should not detect hash(keycap)', () => {
expect(isKeycapEmoji('#⃣')).toBeFalsy();
});
});
describe('isSkinToneComboEmoji', () => {
it('should detect hand_splayed_tone5', () => {
expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy();
});
it('should not detect hand_splayed', () => {
expect(isSkinToneComboEmoji('🖐')).toBeFalsy();
});
it('should detect lifter_tone1', () => {
expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy();
});
it('should not detect lifter', () => {
expect(isSkinToneComboEmoji('🏋')).toBeFalsy();
});
it('should detect rowboat_tone4', () => {
expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy();
});
it('should not detect rowboat', () => {
expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
});
it('should not detect individual tone emoji', () => {
expect(isSkinToneComboEmoji('🏻')).toBeFalsy();
});
});
describe('isHorceRacingSkinToneComboEmoji', () => {
it('should detect horse_racing_tone2', () => {
expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy();
});
it('should not detect horse_racing', () => {
expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy();
});
});
describe('isPersonZwjEmoji', () => {
it('should detect couple_mm', () => {
expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy();
});
it('should not detect couple_with_heart', () => {
expect(isPersonZwjEmoji('💑')).toBeFalsy();
});
it('should not detect couplekiss', () => {
expect(isPersonZwjEmoji('💏')).toBeFalsy();
});
it('should detect family_mmb', () => {
expect(isPersonZwjEmoji('👨‍👨‍👦')).toBeTruthy();
});
it('should detect family_mwgb', () => {
expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBeTruthy();
});
it('should not detect family', () => {
expect(isPersonZwjEmoji('👪')).toBeFalsy();
});
it('should detect kiss_ww', () => {
expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBeTruthy();
});
it('should not detect girl', () => {
expect(isPersonZwjEmoji('👧')).toBeFalsy();
});
it('should not detect girl_tone5', () => {
expect(isPersonZwjEmoji('👧🏿')).toBeFalsy();
});
it('should not detect man', () => {
expect(isPersonZwjEmoji('👨')).toBeFalsy();
});
it('should not detect woman', () => {
expect(isPersonZwjEmoji('👩')).toBeFalsy();
});
});
describe('isEmojiUnicodeSupported', () => {
it('bomb(6.0) with 6.0 support', () => {
const emojiKey = 'bomb';
const unicodeSupportMap = Object.assign({}, emptySupportMap, {
'6.0': true,
});
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBeTruthy();
});
it('bomb(6.0) without 6.0 support', () => {
const emojiKey = 'bomb';
const unicodeSupportMap = emptySupportMap;
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBeFalsy();
});
it('bomb(6.0) without 6.0 but with 9.0 support', () => {
const emojiKey = 'bomb';
const unicodeSupportMap = Object.assign({}, emptySupportMap, {
'9.0': true,
});
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBeFalsy();
});
it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
const emojiKey = 'construction_worker_tone5';
const unicodeSupportMap = Object.assign({}, emptySupportMap, {
skinToneModifier: false,
'9.0': true,
'8.0': true,
'7.0': true,
6.1: true,
'6.0': true,
5.2: true,
5.1: true,
4.1: true,
'4.0': true,
3.2: true,
'3.0': true,
1.1: true,
});
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBeFalsy();
});
it('use native keycap on >=57 chrome', () => {
const emojiKey = 'five';
const unicodeSupportMap = Object.assign({}, emptySupportMap, {
'3.0': true,
meta: {
isChrome: true,
chromeVersion: 57,
},
});
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBeTruthy();
});
it('fallback keycap on <57 chrome', () => {
const emojiKey = 'five';
const unicodeSupportMap = Object.assign({}, emptySupportMap, {
'3.0': true,
meta: {
isChrome: true,
chromeVersion: 50,
},
});
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBeFalsy();
});
});
});
......@@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'replaces supported unicode emoji' do
doc = filter('<p>❤️</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'ignores unsupported emoji' do
......@@ -30,152 +30,97 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'correctly encodes the URL' do
doc = filter('<p>:+1:</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
expect(doc.css('gl-emoji').first.text).to eq '👍'
end
it 'correctly encodes unicode to the URL' do
doc = filter('<p>👍</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
expect(doc.css('gl-emoji').first.text).to eq '👍'
end
it 'matches at the start of a string' do
doc = filter(':+1:')
expect(doc.css('img').size).to eq 1
expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches at the start of a string' do
doc = filter("'👍'")
expect(doc.css('img').size).to eq 1
expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches at the end of a string' do
doc = filter('This gets a :-1:')
expect(doc.css('img').size).to eq 1
expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches at the end of a string' do
doc = filter('This gets a 👍')
expect(doc.css('img').size).to eq 1
expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches with adjacent text' do
doc = filter('+1 (:+1:)')
expect(doc.css('img').size).to eq 1
expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches with adjacent text' do
doc = filter('+1 (👍)')
expect(doc.css('img').size).to eq 1
expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
expect(doc.css('img').size).to eq 3
expect(doc.css('gl-emoji').size).to eq 3
end
it 'unicode matches multiple emoji in a row' do
doc = filter("'🙈🙉🙊'")
expect(doc.css('img').size).to eq 3
expect(doc.css('gl-emoji').size).to eq 3
end
it 'mixed matches multiple emoji in a row' do
doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
expect(doc.css('img').size).to eq 6
expect(doc.css('gl-emoji').size).to eq 6
end
it 'has a title attribute' do
it 'has a data-name attribute' do
doc = filter(':-1:')
expect(doc.css('img').first.attr('title')).to eq ':-1:'
expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
end
it 'unicode has a title attribute' do
doc = filter("'👎'")
expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
end
it 'has an alt attribute' do
it 'has a data-fallback-src attribute' do
doc = filter(':-1:')
expect(doc.css('img').first.attr('alt')).to eq ':-1:'
end
it 'unicode has an alt attribute' do
doc = filter("'👎'")
expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
end
it 'has an align attribute' do
doc = filter(':8ball:')
expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
end
it 'unicode has an align attribute' do
doc = filter("'🎱'")
expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
end
it 'has an emoji class' do
doc = filter(':cat:')
expect(doc.css('img').first.attr('class')).to eq 'emoji'
end
it 'unicode has an emoji class' do
doc = filter("'🐱'")
expect(doc.css('img').first.attr('class')).to eq 'emoji'
expect(doc.css('gl-emoji').first.attr('data-fallback-src')).to end_with '.png'
end
it 'has height and width attributes' do
doc = filter(':dog:')
img = doc.css('img').first
expect(img.attr('width')).to eq '20'
expect(img.attr('height')).to eq '20'
end
it 'unicode has height and width attributes' do
doc = filter("'🐶'")
img = doc.css('img').first
expect(img.attr('width')).to eq '20'
expect(img.attr('height')).to eq '20'
it 'has a data-unicode-version attribute' do
doc = filter(':-1:')
expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0'
end
it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.')
expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'unicode keeps whitespace intact' do
doc = filter('This deserves a 🎱, big time.')
expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
end
it 'uses a custom asset_root context' do
root = Gitlab.config.gitlab.url + 'gitlab/root'
doc = filter(':smile:', asset_root: root)
expect(doc.css('img').first.attr('src')).to start_with(root)
expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'uses a custom asset_host context' do
ActionController::Base.asset_host = 'https://cdn.example.com'
doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
end
it 'uses a custom asset_root context' do
root = Gitlab.config.gitlab.url + 'gitlab/root'
doc = filter("'🎱'", asset_root: root)
expect(doc.css('img').first.attr('src')).to start_with(root)
expect(doc.css('gl-emoji').first.attr('data-fallback-src')).to start_with('https://cdn.example.com')
end
it 'uses a custom asset_host context' do
ActionController::Base.asset_host = 'https://cdn.example.com'
doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
expect(doc.css('gl-emoji').first.attr('data-fallback-src')).to start_with('https://cdn.example.com')
end
end
require 'spec_helper'
describe Gitlab::AwardEmoji do
describe '.urls' do
after do
Gitlab::AwardEmoji.instance_variable_set(:@urls, nil)
end
subject { Gitlab::AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) }
it { is_expected.not_to be_empty }
context 'every Hash in the Array' do
it 'has the correct keys and values' do
subject.each do |hash|
expect(hash[:name]).to be_an_instance_of(String)
expect(hash[:path]).to be_an_instance_of(String)
end
end
end
context 'handles relative root' do
it 'includes the full path' do
allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab')
subject.each do |hash|
expect(hash[:name]).to be_an_instance_of(String)
expect(hash[:path]).to start_with('/gitlab')
end
end
end
end
describe '.emoji_by_category' do
it "only contains known categories" do
undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
expect(undefined_categories).to be_empty
end
end
end
......@@ -120,7 +120,6 @@ describe 'project routing' do
end
end
# emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
# members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
# issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
# merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
......@@ -128,7 +127,7 @@ describe 'project routing' do
# milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
# commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
describe Projects::AutocompleteSourcesController, 'routing' do
[:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
[:members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
it "to ##{action}" do
expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end
......
......@@ -26,10 +26,10 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
expect(actual).to have_selector('img.emoji', count: 10)
expect(actual).to have_selector('gl-emoji', count: 10)
image = actual.at_css('img.emoji')
expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets')
emoji_element = actual.at_css('gl-emoji')
expect(emoji_element['data-fallback-src'].to_s).to start_with('/assets')
end
end
......
......@@ -1395,6 +1395,10 @@ doctrine@1.5.0, doctrine@^1.2.2:
esutils "^2.0.2"
isarray "^1.0.0"
document-register-element@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940"
dom-serialize@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
......@@ -1439,6 +1443,10 @@ elliptic@^6.0.0:
hash.js "^1.0.0"
inherits "^2.0.1"
emoji-unicode-version@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/emoji-unicode-version/-/emoji-unicode-version-0.2.1.tgz#0ebf3666b5414097971d34994e299fce75cdbafc"
emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
......@@ -4115,6 +4123,14 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
string.fromcodepoint@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653"
string.prototype.codepointat@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78"
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册