diff --git a/.meteor/versions b/.meteor/versions index 4f90fb6bc3b5..63ec00a995dd 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -171,7 +171,7 @@ rocketchat:logger@0.0.1 rocketchat:login-token@1.0.0 rocketchat:mailer@0.0.1 rocketchat:mapview@0.0.1 -rocketchat:markdown@0.0.1 +rocketchat:markdown@0.0.2 rocketchat:mentions@0.0.1 rocketchat:mentions-flextab@0.0.1 rocketchat:message-attachments@0.0.1 diff --git a/package.json b/package.json index 07711bd921f6..422d8dd84843 100644 --- a/package.json +++ b/package.json @@ -117,3 +117,4 @@ "wolfy87-eventemitter": "^5.2.2" } } + diff --git a/packages/rocketchat-autotranslate/server/autotranslate.js b/packages/rocketchat-autotranslate/server/autotranslate.js index c5349120f1ff..f6579b8ab87b 100644 --- a/packages/rocketchat-autotranslate/server/autotranslate.js +++ b/packages/rocketchat-autotranslate/server/autotranslate.js @@ -91,8 +91,7 @@ class AutoTranslate { let count = message.tokens.length; message.html = message.msg; - RocketChat.MarkdownCode.handle_codeblocks(message); - RocketChat.MarkdownCode.handle_inlinecode(message); + message = RocketChat.Markdown.parse(message); message.msg = message.html; for (const tokenIndex in message.tokens) { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 96d9a908429c..f7b393832a2c 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1010,9 +1010,17 @@ "MapView_GMapsAPIKey_Description": "This can be obtained from the Google Developers Console for free.", "Mark_as_read": "Mark as read", "Mark_as_unread": "Mark as unread", + "Markdown_Parser": "Markdown Parser", + "Original": "Original", "Markdown_Headers": "Allow Markdown headers in messages", "Markdown_SupportSchemesForLink": "Markdown Support Schemes for Link", "Markdown_SupportSchemesForLink_Description": "Comma-separated list of allowed schemes", + "Markdown_Marked_GFM": "Enable Marked GFM", + "Markdown_Marked_Tables": "Enable Marked Tables", + "Markdown_Marked_Breaks": "Enable Marked Breaks", + "Markdown_Marked_Pedantic": "Enable Marked Pedantic", + "Markdown_Marked_SmartLists": "Enable Marked Smart Lists", + "Markdown_Marked_Smartypants": "Enable Marked Smartypants", "Max_length_is": "Max length is %s", "Members_List": "Members List", "Mentions": "Mentions", diff --git a/packages/rocketchat-i18n/i18n/ja.i18n.json b/packages/rocketchat-i18n/i18n/ja.i18n.json index 39155bf24657..5eccae70e891 100644 --- a/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -657,9 +657,17 @@ "Managing_assets": "資産を管理します", "Managing_integrations": "統合管理", "Mark_as_read": "既読にする", - "Markdown_Headers": "Markdown ヘッダー", - "Markdown_SupportSchemesForLink": "Markdown リンクでサポートするスキーマ", + "Markdown_Parser": "Markdown パーサー", + "Original": "オリジナル", + "Markdown_Headers": "ヘッダーを有効にする", + "Markdown_SupportSchemesForLink": "リンクでサポートするスキーマリスト", "Markdown_SupportSchemesForLink_Description": "許可するスキーマをカンマ区切りで記述してください。", + "Markdown_Marked_GFM": "Marked GFM を有効にする", + "Markdown_Marked_Tables": "Marked Tables を有効にする", + "Markdown_Marked_Breaks": "Marked Breaks を有効にする", + "Markdown_Marked_Pedantic": "Marked Pedantic を有効にする", + "Markdown_Marked_SmartLists": "Marked Smart Lists を有効にする", + "Markdown_Marked_Smartypants": "Marked Smartypants を有効にする", "Members_List": "メンバーリスト", "Mentions": "メンション", "Mentions_default": "メンション (デフォルト)", @@ -1237,4 +1245,4 @@ "Your_mail_was_sent_to_s": "メールは %s へを送信されました", "Your_password_is_wrong": "パスワードが間違っています!", "Your_push_was_sent_to_s_devices": "プッシュ通知が %s 台のデバイスへ送信されました" -} \ No newline at end of file +} diff --git a/packages/rocketchat-markdown/.npm/package/.gitignore b/packages/rocketchat-markdown/.npm/package/.gitignore new file mode 100644 index 000000000000..3c3629e647f5 --- /dev/null +++ b/packages/rocketchat-markdown/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/rocketchat-markdown/.npm/package/README b/packages/rocketchat-markdown/.npm/package/README new file mode 100644 index 000000000000..3d492553a438 --- /dev/null +++ b/packages/rocketchat-markdown/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/rocketchat-markdown/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-markdown/.npm/package/npm-shrinkwrap.json new file mode 100644 index 000000000000..f1daeccebc3e --- /dev/null +++ b/packages/rocketchat-markdown/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,14 @@ +{ + "dependencies": { + "highlight.js": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.9.0.tgz", + "from": "highlight.js@9.9.0" + }, + "marked": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", + "from": "marked@0.3.6" + } + } +} diff --git a/packages/rocketchat-markdown/markdown.js b/packages/rocketchat-markdown/markdown.js index 2bcb60d1cd2b..b70aa359fc3b 100644 --- a/packages/rocketchat-markdown/markdown.js +++ b/packages/rocketchat-markdown/markdown.js @@ -2,92 +2,32 @@ * Markdown is a named function that will parse markdown syntax * @param {Object} message - The message object */ +import { Meteor } from 'meteor/meteor'; +import { Blaze } from 'meteor/blaze'; +import { RocketChat } from 'meteor/rocketchat:lib'; + +import { marked } from './parser/marked/marked.js'; +import { original } from './parser/original/original.js'; + +const parsers = { + original, + marked +}; class MarkdownClass { parse(text) { - return this.parseNotEscaped(_.escapeHTML(text)); + const message = { + html: _.escapeHTML(text) + }; + return this.parseNotEscaped(message).html; } - parseNotEscaped(msg, message) { - if (message && message.tokens == null) { - message.tokens = []; + parseNotEscaped(message) { + const parser = RocketChat.settings.get('Markdown_Parser'); + if (typeof parsers[parser] === 'function') { + return parsers[parser](message); } - - const schemes = RocketChat.settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); - - if (RocketChat.settings.get('Markdown_Headers')) { - // Support # Text for h1 - msg = msg.replace(/^# (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); - - // Support # Text for h2 - msg = msg.replace(/^## (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); - - // Support # Text for h3 - msg = msg.replace(/^### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); - - // Support # Text for h4 - msg = msg.replace(/^#### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); - } - - // Support *text* to make bold - msg = msg.replace(/(^|>|[ >_~`])\*{1,2}([^\*\r\n]+)\*{1,2}([<_~`]|\B|\b|$)/gm, '$1*$2*$3'); - - // Support _text_ to make italics - msg = msg.replace(/(^|>|[ >*~`])\_([^\_\r\n]+)\_([<*~`]|\B|\b|$)/gm, '$1_$2_$3'); - - // Support ~text~ to strike through text - msg = msg.replace(/(^|>|[ >_*`])\~{1,2}([^~\r\n]+)\~{1,2}([<_*`]|\B|\b|$)/gm, '$1~$2~$3'); - - // Support for block quote - // >>> - // Text - // <<< - msg = msg.replace(/(?:>){3}\n+([\s\S]*?)\n+(?:<){3}/g, '
>>>$1<<<
'); - - // Support >Text for quote - msg = msg.replace(/^>(.*)$/gm, '
>$1
'); - - // Remove white-space around blockquote (prevent
). Because blockquote is block element. - msg = msg.replace(/\s*
/gm, '
'); - msg = msg.replace(/<\/blockquote>\s*/gm, '
'); - - // Remove new-line between blockquotes. - msg = msg.replace(/<\/blockquote>\n
`; - - if (message && message.tokens) { - const token = `=!=${ Random.id() }=!=`; - - message.tokens.push({ - token, - text: html - }); - - return token; - } - - return html; - }); - - // Support [Text](http://link) - msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), function(match, title, url) { - const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; - return `${ _.escapeHTML(title) }`; - }); - - // Support - msg = msg.replace(new RegExp(`(?:<|<)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, url, title) => { - const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; - return `${ _.escapeHTML(title) }`; - }); - - if (typeof window !== 'undefined' && window !== null ? window.rocketDebug : undefined) { console.log('Markdown', msg); } - - return msg; + return parsers['original'](message); } } @@ -97,7 +37,7 @@ RocketChat.Markdown = Markdown; // renderMessage already did html escape const MarkdownMessage = (message) => { if (_.trim(message != null ? message.html : undefined)) { - message.html = Markdown.parseNotEscaped(message.html, message); + message = Markdown.parseNotEscaped(message); } return message; diff --git a/packages/rocketchat-markdown/markdowncode.js b/packages/rocketchat-markdown/markdowncode.js deleted file mode 100644 index e00b030d01bc..000000000000 --- a/packages/rocketchat-markdown/markdowncode.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * MarkdownCode is a named function that will parse `inline code` and ```codeblock``` syntaxes - * @param {Object} message - The message object - */ -import hljs from 'highlight.js'; - -class MarkdownCode { - constructor(message) { - - if (s.trim(message.html)) { - if (message.tokens == null) { - message.tokens = []; - } - - MarkdownCode.handle_codeblocks(message); - MarkdownCode.handle_inlinecode(message); - - if (window && window.rocketDebug) { - console.log('Markdown', message); - } - } - - return message; - } - - static handle_inlinecode(message) { - // Support `text` - return message.html = message.html.replace(/(^|>|[ >_*~])\`([^`\r\n]+)\`([<_*~]|\B|\b|$)/gm, (match, p1, p2, p3) => { - const token = `=!=${ Random.id() }=!=`; - - message.tokens.push({ - token, - text: `${ p1 }\`${ p2 }\`${ p3 }`, - noHtml: match - }); - - return token; - }); - } - - static handle_codeblocks(message) { - // Count occurencies of ``` - const count = (message.html.match(/```/g) || []).length; - - if (count) { - - // Check if we need to add a final ``` - if ((count % 2) > 0) { - message.html = `${ message.html }\n\`\`\``; - message.msg = `${ message.msg }\n\`\`\``; - } - - // Separate text in code blocks and non code blocks - const msgParts = message.html.split(/(^.*)(```(?:[a-zA-Z]+)?(?:(?:.|\r|\n)*?)```)(.*\n?)$/gm); - - for (let index = 0; index < msgParts.length; index++) { - // Verify if this part is code - const part = msgParts[index]; - const codeMatch = part.match(/^```(.*[\r\n\ ]?)([\s\S]*?)```+?$/); - - if (codeMatch != null) { - // Process highlight if this part is code - const singleLine = codeMatch[0].indexOf('\n') === -1; - const lang = !singleLine && Array.from(hljs.listLanguages()).includes(s.trim(codeMatch[1])) ? s.trim(codeMatch[1]) : ''; - const code = - singleLine ? - _.unescapeHTML(codeMatch[1]) : - lang === '' ? - _.unescapeHTML(codeMatch[1] + codeMatch[2]) : - _.unescapeHTML(codeMatch[2]); - - const result = lang === '' ? hljs.highlightAuto((lang + code)) : hljs.highlight(lang, code); - const token = `=!=${ Random.id() }=!=`; - - message.tokens.push({ - highlight: true, - token, - text: `
\`\`\`
${ result.value }
\`\`\`
`, - noHtml: `\`\`\`\n${ s.stripTags(result.value) }\n\`\`\`` - }); - - msgParts[index] = token; - } else { - msgParts[index] = part; - } - } - - // Re-mount message - return message.html = msgParts.join(''); - } - } -} - -RocketChat.MarkdownCode = MarkdownCode; - -const MarkdownCodeCB = (message) => new MarkdownCode(message); - -// MarkdownCode gets higher priority over Markdown so it's possible place a callback in between (katex for exmaple) -RocketChat.callbacks.add('renderMessage', MarkdownCodeCB, RocketChat.callbacks.priority.HIGH - 2, 'markdowncode'); diff --git a/packages/rocketchat-markdown/package.js b/packages/rocketchat-markdown/package.js index d4dcf611e881..4c1c81608698 100644 --- a/packages/rocketchat-markdown/package.js +++ b/packages/rocketchat-markdown/package.js @@ -1,20 +1,22 @@ Package.describe({ name: 'rocketchat:markdown', - version: '0.0.1', + version: '0.0.2', summary: 'Message pre-processor that will process selected markdown notations', git: '' }); +Npm.depends({ + 'marked': '0.3.6' +}); + Package.onUse(function(api) { api.use([ 'ecmascript', 'underscore', 'templating', - 'underscorestring:underscore.string', 'rocketchat:lib' ]); api.addFiles('settings.js', 'server'); - api.addFiles('markdown.js'); - api.addFiles('markdowncode.js'); + api.mainModule('markdown.js'); }); diff --git a/packages/rocketchat-markdown/parser/marked/marked.js b/packages/rocketchat-markdown/parser/marked/marked.js new file mode 100644 index 000000000000..15fb3755a9d2 --- /dev/null +++ b/packages/rocketchat-markdown/parser/marked/marked.js @@ -0,0 +1,105 @@ +import { Random } from 'meteor/random'; +import { _ } from 'meteor/underscore'; +import hljs from 'highlight.js'; +import _marked from 'marked'; + +const renderer = new _marked.Renderer(); + +let msg = null; + +renderer.code = function(code, lang, escaped) { + if (this.options.highlight) { + const out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + let text = null; + + if (!lang) { + text = `
${ (escaped ? code : _.escapeHTML(code, true)) }
`; + } else { + text = `
${ (escaped ? code : _.escapeHTML(code, true)) }
`; + } + + if (_.isString(msg)) { + return text; + } + + const token = `=!=${ Random.id() }=!=`; + msg.tokens.push({ + highlight: true, + token, + text + }); + + return token; +}; + +renderer.codespan = function(text) { + text = `${ text }`; + if (_.isString(msg)) { + return text; + } + + const token = `=!=${ Random.id() }=!=`; + msg.tokens.push({ + token, + text + }); + + return token; +}; + +renderer.blockquote = function(quote) { + return `
${ quote }
`; +}; + +const highlight = function(code, lang) { + if (!lang) { + return code; + } + try { + return hljs.highlight(lang, code).value; + } catch (e) { + // Unknown language + return code; + } +}; + +let gfm = null; +let tables = null; +let breaks = null; +let pedantic = null; +let smartLists = null; +let smartypants = null; + +export const marked = (message) => { + msg = message; + + if (!msg.tokens) { + msg.tokens = []; + } + + if (gfm == null) { gfm = RocketChat.settings.get('Markdown_Marked_GFM'); } + if (tables == null) { tables = RocketChat.settings.get('Markdown_Marked_Tables'); } + if (breaks == null) { breaks = RocketChat.settings.get('Markdown_Marked_Breaks'); } + if (pedantic == null) { pedantic = RocketChat.settings.get('Markdown_Marked_Pedantic'); } + if (smartLists == null) { smartLists = RocketChat.settings.get('Markdown_Marked_SmartLists'); } + if (smartypants == null) { smartypants = RocketChat.settings.get('Markdown_Marked_Smartypants'); } + + msg.html = _marked(_.unescapeHTML(msg.html), { + gfm, + tables, + breaks, + pedantic, + smartLists, + smartypants, + renderer, + highlight + }); + + return msg; +}; diff --git a/packages/rocketchat-markdown/parser/original/code.js b/packages/rocketchat-markdown/parser/original/code.js new file mode 100644 index 000000000000..1516a647c501 --- /dev/null +++ b/packages/rocketchat-markdown/parser/original/code.js @@ -0,0 +1,91 @@ +/* + * code() is a named function that will parse `inline code` and ```codeblock``` syntaxes + * @param {Object} message - The message object + */ +import { Random } from 'meteor/random'; +import { _ } from 'meteor/underscore'; +import hljs from 'highlight.js'; + +const inlinecode = (message) => { + // Support `text` + return message.html = message.html.replace(/(^|>|[ >_*~])\`([^`\r\n]+)\`([<_*~]|\B|\b|$)/gm, (match, p1, p2, p3) => { + const token = `=!=${ Random.id() }=!=`; + + message.tokens.push({ + token, + text: `${ p1 }\`${ p2 }\`${ p3 }`, + noHtml: match + }); + + return token; + }); +}; + +const codeblocks = (message) => { + // Count occurencies of ``` + const count = (message.html.match(/```/g) || []).length; + + if (count) { + + // Check if we need to add a final ``` + if ((count % 2) > 0) { + message.html = `${ message.html }\n\`\`\``; + message.msg = `${ message.msg }\n\`\`\``; + } + + // Separate text in code blocks and non code blocks + const msgParts = message.html.split(/(^.*)(```(?:[a-zA-Z]+)?(?:(?:.|\r|\n)*?)```)(.*\n?)$/gm); + + for (let index = 0; index < msgParts.length; index++) { + // Verify if this part is code + const part = msgParts[index]; + const codeMatch = part.match(/^```(.*[\r\n\ ]?)([\s\S]*?)```+?$/); + + if (codeMatch != null) { + // Process highlight if this part is code + const singleLine = codeMatch[0].indexOf('\n') === -1; + const lang = !singleLine && Array.from(hljs.listLanguages()).includes(s.trim(codeMatch[1])) ? s.trim(codeMatch[1]) : ''; + const code = + singleLine ? + _.unescapeHTML(codeMatch[1]) : + lang === '' ? + _.unescapeHTML(codeMatch[1] + codeMatch[2]) : + _.unescapeHTML(codeMatch[2]); + + const result = lang === '' ? hljs.highlightAuto((lang + code)) : hljs.highlight(lang, code); + const token = `=!=${ Random.id() }=!=`; + + message.tokens.push({ + highlight: true, + token, + text: `
\`\`\`
${ result.value }
\`\`\`
`, + noHtml: `\`\`\`\n${ s.stripTags(result.value) }\n\`\`\`` + }); + + msgParts[index] = token; + } else { + msgParts[index] = part; + } + } + + // Re-mount message + return message.html = msgParts.join(''); + } +}; + +export const code = (message) => { + if (s.trim(message.html)) { + if (message.tokens == null) { + message.tokens = []; + } + + codeblocks(message); + inlinecode(message); + + if (window && window.rocketDebug) { + console.log('Markdown', message); + } + } + + return message; +}; diff --git a/packages/rocketchat-markdown/parser/original/markdown.js b/packages/rocketchat-markdown/parser/original/markdown.js new file mode 100644 index 000000000000..6e1a1142f08f --- /dev/null +++ b/packages/rocketchat-markdown/parser/original/markdown.js @@ -0,0 +1,94 @@ +/* + * Markdown is a named function that will parse markdown syntax + * @param {String} msg - The message html + */ +import { Meteor } from 'meteor/meteor'; +import { _ } from 'meteor/underscore'; +import { RocketChat } from 'meteor/rocketchat:lib'; + +const parseNotEscaped = function(msg, message) { + if (message && message.tokens == null) { + message.tokens = []; + } + + const schemes = RocketChat.settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); + + if (RocketChat.settings.get('Markdown_Headers')) { + // Support # Text for h1 + msg = msg.replace(/^# (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); + + // Support # Text for h2 + msg = msg.replace(/^## (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); + + // Support # Text for h3 + msg = msg.replace(/^### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); + + // Support # Text for h4 + msg = msg.replace(/^#### (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '

