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:
Pranav Raj S
2023-03-02 23:56:54 -08:00
committed by GitHub
parent ec04ddc725
commit 2a385f377c
13 changed files with 332 additions and 106 deletions

View File

@@ -380,14 +380,6 @@
text-decoration: underline;
}
blockquote {
border-left-color: var(--s-300);
p {
color: var(--s-300);
}
}
p:last-child {
margin-bottom: 0;
}
@@ -408,19 +400,8 @@
text-decoration: underline;
}
blockquote {
border-left-color: var(--w-100);
p {
color: var(--w-100);
}
}
pre code {
background: var(--color-background);
}
p:last-child {
margin-bottom: 0;
}
}

View File

@@ -243,14 +243,14 @@ export default {
return this.contentAttributes.translations || {};
},
displayQuotedButton() {
if (!this.isIncoming) {
return false;
}
if (this.emailMessageContent.includes('<blockquote')) {
return true;
}
if (!this.isIncoming) {
return false;
}
return false;
},
translationsAvailable() {
@@ -660,4 +660,71 @@ li.right {
.context-menu {
position: relative;
}
/* Markdown styling */
.bubble .text-content {
p code {
background-color: var(--s-75);
display: inline-block;
line-height: 1;
border-radius: var(--border-radius-small);
padding: var(--space-smaller);
}
pre {
background-color: var(--s-75);
border-color: var(--s-75);
color: var(--s-800);
border-radius: var(--border-radius-normal);
padding: var(--space-small);
margin-top: var(--space-smaller);
margin-bottom: var(--space-small);
display: block;
line-height: 1.7;
white-space: pre-wrap;
code {
background-color: transparent;
color: var(--s-800);
padding: 0;
}
}
blockquote {
border-left: var(--space-micro) solid var(--s-75);
color: var(--s-800);
padding: var(--space-smaller) var(--space-small);
margin: var(--space-smaller) 0;
padding: var(--space-small) var(--space-small) 0 var(--space-normal);
}
}
.right .bubble .text-content {
p code {
background-color: var(--w-600);
color: var(--white);
}
pre {
background-color: var(--w-800);
border-color: var(--w-700);
color: var(--white);
code {
background-color: transparent;
color: var(--white);
}
}
blockquote {
border-left: var(--space-micro) solid var(--w-400);
color: var(--white);
p {
color: var(--w-75);
}
}
}
</style>

View File

@@ -2,14 +2,14 @@
<div
class="message-text__wrap"
:class="{
'show--quoted': showQuotedContent,
'hide--quoted': !showQuotedContent,
'show--quoted': isQuotedContentPresent,
'hide--quoted': !isQuotedContentPresent,
}"
>
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<letter v-else class="text-content" :html="message" />
<button
v-if="displayQuotedButton"
v-if="showQuoteToggle"
class="quoted-text--button"
@click="toggleQuotedContent"
>
@@ -49,6 +49,20 @@ export default {
showQuotedContent: false,
};
},
computed: {
isQuotedContentPresent() {
if (!this.isEmail) {
return this.message.includes('<blockquote');
}
return this.showQuotedContent;
},
showQuoteToggle() {
if (!this.isEmail) {
return false;
}
return this.displayQuotedButton;
},
},
methods: {
toggleQuotedContent() {
this.showQuotedContent = !this.showQuotedContent;

View File

@@ -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() {

View 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));
}

View File

