summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
blob: c5f9fcf635811a6f14a208c596c1c8db2dc06d6d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// 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;
}

// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters
const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16)
const rainbowCodePoint = 127752; // parseInt('1F308', 16)
function isRainbowFlagEmoji(emojiUnicode) {
  const characters = Array.from(emojiUnicode);
  // Length 4 because flags are made of 2 characters which are surrogate pairs
  return (
    emojiUnicode.length === 4 &&
    characters[0].codePointAt(0) === baseFlagCodePoint &&
    characters[1].codePointAt(0) === rainbowCodePoint
  );
}

// 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 &&
    Array.from(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) {
  const firstCharacter = Array.from(emojiUnicode)[0];
  return (
    firstCharacter &&
    firstCharacter.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;
  Array.from(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);
  const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode);
  return (
    (unicodeSupportMap.flag && isFlagResult) ||
    (unicodeSupportMap.rainbowFlag && isRainbowFlagResult) ||
    (!isFlagResult && !isRainbowFlagResult)
  );
}

// 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)
  );
}

export {
  isEmojiUnicodeSupported as default,
  isFlagEmoji,
  isRainbowFlagEmoji,
  isKeycapEmoji,
  isSkinToneComboEmoji,
  isHorceRacingSkinToneComboEmoji,
  isPersonZwjEmoji,
};