diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index b67932e6d..6c892f8fb 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -422,11 +422,27 @@ } } - pre code { - background: var(--color-background); - } - p:last-child { margin-bottom: 0; } } + +.bubble { + pre { + background: var(--b-50); + border-radius: var(--border-radius-normal); + border: 1px solid var(--color-border); + display: block; + margin: var(--space-small) 0; + padding: var(--space-slab); + } + + pre code { + display: block; + padding: 0; + } + + code { + background: var(--b-50); + } +} diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js index c4d0bb62e..59e1173ac 100644 --- a/app/javascript/shared/helpers/MessageFormatter.js +++ b/app/javascript/shared/helpers/MessageFormatter.js @@ -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@$2'; - +const TWITTER_USERNAME_REPLACEMENT = '$1[@$2](http://twitter.com/$2)'; const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g; -const TWITTER_HASH_REPLACEMENT = - '$1#$2'; - -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 `${text}`; - }, - link(url, title, text) { - const mentionRegex = new RegExp(USER_MENTIONS_REGEX); - if (url.match(mentionRegex)) { - return `${text}`; - } - return `${text}`; - }, - }; - 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() { diff --git a/app/javascript/shared/helpers/markdownIt/link.js b/app/javascript/shared/helpers/markdownIt/link.js new file mode 100644 index 000000000..c5300391b --- /dev/null +++ b/app/javascript/shared/helpers/markdownIt/link.js @@ -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 `${tokens[idx].content}`; +}; + +export default function mentionPlugin(md) { + md.renderer.rules.mention = renderMentions(md); + md.inline.ruler.before('link', 'mention', buildMentionTokens(md)); +} diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js index 1f2209b71..60c875c8a 100644 --- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js +++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js @@ -6,14 +6,14 @@ describe('#MessageFormatter', () => { const message = 'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)'; expect(new MessageFormatter(message).formattedMessage).toMatch( - '

Chatwoot is an opensource tool. Chatwoot

' + '

Chatwoot is an opensource tool. Chatwoot

' ); }); it('should format correctly', () => { const message = 'Chatwoot is an opensource tool. https://www.chatwoot.com'; expect(new MessageFormatter(message).formattedMessage).toMatch( - '

Chatwoot is an opensource tool. https://www.chatwoot.com

' + '

Chatwoot is an opensource tool. https://www.chatwoot.com

' ); }); }); @@ -22,7 +22,8 @@ describe('#MessageFormatter', () => { it('should format correctly', () => { const message = '### opensource \n ## tool'; expect(new MessageFormatter(message).formattedMessage).toMatch( - 'opensourcetool' + `

opensource

+

tool

` ); }); }); @@ -39,7 +40,7 @@ describe('#MessageFormatter', () => { expect( new MessageFormatter(message, true, false).formattedMessage ).toMatch( - '

@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername

' + '

@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername

' ); }); @@ -48,7 +49,7 @@ describe('#MessageFormatter', () => { expect( new MessageFormatter(message, true, false).formattedMessage ).toMatch( - '

#chatwootapp is an opensource tool

' + '

#chatwootapp is an opensource tool

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

xssLink
normalLinkI am a bold text paragraph

' + `

[xssLink](javascript:alert(document.cookie))
+normalLinkI am a bold text paragraph

` ); }); }); diff --git a/app/javascript/widget/assets/scss/views/_conversation.scss b/app/javascript/widget/assets/scss/views/_conversation.scss index 7124c52fb..154fd2632 100644 --- a/app/javascript/widget/assets/scss/views/_conversation.scss +++ b/app/javascript/widget/assets/scss/views/_conversation.scss @@ -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; @@ -217,12 +214,13 @@ padding: $space-slab $space-normal; text-align: left; word-break: break-word; + max-width: 100%; :not([audio]) { 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; } } diff --git a/package.json b/package.json index 6d9bfeb46..7ae94bd96 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "highlight.js": "~10.4.1", "ionicons": "~2.0.1", "js-cookie": "^2.2.1", - "marked": "4.0.10", + "markdown-it": "^13.0.1", + "markdown-it-link-attributes": "^4.0.1", "md5": "^2.3.0", "ninja-keys": "^1.1.9", "opus-recorder": "^8.0.5", diff --git a/yarn.lock b/yarn.lock index 2615cdb42..5eee21a60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4819,6 +4819,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -7482,6 +7487,11 @@ entities@~2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== +entities@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" + integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -10914,6 +10924,13 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" +linkify-it@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" + integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== + dependencies: + uc.micro "^1.0.1" + lint-staged@10.5.4: version "10.5.4" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.5.4.tgz#cd153b5f0987d2371fc1d2847a409a2fe705b665" @@ -11314,6 +11331,11 @@ markdown-escapes@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg== +markdown-it-link-attributes@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz#25751f2cf74fd91f0a35ba7b3247fa45f2056d88" + integrity sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ== + markdown-it@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc" @@ -11325,10 +11347,16 @@ markdown-it@^10.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" -marked@4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.10.tgz#423e295385cc0c3a70fa495e0df68b007b879423" - integrity sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw== +markdown-it@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" + integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== + dependencies: + argparse "^2.0.1" + entities "~3.0.1" + linkify-it "^4.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" material-colors@^1.0.0: version "1.2.6"