Skip to content

Commit

Permalink
feat: transform emoji tags into standard HTML (#2870)
Browse files Browse the repository at this point in the history
* Update current tests for emojis in different positions

Signed-off-by: Martin Musale <[email protected]>

* Change emoji translation to standard HTML

Signed-off-by: Martin Musale <[email protected]>

* Use the fallback emoji transformation if no match is found

Signed-off-by: Martin Musale <[email protected]>

* Check for emoji content with other content and size appropriately

Signed-off-by: Martin Musale <[email protected]>

* Process multiple emojis in content and style them to be centered

Signed-off-by: Martin Musale <[email protected]>

* Add docs for the functions declared

Signed-off-by: Martin Musale <[email protected]>

* Add some tests for the rewriteEmojiContentToHTML functoin

Signed-off-by: Martin Musale <[email protected]>

* Change to use DOMParser to replace emoji content

Signed-off-by: Martin Musale <[email protected]>

* Update tests and remove dead code

Signed-off-by: Martin Musale <[email protected]>

* Update to use the DOMParser function

Signed-off-by: Martin Musale <[email protected]>

* Add documentation for new functions

Signed-off-by: Martin Musale <[email protected]>

* rename rewriteEmojiContentToHTMLDOMParsing to rewriteEmojiContentToHTML

Signed-off-by: Martin Musale <[email protected]>

* Add test for emojis in multiple p tags

Signed-off-by: Martin Musale <[email protected]>

* Fix prettier errors in test files

---------

Signed-off-by: Martin Musale <[email protected]>
Co-authored-by: Gavin Barron <[email protected]>
  • Loading branch information
musale and gavinbarron authored Jan 23, 2024
1 parent 040beb6 commit 7223112
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 41 deletions.
4 changes: 4 additions & 0 deletions packages/mgt-chat/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ const messageThreadStyles: MessageThreadStyles = {
chatContainer: {
'& .ui-box': {
zIndex: 'unset'
},
'& p': {
display: 'inline-flex',
justifyContent: 'center'
}
},
chatMessageContainer: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import {
} from './graph.chat';
import { updateMessageContentWithImage } from '../utils/updateMessageContentWithImage';
import { isChatMessage } from '../utils/types';
import { rewriteEmojiContent } from '../utils/rewriteEmojiContent';
import { rewriteEmojiContentToHTML } from '../utils/rewriteEmojiContent';

// 1x1 grey pixel
const placeholderImageContent =
Expand Down Expand Up @@ -1020,8 +1020,7 @@ detail: ${JSON.stringify(eventDetail)}`);
}
let content = graphMessage.body?.content ?? 'undefined';
let result: MessageConversion = {};
// do simple emoji replacement first
content = rewriteEmojiContent(content);
content = rewriteEmojiContentToHTML(content);
// Handle any mentions in the content
content = this.updateMentionsContent(content);

Expand Down
43 changes: 30 additions & 13 deletions packages/mgt-chat/src/utils/rewriteEmojiContent.tests.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/

import { expect } from '@open-wc/testing';
import { rewriteEmojiContent } from './rewriteEmojiContent';
import { rewriteEmojiContentToHTML } from './rewriteEmojiContent';

describe('emoji rewrite tests', () => {
describe('rewrite emoji to standard HTML', () => {
it('rewrites an emoji correctly', async () => {
const result = rewriteEmojiContent(`<emoji id="cool" alt="😎" title="Cool"></emoji>`);
await expect(result).to.be.equal('😎');
const result = rewriteEmojiContentToHTML(`<emoji id="cool" alt="😎" title="Cool"></emoji>`);
await expect(result).to.be.equal(
`<span contenteditable="false" title="Cool" type="(cool)" class="animated-emoticon-50-cool"><img itemscope="" itemtype="http://schema.skype.com/Emoji" itemid="cool" src="https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/cool/default/50_f.png" title="Cool" alt="😎" style="width:50px;height:50px;"></span>`
);
});
it('rewrites an emoji in a p tag correctly', async () => {
const result = rewriteEmojiContent(`<p><emoji id="cool" alt="😎" title="Cool"></emoji></p>`);
await expect(result).to.be.equal('<p>😎</p>');
const result = rewriteEmojiContentToHTML(`<p><emoji id="cool" alt="😎" title="Cool"></emoji></p>`);
await expect(result).to.be.equal(
`<p><span contenteditable="false" title="Cool" type="(cool)" class="animated-emoticon-50-cool"><img itemscope="" itemtype="http://schema.skype.com/Emoji" itemid="cool" src="https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/cool/default/50_f.png" title="Cool" alt="😎" style="width:50px;height:50px;"></span></p>`
);
});
it('rewrites multiple emoji in a p correctly', async () => {
const result = rewriteEmojiContent(
`<p><emoji id="cool" alt="😎" title="Cool"></emoji><emoji id="1f92a_zanyface" alt="🤪" title="Zany face"></emoji></p>`

it('rewrites an emoji in a p tag with additional content correctly', async () => {
const result = rewriteEmojiContentToHTML(`<p>Hello <emoji id="cool" alt="😎" title="Cool"></emoji></p>`);
await expect(result).to.be.equal(
`<p>Hello <span contenteditable="false" title="Cool" type="(cool)" class="animated-emoticon-20-cool"><img itemscope="" itemtype="http://schema.skype.com/Emoji" itemid="cool" src="https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/cool/default/20_f.png" title="Cool" alt="😎" style="width:20px;height:20px;"></span></p>`
);
await expect(result).to.be.equal('<p>😎🤪</p>');
});
it('returns the original value if there is no emoji', async () => {
const result = rewriteEmojiContent('<p><em>Seb is cool</em></p>');
await expect(result).to.be.equal('<p><em>Seb is cool</em></p>');

it('rewrites emojis in multiple p tags correctly', async () => {
const result = rewriteEmojiContentToHTML(
`<p><emoji id="hearteyes" alt="😍" title="Heart eyes"></emoji></p><p><emoji id="1f92a_zanyface" alt="🤪" title="Zany face"></emoji></p><p><emoji id="cool" alt="😎" title="Cool"></emoji></p>`
);
await expect(result).to.be.equal(
`<p><span contenteditable="false" title="Heart eyes" type="(hearteyes)" class="animated-emoticon-20-cool"><img itemscope="" itemtype="http://schema.skype.com/Emoji" itemid="hearteyes" src="https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/hearteyes/default/20_f.png" title="Heart eyes" alt="😍" style="width:20px;height:20px;"></span></p><p><span contenteditable="false" title="Zany face" type="(1f92a_zanyface)" class="animated-emoticon-20-cool"><img itemscope="" itemtype="http://schema.skype.com/Emoji" itemid="1f92a_zanyface" src="https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/1f92a_zanyface/default/20_f.png" title="Zany face" alt="🤪" style="width:20px;height:20px;"></span></p><p><span contenteditable="false" title="Cool" type="(cool)" class="animated-emoticon-20-cool"><img itemscope="" itemtype="http://schema.skype.com/Emoji" itemid="cool" src="https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/cool/default/20_f.png" title="Cool" alt="😎" style="width:20px;height:20px;"></span></p>`
);
});
});
81 changes: 56 additions & 25 deletions packages/mgt-chat/src/utils/rewriteEmojiContent.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,63 @@
/**
* Regex to detect and extract emoji alt text
*
* Pattern breakdown:
* (<emoji[^>]+): Captures the opening emoji tag, including any attributes.
* alt=["'](\w*[^"']*)["']: Matches and captures the "alt" attribute value within single or double quotes. The value can contain word characters but not quotes.
* (.[^>]): Captures any remaining text within the opening emoji tag, excluding the closing angle bracket.
* ><\/emoji>: Matches the remaining part of the tag.
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
const emojiRegex = /(<emoji[^>]+)alt=["'](\w*[^"']*)["'](.[^>]+)><\/emoji>/;
const emojiMatch = (messageContent: string): RegExpMatchArray | null => {
return messageContent.match(emojiRegex);
};
// iterative repave the emoji custom element with the content of the alt attribute
// on the emoji element
const processEmojiContent = (messageContent: string): string => {
let result = messageContent;
let match = emojiMatch(result);
while (match) {
result = result.replace(emojiRegex, '$2');
match = emojiMatch(result);

/**
* Checks if DOM content has emojis and other HTML content.
* @param dom the html content parsed into HTMLDocument.
* @param emojisCount number of emojis in the content.
* @returns true if only one emoji is in the content without other content, otherwise false.
*/
const hasOtherContent = (dom: Document, emojisCount: number): boolean => {
const isPtag = dom.body.firstChild?.nodeName === 'P';
if (isPtag) {
const firstChildNodes = dom.body.firstChild?.childNodes;
return firstChildNodes?.length !== emojisCount;
}
return result;
return false;
};

/**
* if the content contains an <emoji> tag with an alt attribute the content is replaced by replacing the emoji tags with the content of their alt attribute.
* @param {string} content
* @returns {string} the content with any emoji tags replaced by the content of their alt attribute.
* Parses html content string into HTMLDocument, then replaces instances of the
* emoji tag.
* @param content the HTML string.
* @returns HTML string with emoji tags changed to the HTML representation.
*/
export const rewriteEmojiContent = (content: string): string =>
emojiMatch(content) ? processEmojiContent(content) : content;
export const rewriteEmojiContentToHTML = (content: string): string => {
const parser = new DOMParser();
const dom = parser.parseFromString(content, 'text/html');
const emojis = dom.querySelectorAll('emoji');
const emojisCount = emojis.length;
const size = emojisCount > 1 || hasOtherContent(dom, emojisCount) ? 20 : 50;

for (const emoji of emojis) {
const id = emoji.getAttribute('id') ?? '';
const alt = emoji.getAttribute('alt') ?? '';
const title = emoji.getAttribute('title') ?? '';

const span = document.createElement('span');
span.setAttribute('contentEditable', 'false');
span.setAttribute('title', title);
span.setAttribute('type', `(${id})`);
span.setAttribute('class', `animated-emoticon-${size}-cool`);

const img = document.createElement('img');
img.setAttribute('itemscope', '');
img.setAttribute('itemtype', 'http://schema.skype.com/Emoji');
img.setAttribute('itemid', id);
img.setAttribute(
'src',
`https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/${id}/default/${size}_f.png`
);
img.setAttribute('title', title);
img.setAttribute('alt', alt);
img.setAttribute('style', `width:${size}px;height:${size}px;`);

span.appendChild(img);
emoji.replaceWith(span);
}
return dom.body.innerHTML;
};

0 comments on commit 7223112

Please sign in to comment.