From 81cec35dbf1b348d23363559e3f4e6b1ec3415c5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Tue, 19 Sep 2017 02:42:40 +0200 Subject: [PATCH] Custom emoji (#4988) * Custom emoji - In OStatus: `<link rel="emoji" name="coolcat" href="http://..." />` - In ActivityPub: `{ type: "Emoji", name: ":coolcat:", href: "http://..." }` - In REST API: Status object includes `emojis` array (`shortcode`, `url`) - Domain blocks with reject media stop emojis - Emoji file up to 50KB - Web UI handles custom emojis - Static pages render custom emojis as `<img />` tags Side effects: - Undo #4500 optimization, as I needed to modify it to restore shortcode handling in emojify() - Formatter#plaintext should now make sure stripped out line-breaks and paragraphs are replaced with newlines * Fix emoji at the start not being converted --- app/javascript/mastodon/emoji.js | 60 +++++++++----- app/javascript/mastodon/reducers/statuses.js | 9 +- app/lib/activitypub/activity/create.rb | 13 +++ app/lib/formatter.rb | 54 +++++++++++- app/lib/ostatus/activity/creation.rb | 20 +++++ app/lib/ostatus/atom_serializer.rb | 4 + app/models/custom_emoji.rb | 38 +++++++++ app/models/status.rb | 4 + .../activitypub/note_serializer.rb | 20 ++++- app/serializers/rest/status_serializer.rb | 11 +++ .../stream_entries/_detailed_status.html.haml | 2 +- .../stream_entries/_simple_status.html.haml | 2 +- .../20170917153509_create_custom_emojis.rb | 13 +++ db/schema.rb | 14 +++- spec/fabricators/custom_emoji_fabricator.rb | 5 ++ spec/fixtures/files/emojo.png | Bin 0 -> 29814 bytes spec/lib/activitypub/activity/create_spec.rb | 25 ++++++ spec/lib/formatter_spec.rb | 78 ++++++++++++++++++ spec/lib/ostatus/atom_serializer_spec.rb | 16 +++- spec/models/custom_emoji_spec.rb | 25 ++++++ 20 files changed, 382 insertions(+), 31 deletions(-) create mode 100644 app/models/custom_emoji.rb create mode 100644 db/migrate/20170917153509_create_custom_emojis.rb create mode 100644 spec/fabricators/custom_emoji_fabricator.rb create mode 100644 spec/fixtures/files/emojo.png create mode 100644 spec/models/custom_emoji_spec.rb diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index a41dfdd1d..865b85b61 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -3,28 +3,48 @@ import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); -const emojify = str => { - let rtn = ''; - for (;;) { - let match, i = 0; - while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) { - i += str.codePointAt(i) < 65536 ? 1 : 2; - } - if (i === str.length) - break; - else if (str[i] === '<') { - let tagend = str.indexOf('>', i + 1) + 1; - if (!tagend) - break; - rtn += str.slice(0, tagend); - str = str.slice(tagend); - } else { - const [filename, shortCode] = unicodeMapping[match]; - rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; - str = str.slice(i + match.length); +const emojify = (str, customEmojis = {}) => { + // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) + // and replacing valid unicode strings + // that _aren't_ within tags with an <img> version. + // The goal is to be the same as an emojione.regUnicode replacement, but faster. + let i = -1; + let insideTag = false; + let insideShortname = false; + let shortnameStartIndex = -1; + let match; + while (++i < str.length) { + const char = str.charAt(i); + if (insideShortname && char === ':') { + const shortname = str.substring(shortnameStartIndex, i + 1); + if (shortname in customEmojis) { + const replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`; + str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1); + i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string + } else { + i--; + } + insideShortname = false; + } else if (insideTag && char === '>') { + insideTag = false; + } else if (char === '<') { + insideTag = true; + insideShortname = false; + } else if (!insideTag && char === ':') { + insideShortname = true; + shortnameStartIndex = i; + } else if (!insideTag && (match = trie.search(str.substring(i)))) { + const unicodeStr = match; + if (unicodeStr in unicodeMapping) { + const [filename, shortCode] = unicodeMapping[unicodeStr]; + const alt = unicodeStr; + const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; + str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); + i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string + } } } - return rtn + str; + return str; }; export default emojify; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 7f906bef6..38b23504e 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -58,9 +58,14 @@ const normalizeStatus = (state, status) => { } const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji.url; + return obj; + }, {}); + normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || '')); + normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 894759d9a..41f2b0bad 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -61,6 +61,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity process_hashtag tag, status when 'Mention' process_mention tag, status + when 'Emoji' + process_emoji tag, status end end end @@ -79,6 +81,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity account.mentions.create(status: status) end + def process_emoji(tag, _status) + shortcode = tag['name'].delete(':') + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain) + + return if !emoji.nil? || skip_download? + + emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode) + emoji.image_remote_url = tag['href'] + emoji.save + end + def process_attachments(status) return unless @object['attachment'].is_a?(Array) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 575830190..29fea27de 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -9,7 +9,7 @@ class Formatter include ActionView::Helpers::TextHelper - def format(status) + def format(status, options = {}) if status.reblog? prepend_reblog = status.reblog.account.acct status = status.proper @@ -19,7 +19,11 @@ class Formatter raw_content = status.text - return reformat(raw_content) unless status.local? + unless status.local? + html = reformat(raw_content) + html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] + return html + end linkable_accounts = status.mentions.map(&:account) linkable_accounts << status.account @@ -27,6 +31,7 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = encode_and_link_urls(html, linkable_accounts) + html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify] html = simple_format(html, {}, sanitize: false) html = html.delete("\n") @@ -39,7 +44,9 @@ class Formatter def plaintext(status) return status.text if status.local? - strip_tags(status.text) + + text = status.text.gsub(/(<br \/>|<br>|<\/p>)+/) { |match| "#{match}\n" } + strip_tags(text) end def simplified_format(account) @@ -76,6 +83,47 @@ class Formatter end end + def encode_custom_emojis(html, emojis) + return html if emojis.empty? + + emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h + + i = -1 + inside_tag = false + inside_shortname = false + shortname_start_index = -1 + + while i + 1 < html.size + i += 1 + + if inside_shortname && html[i] == ':' + shortcode = html[shortname_start_index + 1..i - 1] + emoji = emoji_map[shortcode] + + if emoji + replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{shortcode}:\" title=\":#{shortcode}:\" src=\"#{emoji}\" />" + before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : '' + html = before_html + replacement + html[i + 1..-1] + i += replacement.size - (shortcode.size + 2) - 1 + else + i -= 1 + end + + inside_shortname = false + elsif inside_tag && html[i] == '>' + inside_tag = false + elsif html[i] == '<' + inside_tag = true + inside_shortname = false + elsif !inside_tag && html[i] == ':' + inside_shortname = true + shortname_start_index = i + end + end + + html + end + def rewrite(text, entities) chars = text.to_s.to_char_a diff --git a/app/lib/ostatus/activity/creation.rb b/app/lib/ostatus/activity/creation.rb index 1a23c9efa..d3f1629c4 100644 --- a/app/lib/ostatus/activity/creation.rb +++ b/app/lib/ostatus/activity/creation.rb @@ -42,6 +42,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base save_mentions(status) save_hashtags(status) save_media(status) + save_emojis(status) end if thread? && status.thread.nil? @@ -150,6 +151,25 @@ class OStatus::Activity::Creation < OStatus::Activity::Base end end + def save_emojis(parent) + do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + + return if do_not_download + + @xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link| + next unless link['href'] && link['name'] + + shortcode = link['name'].delete(':') + emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) + + next unless emoji.nil? + + emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) + emoji.image_remote_url = link['href'] + emoji.save + end + end + def account_from_href(href) url = Addressable::URI.parse(href).normalize diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index b8e22a381..a6a5cb0c4 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -368,5 +368,9 @@ class OStatus::AtomSerializer end append_element(entry, 'mastodon:scope', status.visibility) + + status.emojis.each do |emoji| + append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) + end end end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb new file mode 100644 index 000000000..f4d3b16a0 --- /dev/null +++ b/app/models/custom_emoji.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: custom_emojis +# +# id :integer not null, primary key +# shortcode :string default(""), not null +# domain :string +# image_file_name :string +# image_content_type :string +# image_file_size :integer +# image_updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# + +class CustomEmoji < ApplicationRecord + SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' + + SCAN_RE = /(?<=[^[:alnum:]:]|\n|^) + :(#{SHORTCODE_RE_FRAGMENT}): + (?=[^[:alnum:]:]|$)/x + + has_attached_file :image + + validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes } + validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 } + + include Remotable + + class << self + def from_text(text, domain) + return [] if text.blank? + shortcodes = text.scan(SCAN_RE).map(&:first) + where(shortcode: shortcodes, domain: domain) + end + end +end diff --git a/app/models/status.rb b/app/models/status.rb index 2a2cdcf6e..326d128d6 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -131,6 +131,10 @@ class Status < ApplicationRecord !sensitive? && media_attachments.any? end + def emojis + CustomEmoji.from_text(text, account.domain) + end + after_create :store_uri, if: :local? before_validation :prepare_contents, if: :local? diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 166214eee..e5d8e3f03 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -57,7 +57,7 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer end def virtual_tags - object.mentions + object.tags + object.mentions + object.tags + object.emojis end def atom_uri @@ -137,4 +137,22 @@ class ActivityPub::NoteSerializer < ActiveModel::Serializer "##{object.name}" end end + + class CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :type, :href, :name + + def type + 'Emoji' + end + + def href + full_asset_url(object.image.url) + end + + def name + ":#{object.shortcode}:" + end + end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 298a3bb40..d8efa8e60 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -17,6 +17,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :mentions has_many :tags + has_many :emojis def current_user? !current_user.nil? @@ -106,4 +107,14 @@ class REST::StatusSerializer < ActiveModel::Serializer tag_url(object) end end + + class CustomEmojiSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :shortcode, :url + + def url + full_asset_url(object.image.url) + end + end end diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index dd9456260..692d5a6d5 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -17,7 +17,7 @@ %p{ style: 'margin-bottom: 0' }< %span.p-summary> #{status.spoiler_text} %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) + .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true) - if !status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index 55aa97f32..f9a530d38 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -18,7 +18,7 @@ %p{ style: 'margin-bottom: 0' }< %span.p-summary> #{status.spoiler_text} %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more') - .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status) + .e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true) - unless status.media_attachments.empty? - if status.media_attachments.first.video? diff --git a/db/migrate/20170917153509_create_custom_emojis.rb b/db/migrate/20170917153509_create_custom_emojis.rb new file mode 100644 index 000000000..4040c8312 --- /dev/null +++ b/db/migrate/20170917153509_create_custom_emojis.rb @@ -0,0 +1,13 @@ +class CreateCustomEmojis < ActiveRecord::Migration[5.1] + def change + create_table :custom_emojis do |t| + t.string :shortcode, null: false, default: '' + t.string :domain + t.attachment :image + + t.timestamps + end + + add_index :custom_emojis, [:shortcode, :domain], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index f2ca2af69..9f42d46dd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170913000752) do +ActiveRecord::Schema.define(version: 20170917153509) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -89,6 +89,18 @@ ActiveRecord::Schema.define(version: 20170913000752) do t.index ["uri"], name: "index_conversations_on_uri", unique: true end + create_table "custom_emojis", force: :cascade do |t| + t.string "shortcode", default: "", null: false + t.string "domain" + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true + end + create_table "domain_blocks", id: :serial, force: :cascade do |t| t.string "domain", default: "", null: false t.datetime "created_at", null: false diff --git a/spec/fabricators/custom_emoji_fabricator.rb b/spec/fabricators/custom_emoji_fabricator.rb new file mode 100644 index 000000000..18a7d23dc --- /dev/null +++ b/spec/fabricators/custom_emoji_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:custom_emoji) do + shortcode 'coolcat' + domain nil + image { File.open(Rails.root.join('spec', 'fixtures', 'files', 'emojo.png')) } +end diff --git a/spec/fixtures/files/emojo.png b/spec/fixtures/files/emojo.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5993499f059fa4118504aee1e71c9181810e3e GIT binary patch literal 29814 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 literal 0 HcmV?d00001 diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index fcb044ebc..1a9520f04 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -17,6 +17,7 @@ RSpec.describe ActivityPub::Activity::Create do before do stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt')) + stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png')) end describe '#perform' do @@ -217,5 +218,29 @@ RSpec.describe ActivityPub::Activity::Create do expect(status.tags.map(&:name)).to include('test') end end + + context 'with emojis' do + let(:object_json) do + { + id: 'bar', + type: 'Note', + content: 'Lorem ipsum :tinking:', + tag: [ + { + type: 'Emoji', + href: 'http://example.com/emoji.png', + name: 'tinking', + }, + ], + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.emojis.map(&:shortcode)).to include('tinking') + end + end end end diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb index b714b317a..71b6b78d2 100644 --- a/spec/lib/formatter_spec.rb +++ b/spec/lib/formatter_spec.rb @@ -223,6 +223,45 @@ RSpec.describe Formatter do include_examples 'encode and link URLs' end + + context 'with custom_emojify option' do + let!(:emoji) { Fabricate(:custom_emoji) } + let(:status) { Fabricate(:status, account: local_account, text: text) } + + subject { Formatter.instance.format(status, custom_emojify: true) } + + context 'with emoji at the start' do + let(:text) { ':coolcat: Beep boop' } + + it 'converts shortcode to image tag' do + is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/) + end + end + + context 'with emoji in the middle' do + let(:text) { 'Beep :coolcat: boop' } + + it 'converts shortcode to image tag' do + is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/) + end + end + + context 'with concatenated emoji' do + let(:text) { ':coolcat::coolcat:' } + + it 'does not touch the shortcodes' do + is_expected.to match(/:coolcat::coolcat:/) + end + end + + context 'with emoji at the end' do + let(:text) { 'Beep boop :coolcat:' } + + it 'converts shortcode to image tag' do + is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/) + end + end + end end context 'with remote status' do @@ -231,6 +270,45 @@ RSpec.describe Formatter do it 'reformats' do is_expected.to eq 'Beep boop' end + + context 'with custom_emojify option' do + let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) } + let(:status) { Fabricate(:status, account: remote_account, text: text) } + + subject { Formatter.instance.format(status, custom_emojify: true) } + + context 'with emoji at the start' do + let(:text) { '<p>:coolcat: Beep boop<br />' } + + it 'converts shortcode to image tag' do + is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/) + end + end + + context 'with emoji in the middle' do + let(:text) { '<p>Beep :coolcat: boop</p>' } + + it 'converts shortcode to image tag' do + is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/) + end + end + + context 'with concatenated emoji' do + let(:text) { '<p>:coolcat::coolcat:</p>' } + + it 'does not touch the shortcodes' do + is_expected.to match(/<p>:coolcat::coolcat:<\/p>/) + end + end + + context 'with emoji at the end' do + let(:text) { '<p>Beep boop<br />:coolcat:</p>' } + + it 'converts shortcode to image tag' do + is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/) + end + end + end end end diff --git a/spec/lib/ostatus/atom_serializer_spec.rb b/spec/lib/ostatus/atom_serializer_spec.rb index 0451eceeb..b2480a53b 100644 --- a/spec/lib/ostatus/atom_serializer_spec.rb +++ b/spec/lib/ostatus/atom_serializer_spec.rb @@ -97,11 +97,23 @@ RSpec.describe OStatus::AtomSerializer do mentioned = element.nodes.find do |node| node.name == 'link' && - node[:rel] == 'mentioned' && - node['ostatus:object-type'] == TagManager::TYPES[:person] + node[:rel] == 'mentioned' && + node['ostatus:object-type'] == TagManager::TYPES[:person] end + expect(mentioned[:href]).to eq 'https://cb6e6126.ngrok.io/users/username' end + + it 'appends link elements for emojis' do + Fabricate(:custom_emoji) + + status = Fabricate(:status, text: ':coolcat:') + element = serialize(status) + emoji = element.nodes.find { |node| node.name == 'link' && node[:rel] == 'emoji' } + + expect(emoji[:name]).to eq 'coolcat' + expect(emoji[:href]).to_not be_blank + end end describe 'render' do diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb new file mode 100644 index 000000000..cb51e9519 --- /dev/null +++ b/spec/models/custom_emoji_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe CustomEmoji, type: :model do + describe '.from_text' do + let!(:emojo) { Fabricate(:custom_emoji) } + + subject { described_class.from_text(text, nil) } + + context 'with plain text' do + let(:text) { 'Hello :coolcat:' } + + it 'returns records used via shortcodes in text' do + is_expected.to include(emojo) + end + end + + context 'with html' do + let(:text) { '<p>Hello :coolcat:</p>' } + + it 'returns records used via shortcodes in text' do + is_expected.to include(emojo) + end + end + end +end