Page MenuHomePhorge

No OneTemporary

Size
26 KB
Referenced Files
None
Subscribers
None
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index ca79df6f6f..ca2075a46b 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,369 +1,375 @@
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
return 'status'
}
if (status.retweeted_status) {
return 'retweet'
}
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
return 'favorite'
}
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
return 'deletion'
}
if (status.text.match(/started following/) || status.activity_type === 'follow') {
return 'follow'
}
return 'unknown'
}
export const parseUser = (data) => {
const output = {}
const masto = data.hasOwnProperty('acct')
// case for users in "mentions" property for statuses in MastoAPI
const mastoShort = masto && !data.hasOwnProperty('avatar')
output.id = String(data.id)
if (masto) {
output.screen_name = data.acct
output.statusnet_profile_url = data.url
// There's nothing else to get
if (mastoShort) {
return output
}
output.name = data.display_name
output.name_html = addEmojis(data.display_name, data.emojis)
output.description = data.note
output.description_html = addEmojis(data.note, data.emojis)
output.fields = data.fields
output.fields_html = data.fields.map(field => {
return {
name: addEmojis(field.name, data.emojis),
value: addEmojis(field.value, data.emojis)
}
})
+ output.fields_text = data.fields.map(field => {
+ return {
+ name: field.name.replace(/<[^>]*>/g, ''),
+ value: field.value.replace(/<[^>]*>/g, '')
+ }
+ })
// Utilize avatar_static for gif avatars?
output.profile_image_url = data.avatar
output.profile_image_url_original = data.avatar
// Same, utilize header_static?
output.cover_photo = data.header
output.friends_count = data.following_count
output.bot = data.bot
if (data.pleroma) {
const relationship = data.pleroma.relationship
output.background_image = data.pleroma.background_image
output.token = data.pleroma.chat_token
if (relationship) {
output.follows_you = relationship.followed_by
output.requested = relationship.requested
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing
}
output.hide_follows = data.pleroma.hide_follows
output.hide_followers = data.pleroma.hide_followers
output.hide_follows_count = data.pleroma.hide_follows_count
output.hide_followers_count = data.pleroma.hide_followers_count
output.rights = {
moderator: data.pleroma.is_moderator,
admin: data.pleroma.is_admin
}
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
if (output.rights.admin) {
output.role = 'admin'
} else if (output.rights.moderator) {
output.role = 'moderator'
} else {
output.role = 'member'
}
}
if (data.source) {
output.description = data.source.note
output.default_scope = data.source.privacy
output.fields = data.source.fields
if (data.source.pleroma) {
output.no_rich_text = data.source.pleroma.no_rich_text
output.show_role = data.source.pleroma.show_role
output.discoverable = data.source.pleroma.discoverable
}
}
// TODO: handle is_local
output.is_local = !output.screen_name.includes('@')
} else {
output.screen_name = data.screen_name
output.name = data.name
output.name_html = data.name_html
output.description = data.description
output.description_html = data.description_html
output.profile_image_url = data.profile_image_url
output.profile_image_url_original = data.profile_image_url_original
output.cover_photo = data.cover_photo
output.friends_count = data.friends_count
// output.bot = ??? missing
output.statusnet_profile_url = data.statusnet_profile_url
output.statusnet_blocking = data.statusnet_blocking
output.is_local = data.is_local
output.role = data.role
output.show_role = data.show_role
output.follows_you = data.follows_you
output.muted = data.muted
if (data.rights) {
output.rights = {
moderator: data.rights.delete_others_notice,
admin: data.rights.admin
}
}
output.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope
output.hide_follows = data.hide_follows
output.hide_followers = data.hide_followers
output.hide_follows_count = data.hide_follows_count
output.hide_followers_count = data.hide_followers_count
output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
// Websocket token
output.token = data.token
}
output.created_at = new Date(data.created_at)
output.locked = data.locked
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
output.friendIds = []
output.followerIds = []
output.pinnedStatusIds = []
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
output.tags = data.pleroma.tags
output.deactivated = data.pleroma.deactivated
output.notification_settings = data.pleroma.notification_settings
}
output.tags = output.tags || []
output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {}
return output
}
export const parseAttachment = (data) => {
const output = {}
const masto = !data.hasOwnProperty('oembed')
if (masto) {
// Not exactly same...
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
output.meta = data.meta // not present in BE yet
output.id = data.id
} else {
output.mimetype = data.mimetype
// output.meta = ??? missing
}
output.url = data.url
output.description = data.description
return output
}
export const addEmojis = (string, emojis) => {
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
return emojis.reduce((acc, emoji) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace(
new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />`
)
}, string)
}
export const parseStatus = (data) => {
const output = {}
const masto = data.hasOwnProperty('account')
if (masto) {
output.favorited = data.favourited
output.fave_num = data.favourites_count
output.repeated = data.reblogged
output.repeat_num = data.reblogs_count
output.type = data.reblog ? 'retweet' : 'status'
output.nsfw = data.sensitive
output.statusnet_html = addEmojis(data.content, data.emojis)
output.tags = data.tags
if (data.pleroma) {
const { pleroma } = data
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
output.statusnet_conversation_id = data.pleroma.conversation_id
output.is_local = pleroma.local
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
output.thread_muted = pleroma.thread_muted
} else {
output.text = data.content
output.summary = data.spoiler_text
}
output.in_reply_to_status_id = data.in_reply_to_id
output.in_reply_to_user_id = data.in_reply_to_account_id
output.replies_count = data.replies_count
if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.reblog)
}
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
output.external_url = data.url
output.poll = data.poll
output.pinned = data.pinned
output.muted = data.muted
} else {
output.favorited = data.favorited
output.fave_num = data.fave_num
output.repeated = data.repeated
output.repeat_num = data.repeat_num
// catchall, temporary
// Object.assign(output, data)
output.type = qvitterStatusType(data)
if (data.nsfw === undefined) {
output.nsfw = isNsfw(data)
if (data.retweeted_status) {
output.nsfw = data.retweeted_status.nsfw
}
} else {
output.nsfw = data.nsfw
}
output.statusnet_html = data.statusnet_html
output.text = data.text
output.in_reply_to_status_id = data.in_reply_to_status_id
output.in_reply_to_user_id = data.in_reply_to_user_id
output.in_reply_to_screen_name = data.in_reply_to_screen_name
output.statusnet_conversation_id = data.statusnet_conversation_id
if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.retweeted_status)
}
output.summary = data.summary
output.summary_html = data.summary_html
output.external_url = data.external_url
output.is_local = data.is_local
}
output.id = String(data.id)
output.visibility = data.visibility
output.card = data.card
output.created_at = new Date(data.created_at)
// Converting to string, the right way.
output.in_reply_to_status_id = output.in_reply_to_status_id
? String(output.in_reply_to_status_id)
: null
output.in_reply_to_user_id = output.in_reply_to_user_id
? String(output.in_reply_to_user_id)
: null
output.user = parseUser(masto ? data.account : data.user)
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
.map(parseAttachment)
const retweetedStatus = masto ? data.reblog : data.retweeted_status
if (retweetedStatus) {
output.retweeted_status = parseStatus(retweetedStatus)
}
output.favoritedBy = []
output.rebloggedBy = []
return output
}
export const parseNotification = (data) => {
const mastoDict = {
'favourite': 'like',
'reblog': 'repeat'
}
const masto = !data.hasOwnProperty('ntype')
const output = {}
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = output.type === 'follow'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
output.from_profile = parseUser(data.account)
} else {
const parsedNotice = parseStatus(data.notice)
output.type = data.ntype
output.seen = Boolean(data.is_seen)
output.status = output.type === 'like'
? parseStatus(data.notice.favorited_status)
: parsedNotice
output.action = parsedNotice
output.from_profile = parseUser(data.from_profile)
}
output.created_at = new Date(data.created_at)
output.id = parseInt(data.id)
return output
}
const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
index cfb380bafc..9df84575d0 100644
--- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -1,373 +1,386 @@
import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
import mastoapidata from '../../../../fixtures/mastoapi.json'
import qvitterapidata from '../../../../fixtures/statuses.json'
const makeMockStatusQvitter = (overrides = {}) => {
return Object.assign({
activity_type: 'post',
attachments: [],
attentions: [],
created_at: 'Tue Jan 15 13:57:56 +0000 2019',
external_url: 'https://ap.example/whatever',
fave_num: 1,
favorited: false,
id: '10335970',
in_reply_to_ostatus_uri: null,
in_reply_to_profileurl: null,
in_reply_to_screen_name: null,
in_reply_to_status_id: null,
in_reply_to_user_id: null,
is_local: false,
is_post_verb: true,
possibly_sensitive: false,
repeat_num: 0,
repeated: false,
statusnet_conversation_id: '16300488',
statusnet_html: '<p>haha benis</p>',
summary: null,
tags: [],
text: 'haha benis',
uri: 'https://ap.example/whatever',
user: makeMockUserQvitter(),
visibility: 'public'
}, overrides)
}
const makeMockUserQvitter = (overrides = {}) => {
return Object.assign({
background_image: null,
cover_photo: '',
created_at: 'Mon Jan 14 13:56:51 +0000 2019',
default_scope: 'public',
description: 'ebin',
description_html: '<p>ebin</p>',
favourites_count: 0,
fields: [],
followers_count: 1,
following: true,
follows_you: true,
friends_count: 1,
id: '60717',
is_local: false,
locked: false,
name: 'Spurdo :ebin:',
name_html: 'Spurdo <img src="whatever">',
no_rich_text: false,
pleroma: { confirmation_pending: false, tags: [] },
profile_image_url: 'https://ap.example/whatever',
profile_image_url_https: 'https://ap.example/whatever',
profile_image_url_original: 'https://ap.example/whatever',
profile_image_url_profile_size: 'https://ap.example/whatever',
rights: { delete_others_notice: false },
screen_name: 'spurdo@ap.example',
statuses_count: 46,
statusnet_blocking: false,
statusnet_profile_url: ''
}, overrides)
}
const makeMockUserMasto = (overrides = {}) => {
return Object.assign({
acct: 'hj',
avatar:
'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png',
avatar_static:
'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png',
bot: false,
created_at: '2017-12-17T21:54:14.000Z',
display_name: 'whatever whatever whatever witch',
emojis: [],
fields: [],
followers_count: 705,
following_count: 326,
header:
'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png',
header_static:
'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png',
id: '1',
locked: false,
note:
'Volatile Internet Weirdo. Name pronounced as Hee Jay. JS and Java dark arts mage, Elixir trainee. I love sampo and lain. Matrix is <span><a data-user="1" href="https://shigusegubu.club/users/hj">@<span>hj</span></a></span>:matrix.heldscal.la Pronouns are whatever. Do not DM me unless it\'s truly private matter and you\'re instance\'s admin or you risk your DM to be reposted publicly.Wish i was Finnish girl.',
pleroma: { confirmation_pending: false, tags: null },
source: { note: '', privacy: 'public', sensitive: false },
statuses_count: 41775,
url: 'https://shigusegubu.club/users/hj',
username: 'hj'
}, overrides)
}
const makeMockStatusMasto = (overrides = {}) => {
return Object.assign({
account: makeMockUserMasto(),
application: { name: 'Web', website: null },
content:
'<span><a data-user="14660" href="https://pleroma.soykaf.com/users/sampo">@<span>sampo</span></a></span> god i wish i was there',
created_at: '2019-01-17T16:29:23.000Z',
emojis: [],
favourited: false,
favourites_count: 1,
id: '10423476',
in_reply_to_account_id: '14660',
in_reply_to_id: '10423197',
language: null,
media_attachments: [],
mentions: [
{
acct: 'sampo@pleroma.soykaf.com',
id: '14660',
url: 'https://pleroma.soykaf.com/users/sampo',
username: 'sampo'
}
],
muted: false,
reblog: null,
reblogged: false,
reblogs_count: 0,
replies_count: 0,
sensitive: false,
spoiler_text: '',
tags: [],
uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
url: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
visibility: 'public',
pleroma: {
local: true
}
}, overrides)
}
const makeMockNotificationQvitter = (overrides = {}) => {
return Object.assign({
notice: makeMockStatusQvitter(),
ntype: 'follow',
from_profile: makeMockUserQvitter(),
is_seen: 0,
id: 123
}, overrides)
}
const makeMockEmojiMasto = (overrides = [{}]) => {
return [
Object.assign({
shortcode: 'image',
static_url: 'https://example.com/image.png',
url: 'https://example.com/image.png',
visible_in_picker: false
}, overrides[0]),
Object.assign({
shortcode: 'thinking',
static_url: 'https://example.com/think.png',
url: 'https://example.com/think.png',
visible_in_picker: false
}, overrides[1])
]
}
describe('API Entities normalizer', () => {
describe('parseStatus', () => {
describe('QVitter preprocessing', () => {
it('doesn\'t blow up', () => {
const parsed = qvitterapidata.map(parseStatus)
expect(parsed.length).to.eq(qvitterapidata.length)
})
it('identifies favorites', () => {
const fav = {
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00',
is_post_verb: false
}
const mastoFav = {
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite',
is_post_verb: false
}
expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property('type', 'favorite')
expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property('type', 'favorite')
})
it('processes repeats correctly', () => {
const post = makeMockStatusQvitter({ retweeted_status: null, id: 'deadbeef' })
const repeat = makeMockStatusQvitter({ retweeted_status: post, is_post_verb: false, id: 'foobar' })
const parsedPost = parseStatus(post)
const parsedRepeat = parseStatus(repeat)
expect(parsedPost).to.have.property('type', 'status')
expect(parsedRepeat).to.have.property('type', 'retweet')
expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
})
it('sets nsfw for statuses with the #nsfw tag', () => {
const safe = makeMockStatusQvitter({ id: '1', text: 'Hello oniichan' })
const nsfw = makeMockStatusQvitter({ id: '1', text: 'Hello oniichan #nsfw' })
expect(parseStatus(safe).nsfw).to.eq(false)
expect(parseStatus(nsfw).nsfw).to.eq(true)
})
it('leaves existing nsfw settings alone', () => {
const nsfw = makeMockStatusQvitter({ id: '1', text: 'Hello oniichan #nsfw', nsfw: false })
expect(parseStatus(nsfw).nsfw).to.eq(false)
})
})
describe('Mastoapi preprocessing and converting', () => {
it('doesn\'t blow up', () => {
const parsed = mastoapidata.map(parseStatus)
expect(parsed.length).to.eq(mastoapidata.length)
})
it('processes repeats correctly', () => {
const post = makeMockStatusMasto({ reblog: null, id: 'deadbeef' })
const repeat = makeMockStatusMasto({ reblog: post, id: 'foobar' })
const parsedPost = parseStatus(post)
const parsedRepeat = parseStatus(repeat)
expect(parsedPost).to.have.property('type', 'status')
expect(parsedRepeat).to.have.property('type', 'retweet')
expect(parsedRepeat).to.have.property('retweeted_status')
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
})
it('adds emojis to post content', () => {
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' })
const parsedPost = parseStatus(post)
expect(parsedPost).to.have.property('statusnet_html').that.contains('<img')
})
it('adds emojis to subject line', () => {
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' })
const parsedPost = parseStatus(post)
expect(parsedPost).to.have.property('summary_html').that.contains('<img')
})
})
})
// Statuses generally already contain some info regarding users and there's nearly 1:1 mapping, so very little to test
describe('parseUsers (MastoAPI)', () => {
it('sets correct is_local for users depending on their screen_name', () => {
const local = makeMockUserMasto({ acct: 'foo' })
const remote = makeMockUserMasto({ acct: 'foo@bar.baz' })
expect(parseUser(local)).to.have.property('is_local', true)
expect(parseUser(remote)).to.have.property('is_local', false)
})
it('adds emojis to user name', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' })
const parsedUser = parseUser(user)
expect(parsedUser).to.have.property('name_html').that.contains('<img')
})
it('adds emojis to user bio', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' })
const parsedUser = parseUser(user)
expect(parsedUser).to.have.property('description_html').that.contains('<img')
})
it('adds emojis to user profile fields', () => {
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] })
const parsedUser = parseUser(user)
expect(parsedUser).to.have.property('fields_html').to.be.an('array')
const field = parsedUser.fields_html[0]
expect(field).to.have.property('name').that.contains('<img')
expect(field).to.have.property('value').that.contains('<img')
})
+ it('removes html tags from user profile fields', () => {
+ const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
+
+ const parsedUser = parseUser(user)
+
+ expect(parsedUser).to.have.property('fields_text').to.be.an('array')
+
+ const field = parsedUser.fields_text[0]
+
+ expect(field).to.have.property('name').that.equal('user')
+ expect(field).to.have.property('value').that.equal('@user')
+ })
+
it('adds hide_follows and hide_followers user settings', () => {
const user = makeMockUserMasto({ pleroma: { hide_followers: true, hide_follows: false, hide_followers_count: false, hide_follows_count: true } })
expect(parseUser(user)).to.have.property('hide_followers', true)
expect(parseUser(user)).to.have.property('hide_follows', false)
expect(parseUser(user)).to.have.property('hide_followers_count', false)
expect(parseUser(user)).to.have.property('hide_follows_count', true)
})
})
// We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI
// is more of an afterthought
describe('parseNotifications (QvitterAPI)', () => {
it('correctly normalizes data to FE\'s format', () => {
const notif = makeMockNotificationQvitter({
id: 123,
notice: makeMockStatusQvitter({ id: 444 }),
from_profile: makeMockUserQvitter({ id: 'spurdo' })
})
expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('seen', false)
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
})
it('correctly normalizes favorite notifications', () => {
const notif = makeMockNotificationQvitter({
id: 123,
ntype: 'like',
notice: makeMockStatusQvitter({
id: 444,
favorited_status: makeMockStatusQvitter({ id: 4412 })
}),
is_seen: 1,
from_profile: makeMockUserQvitter({ id: 'spurdo' })
})
expect(parseNotification(notif)).to.have.property('id', 123)
expect(parseNotification(notif)).to.have.property('type', 'like')
expect(parseNotification(notif)).to.have.property('seen', true)
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
})
})
describe('MastoAPI emoji adder', () => {
const emojis = makeMockEmojiMasto()
const imageHtml = '<img src="https://example.com/image.png" alt="image" title="image" class="emoji" />'
.replace(/"/g, '\'')
const thinkHtml = '<img src="https://example.com/think.png" alt="thinking" title="thinking" class="emoji" />'
.replace(/"/g, '\'')
it('correctly replaces shortcodes in supplied string', () => {
const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis)
expect(result).to.include(thinkHtml)
expect(result).to.include(imageHtml)
})
it('handles consecutive emojis correctly', () => {
const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis)
expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml)
})
it('Doesn\'t replace nonexistent emojis', () => {
const result = addEmojis('Admin add the :tenshi: emoji', emojis)
expect(result).to.equal('Admin add the :tenshi: emoji')
})
it('Doesn\'t blow up on regex special characters', () => {
const emojis = makeMockEmojiMasto([{
shortcode: 'c++'
}, {
shortcode: '[a-z] {|}*'
}])
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
expect(result).to.include('title=\'c++\'')
expect(result).to.include('title=\'[a-z] {|}*\'')
})
})
})

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 19, 7:20 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1765319
Default Alt Text
(26 KB)

Event Timeline