2018-06-26 14:09:39 +02:00
|
|
|
#include "providers/emoji/Emojis.hpp"
|
2018-06-05 18:53:49 +02:00
|
|
|
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "Application.hpp"
|
|
|
|
#include "debug/Log.hpp"
|
|
|
|
#include "singletons/SettingsManager.hpp"
|
2018-06-06 01:29:43 +02:00
|
|
|
|
2018-06-20 19:10:54 +02:00
|
|
|
#include <QFile>
|
|
|
|
|
2018-06-05 18:53:49 +02:00
|
|
|
namespace chatterino {
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
void parseEmoji(const std::shared_ptr<EmojiData> &emojiData, const rapidjson::Value &unparsedEmoji,
|
|
|
|
QString shortCode = QString())
|
|
|
|
{
|
|
|
|
static uint unicodeBytes[4];
|
|
|
|
|
|
|
|
struct {
|
|
|
|
bool apple;
|
|
|
|
bool google;
|
|
|
|
bool twitter;
|
|
|
|
bool emojione;
|
|
|
|
bool facebook;
|
|
|
|
bool messenger;
|
|
|
|
} capabilities;
|
|
|
|
|
|
|
|
if (!shortCode.isEmpty()) {
|
2018-06-22 22:42:53 +02:00
|
|
|
emojiData->shortCodes.push_back(shortCode);
|
|
|
|
} else {
|
|
|
|
const auto &shortCodes = unparsedEmoji["short_names"];
|
|
|
|
for (const auto &shortCode : shortCodes.GetArray()) {
|
|
|
|
emojiData->shortCodes.emplace_back(shortCode.GetString());
|
|
|
|
}
|
2018-06-06 01:29:43 +02:00
|
|
|
}
|
2018-06-22 22:42:53 +02:00
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
rj::getSafe(unparsedEmoji, "non_qualified", emojiData->nonQualifiedCode);
|
|
|
|
rj::getSafe(unparsedEmoji, "unified", emojiData->unifiedCode);
|
|
|
|
|
|
|
|
rj::getSafe(unparsedEmoji, "has_img_apple", capabilities.apple);
|
|
|
|
rj::getSafe(unparsedEmoji, "has_img_google", capabilities.google);
|
|
|
|
rj::getSafe(unparsedEmoji, "has_img_twitter", capabilities.twitter);
|
|
|
|
rj::getSafe(unparsedEmoji, "has_img_emojione", capabilities.emojione);
|
|
|
|
rj::getSafe(unparsedEmoji, "has_img_facebook", capabilities.facebook);
|
|
|
|
rj::getSafe(unparsedEmoji, "has_img_messenger", capabilities.messenger);
|
|
|
|
|
|
|
|
if (capabilities.apple) {
|
|
|
|
emojiData->capabilities.insert("Apple");
|
|
|
|
}
|
|
|
|
if (capabilities.google) {
|
|
|
|
emojiData->capabilities.insert("Google");
|
|
|
|
}
|
|
|
|
if (capabilities.twitter) {
|
|
|
|
emojiData->capabilities.insert("Twitter");
|
|
|
|
}
|
|
|
|
if (capabilities.emojione) {
|
|
|
|
emojiData->capabilities.insert("EmojiOne 3");
|
|
|
|
}
|
|
|
|
if (capabilities.facebook) {
|
|
|
|
emojiData->capabilities.insert("Facebook");
|
|
|
|
}
|
|
|
|
if (capabilities.messenger) {
|
|
|
|
emojiData->capabilities.insert("Messenger");
|
|
|
|
}
|
|
|
|
|
|
|
|
QStringList unicodeCharacters;
|
|
|
|
if (!emojiData->nonQualifiedCode.isEmpty()) {
|
|
|
|
unicodeCharacters = emojiData->nonQualifiedCode.toLower().split('-');
|
|
|
|
} else {
|
|
|
|
unicodeCharacters = emojiData->unifiedCode.toLower().split('-');
|
|
|
|
}
|
|
|
|
if (unicodeCharacters.length() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int numUnicodeBytes = 0;
|
|
|
|
|
|
|
|
for (const QString &unicodeCharacter : unicodeCharacters) {
|
|
|
|
unicodeBytes[numUnicodeBytes++] = QString(unicodeCharacter).toUInt(nullptr, 16);
|
|
|
|
}
|
|
|
|
|
|
|
|
emojiData->value = QString::fromUcs4(unicodeBytes, numUnicodeBytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2018-06-05 18:53:49 +02:00
|
|
|
void Emojis::load()
|
2018-06-06 01:29:43 +02:00
|
|
|
{
|
|
|
|
this->loadEmojis();
|
|
|
|
|
|
|
|
this->loadEmojiOne2Capabilities();
|
2018-06-06 11:43:02 +02:00
|
|
|
|
|
|
|
this->sortEmojis();
|
|
|
|
|
|
|
|
this->loadEmojiSet();
|
2018-06-06 01:29:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void Emojis::loadEmojis()
|
|
|
|
{
|
2018-06-06 11:43:02 +02:00
|
|
|
std::map<std::string, QString> toneNames;
|
|
|
|
toneNames["1F3FB"] = "tone1";
|
|
|
|
toneNames["1F3FC"] = "tone2";
|
|
|
|
toneNames["1F3FD"] = "tone3";
|
|
|
|
toneNames["1F3FE"] = "tone4";
|
|
|
|
toneNames["1F3FF"] = "tone5";
|
2018-06-06 12:53:19 +02:00
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
QFile file(":/emoji.json");
|
|
|
|
file.open(QFile::ReadOnly);
|
|
|
|
QTextStream s1(&file);
|
|
|
|
QString data = s1.readAll();
|
|
|
|
rapidjson::Document root;
|
|
|
|
rapidjson::ParseResult result = root.Parse(data.toUtf8(), data.length());
|
|
|
|
|
|
|
|
if (result.Code() != rapidjson::kParseErrorNone) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()),
|
2018-06-26 17:20:03 +02:00
|
|
|
result.Offset());
|
2018-06-06 01:29:43 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const auto &unparsedEmoji : root.GetArray()) {
|
|
|
|
auto emojiData = std::make_shared<EmojiData>();
|
|
|
|
parseEmoji(emojiData, unparsedEmoji);
|
|
|
|
|
2018-06-22 22:42:53 +02:00
|
|
|
for (const auto &shortCode : emojiData->shortCodes) {
|
|
|
|
this->emojiShortCodeToEmoji.insert(shortCode, emojiData);
|
|
|
|
this->shortCodes.emplace_back(shortCode);
|
|
|
|
}
|
2018-06-06 01:29:43 +02:00
|
|
|
|
|
|
|
this->emojiFirstByte[emojiData->value.at(0)].append(emojiData);
|
|
|
|
|
|
|
|
this->emojis.insert(emojiData->unifiedCode, emojiData);
|
|
|
|
|
2018-06-06 11:43:02 +02:00
|
|
|
if (unparsedEmoji.HasMember("skin_variations")) {
|
2018-06-06 01:29:43 +02:00
|
|
|
for (const auto &skinVariation : unparsedEmoji["skin_variations"].GetObject()) {
|
2018-06-06 12:53:19 +02:00
|
|
|
std::string tone = skinVariation.name.GetString();
|
2018-06-06 01:29:43 +02:00
|
|
|
const auto &variation = skinVariation.value;
|
|
|
|
|
|
|
|
auto variationEmojiData = std::make_shared<EmojiData>();
|
|
|
|
|
2018-06-06 12:40:26 +02:00
|
|
|
auto toneNameIt = toneNames.find(tone);
|
|
|
|
if (toneNameIt == toneNames.end()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Tone with key {} does not exist in tone names map", tone);
|
2018-06-06 12:40:26 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
parseEmoji(variationEmojiData, variation,
|
2018-06-22 22:42:53 +02:00
|
|
|
emojiData->shortCodes[0] + "_" + toneNameIt->second);
|
2018-06-06 01:29:43 +02:00
|
|
|
|
2018-06-22 22:42:53 +02:00
|
|
|
this->emojiShortCodeToEmoji.insert(variationEmojiData->shortCodes[0],
|
2018-06-06 01:29:43 +02:00
|
|
|
variationEmojiData);
|
2018-06-22 22:42:53 +02:00
|
|
|
this->shortCodes.push_back(variationEmojiData->shortCodes[0]);
|
2018-06-06 01:29:43 +02:00
|
|
|
|
|
|
|
this->emojiFirstByte[variationEmojiData->value.at(0)].append(variationEmojiData);
|
|
|
|
|
|
|
|
this->emojis.insert(variationEmojiData->unifiedCode, variationEmojiData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-06-06 11:43:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void Emojis::loadEmojiOne2Capabilities()
|
|
|
|
{
|
|
|
|
QFile file(":/emojidata.txt");
|
|
|
|
file.open(QFile::ReadOnly);
|
|
|
|
QTextStream in(&file);
|
|
|
|
|
|
|
|
while (!in.atEnd()) {
|
|
|
|
// Line example: sunglasses 1f60e
|
|
|
|
QString line = in.readLine();
|
|
|
|
|
|
|
|
if (line.at(0) == '#') {
|
|
|
|
// Ignore lines starting with # (comments)
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
QStringList parts = line.split(' ');
|
|
|
|
if (parts.length() < 2) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString shortCode = parts[0];
|
|
|
|
|
|
|
|
auto emojiIt = this->emojiShortCodeToEmoji.find(shortCode);
|
|
|
|
if (emojiIt != this->emojiShortCodeToEmoji.end()) {
|
|
|
|
std::shared_ptr<EmojiData> emoji = *emojiIt;
|
|
|
|
emoji->capabilities.insert("EmojiOne 2");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-06-06 01:29:43 +02:00
|
|
|
|
2018-06-06 11:43:02 +02:00
|
|
|
void Emojis::sortEmojis()
|
|
|
|
{
|
2018-06-06 01:29:43 +02:00
|
|
|
for (auto &p : this->emojiFirstByte) {
|
|
|
|
std::stable_sort(p.begin(), p.end(), [](const auto &lhs, const auto &rhs) {
|
|
|
|
return lhs->value.length() > rhs->value.length();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
auto &p = this->shortCodes;
|
|
|
|
std::stable_sort(p.begin(), p.end(),
|
|
|
|
[](const auto &lhs, const auto &rhs) { return lhs < rhs; });
|
2018-06-06 11:43:02 +02:00
|
|
|
}
|
2018-06-06 01:29:43 +02:00
|
|
|
|
2018-06-06 11:43:02 +02:00
|
|
|
void Emojis::loadEmojiSet()
|
|
|
|
{
|
2018-06-06 01:29:43 +02:00
|
|
|
auto app = getApp();
|
|
|
|
|
|
|
|
app->settings->emojiSet.connect([=](const auto &emojiSet, auto) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Using emoji set {}", emojiSet);
|
2018-06-06 01:29:43 +02:00
|
|
|
this->emojis.each([=](const auto &name, std::shared_ptr<EmojiData> &emoji) {
|
|
|
|
QString emojiSetToUse = emojiSet;
|
|
|
|
// clang-format off
|
|
|
|
static std::map<QString, QString> emojiSets = {
|
|
|
|
{"EmojiOne 2", "https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.6/assets/png/"},
|
2018-06-20 20:30:54 +02:00
|
|
|
// {"EmojiOne 3", "https://cdn.jsdelivr.net/npm/emoji-datasource-emojione@4.0.4/img/emojione/64/"},
|
|
|
|
// {"Twitter", "https://cdn.jsdelivr.net/npm/emoji-datasource-twitter@4.0.4/img/twitter/64/"},
|
|
|
|
// {"Facebook", "https://cdn.jsdelivr.net/npm/emoji-datasource-facebook@4.0.4/img/facebook/64/"},
|
|
|
|
// {"Apple", "https://cdn.jsdelivr.net/npm/emoji-datasource-apple@4.0.4/img/apple/64/"},
|
|
|
|
// {"Google", "https://cdn.jsdelivr.net/npm/emoji-datasource-google@4.0.4/img/google/64/"},
|
|
|
|
// {"Messenger", "https://cdn.jsdelivr.net/npm/emoji-datasource-messenger@4.0.4/img/messenger/64/"},
|
|
|
|
|
|
|
|
// {"EmojiOne 2", "https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.6/assets/png/"},
|
2018-06-20 21:30:18 +02:00
|
|
|
// {"EmojiOne 3", "https://braize.pajlada.com/emoji/img/emojione/64/"},
|
|
|
|
// {"Twitter", "https://braize.pajlada.com/emoji/img/twitter/64/"},
|
|
|
|
// {"Facebook", "https://braize.pajlada.com/emoji/img/facebook/64/"},
|
|
|
|
// {"Apple", "https://braize.pajlada.com/emoji/img/apple/64/"},
|
|
|
|
// {"Google", "https://braize.pajlada.com/emoji/img/google/64/"},
|
|
|
|
// {"Messenger", "https://braize.pajlada.com/emoji/img/messenger/64/"},
|
|
|
|
|
|
|
|
{"EmojiOne 3", "https://pajbot.com/static/emoji/img/emojione/64/"},
|
|
|
|
{"Twitter", "https://pajbot.com/static/emoji/img/twitter/64/"},
|
|
|
|
{"Facebook", "https://pajbot.com/static/emoji/img/facebook/64/"},
|
|
|
|
{"Apple", "https://pajbot.com/static/emoji/img/apple/64/"},
|
|
|
|
{"Google", "https://pajbot.com/static/emoji/img/google/64/"},
|
|
|
|
{"Messenger", "https://pajbot.com/static/emoji/img/messenger/64/"},
|
2018-06-20 20:30:54 +02:00
|
|
|
|
2018-06-20 19:32:54 +02:00
|
|
|
// {"EmojiOne 3", "https://cdn.fourtf.com/emoji/emojione/64/"},
|
|
|
|
// {"Twitter", "https://cdn.fourtf.com/emoji/twitter/64/"},
|
|
|
|
// {"Facebook", "https://cdn.fourtf.com/emoji/facebook/64/"},
|
|
|
|
// {"Apple", "https://cdn.fourtf.com/emoji/apple/64/"},
|
|
|
|
// {"Google", "https://cdn.fourtf.com/emoji/google/64/"},
|
|
|
|
// {"Messenger", "https://cdn.fourtf.com/emoji/messenger/64/"},
|
2018-06-06 01:29:43 +02:00
|
|
|
};
|
|
|
|
// clang-format on
|
|
|
|
|
|
|
|
if (emoji->capabilities.count(emojiSetToUse) == 0) {
|
|
|
|
emojiSetToUse = "EmojiOne 3";
|
|
|
|
}
|
|
|
|
|
|
|
|
QString code = emoji->unifiedCode;
|
|
|
|
if (emojiSetToUse == "EmojiOne 2") {
|
|
|
|
if (!emoji->nonQualifiedCode.isEmpty()) {
|
|
|
|
code = emoji->nonQualifiedCode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
code = code.toLower();
|
|
|
|
QString urlPrefix = "https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.6/assets/png/";
|
|
|
|
auto it = emojiSets.find(emojiSetToUse);
|
|
|
|
if (it != emojiSets.end()) {
|
|
|
|
urlPrefix = it->second;
|
|
|
|
}
|
|
|
|
QString url = urlPrefix + code + ".png";
|
2018-06-28 19:38:57 +02:00
|
|
|
emoji->emoteData.image1x = new Image(
|
2018-06-22 22:42:53 +02:00
|
|
|
url, 0.35, emoji->value, ":" + emoji->shortCodes[0] + ":<br/>Emoji");
|
2018-06-06 01:29:43 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:20:03 +02:00
|
|
|
void Emojis::parse(std::vector<std::tuple<EmoteData, QString>> &parsedWords, const QString &text)
|
2018-06-05 18:53:49 +02:00
|
|
|
{
|
|
|
|
int lastParsedEmojiEndIndex = 0;
|
|
|
|
|
|
|
|
for (auto i = 0; i < text.length(); ++i) {
|
|
|
|
const QChar character = text.at(i);
|
|
|
|
|
|
|
|
if (character.isLowSurrogate()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto it = this->emojiFirstByte.find(character);
|
|
|
|
if (it == this->emojiFirstByte.end()) {
|
|
|
|
// No emoji starts with this character
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
const auto &possibleEmojis = it.value();
|
2018-06-05 18:53:49 +02:00
|
|
|
|
|
|
|
int remainingCharacters = text.length() - i - 1;
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
std::shared_ptr<EmojiData> matchedEmoji;
|
2018-06-05 18:53:49 +02:00
|
|
|
|
|
|
|
int matchedEmojiLength = 0;
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
for (const std::shared_ptr<EmojiData> &emoji : possibleEmojis) {
|
|
|
|
int emojiExtraCharacters = emoji->value.length() - 1;
|
2018-06-05 18:53:49 +02:00
|
|
|
if (emojiExtraCharacters > remainingCharacters) {
|
|
|
|
// It cannot be this emoji, there's not enough space for it
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool match = true;
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
for (int j = 1; j < emoji->value.length(); ++j) {
|
|
|
|
if (text.at(i + j) != emoji->value.at(j)) {
|
2018-06-05 18:53:49 +02:00
|
|
|
match = false;
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
matchedEmoji = emoji;
|
2018-06-06 01:29:43 +02:00
|
|
|
matchedEmojiLength = emoji->value.length();
|
2018-06-05 18:53:49 +02:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (matchedEmojiLength == 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
int currentParsedEmojiFirstIndex = i;
|
|
|
|
int currentParsedEmojiEndIndex = i + (matchedEmojiLength);
|
|
|
|
|
|
|
|
int charactersFromLastParsedEmoji = currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex;
|
|
|
|
|
|
|
|
if (charactersFromLastParsedEmoji > 0) {
|
|
|
|
// Add characters inbetween emojis
|
2018-06-26 17:20:03 +02:00
|
|
|
parsedWords.emplace_back(
|
|
|
|
EmoteData(), text.mid(lastParsedEmojiEndIndex, charactersFromLastParsedEmoji));
|
2018-06-05 18:53:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Push the emoji as a word to parsedWords
|
2018-06-26 17:20:03 +02:00
|
|
|
parsedWords.push_back(std::tuple<EmoteData, QString>(matchedEmoji->emoteData, QString()));
|
2018-06-05 18:53:49 +02:00
|
|
|
|
|
|
|
lastParsedEmojiEndIndex = currentParsedEmojiEndIndex;
|
|
|
|
|
|
|
|
i += matchedEmojiLength - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastParsedEmojiEndIndex < text.length()) {
|
|
|
|
// Add remaining characters
|
2018-06-26 17:06:17 +02:00
|
|
|
parsedWords.emplace_back(EmoteData(), text.mid(lastParsedEmojiEndIndex));
|
2018-06-05 18:53:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString Emojis::replaceShortCodes(const QString &text)
|
|
|
|
{
|
|
|
|
QString ret(text);
|
|
|
|
auto it = this->findShortCodesRegex.globalMatch(text);
|
|
|
|
|
|
|
|
int32_t offset = 0;
|
|
|
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
auto match = it.next();
|
|
|
|
|
|
|
|
auto capturedString = match.captured();
|
|
|
|
|
|
|
|
QString matchString = capturedString.toLower().mid(1, capturedString.size() - 2);
|
|
|
|
|
|
|
|
auto emojiIt = this->emojiShortCodeToEmoji.constFind(matchString);
|
|
|
|
|
|
|
|
if (emojiIt == this->emojiShortCodeToEmoji.constEnd()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto emojiData = emojiIt.value();
|
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
ret.replace(offset + match.capturedStart(), match.capturedLength(), emojiData->value);
|
2018-06-05 18:53:49 +02:00
|
|
|
|
2018-06-06 01:29:43 +02:00
|
|
|
offset += emojiData->value.size() - match.capturedLength();
|
2018-06-05 18:53:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace chatterino
|