-
Notifications
You must be signed in to change notification settings - Fork 304
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: transform emoji tags into standard HTML (#2870)
* 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
1 parent
040beb6
commit 7223112
Showing
4 changed files
with
92 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>` | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |