chore: Use markdown-it instead of marked (#6123)
* chore: Use markdown-it instead of marked * Adds styling for markdown rendered content * fixes codeclimate issue * Fixes blockquote styles for widget in darkmode * fix: issue block quote color issue in light mode * fix: issue block quote color issue in light mode * Fixes blockquote color in dark mode * Remove usage of dark mode mixin in user bubble * chore: code clean up --------- Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
@@ -1,57 +1,49 @@
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { escapeHtml, afterSanitizeAttributes } from './HTMLSanitizer';
|
||||
import mila from 'markdown-it-link-attributes';
|
||||
import mentionPlugin from './markdownIt/link';
|
||||
const md = require('markdown-it')({
|
||||
html: false,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
langPrefix: 'language-',
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
quotes: '\u201c\u201d\u2018\u2019',
|
||||
maxNesting: 20,
|
||||
})
|
||||
.use(mentionPlugin)
|
||||
.use(mila, {
|
||||
attrs: {
|
||||
class: 'link',
|
||||
rel: 'noreferrer noopener nofollow',
|
||||
target: '_blank',
|
||||
},
|
||||
});
|
||||
|
||||
const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
|
||||
const TWITTER_USERNAME_REPLACEMENT =
|
||||
'$1<a href="http://twitter.com/$2" target="_blank" rel="noreferrer nofollow noopener">@$2</a>';
|
||||
|
||||
const TWITTER_USERNAME_REPLACEMENT = '$1[@$2](http://twitter.com/$2)';
|
||||
const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g;
|
||||
const TWITTER_HASH_REPLACEMENT =
|
||||
'$1<a href="https://twitter.com/hashtag/$2" target="_blank" rel="noreferrer nofollow noopener">#$2</a>';
|
||||
|
||||
const USER_MENTIONS_REGEX = /mention:\/\/(user|team)\/(\d+)\/(.+)/gm;
|
||||
const TWITTER_HASH_REPLACEMENT = '$1[#$2](https://twitter.com/hashtag/$2)';
|
||||
|
||||
class MessageFormatter {
|
||||
constructor(message, isATweet = false, isAPrivateNote = false) {
|
||||
this.message = DOMPurify.sanitize(escapeHtml(message || ''));
|
||||
this.message = message || '';
|
||||
this.isAPrivateNote = isAPrivateNote;
|
||||
this.isATweet = isATweet;
|
||||
this.marked = marked;
|
||||
|
||||
const renderer = {
|
||||
heading(text) {
|
||||
return `<strong>${text}</strong>`;
|
||||
},
|
||||
link(url, title, text) {
|
||||
const mentionRegex = new RegExp(USER_MENTIONS_REGEX);
|
||||
if (url.match(mentionRegex)) {
|
||||
return `<span class="prosemirror-mention-node">${text}</span>`;
|
||||
}
|
||||
return `<a rel="noreferrer noopener nofollow" href="${url}" class="link" title="${title ||
|
||||
''}" target="_blank">${text}</a>`;
|
||||
},
|
||||
};
|
||||
this.marked.use({ renderer });
|
||||
}
|
||||
|
||||
formatMessage() {
|
||||
let updatedMessage = this.message;
|
||||
if (this.isATweet && !this.isAPrivateNote) {
|
||||
const withUserName = this.message.replace(
|
||||
updatedMessage = updatedMessage.replace(
|
||||
TWITTER_USERNAME_REGEX,
|
||||
TWITTER_USERNAME_REPLACEMENT
|
||||
);
|
||||
const withHash = withUserName.replace(
|
||||
updatedMessage = updatedMessage.replace(
|
||||
TWITTER_HASH_REGEX,
|
||||
TWITTER_HASH_REPLACEMENT
|
||||
);
|
||||
const markedDownOutput = marked(withHash);
|
||||
return markedDownOutput;
|
||||
}
|
||||
DOMPurify.addHook('afterSanitizeAttributes', afterSanitizeAttributes);
|
||||
return DOMPurify.sanitize(
|
||||
marked(this.message, { breaks: true, gfm: true })
|
||||
);
|
||||
return md.render(updatedMessage);
|
||||
}
|
||||
|
||||
get formattedMessage() {
|
||||
|
||||
69
app/javascript/shared/helpers/markdownIt/link.js
Normal file
69
app/javascript/shared/helpers/markdownIt/link.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// Process [@mention](mention://user/1/Pranav)
|
||||
const USER_MENTIONS_REGEX = /mention:\/\/(user|team)\/(\d+)\/(.+)/gm;
|
||||
|
||||
const buildMentionTokens = () => (state, silent) => {
|
||||
var label;
|
||||
var labelEnd;
|
||||
var labelStart;
|
||||
var pos;
|
||||
var res;
|
||||
var token;
|
||||
var href = '';
|
||||
var max = state.posMax;
|
||||
|
||||
if (state.src.charCodeAt(state.pos) !== 0x5b /* [ */) {
|
||||
return false;
|
||||
}
|
||||
|
||||
labelStart = state.pos + 1;
|
||||
labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true);
|
||||
|
||||
// parser failed to find ']', so it's not a valid link
|
||||
if (labelEnd < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
label = state.src.slice(labelStart, labelEnd);
|
||||
pos = labelEnd + 1;
|
||||
|
||||
if (pos < max && state.src.charCodeAt(pos) === 0x28 /* ( */) {
|
||||
pos += 1;
|
||||
res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax);
|
||||
if (res.ok) {
|
||||
href = state.md.normalizeLink(res.str);
|
||||
if (state.md.validateLink(href)) {
|
||||
pos = res.pos;
|
||||
} else {
|
||||
href = '';
|
||||
}
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
if (!href.match(new RegExp(USER_MENTIONS_REGEX))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
state.pos = labelStart;
|
||||
state.posMax = labelEnd;
|
||||
|
||||
token = state.push('mention', '');
|
||||
token.href = href;
|
||||
token.content = label;
|
||||
}
|
||||
|
||||
state.pos = pos;
|
||||
state.posMax = max;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const renderMentions = () => (tokens, idx) => {
|
||||
return `<span class="prosemirror-mention-node">${tokens[idx].content}</span>`;
|
||||
};
|
||||
|
||||
export default function mentionPlugin(md) {
|
||||
md.renderer.rules.mention = renderMentions(md);
|
||||
md.inline.ruler.before('link', 'mention', buildMentionTokens(md));
|
||||
}
|
||||
@@ -6,14 +6,14 @@ describe('#MessageFormatter', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <a title="" class="link" href="https://www.chatwoot.com" rel="noreferrer noopener nofollow" target="_blank">Chatwoot</a></p>'
|
||||
'<p>Chatwoot is an opensource tool. <a href="https://www.chatwoot.com" class="link" rel="noreferrer noopener nofollow" target="_blank">Chatwoot</a></p>'
|
||||
);
|
||||
});
|
||||
it('should format correctly', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. https://www.chatwoot.com';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <a title="" class="link" href="https://www.chatwoot.com" rel="noreferrer noopener nofollow" target="_blank">https://www.chatwoot.com</a></p>'
|
||||
'<p>Chatwoot is an opensource tool. <a href="https://www.chatwoot.com" class="link" rel="noreferrer noopener nofollow" target="_blank">https://www.chatwoot.com</a></p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,8 @@ describe('#MessageFormatter', () => {
|
||||
it('should format correctly', () => {
|
||||
const message = '### opensource \n ## tool';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<strong>opensource</strong><strong>tool</strong>'
|
||||
`<h3>opensource</h3>
|
||||
<h2>tool</h2>`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -39,7 +40,7 @@ describe('#MessageFormatter', () => {
|
||||
expect(
|
||||
new MessageFormatter(message, true, false).formattedMessage
|
||||
).toMatch(
|
||||
'<p><a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>'
|
||||
'<p><a href="http://twitter.com/chatwootapp" class="link" rel="noreferrer noopener nofollow" target="_blank">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -48,7 +49,7 @@ describe('#MessageFormatter', () => {
|
||||
expect(
|
||||
new MessageFormatter(message, true, false).formattedMessage
|
||||
).toMatch(
|
||||
'<p><a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool</p>'
|
||||
'<p><a href="https://twitter.com/hashtag/chatwootapp" class="link" rel="noreferrer noopener nofollow" target="_blank">#chatwootapp</a> is an opensource tool</p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -90,7 +91,8 @@ describe('#MessageFormatter', () => {
|
||||
const message =
|
||||
'[xssLink](javascript:alert(document.cookie))\n[normalLink](https://google.com)**I am a bold text paragraph**';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p><a title="" class="link" rel="noreferrer noopener nofollow" target="_blank">xssLink</a><br><a title="" class="link" href="https://google.com" rel="noreferrer noopener nofollow" target="_blank">normalLink</a><strong>I am a bold text paragraph</strong></p>'
|
||||
`<p>[xssLink](javascript:alert(document.cookie))<br />
|
||||
<a href="https://google.com" class="link" rel="noreferrer noopener nofollow" target="_blank">normalLink</a><strong>I am a bold text paragraph</strong></p>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user