mirror of
https://github.com/discourse/discourse.git
synced 2025-04-20 00:49:11 +08:00

This gems bring a whole improved fluentui emoji set. Each image has been recomputed to optimise the size of the emoji and remove useless margins. For this reason the version has been bumped. Other changes: - not loading rake task dependencies in production - renamed yo_yo/yo-yo to yoyo
373 lines
9.3 KiB
Ruby
373 lines
9.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Emoji
|
|
# update this to clear the cache
|
|
EMOJI_VERSION = "14"
|
|
|
|
FITZPATRICK_SCALE = %w[1f3fb 1f3fc 1f3fd 1f3fe 1f3ff]
|
|
|
|
DEFAULT_GROUP = "default"
|
|
|
|
include ActiveModel::SerializerSupport
|
|
|
|
attr_accessor :name, :url, :tonable, :group, :search_aliases, :created_by
|
|
|
|
def self.global_emoji_cache
|
|
@global_emoji_cache ||= DistributedCache.new("global_emoji_cache", namespace: false)
|
|
end
|
|
|
|
def self.site_emoji_cache
|
|
@site_emoji_cache ||= DistributedCache.new("site_emoji_cache")
|
|
end
|
|
|
|
def self.all
|
|
Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom }
|
|
end
|
|
|
|
def self.standard
|
|
Discourse.cache.fetch(cache_key("standard_emojis")) { load_standard }
|
|
end
|
|
|
|
def self.allowed
|
|
Discourse.cache.fetch(cache_key("allowed_emojis")) { load_allowed }
|
|
end
|
|
|
|
def self.denied
|
|
Discourse.cache.fetch(cache_key("denied_emojis")) { load_denied }
|
|
end
|
|
|
|
def self.aliases
|
|
aliases_db
|
|
end
|
|
|
|
def self.search_aliases
|
|
search_aliases_db
|
|
end
|
|
|
|
def self.translations
|
|
Discourse.cache.fetch(cache_key("translations_emojis")) { load_translations }
|
|
end
|
|
|
|
def self.custom
|
|
Discourse.cache.fetch(cache_key("custom_emojis")) { load_custom }
|
|
end
|
|
|
|
def self.tonable_emojis
|
|
tonable_emojis_db
|
|
end
|
|
|
|
def self.custom?(name)
|
|
name = name.delete_prefix(":").delete_suffix(":")
|
|
Emoji.custom.detect { |e| e.name == name }.present?
|
|
end
|
|
|
|
def self.exists?(name)
|
|
Emoji[name].present? || Emoji.aliases_values.include?(name)
|
|
end
|
|
|
|
def self.[](name)
|
|
name = name.delete_prefix(":").delete_suffix(":")
|
|
is_toned = name.match?(/\A.+:t[1-6]\z/)
|
|
normalized_name = name.gsub(/\A(.+):t[1-6]\z/, '\1')
|
|
|
|
found_emoji = nil
|
|
|
|
[[global_emoji_cache, :standard], [site_emoji_cache, :custom]].each do |cache, list_key|
|
|
found_emoji =
|
|
cache.defer_get_set(normalized_name) do
|
|
[
|
|
Emoji
|
|
.public_send(list_key)
|
|
.detect { |e| e.name == normalized_name && (!is_toned || (is_toned && e.tonable)) },
|
|
]
|
|
end[
|
|
0
|
|
]
|
|
|
|
break if found_emoji
|
|
end
|
|
|
|
found_emoji
|
|
end
|
|
|
|
def self.create_from_db_item(emoji)
|
|
name = emoji["name"]
|
|
return unless group = groups[name]
|
|
filename = emoji["filename"] || name
|
|
|
|
Emoji.new.tap do |e|
|
|
e.name = name
|
|
e.tonable = Emoji.tonable_emojis.include?(name)
|
|
e.url = Emoji.url_for(filename)
|
|
e.group = group
|
|
e.search_aliases = search_aliases[name] || []
|
|
end
|
|
end
|
|
|
|
def self.url_for(name)
|
|
name = name.delete_prefix(":").delete_suffix(":").gsub(/(.+):t([1-6])/, '\1/\2')
|
|
if SiteSetting.external_emoji_url.blank?
|
|
"#{Discourse.base_path}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}"
|
|
else
|
|
"#{SiteSetting.external_emoji_url}/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}"
|
|
end
|
|
end
|
|
|
|
def self.cache_key(name)
|
|
"#{name}#{cache_postfix}"
|
|
end
|
|
|
|
def self.cache_postfix
|
|
":v#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}"
|
|
end
|
|
|
|
def self.clear_cache
|
|
%w[custom standard translations allowed denied all].each do |key|
|
|
Discourse.cache.delete(cache_key("#{key}_emojis"))
|
|
end
|
|
global_emoji_cache.clear
|
|
site_emoji_cache.clear
|
|
end
|
|
|
|
def self.groups_file
|
|
@groups_file ||= DiscourseEmojis.paths[:groups]
|
|
end
|
|
|
|
def self.groups
|
|
@groups ||=
|
|
begin
|
|
groups = {}
|
|
|
|
File
|
|
.open(groups_file, "r:UTF-8") { |f| JSON.parse(f.read) }
|
|
.each { |group| group["icons"].each { |icon| groups[icon["name"]] = group["name"] } }
|
|
|
|
groups
|
|
end
|
|
end
|
|
|
|
def self.emojis_db_file
|
|
@emojis_db_file ||= DiscourseEmojis.paths[:emojis]
|
|
end
|
|
|
|
def self.emojis_db
|
|
@emojis_db ||= Emoji.parse_emoji_file(emojis_db_file)
|
|
end
|
|
|
|
def self.translations_db_file
|
|
@translations_db_file ||= DiscourseEmojis.paths[:translations]
|
|
end
|
|
|
|
def self.translations_db
|
|
@translations_db ||= Emoji.parse_emoji_file(translations_db_file)
|
|
end
|
|
|
|
def self.tonable_emojis_db_file
|
|
@tonable_emojis_db_file ||= DiscourseEmojis.paths[:tonable_emojis]
|
|
end
|
|
|
|
def self.tonable_emojis_db
|
|
@tonable_emojis_db ||= Emoji.parse_emoji_file(tonable_emojis_db_file)
|
|
end
|
|
|
|
def self.aliases_db_file
|
|
@aliases_db_file ||= DiscourseEmojis.paths[:aliases]
|
|
end
|
|
|
|
def self.aliases_db
|
|
@aliases_db ||= Emoji.parse_emoji_file(aliases_db_file)
|
|
end
|
|
|
|
def self.aliases_values
|
|
@aliases_values ||= Set.new(Emoji.aliases_db.values.flatten)
|
|
end
|
|
|
|
def self.search_aliases_db_file
|
|
@search_aliases_db_file ||= DiscourseEmojis.paths[:search_aliases]
|
|
end
|
|
|
|
def self.search_aliases_db
|
|
@search_aliases_db ||= Emoji.parse_emoji_file(search_aliases_db_file)
|
|
end
|
|
|
|
def self.load_standard
|
|
emojis_db.map { |e| Emoji.create_from_db_item(e) }.compact
|
|
end
|
|
|
|
def self.load_allowed
|
|
denied_emojis = denied
|
|
all_emojis = load_standard + load_custom
|
|
|
|
if denied_emojis.present?
|
|
all_emojis.reject { |e| denied_emojis.include?(e.name) }
|
|
else
|
|
all_emojis
|
|
end
|
|
end
|
|
|
|
def self.load_denied
|
|
if SiteSetting.emoji_deny_list.present?
|
|
denied_emoji = SiteSetting.emoji_deny_list.split("|")
|
|
if denied_emoji.size > 0
|
|
denied_emoji.concat(denied_emoji.flat_map { |e| Emoji.aliases[e] }.compact)
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.load_custom
|
|
result = []
|
|
|
|
if !GlobalSetting.skip_db?
|
|
CustomEmoji
|
|
.includes(:upload)
|
|
.order(:name)
|
|
.each do |emoji|
|
|
result << Emoji.new.tap do |e|
|
|
e.name = emoji.name
|
|
e.url = emoji.upload&.url
|
|
e.group = emoji.group || DEFAULT_GROUP
|
|
e.created_by = User.where(id: emoji.user_id).pick(:username)
|
|
end
|
|
end
|
|
end
|
|
|
|
Plugin::CustomEmoji.emojis.each do |group, emojis|
|
|
emojis.each do |name, url|
|
|
result << Emoji.new.tap do |e|
|
|
e.name = name
|
|
url = (Discourse.base_path + url) if url[%r{\A/[^/]}]
|
|
e.url = url
|
|
e.group = group || DEFAULT_GROUP
|
|
end
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def self.load_translations
|
|
translations_db
|
|
end
|
|
|
|
def self.base_directory
|
|
"public#{base_url}"
|
|
end
|
|
|
|
def self.base_url
|
|
db = RailsMultisite::ConnectionManagement.current_db
|
|
"#{Discourse.base_path}/uploads/#{db}/_emoji"
|
|
end
|
|
|
|
def self.replacement_code(code)
|
|
code.split("-").map!(&:hex).pack("U*")
|
|
end
|
|
|
|
def self.unicode_replacements
|
|
@unicode_replacements ||=
|
|
begin
|
|
replacements = {}
|
|
is_tonable_emojis = Emoji.tonable_emojis
|
|
fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) }
|
|
|
|
emojis_db.each do |e|
|
|
name = e["name"]
|
|
|
|
# special cased as we prefer to keep these as symbols
|
|
next if name == "registered"
|
|
next if name == "copyright"
|
|
next if name == "tm"
|
|
next if name == "left_right_arrow"
|
|
|
|
code = replacement_code(e["code"])
|
|
next unless code
|
|
|
|
replacements[code] = name
|
|
if is_tonable_emojis.include?(name)
|
|
fitzpatrick_scales.each_with_index do |scale, index|
|
|
toned_code = code.codepoints.insert(1, scale).pack("U*")
|
|
replacements[toned_code] = "#{name}:t#{index + 2}"
|
|
end
|
|
end
|
|
end
|
|
|
|
replacements["\u{2639}"] = "frowning"
|
|
replacements["\u{263B}"] = "slight_smile"
|
|
replacements["\u{2661}"] = "heart"
|
|
replacements["\u{2665}"] = "heart"
|
|
|
|
replacements
|
|
end
|
|
end
|
|
|
|
def self.unicode_unescape(string)
|
|
PrettyText.escape_emoji(string)
|
|
end
|
|
|
|
def self.gsub_emoji_to_unicode(str)
|
|
str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } if str
|
|
end
|
|
|
|
def self.lookup_unicode(name)
|
|
return "" if denied&.include?(name)
|
|
|
|
@reverse_map ||=
|
|
begin
|
|
map = {}
|
|
is_tonable_emojis = Emoji.tonable_emojis
|
|
|
|
emojis_db.each do |e|
|
|
next if e["name"] == "tm"
|
|
|
|
code = replacement_code(e["code"])
|
|
next unless code
|
|
|
|
map[e["name"]] = code
|
|
if is_tonable_emojis.include?(e["name"])
|
|
FITZPATRICK_SCALE.each_with_index do |scale, index|
|
|
toned_code = (code.codepoints.insert(1, scale.to_i(16))).pack("U*")
|
|
map["#{e["name"]}:t#{index + 2}"] = toned_code
|
|
end
|
|
end
|
|
end
|
|
|
|
Emoji.aliases.each do |key, alias_names|
|
|
next unless alias_code = map[key]
|
|
alias_names.each { |alias_name| map[alias_name] = alias_code }
|
|
end
|
|
|
|
map
|
|
end
|
|
@reverse_map[name]
|
|
end
|
|
|
|
def self.unicode_replacements_json
|
|
@unicode_replacements_json ||= unicode_replacements.to_json
|
|
end
|
|
|
|
def self.codes_to_img(str)
|
|
return if str.blank?
|
|
|
|
str =
|
|
str.gsub(/:([\w\-+]*(?::t\d)?):/) do |name|
|
|
code = $1
|
|
|
|
if code && Emoji.custom?(code)
|
|
emoji = Emoji[code]
|
|
"<img src=\"#{emoji.url}\" title=\"#{code}\" class=\"emoji\" alt=\"#{code}\" loading=\"lazy\" width=\"20\" height=\"20\">"
|
|
elsif code && Emoji.exists?(code)
|
|
"<img src=\"#{Emoji.url_for(code)}\" title=\"#{code}\" class=\"emoji\" alt=\"#{code}\" loading=\"lazy\" width=\"20\" height=\"20\">"
|
|
else
|
|
name
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.sanitize_emoji_name(name)
|
|
name.gsub(/[^a-z0-9\+\-]+/i, "_").gsub(/_{2,}/, "_").downcase
|
|
end
|
|
|
|
def self.parse_emoji_file(file)
|
|
File.open(file, "r:UTF-8") { |f| JSON.parse(f.read) }
|
|
end
|
|
end
|