Skip to content

Commit

Permalink
refactor: Fix typescript errors in mention rendering (#14869) (#14920)
Browse files Browse the repository at this point in the history
* refactor: Fix typescript errors in mention rendering (refactor/SQSERVICES-1944)

* update test case titles

* update tests
  • Loading branch information
thisisamir98 authored Mar 27, 2023
1 parent 1243532 commit 2cd4ac3
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,23 @@ const TextMessage: FC<TextMessageRendererProps> = ({
if (!target) {
return;
}
const isEmail = target.closest('[data-email-link]');
const isMarkdownLink = target.closest('[data-md-link]');
const isMention = target.closest('.message-mention');
const emailElement = target.closest('[data-email-link]');
const markdownLinkElement = target.closest('[data-md-link]');
const mentionElement = target.closest('.message-mention');

if (isEmail || isMarkdownLink) {
const href = (event.target as HTMLAnchorElement).href;
if (markdownLinkElement) {
const href = (markdownLinkElement as HTMLAnchorElement).href;
const markdownLinkDetails = {
href: href,
};
forwardEvent(event.nativeEvent, isEmail ? 'email' : 'markdownLink', markdownLinkDetails);
} else if (isMention) {
forwardEvent(event.nativeEvent, 'markdownLink', markdownLinkDetails);
} else if (emailElement) {
const href = (emailElement as HTMLAnchorElement).href;
const markdownLinkDetails = {
href: href,
};
forwardEvent(event.nativeEvent, 'email', markdownLinkDetails);
} else if (mentionElement) {
const mentionMsgDetails = {
userId: target.dataset.userId,
userDomain: target.dataset.userDomain,
Expand Down
128 changes: 66 additions & 62 deletions src/script/util/messageRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import {QualifiedId} from '@wireapp/api-client/lib/user';
import hljs from 'highlight.js';
import MarkdownIt from 'markdown-it';
import type Token from 'markdown-it/lib/token';
import {escapeHtml} from 'markdown-it/lib/common/utils';
import {escape} from 'underscore';

import {replaceInRange} from './StringUtil';
Expand All @@ -46,48 +46,44 @@ const markdownit = new MarkdownIt('zero', {
linkify: true,
}).enable(['autolink', 'backticks', 'code', 'emphasis', 'escape', 'fence', 'heading', 'link', 'linkify', 'newline']);

const originalFenceRule = markdownit.renderer.rules.fence;

markdownit.renderer.rules.fence = (tokens, idx, options, env, self) => {
const highlighted = originalFenceRule(tokens, idx, options, env, self);
tokens[idx].map[1] += 1;
return highlighted.replace(/\n$/, '');
};
const originalFenceRule = markdownit.renderer.rules.fence!;

markdownit.renderer.rules.heading_open = () => '<div class="md-heading">';
markdownit.renderer.rules.heading_close = () => '</div>';

markdownit.renderer.rules.softbreak = () => '<br>';
markdownit.renderer.rules.hardbreak = () => '<br>';
markdownit.renderer.rules.paragraph_open = (tokens, idx) => {
const [position] = tokens[idx].map;
const [position] = tokens[idx].map || [0, 0];

const previousWithMap = tokens
.slice(0, idx)
.reverse()
.find(({map}) => map?.length);
const previousPosition = previousWithMap ? previousWithMap.map[1] - 1 : 0;
const previousPosition = previousWithMap ? (previousWithMap.map || [0, 0])[1] - 1 : 0;
const count = position - previousPosition;
return '<br>'.repeat(Math.max(count, 0));
};
markdownit.renderer.rules.paragraph_close = () => '';

const renderMention = (mentionData: MentionText) => {
const elementClasses = mentionData.isSelfMentioned ? ' self-mention' : '';
let elementAttributes = mentionData.isSelfMentioned
? ' data-uie-name="label-self-mention" role="button"'
: ` data-uie-name="label-other-mention" data-user-id="${escape(mentionData.userId)}" role="button"`;
if (!mentionData.isSelfMentioned && mentionData.domain) {
elementAttributes += ` data-user-domain="${escape(mentionData.domain)}"`;
}

const mentionText = mentionData.text.replace(/^@/, '');
const content = `<span class="mention-at-sign">@</span>${escape(mentionText)}`;
return `<div class="message-mention${elementClasses}"${elementAttributes}>${content}</div>`;
};

markdownit.normalizeLinkText = text => text;

export const renderMessage = (message: string, selfId: QualifiedId | null, mentionEntities: MentionEntity[] = []) => {
const createMentionHash = (mention: MentionEntity) => `@@${window.btoa(JSON.stringify(mention)).replace(/=/g, '')}`;
const renderMention = (mentionData: MentionText) => {
const elementClasses = mentionData.isSelfMentioned ? ' self-mention' : '';
let elementAttributes = mentionData.isSelfMentioned
? ' data-uie-name="label-self-mention" role="button"'
: ` data-uie-name="label-other-mention" data-user-id="${escape(mentionData.userId)}" role="button"`;
if (!mentionData.isSelfMentioned && mentionData.domain) {
elementAttributes += ` data-user-domain="${escape(mentionData.domain)}"`;
}

const mentionText = mentionData.text.replace(/^@/, '');
const content = `<span class="mention-at-sign">@</span>${escape(mentionText)}`;
return `<div class="message-mention${elementClasses}"${elementAttributes}>${content}</div>`;
};

const mentionTexts: Record<string, MentionText> = {};

Expand All @@ -107,6 +103,38 @@ export const renderMessage = (message: string, selfId: QualifiedId | null, menti
return replaceInRange(strippedText, mentionKey, mention.startIndex, mention.startIndex + mention.length);
}, message);

const removeMentionsHashes = (hashedText: string): string => {
return Object.entries(mentionTexts).reduce(
(text, [mentionHash, mention]) => text.replace(mentionHash, () => mention.text),
hashedText,
);
};

const renderMentions = (inputText: string): string => {
const replacedText = Object.keys(mentionTexts).reduce((text, mentionHash) => {
const mentionMarkup = renderMention(mentionTexts[mentionHash]);
return text.replace(mentionHash, () => mentionMarkup);
}, inputText);
return replacedText;
};

markdownit.renderer.rules.text = (tokens, idx) => {
const escapedText = escapeHtml(tokens[idx].content);

return renderMentions(escapedText);
};

markdownit.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx];
token.info = removeMentionsHashes(token.info);
const highlighted = originalFenceRule(tokens, idx, options, env, self);
(tokens[idx].map ?? [0, 0])[1] += 1;

const replacedText = renderMentions(highlighted);

return replacedText.replace(/\n$/, '');
};

markdownit.set({
highlight: function (code, lang): string {
const containsMentions = mentionEntities.some(mention => {
Expand All @@ -118,30 +146,26 @@ export const renderMessage = (message: string, selfId: QualifiedId | null, menti
// highlighting will be wrong anyway because this is not valid code
return escape(code);
}
return hljs.highlightAuto(code, lang && [lang]).value;
return hljs.highlightAuto(code, lang ? [lang] : undefined).value;
},
});

markdownit.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const cleanString = (hashedString: string) =>
escape(
Object.entries(mentionTexts).reduce(
(text, [mentionHash, mention]) => text.replace(mentionHash, () => mention.text),
hashedString,
),
);
const cleanString = (hashedString: string) => escape(removeMentionsHashes(hashedString));
const link = tokens[idx];
const href = link.attrGet('href');
const isEmail = href.startsWith('mailto:');
const isWireDeepLink = href.toLowerCase().startsWith('wire://');
const href = removeMentionsHashes(link.attrGet('href') ?? '');
const isEmail = href?.startsWith('mailto:');
const isWireDeepLink = href?.toLowerCase().startsWith('wire://');
const nextToken = tokens[idx + 1];
const text = nextToken?.type === 'text' ? nextToken.content : '';

if (!href || !text.trim()) {
nextToken.content = '';
const closeToken = tokens.slice(idx).find(token => token.type === 'link_close');
closeToken.type = 'text';
closeToken.content = '';
if (closeToken) {
closeToken.type = 'text';
closeToken.content = '';
}
return `[${cleanString(text)}](${cleanString(href)})`;
}
if (isEmail) {
Expand All @@ -150,14 +174,11 @@ export const renderMessage = (message: string, selfId: QualifiedId | null, menti
link.attrPush(['target', '_blank']);
link.attrPush(['rel', 'nofollow noopener noreferrer']);
}
link.attrSet('href', href);
if (!isWireDeepLink && !['autolink', 'linkify'].includes(link.markup)) {
const title = link.attrGet('title');
if (title) {
link.attrSet('title', title);
}
link.attrSet('href', href);
if (nextToken?.type === 'text') {
nextToken.content = text;
link.attrSet('title', removeMentionsHashes(title));
}
link.attrPush(['data-md-link', 'true']);
link.attrPush(['data-uie-name', 'markdown-link']);
Expand All @@ -170,30 +191,13 @@ export const renderMessage = (message: string, selfId: QualifiedId | null, menti
}
return self.renderToken(tokens, idx, options);
};
const originalTokens = markdownit.parse(mentionlessText, {});
const modifiedLinksTokens = markdownit.parse(mentionlessText, {});
const fixCodeTokens = (modifiedTokens: Token[], originalTokens: Token[]) =>
modifiedTokens.map((modifiedToken, index) => {
const originalToken = originalTokens[index];
if (modifiedToken.tag === 'code') {
return originalToken;
}
if (modifiedToken.children) {
modifiedToken.children = fixCodeTokens(modifiedToken.children, originalToken.children);
}
return modifiedToken;
});
const fixedTokens = fixCodeTokens(modifiedLinksTokens, originalTokens);
mentionlessText = markdownit.renderer.render(fixedTokens, (markdownit as MarkdownItWithOptions).options, {});

const tokens = markdownit.parse(mentionlessText, {});
mentionlessText = markdownit.renderer.render(tokens, (markdownit as MarkdownItWithOptions).options, {});
// Remove <br> and \n if it is the last thing in a message
mentionlessText = mentionlessText.replace(/(<br>|\n)*$/, '');

const parsedText = Object.keys(mentionTexts).reduce((text, mentionHash) => {
const mentionMarkup = renderMention(mentionTexts[mentionHash]);

return text.replace(mentionHash, () => mentionMarkup);
}, mentionlessText);
return parsedText;
return mentionlessText;
};

export const getRenderedTextContent = (text: string): string => {
Expand Down
34 changes: 34 additions & 0 deletions test/unit_tests/util/messageRendererSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,40 @@ describe('renderMessage', () => {
testCase: 'displays mention inside code block',
text: 'salut\n```\n@you\n```',
},
{
expected: 'salut<pre><code class="lang-@you"></code></pre>',
mentions: [{length: 4, startIndex: 10, userId: 'pain-id'}],
testCase: 'mention does not affect code language',
text: 'salut\n\n```@you\n```',
},
{
expected:
'<a href="https://wire.com/#@you" target="_blank" rel="nofollow noopener noreferrer" data-md-link="true" data-uie-name="markdown-link">text</a>',
mentions: [{length: 4, startIndex: 25, userId: 'pain-id'}],
testCase: 'mention does not affect url',
text: '[text](https://wire.com/#@you)',
},
{
expected:
'<a href="https://wire.com/#" title="@you" target="_blank" rel="nofollow noopener noreferrer" data-md-link="true" data-uie-name="markdown-link">text</a>',
mentions: [{length: 4, startIndex: 27, userId: 'pain-id'}],
testCase: 'mention in title is displayed correctly',
text: '[text](https://wire.com/# "@you")',
},
{
expected:
'<a href="https://wire.com" target="_blank" rel="nofollow noopener noreferrer" data-md-link="true" data-uie-name="markdown-link"><div class="message-mention" data-uie-name="label-other-mention" data-user-id="pain-id" role="button"><span class="mention-at-sign">@</span>you</div></a>',
mentions: [{length: 4, startIndex: 1, userId: 'pain-id'}],
testCase: 'mention works correctly as part of a link text',
text: '[@you](https://wire.com)',
},
{
expected:
'<a href="http://wire.com#@you" target="_blank" rel="nofollow noopener noreferrer">http://wire.com#<div class="message-mention" data-uie-name="label-other-mention" data-user-id="pain-id" role="button"><span class="mention-at-sign">@</span>you</div></a>',
mentions: [{length: 4, startIndex: 16, userId: 'pain-id'}],
testCase: "mentions don't affect auto-detected links",
text: 'http://wire.com#@you',
},
];

tests.forEach(({expected, mentions, testCase, text}) => {
Expand Down

0 comments on commit 2cd4ac3

Please sign in to comment.