$1

'); + } + + // Support *text* to make bold + msg = msg.replace(/(^|>|[ >_~`])\*{1,2}([^\*\r\n]+)\*{1,2}([<_~`]|\B|\b|$)/gm, '$1*$2*$3'); + + // Support _text_ to make italics + msg = msg.replace(/(^|>|[ >*~`])\_([^\_\r\n]+)\_([<*~`]|\B|\b|$)/gm, '$1_$2_$3'); + + // Support ~text~ to strike through text + msg = msg.replace(/(^|>|[ >_*`])\~{1,2}([^~\r\n]+)\~{1,2}([<_*`]|\B|\b|$)/gm, '$1~$2~$3'); + + // Support for block quote + // >>> + // Text + // <<< + msg = msg.replace(/(?:>){3}\n+([\s\S]*?)\n+(?:<){3}/g, '
>>>$1<<<
'); + + // Support >Text for quote + msg = msg.replace(/^>(.*)$/gm, '
>$1
'); + + // Remove white-space around blockquote (prevent
). Because blockquote is block element. + msg = msg.replace(/\s*
/gm, '
'); + msg = msg.replace(/<\/blockquote>\s*/gm, '
'); + + // Remove new-line between blockquotes. + msg = msg.replace(/<\/blockquote>\n
`; + + if (message && message.tokens) { + const token = `=!=${ Random.id() }=!=`; + + message.tokens.push({ + token, + text: html + }); + + return token; + } + + return html; + }); + + // Support [Text](http://link) + msg = msg.replace(new RegExp(`\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), function(match, title, url) { + const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; + return `${ _.escapeHTML(title) }`; + }); + + // Support + msg = msg.replace(new RegExp(`(?:<|<)((?:${ schemes }):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (match, url, title) => { + const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; + return `${ _.escapeHTML(title) }`; + }); + + if (typeof window !== 'undefined' && window !== null ? window.rocketDebug : undefined) { console.log('Markdown', msg); } + + return msg; +}; + +export const markdown = function(message) { + message.html = parseNotEscaped(message.html, message); + return message; +}; diff --git a/packages/rocketchat-markdown/parser/original/original.js b/packages/rocketchat-markdown/parser/original/original.js new file mode 100644 index 000000000000..d03f41906b38 --- /dev/null +++ b/packages/rocketchat-markdown/parser/original/original.js @@ -0,0 +1,19 @@ +/* + * Markdown is a named function that will parse markdown syntax + * @param {Object} message - The message object + */ +import { markdown } from './markdown.js'; +import { code } from './code.js'; + +export const original = (message) => { + // Parse markdown + message = markdown(message); + + // Parse markdown code + message = code(message); + + // Replace linebreak to br + message.html = message.html.replace(/\n/gm, '
'); + + return message; +}; diff --git a/packages/rocketchat-markdown/settings.js b/packages/rocketchat-markdown/settings.js index db5844392b54..e03cdbe2e311 100644 --- a/packages/rocketchat-markdown/settings.js +++ b/packages/rocketchat-markdown/settings.js @@ -1,16 +1,85 @@ +import { Meteor } from 'meteor/meteor'; +import { RocketChat } from 'meteor/rocketchat:lib'; + Meteor.startup(() => { - RocketChat.settings.add('Markdown_Headers', false, { - type: 'boolean', + RocketChat.settings.add('Markdown_Parser', 'original', { + type: 'select', + values: [{ + key: 'original', + i18nLabel: 'Original' + }, { + key: 'marked', + i18nLabel: 'Marked' + }], group: 'Message', section: 'Markdown', public: true }); - return RocketChat.settings.add('Markdown_SupportSchemesForLink', 'http,https', { + const enableQueryOriginal = {_id: 'Markdown_Parser', value: 'original'}; + RocketChat.settings.add('Markdown_Headers', false, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryOriginal + }); + RocketChat.settings.add('Markdown_SupportSchemesForLink', 'http,https', { type: 'string', group: 'Message', section: 'Markdown', public: true, - i18nDescription: 'Markdown_SupportSchemesForLink_Description' + i18nDescription: 'Markdown_SupportSchemesForLink_Description', + enableQuery: enableQueryOriginal + }); + + const enableQueryMarked = {_id: 'Markdown_Parser', value: 'marked'}; + RocketChat.settings.add('Markdown_Marked_GFM', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked + }); + RocketChat.settings.add('Markdown_Marked_Tables', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked + }); + RocketChat.settings.add('Markdown_Marked_Breaks', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked + }); + RocketChat.settings.add('Markdown_Marked_Pedantic', false, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: [{ + _id: 'Markdown_Parser', + value: 'marked' + }, { + _id: 'Markdown_Marked_GFM', + value: false + }] + }); + RocketChat.settings.add('Markdown_Marked_SmartLists', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked + }); + RocketChat.settings.add('Markdown_Marked_Smartypants', true, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true, + enableQuery: enableQueryMarked }); }); diff --git a/packages/rocketchat-message-snippet/client/page/snippetPage.js b/packages/rocketchat-message-snippet/client/page/snippetPage.js index 7e1351cc2d22..0700dcf9d611 100644 --- a/packages/rocketchat-message-snippet/client/page/snippetPage.js +++ b/packages/rocketchat-message-snippet/client/page/snippetPage.js @@ -11,8 +11,8 @@ Template.snippetPage.helpers({ return null; } message.html = message.msg; - const markdownCode = new RocketChat.MarkdownCode(message); - return markdownCode.tokens[0].text; + const markdown = RocketChat.Markdown.parse(message); + return markdown.tokens[0].text; }, date() { const snippet = SnippetedMessages.findOne({ _id: FlowRouter.getParam('snippetId') }); diff --git a/packages/rocketchat-theme/client/imports/general/base_old.css b/packages/rocketchat-theme/client/imports/general/base_old.css index 95c55b059506..939b6b3c3c4a 100644 --- a/packages/rocketchat-theme/client/imports/general/base_old.css +++ b/packages/rocketchat-theme/client/imports/general/base_old.css @@ -3221,6 +3221,55 @@ display: block; } } + + & ul, + & ol { + padding: 0 0 0 24px; + } + + & ul { + list-style-type: disc; + } + + & ol { + list-style-type: decimal; + } + + & > * + * { + padding-top: 10px; + } + + & > table { + display: block; + overflow: auto; + + & tr { + background-color: #ffffff; + border-top: 1px solid #cccccc; + + &:nth-child(2n) { + background-color: #f6f8fa; + } + + & th { + font-weight: 600; + } + + & th, + & td { + padding: 6px 13px; + border: 1px solid #dddddd; + } + } + } + + & > hr { + height: 3px; + padding: 0; + margin: 12px 0; + background-color: #e7e7e7; + border: 0; + } } &.temp .body { diff --git a/packages/rocketchat-ui-message/client/messageBox.js b/packages/rocketchat-ui-message/client/messageBox.js index 886f044f780e..9bde15d0c0ec 100644 --- a/packages/rocketchat-ui-message/client/messageBox.js +++ b/packages/rocketchat-ui-message/client/messageBox.js @@ -85,7 +85,14 @@ const markdownButtons = [ icon: 'bold', pattern: '*{{text}}*', command: 'b', - condition: () => RocketChat.Markdown + condition: () => RocketChat.Markdown && RocketChat.settings.get('Markdown_Parser') === 'original' + }, + { + label: 'bold', + icon: 'bold', + pattern: '**{{text}}**', + command: 'b', + condition: () => RocketChat.Markdown && RocketChat.settings.get('Markdown_Parser') === 'marked' }, { label: 'italic', @@ -98,7 +105,13 @@ const markdownButtons = [ label: 'strike', icon: 'strike', pattern: '~{{text}}~', - condition: () => RocketChat.Markdown + condition: () => RocketChat.Markdown && RocketChat.settings.get('Markdown_Parser') === 'original' + }, + { + label: 'strike', + icon: 'strike', + pattern: '~~{{text}}~~', + condition: () => RocketChat.Markdown && RocketChat.settings.get('Markdown_Parser') === 'marked' }, { label: 'inline_code', @@ -110,7 +123,7 @@ const markdownButtons = [ label: 'multi_line', icon: 'multi-line', pattern: '```\n{{text}}\n``` ', - condition: () => RocketChat.MarkdownCode + condition: () => RocketChat.Markdown }, { label: katexSyntax, @@ -397,7 +410,7 @@ Template.messageBox.events({ }, 'keydown .js-input-message': firefoxPasteUpload(function(event, t) { if ((navigator.platform.indexOf('Mac') !== -1 && event.metaKey) || (navigator.platform.indexOf('Mac') === -1 && event.ctrlKey)) { - const action = markdownButtons.find(action => action.command === event.key.toLowerCase()); + const action = markdownButtons.find(action => action.command === event.key.toLowerCase() && (!action.condition || action.condition())); if (action) { applyMd.apply(action, [event, t]); } diff --git a/packages/rocketchat-ui-message/client/renderMessageBody.js b/packages/rocketchat-ui-message/client/renderMessageBody.js index ec6dc6a89056..f9efbb534929 100644 --- a/packages/rocketchat-ui-message/client/renderMessageBody.js +++ b/packages/rocketchat-ui-message/client/renderMessageBody.js @@ -15,8 +15,6 @@ renderMessageBody = function(msg) { } } - msg.html = message.html.replace(/\n/gm, '
'); - return msg.html; };