mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
feat: Add support for markdown in messages (#1642)
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
committed by
GitHub
parent
5adbc84e0c
commit
a5c3c4301c
@@ -3,7 +3,7 @@
|
|||||||
@include margin($zero);
|
@include margin($zero);
|
||||||
background: $color-woot;
|
background: $color-woot;
|
||||||
border-radius: $space-one;
|
border-radius: $space-one;
|
||||||
color: $color-white;
|
color: var(--white);
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: $font-weight-normal;
|
font-weight: $font-weight-normal;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -11,9 +11,8 @@
|
|||||||
.message-text__wrap {
|
.message-text__wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
color: $color-white;
|
color: var(--white);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,8 +87,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.content-box {
|
.content-box {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -138,7 +135,6 @@
|
|||||||
@include flex-weight(1);
|
@include flex-weight(1);
|
||||||
@include margin($zero);
|
@include margin($zero);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
// Firefox flexbox fix
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-bottom: var(--space-normal);
|
padding-bottom: var(--space-normal);
|
||||||
@@ -164,7 +160,7 @@
|
|||||||
@include elegant-card;
|
@include elegant-card;
|
||||||
@include round-corner;
|
@include round-corner;
|
||||||
background: $color-woot;
|
background: $color-woot;
|
||||||
color: $color-white;
|
color: var(--white);
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
margin: $space-one auto;
|
margin: $space-one auto;
|
||||||
@@ -215,6 +211,7 @@
|
|||||||
color: $color-primary-dark;
|
color: $color-primary-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
+.right {
|
+.right {
|
||||||
@@ -303,6 +300,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-wrap .message-text__wrap {
|
||||||
|
.text-content p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-footer {
|
.conversation-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -320,7 +323,7 @@
|
|||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
@include elegant-card;
|
@include elegant-card;
|
||||||
@include round-corner;
|
@include round-corner;
|
||||||
background: $color-white;
|
background: var(--white);
|
||||||
color: $color-light-gray;
|
color: $color-light-gray;
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
@@ -333,3 +336,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left .bubble .text-content {
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: var(--color-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-woot);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left-color: var(--s-300);
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--s-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right .bubble .text-content {
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--white);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<p v-if="lastMessageInChat" class="conversation--message">
|
<p v-if="lastMessageInChat" class="conversation--message">
|
||||||
<i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i>
|
<i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i>
|
||||||
<span v-if="lastMessageInChat.content">
|
<span v-if="lastMessageInChat.content">
|
||||||
{{ lastMessageInChat.content }}
|
{{ parsedLastMessage }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span>
|
<span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
|
||||||
import Thumbnail from '../Thumbnail';
|
import Thumbnail from '../Thumbnail';
|
||||||
import conversationMixin from '../../../mixins/conversations';
|
import conversationMixin from '../../../mixins/conversations';
|
||||||
@@ -59,7 +60,7 @@ export default {
|
|||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [timeMixin, conversationMixin],
|
mixins: [timeMixin, conversationMixin, messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
activeLabel: {
|
activeLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -129,6 +130,10 @@ export default {
|
|||||||
const { message_type: messageType } = lastMessage;
|
const { message_type: messageType } = lastMessage;
|
||||||
return messageType === MESSAGE_TYPE.OUTGOING;
|
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
parsedLastMessage() {
|
||||||
|
return this.getPlainText(this.lastMessageInChat.content);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="message-text__wrap">
|
<div class="message-text__wrap">
|
||||||
<span v-html="message"></span>
|
<div class="text-content" v-html="message"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { frontendURL, conversationUrl } from '../../../../helper/URLHelper';
|
import { frontendURL, conversationUrl } from '../../../../helper/URLHelper';
|
||||||
import timeMixin from '../../../../mixins/time';
|
import timeMixin from '../../../../mixins/time';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
directives: {
|
directives: {
|
||||||
@@ -66,7 +67,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
mixins: [timeMixin, messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -107,7 +108,8 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
prepareContent(content = '') {
|
prepareContent(content = '') {
|
||||||
return content.replace(
|
const plainTextContent = this.getPlainText(content);
|
||||||
|
return plainTextContent.replace(
|
||||||
new RegExp(`(${this.searchTerm})`, 'ig'),
|
new RegExp(`(${this.searchTerm})`, 'ig'),
|
||||||
'<span class="searchkey--highlight">$1</span>'
|
'<span class="searchkey--highlight">$1</span>'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import marked from 'marked';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
import { escapeHtml } from './HTMLSanitizer';
|
import { escapeHtml } from './HTMLSanitizer';
|
||||||
|
|
||||||
const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
|
const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
|
||||||
const TWITTER_USERNAME_REPLACEMENT =
|
const TWITTER_USERNAME_REPLACEMENT =
|
||||||
'$1<a href="http://twitter.com/$2" target="_blank" rel="noreferrer nofollow noopener">@$2</a>';
|
'$1<a href="http://twitter.com/$2" target="_blank" rel="noreferrer nofollow noopener">@$2</a>';
|
||||||
@@ -9,41 +12,49 @@ const TWITTER_HASH_REPLACEMENT =
|
|||||||
|
|
||||||
class MessageFormatter {
|
class MessageFormatter {
|
||||||
constructor(message, isATweet = false) {
|
constructor(message, isATweet = false) {
|
||||||
this.message = escapeHtml(message || '') || '';
|
this.message = DOMPurify.sanitize(escapeHtml(message) || '');
|
||||||
this.isATweet = isATweet;
|
this.isATweet = isATweet;
|
||||||
|
this.marked = marked;
|
||||||
|
|
||||||
|
const renderer = {
|
||||||
|
heading(text) {
|
||||||
|
return `<strong>${text}</strong>`;
|
||||||
|
},
|
||||||
|
link(url, title, text) {
|
||||||
|
return `<a rel="noreferrer noopener nofollow" href="${url}" class="link" title="${title ||
|
||||||
|
''}" target="_blank">${text}</a>`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.marked.use({ renderer });
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMessage() {
|
formatMessage() {
|
||||||
const linkifiedMessage = this.linkify();
|
|
||||||
const messageWithNextLines = linkifiedMessage.replace(/\n/g, '<br>');
|
|
||||||
if (this.isATweet) {
|
if (this.isATweet) {
|
||||||
const messageWithUserName = messageWithNextLines.replace(
|
const withUserName = this.message.replace(
|
||||||
TWITTER_USERNAME_REGEX,
|
TWITTER_USERNAME_REGEX,
|
||||||
TWITTER_USERNAME_REPLACEMENT
|
TWITTER_USERNAME_REPLACEMENT
|
||||||
);
|
);
|
||||||
return messageWithUserName.replace(
|
const withHash = withUserName.replace(
|
||||||
TWITTER_HASH_REGEX,
|
TWITTER_HASH_REGEX,
|
||||||
TWITTER_HASH_REPLACEMENT
|
TWITTER_HASH_REPLACEMENT
|
||||||
);
|
);
|
||||||
|
const markedDownOutput = marked(withHash);
|
||||||
|
return markedDownOutput;
|
||||||
}
|
}
|
||||||
return messageWithNextLines;
|
return marked(this.message);
|
||||||
}
|
|
||||||
|
|
||||||
linkify() {
|
|
||||||
if (!this.message) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
||||||
return this.message.replace(
|
|
||||||
urlRegex,
|
|
||||||
url =>
|
|
||||||
`<a rel="noreferrer noopener nofollow" href="${url}" class="link" target="_blank">${url}</a>`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get formattedMessage() {
|
get formattedMessage() {
|
||||||
return this.formatMessage();
|
return this.formatMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get plainText() {
|
||||||
|
const strippedOutHtml = new DOMParser().parseFromString(
|
||||||
|
this.formattedMessage,
|
||||||
|
'text/html'
|
||||||
|
);
|
||||||
|
return strippedOutHtml.body.textContent || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MessageFormatter;
|
export default MessageFormatter;
|
||||||
|
|||||||
@@ -4,9 +4,25 @@ describe('#MessageFormatter', () => {
|
|||||||
describe('content with links', () => {
|
describe('content with links', () => {
|
||||||
it('should format correctly', () => {
|
it('should format correctly', () => {
|
||||||
const message =
|
const message =
|
||||||
'Chatwoot is an opensource tool\nSee more at https://www.chatwoot.com';
|
'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)';
|
||||||
expect(new MessageFormatter(message).formattedMessage).toEqual(
|
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||||
'Chatwoot is an opensource tool<br>See more at <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" target="_blank">https://www.chatwoot.com</a>'
|
'<p>Chatwoot is an opensource tool. <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" title="" 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 rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" title="" target="_blank">https://www.chatwoot.com</a></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parses heading to strong', () => {
|
||||||
|
it('should format correctly', () => {
|
||||||
|
const message = '### opensource \n ## tool';
|
||||||
|
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||||
|
'<strong>opensource</strong><strong>tool</strong>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -14,21 +30,31 @@ describe('#MessageFormatter', () => {
|
|||||||
describe('tweets', () => {
|
describe('tweets', () => {
|
||||||
it('should return the same string if not tags or @mentions', () => {
|
it('should return the same string if not tags or @mentions', () => {
|
||||||
const message = 'Chatwoot is an opensource tool';
|
const message = 'Chatwoot is an opensource tool';
|
||||||
expect(new MessageFormatter(message).formattedMessage).toEqual(message);
|
expect(new MessageFormatter(message).formattedMessage).toMatch(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add links to @mentions', () => {
|
it('should add links to @mentions', () => {
|
||||||
const message =
|
const message =
|
||||||
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
||||||
expect(new MessageFormatter(message, true).formattedMessage).toEqual(
|
expect(new MessageFormatter(message, true).formattedMessage).toMatch(
|
||||||
'<a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername'
|
'<p><a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add links to #tags', () => {
|
it('should add links to #tags', () => {
|
||||||
const message = '#chatwootapp is an opensource tool';
|
const message = '#chatwootapp is an opensource tool';
|
||||||
expect(new MessageFormatter(message, true).formattedMessage).toEqual(
|
expect(new MessageFormatter(message, true).formattedMessage).toMatch(
|
||||||
'<a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool'
|
'<p><a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool</p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('plain text content', () => {
|
||||||
|
it('returns the plain text without HTML', () => {
|
||||||
|
const message =
|
||||||
|
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>';
|
||||||
|
expect(new MessageFormatter(message).plainText).toMatch(
|
||||||
|
'Chatwoot is an opensource tool. https://www.chatwoot.com'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export default {
|
|||||||
const messageFormatter = new MessageFormatter(message, isATweet);
|
const messageFormatter = new MessageFormatter(message, isATweet);
|
||||||
return messageFormatter.formattedMessage;
|
return messageFormatter.formattedMessage;
|
||||||
},
|
},
|
||||||
|
getPlainText(message, isATweet) {
|
||||||
|
const messageFormatter = new MessageFormatter(message, isATweet);
|
||||||
|
return messageFormatter.plainText;
|
||||||
|
},
|
||||||
truncateMessage(description = '') {
|
truncateMessage(description = '') {
|
||||||
if (description.length < 100) {
|
if (description.length < 100) {
|
||||||
return description;
|
return description;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import messageFormatterMixin from '../messageFormatterMixin';
|
||||||
|
|
||||||
|
describe('messageFormatterMixin', () => {
|
||||||
|
it('returns correct plain text', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
mixins: [messageFormatterMixin],
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component);
|
||||||
|
const message =
|
||||||
|
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>';
|
||||||
|
expect(wrapper.vm.getPlainText(message)).toMatch(
|
||||||
|
'Chatwoot is an opensource tool. https://www.chatwoot.com'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
v-if="!isCards && !isOptions && !isForm && !isArticle"
|
v-if="!isCards && !isOptions && !isForm && !isArticle"
|
||||||
class="chat-bubble agent"
|
class="chat-bubble agent"
|
||||||
>
|
>
|
||||||
<span v-html="formatMessage(message, false)"></span>
|
<div class="message-content" v-html="formatMessage(message, false)"></div>
|
||||||
<email-input
|
<email-input
|
||||||
v-if="isTemplateEmail"
|
v-if="isTemplateEmail"
|
||||||
:message-id="messageId"
|
:message-id="messageId"
|
||||||
@@ -133,3 +133,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<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: scroll;
|
||||||
|
padding: $space-smaller;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export default {
|
|||||||
|
|
||||||
.message-wrap {
|
.message-wrap {
|
||||||
margin-right: $space-small;
|
margin-right: $space-small;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.in-progress {
|
.in-progress {
|
||||||
|
|||||||
@@ -44,12 +44,17 @@ export default {
|
|||||||
padding: $space-slab $space-normal $space-slab $space-normal;
|
padding: $space-slab $space-normal $space-slab $space-normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
> a {
|
> a {
|
||||||
color: $color-primary;
|
color: $color-primary;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
&.user {
|
&.user {
|
||||||
border-bottom-right-radius: $space-smaller;
|
border-bottom-right-radius: $space-smaller;
|
||||||
|
|
||||||
@@ -59,3 +64,13 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~widget/assets/scss/variables.scss';
|
||||||
|
|
||||||
|
.chat-bubble.user::v-deep pre {
|
||||||
|
background: $color-primary-light;
|
||||||
|
color: $color-body;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: $space-smaller;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -23,12 +23,14 @@
|
|||||||
"core-js": "3",
|
"core-js": "3",
|
||||||
"country-code-emoji": "^1.0.0",
|
"country-code-emoji": "^1.0.0",
|
||||||
"date-fns": "^2.16.1",
|
"date-fns": "^2.16.1",
|
||||||
|
"dompurify": "^2.2.6",
|
||||||
"dotenv": "^8.0.0",
|
"dotenv": "^8.0.0",
|
||||||
"foundation-sites": "~6.5.3",
|
"foundation-sites": "~6.5.3",
|
||||||
"highlight.js": "~10.4.1",
|
"highlight.js": "~10.4.1",
|
||||||
"ionicons": "~2.0.1",
|
"ionicons": "~2.0.1",
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^2.2.1",
|
||||||
"lodash.groupby": "^4.6.0",
|
"lodash.groupby": "^4.6.0",
|
||||||
|
"marked": "^1.2.7",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"query-string": "5",
|
"query-string": "5",
|
||||||
"spinkit": "~1.2.5",
|
"spinkit": "~1.2.5",
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -3860,6 +3860,11 @@ domhandler@^2.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
domelementtype "1"
|
domelementtype "1"
|
||||||
|
|
||||||
|
dompurify@^2.2.6:
|
||||||
|
version "2.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.6.tgz#54945dc5c0b45ce5ae228705777e8e59d7b2edc4"
|
||||||
|
integrity sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ==
|
||||||
|
|
||||||
domutils@^1.5.1, domutils@^1.7.0:
|
domutils@^1.5.1, domutils@^1.7.0:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
|
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
|
||||||
@@ -7016,6 +7021,11 @@ map-visit@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
|
marked@^1.2.7:
|
||||||
|
version "1.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb"
|
||||||
|
integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA==
|
||||||
|
|
||||||
material-colors@^1.0.0:
|
material-colors@^1.0.0:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
||||||
|
|||||||
Reference in New Issue
Block a user