@@ -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>`
);
});
});

View File

@@ -26,6 +26,9 @@ $space-big: 4rem;
$space-jumbo: 5rem;
$space-mega: 6.25rem;
$border-radius-small: 0.1875rem;
$border-radius-normal: 0.3125rem;
// font-weight
$font-weight-feather: 100;
$font-weight-light: 300;
@@ -74,8 +77,16 @@ $line-height: 1;
$footer-height: 11.2rem;
$header-expanded-height: $space-medium * 10;
$font-family: 'Inter', -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Tahoma, Arial, sans-serif;
$font-family: 'Inter',
-apple-system,
system-ui,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Tahoma,
Arial,
sans-serif;
// Break points
$break-point-medium: 667px;

View File

@@ -1,4 +1,4 @@
.file-uploads .attachment-button+label {
.file-uploads .attachment-button + label {
cursor: pointer;
}
@@ -61,7 +61,7 @@
}
.agent-message-wrap {
+.agent-message-wrap {
+ .agent-message-wrap {
margin-top: $space-micro;
.agent-message .chat-bubble {
@@ -69,11 +69,11 @@
}
}
+.user-message-wrap {
+ .user-message-wrap {
margin-top: $space-normal;
}
&.has-response+.user-message-wrap {
&.has-response + .user-message-wrap {
margin-top: $space-micro;
.chat-bubble {
@@ -81,7 +81,7 @@
}
}
&.has-response+.agent-message-wrap {
&.has-response + .agent-message-wrap {
margin-top: $space-normal;
}
}
@@ -117,7 +117,6 @@
}
}
.user.has-attachment {
.icon-wrap {
color: $color-white;
@@ -129,7 +128,7 @@
}
.user-message-wrap {
+.user-message-wrap {
+ .user-message-wrap {
margin-top: $space-micro;
.user-message .chat-bubble {
@@ -137,7 +136,7 @@
}
}
+.agent-message-wrap {
+ .agent-message-wrap {
margin-top: $space-normal;
}
}
@@ -147,7 +146,6 @@
}
}
.unread-messages {
display: flex;
flex-direction: column;
@@ -168,7 +166,7 @@
border: 1px solid $color-border-dark;
}
+.chat-bubble-wrap {
+ .chat-bubble-wrap {
.chat-bubble {
border-top-left-radius: $space-smaller;
}
@@ -190,7 +188,7 @@
border-radius: $space-two;
}
+.chat-bubble-wrap {
+ .chat-bubble-wrap {
.chat-bubble {
border-top-right-radius: $space-smaller;
}
@@ -206,7 +204,6 @@
}
}
.chat-bubble {
@include light-shadow;
border-radius: $space-two;
@@ -214,6 +211,7 @@
display: inline-block;
font-size: $font-size-default;
line-height: 1.5;
max-width: 100%;
padding: $space-slab $space-normal;
text-align: left;
word-break: break-word;
@@ -222,7 +220,7 @@
max-width: 100%;
}
>a {
> a {
color: $color-primary;
word-break: break-all;
}
@@ -234,7 +232,7 @@
&.user {
border-bottom-right-radius: $space-smaller;
>a {
> a {
color: $color-white;
}
}

View File

@@ -5,7 +5,7 @@
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
"
class="chat-bubble agent"
:class="$dm('bg-white', 'dark:bg-slate-700')"
:class="$dm('bg-white', 'dark:bg-slate-700 has-dark-mode')"
>
<div
v-dompurify-html="formatMessage(message, false)"
@@ -142,14 +142,3 @@ export default {
},
};
</script>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
.chat-bubble .message-content::v-deep pre {
background: $color-primary-light;
color: $color-body;
overflow-y: auto;
padding: $space-smaller;
}
</style>

View File

@@ -35,3 +35,56 @@ export default {
max-width: 90%;
}
</style>
<style lang="scss">
@import '~widget/assets/scss/variables.scss';
.chat-bubble .message-content,
.chat-bubble.user {
p code {
background-color: var(--s-75);
display: inline-block;
line-height: 1;
border-radius: $border-radius-small;
padding: $space-smaller;
}
pre {
overflow-y: auto;
background-color: var(--s-75);
border-color: var(--s-75);
color: var(--s-800);
border-radius: $border-radius-normal;
padding: $space-small;
margin-top: $space-smaller;
margin-bottom: $space-small;
display: block;
line-height: 1.7;
white-space: pre-wrap;
code {
background-color: transparent;
color: var(--s-800);
padding: 0;
}
}
blockquote {
border-left: $space-micro solid var(--s-75);
color: var(--s-800);
padding: $space-smaller $space-small;
margin: $space-smaller 0;
padding: $space-small $space-small 0 $space-normal;
}
}
@media (prefers-color-scheme: dark) {
.chat-bubble.agent.has-dark-mode {
blockquote {
border-color: var(--s-200);
color: var(--s-50);
}
}
}
</style>

View File

@@ -38,10 +38,31 @@ export default {
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
.chat-bubble.user::v-deep pre {
background: $color-primary-light;
color: $color-body;
overflow: auto;
padding: $space-smaller;
.chat-bubble.user::v-deep {
p code {
background-color: var(--w-600);
color: var(--white);
}
pre {
background-color: var(--w-800);
border-color: var(--w-700);
color: var(--white);
code {
background-color: transparent;
color: var(--white);
}
}
blockquote {
border-left: $space-micro solid var(--w-400);
background: var(--s-25);
border-color: var(--s-200);
p {
color: var(--s-800);
}
}
}
</style>