diff --git a/packages/rocketchat-markdown/markdown.coffee b/packages/rocketchat-markdown/markdown.coffee deleted file mode 100644 index 4caaaf6374f5..000000000000 --- a/packages/rocketchat-markdown/markdown.coffee +++ /dev/null @@ -1,84 +0,0 @@ -### -# Markdown is a named function that will parse markdown syntax -# @param {Object} message - The message object -### - -Markdown = new class MarkdownClass - parse: (text) -> - @parseNotEscaped(_.escapeHTML(text)) - - parseNotEscaped: (msg) -> - schemes = RocketChat.settings.get('Markdown_SupportSchemesForLink').split(',').join('|') - - # Support ![alt text](http://image url) - msg = msg.replace new RegExp("!\\[([^\\]]+)\\]\\(((?:#{schemes}):\\/\\/[^\\)]+)\\)", 'gm'), (match, title, url) -> - target = if url.indexOf(Meteor.absoluteUrl()) is 0 then '' else '_blank' - return '
' - - # Support [Text](http://link) - msg = msg.replace new RegExp("\\[([^\\]]+)\\]\\(((?:#{schemes}):\\/\\/[^\\)]+)\\)", 'gm'), (match, title, url) -> - target = if url.indexOf(Meteor.absoluteUrl()) is 0 then '' else '_blank' - return '' + _.escapeHTML(title) + '' - - # Support - msg = msg.replace new RegExp("(?:<|<)((?:#{schemes}):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)", 'gm'), (match, url, title) -> - target = if url.indexOf(Meteor.absoluteUrl()) is 0 then '' else '_blank' - return '' + _.escapeHTML(title) + '' - - 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 _.trim message?.html - message.html = Markdown.parseNotEscaped(message.html) - - return message - -RocketChat.callbacks.add 'renderMessage', MarkdownMessage, RocketChat.callbacks.priority.HIGH, 'markdown' - -if Meteor.isClient - Blaze.registerHelper 'RocketChatMarkdown', (text) -> - return Markdown.parse text diff --git a/packages/rocketchat-markdown/markdown.js b/packages/rocketchat-markdown/markdown.js new file mode 100644 index 000000000000..365a0126dd39 --- /dev/null +++ b/packages/rocketchat-markdown/markdown.js @@ -0,0 +1,93 @@ +/* + * Markdown is a named function that will parse markdown syntax + * @param {Object} message - The message object + */ + +class MarkdownClass { + parse(text) { + return this.parseNotEscaped(_.escapeHTML(text)); + } + + parseNotEscaped(msg) { + const schemes = RocketChat.settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); + + // Support ![alt text](http://image url) + msg = msg.replace(new RegExp(`!\\[([^\\]]+)\\]\\(((?:${ schemes }):\\/\\/[^\\)]+)\\)`, 'gm'), function(match, title, url) { + const target = url.indexOf(Meteor.absoluteUrl()) === 0 ? '' : '_blank'; + return `
`; + }); + + // 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 (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 (_.trim(message != null ? message.html : undefined)) { + message.html = Markdown.parseNotEscaped(message.html); + } + + return message; +}; + +RocketChat.callbacks.add('renderMessage', MarkdownMessage, RocketChat.callbacks.priority.HIGH, 'markdown'); + +if (Meteor.isClient) { + Blaze.registerHelper('RocketChatMarkdown', text => Markdown.parse(text)); +} diff --git a/packages/rocketchat-markdown/markdowncode.coffee b/packages/rocketchat-markdown/markdowncode.coffee deleted file mode 100644 index 4f966d46384e..000000000000 --- a/packages/rocketchat-markdown/markdowncode.coffee +++ /dev/null @@ -1,88 +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 - message.tokens ?= [] - - MarkdownCode.handle_codeblocks message - MarkdownCode.handle_inlinecode message - - console.log 'Markdown', message if window?.rocketDebug - - return message - - @handle_inlinecode: (message) -> - # Support `text` - message.html = message.html.replace /(^|>|[ >_*~])\`([^`\r\n]+)\`([<_*~]|\B|\b|$)/gm, (match, p1, p2, p3, offset, text) -> - token = "=!=#{Random.id()}=!=" - - message.tokens.push - token: token - text: "#{p1}`#{p2}`#{p3}" - noHtml: match - - return token - - - @handle_codeblocks: (message) -> - # Count occurencies of ``` - 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 - msgParts = message.html.split(/(^.*)(```(?:[a-zA-Z]+)?(?:(?:.|\n)*?)```)(.*\n?)$/gm) - - for part, index in msgParts - # Verify if this part is code - codeMatch = part.match(/^```(.*[\n\ ]?)([\s\S]*?)```+?$/) - - if codeMatch? - # Process highlight if this part is code - singleLine = codeMatch[0].indexOf('\n') is -1 - - if singleLine - lang = '' - code = _.unescapeHTML codeMatch[1] + codeMatch[2] - else - lang = codeMatch[1] - code = _.unescapeHTML codeMatch[2] - - if s.trim(lang) is '' - lang = '' - - if s.trim(lang) not in hljs.listLanguages() - result = hljs.highlightAuto (lang + code) - else - result = hljs.highlight s.trim(lang), code - - token = "=!=#{Random.id()}=!=" - - message.tokens.push - highlight: true - token: token - text: "
```
" + result.value + "
```
" - noHtml: "```\n#{s.stripTags(result.value)}\n```" - - msgParts[index] = token - else - msgParts[index] = part - - # Re-mount message - message.html = msgParts.join('') - -RocketChat.MarkdownCode = MarkdownCode - -# MarkdownCode gets higher priority over Markdown so it's possible place a callback in between (katex for exmaple) -RocketChat.callbacks.add 'renderMessage', MarkdownCode, RocketChat.callbacks.priority.HIGH - 2, 'markdowncode' diff --git a/packages/rocketchat-markdown/markdowncode.js b/packages/rocketchat-markdown/markdowncode.js new file mode 100644 index 000000000000..1ff5ca80f787 --- /dev/null +++ b/packages/rocketchat-markdown/markdowncode.js @@ -0,0 +1,112 @@ +/* + * 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]+)?(?:(?:.|\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(/^```(.*[\n\ ]?)([\s\S]*?)```+?$/); + + if (codeMatch != null) { + // Process highlight if this part is code + let code; + let lang; + let result; + const singleLine = codeMatch[0].indexOf('\n') === -1; + + if (singleLine) { + lang = ''; + code = _.unescapeHTML(codeMatch[1] + codeMatch[2]); + } else { + lang = codeMatch[1]; + code = _.unescapeHTML(codeMatch[2]); + } + + if (s.trim(lang) === '') { + lang = ''; + } + + if (!Array.from(hljs.listLanguages()).includes(s.trim(lang))) { + result = hljs.highlightAuto((lang + code)); + } else { + result = hljs.highlight(s.trim(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 a329c83095db..4c24cce69764 100644 --- a/packages/rocketchat-markdown/package.js +++ b/packages/rocketchat-markdown/package.js @@ -7,7 +7,6 @@ Package.describe({ Package.onUse(function(api) { api.use([ - 'coffeescript', 'ecmascript', 'underscore', 'templating', @@ -15,18 +14,17 @@ Package.onUse(function(api) { 'rocketchat:lib' ]); - api.addFiles('settings.coffee', 'server'); - api.addFiles('markdown.coffee'); - api.addFiles('markdowncode.coffee'); + api.addFiles('settings.js', 'server'); + api.addFiles('markdown.js'); + api.addFiles('markdowncode.js'); }); Package.onTest(function(api) { api.use([ - 'coffeescript', 'sanjo:jasmine@0.20.2', 'rocketchat:lib', 'rocketchat:markdown' ]); - api.addFiles('tests/jasmine/client/unit/markdown.spec.coffee', 'client'); + api.addFiles('tests/jasmine/client/unit/markdown.spec.js', 'client'); }); diff --git a/packages/rocketchat-markdown/settings.coffee b/packages/rocketchat-markdown/settings.coffee deleted file mode 100644 index 443962e251f3..000000000000 --- a/packages/rocketchat-markdown/settings.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Meteor.startup -> - RocketChat.settings.add 'Markdown_Headers', false, {type: 'boolean', group: 'Message', section: 'Markdown', public: true} - RocketChat.settings.add 'Markdown_SupportSchemesForLink', 'http,https', {type: 'string', group: 'Message', section: 'Markdown', public: true, i18nDescription: 'Markdown_SupportSchemesForLink_Description'} diff --git a/packages/rocketchat-markdown/settings.js b/packages/rocketchat-markdown/settings.js new file mode 100644 index 000000000000..db5844392b54 --- /dev/null +++ b/packages/rocketchat-markdown/settings.js @@ -0,0 +1,16 @@ +Meteor.startup(() => { + RocketChat.settings.add('Markdown_Headers', false, { + type: 'boolean', + group: 'Message', + section: 'Markdown', + public: true + }); + + return RocketChat.settings.add('Markdown_SupportSchemesForLink', 'http,https', { + type: 'string', + group: 'Message', + section: 'Markdown', + public: true, + i18nDescription: 'Markdown_SupportSchemesForLink_Description' + }); +}); diff --git a/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee b/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee deleted file mode 100644 index f154266a873c..000000000000 --- a/packages/rocketchat-markdown/tests/jasmine/client/unit/markdown.spec.coffee +++ /dev/null @@ -1,4 +0,0 @@ -describe 'rocketchat:markdown Client', -> - - it 'should exist', -> - expect(RocketChat.Markdown).toBeDefined()