diff --git a/packages/rocketchat-katex/katex.coffee b/packages/rocketchat-katex/katex.coffee deleted file mode 100644 index 6e7973f94377..000000000000 --- a/packages/rocketchat-katex/katex.coffee +++ /dev/null @@ -1,181 +0,0 @@ -### -# KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web. -# https://github.com/Khan/KaTeX -### - -katex = require('katex') - -class Katex - constructor: -> - @delimiters_map = [ - { opener: '\\[', closer: '\\]', displayMode: true , enabled: () => @parenthesis_syntax_enabled() }, - { opener: '\\(', closer: '\\)', displayMode: false, enabled: () => @parenthesis_syntax_enabled() }, - { opener: '$$' , closer: '$$' , displayMode: true , enabled: () => @dollar_syntax_enabled() }, - { opener: '$' , closer: '$' , displayMode: false, enabled: () => @dollar_syntax_enabled() }, - ] - - # Searches for the first opening delimiter in the string from a given position - find_opening_delimiter: (str, start) -> # Search the string for each opening delimiter - matches = ({options: o, pos: str.indexOf(o.opener, start)} for o in @delimiters_map when o.enabled()) - positions = (m.pos for m in matches when m.pos >= 0) - - # No opening delimiters were found - if positions.length == 0 - return null - - # Take the first delimiter found - pos = Math.min.apply Math, positions - - match_index = (m.pos for m in matches).indexOf(pos) - match = matches[match_index] - - return match - - class Boundary - length: -> - return @end - @start - - extract: (str) -> - return str.substr @start, @length() - - # Returns the outer and inner boundaries of the latex block starting - # at the given opening delimiter - get_latex_boundaries: (str, opening_delimiter_match) -> - inner = new Boundary - outer = new Boundary - - # The closing delimiter matching to the opening one - closer = opening_delimiter_match.options.closer - - outer.start = opening_delimiter_match.pos - inner.start = opening_delimiter_match.pos + closer.length - - # Search for a closer delimiter after the opening one - closer_index = str.substr(inner.start).indexOf(closer) - if closer_index < 0 - return null - - inner.end = inner.start + closer_index - outer.end = inner.end + closer.length - - return { - outer: outer - inner: inner - } - - # Searches for the first latex block in the given string - find_latex: (str) -> - start = 0 - while (opening_delimiter_match = @find_opening_delimiter str, start++)? - - match = @get_latex_boundaries str, opening_delimiter_match - - if match?.inner.extract(str).trim().length - match.options = opening_delimiter_match.options - return match - - return null - - # Breaks a message to what comes before, after and to the content of a - # matched latex block - extract_latex: (str, match) -> - before = str.substr 0, match.outer.start - after = str.substr match.outer.end - - latex = match.inner.extract str - latex = s.unescapeHTML latex - - return { before: before, latex : latex, after : after } - - # Takes a latex math string and the desired display mode and renders it - # to HTML using the KaTeX library - render_latex: (latex, displayMode) -> - try - rendered = katex.renderToString latex , {displayMode: displayMode} - catch e - display_mode = if displayMode then "block" else "inline" - rendered = "
" - rendered += "#{s.escapeHTML e.message}" - rendered += "
" - - return rendered - - # Takes a string and renders all latex blocks inside it - render: (str, render_func) -> - result = '' - - loop - - # Find the first latex block in the string - match = @find_latex str - - unless match? - result += str - break - - parts = @extract_latex str, match - - # Add to the reuslt what comes before the latex block as well as - # the rendered latex content - rendered = render_func parts.latex, match.options.displayMode - result += parts.before + rendered - - # Set what comes after the latex block to be examined next - str = parts.after - - return result - - # Takes a rocketchat message and renders latex in its content - render_message: (message) -> - # Render only if enabled in admin panel - if @katex_enabled() - msg = message - - if not _.isString message - if _.trim message.html - msg = message.html - else - return message - - if _.isString message - render_func = (latex, displayMode) => - return @render_latex latex, displayMode - else - message.tokens ?= [] - - render_func = (latex, displayMode) => - token = "=!=#{Random.id()}=!=" - - message.tokens.push - token: token - text: @render_latex latex, displayMode - - return token - - msg = @render msg, render_func - - if not _.isString message - message.html = msg - else - message = msg - - return message - - katex_enabled: -> - return RocketChat.settings.get('Katex_Enabled') - - dollar_syntax_enabled: -> - return RocketChat.settings.get('Katex_Dollar_Syntax') - - parenthesis_syntax_enabled: -> - return RocketChat.settings.get('Katex_Parenthesis_Syntax') - - -RocketChat.katex = new Katex - -cb = RocketChat.katex.render_message.bind(RocketChat.katex) -RocketChat.callbacks.add 'renderMessage', cb, RocketChat.callbacks.priority.HIGH - 1, 'katex' - -if Meteor.isClient - Blaze.registerHelper 'RocketChatKatex', (text) -> - return RocketChat.katex.render_message text diff --git a/packages/rocketchat-katex/katex.js b/packages/rocketchat-katex/katex.js new file mode 100644 index 000000000000..234165def86d --- /dev/null +++ b/packages/rocketchat-katex/katex.js @@ -0,0 +1,255 @@ +/* + * KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web. + * https://github.com/Khan/KaTeX + */ +const katex = require('katex'); + +class Boundary { + constructor() {} + + length() { + return this.end - this.start; + } + + extract(str) { + return str.substr(this.start, this.length()); + } + +} + +class Katex { + constructor() { + this.delimiters_map = [ + { + opener: '\\[', + closer: '\\]', + displayMode: true, + enabled: () => { + return this.parenthesis_syntax_enabled(); + } + }, { + opener: '\\(', + closer: '\\)', + displayMode: false, + enabled: () => { + return this.parenthesis_syntax_enabled(); + } + }, { + opener: '$$', + closer: '$$', + displayMode: true, + enabled: () => { + return this.dollar_syntax_enabled(); + } + }, { + opener: '$', + closer: '$', + displayMode: false, + enabled: () => { + return this.dollar_syntax_enabled(); + } + } + ]; + } + // Searches for the first opening delimiter in the string from a given position + + find_opening_delimiter(str, start) { // Search the string for each opening delimiter + const matches = (() => { + const map = this.delimiters_map; + const results = []; + + map.forEach((op) => { + if (op.enabled()) { + results.push({ + options: op, + pos: str.indexOf(op.opener, start) + }); + } + }); + return results; + })(); + + const positions = (() => { + const results = []; + matches.forEach((pos) => { + if (pos.pos >= 0) { + results.push(pos.pos); + } + }); + return results; + })(); + + // No opening delimiters were found + if (positions.length === 0) { + return null; + } + + //Take the first delimiter found + const pos = Math.min.apply(Math, positions); + + const match_index = (()=> { + const results = []; + matches.forEach((m) => { + results.push(m.pos); + }); + return results; + })().indexOf(pos); + + const match = matches[match_index]; + return match; + } + + // Returns the outer and inner boundaries of the latex block starting + // at the given opening delimiter + get_latex_boundaries(str, opening_delimiter_match) { + const inner = new Boundary; + const outer = new Boundary; + + // The closing delimiter matching to the opening one + const closer = opening_delimiter_match.options.closer; + outer.start = opening_delimiter_match.pos; + inner.start = opening_delimiter_match.pos + closer.length; + + // Search for a closer delimiter after the opening one + const closer_index = str.substr(inner.start).indexOf(closer); + if (closer_index < 0) { + return null; + } + inner.end = inner.start + closer_index; + outer.end = inner.end + closer.length; + return { + outer, + inner + }; + } + + // Searches for the first latex block in the given string + find_latex(str) { + let start = 0; + let opening_delimiter_match; + + while ((opening_delimiter_match = this.find_opening_delimiter(str, start++)) != null) { + const match = this.get_latex_boundaries(str, opening_delimiter_match); + if (match && match.inner.extract(str).trim().length) { + match.options = opening_delimiter_match.options; + return match; + } + } + return null; + } + + // Breaks a message to what comes before, after and to the content of a + // matched latex block + extract_latex(str, match) { + const before = str.substr(0, match.outer.start); + const after = str.substr(match.outer.end); + let latex = match.inner.extract(str); + latex = s.unescapeHTML(latex); + return { + before, + latex, + after + }; + } + + // Takes a latex math string and the desired display mode and renders it + // to HTML using the KaTeX library + render_latex(latex, displayMode) { + let rendered; + try { + rendered = katex.renderToString(latex, { + displayMode + }); + } catch (error) { + const e = error; + const display_mode = displayMode ? 'block' : 'inline'; + rendered = `
`; + rendered += `${ s.escapeHTML(e.message) }`; + rendered += '
'; + } + return rendered; + } + + // Takes a string and renders all latex blocks inside it + render(str, render_func) { + let result = ''; + while (this.find_latex(str) != null) { + // Find the first latex block in the string + const match = this.find_latex(str); + const parts = this.extract_latex(str, match); + + // Add to the reuslt what comes before the latex block as well as + // the rendered latex content + const rendered = render_func(parts.latex, match.options.displayMode); + result += parts.before + rendered; + // Set what comes after the latex block to be examined next + str = parts.after; + } + return result += str; + } + + // Takes a rocketchat message and renders latex in its content + render_message(message) { + //Render only if enabled in admin panel + let render_func; + if (this.katex_enabled()) { + let msg = message; + if (!_.isString(message)) { + if (_.trim(message.html)) { + msg = message.html; + } else { + return message; + } + } + if (_.isString(message)) { + render_func = (latex, displayMode) => { + return this.render_latex(latex, displayMode); + }; + } else { + if (message.tokens == null) { + message.tokens = []; + } + render_func = (latex, displayMode) => { + const token = `=!=${ Random.id() }=!=`; + message.tokens.push({ + token, + text: this.render_latex(latex, displayMode) + }); + return token; + }; + } + msg = this.render(msg, render_func); + if (!_.isString(message)) { + message.html = msg; + } else { + message = msg; + } + } + return message; + } + + katex_enabled() { + return RocketChat.settings.get('Katex_Enabled'); + } + + dollar_syntax_enabled() { + return RocketChat.settings.get('Katex_Dollar_Syntax'); + } + + parenthesis_syntax_enabled() { + return RocketChat.settings.get('Katex_Parenthesis_Syntax'); + } + +} + +RocketChat.katex = new Katex; + +const cb = RocketChat.katex.render_message.bind(RocketChat.katex); + +RocketChat.callbacks.add('renderMessage', cb, RocketChat.callbacks.priority.HIGH - 1, 'katex'); + +if (Meteor.isClient) { + Blaze.registerHelper('RocketChatKatex', function(text) { + return RocketChat.katex.render_message(text); + }); +} diff --git a/packages/rocketchat-katex/package.js b/packages/rocketchat-katex/package.js index 033ea7efeeea..de08f7f50f5d 100644 --- a/packages/rocketchat-katex/package.js +++ b/packages/rocketchat-katex/package.js @@ -6,15 +6,14 @@ Package.describe({ }); Package.onUse(function(api) { - api.use('coffeescript'); api.use('ecmascript'); api.use('underscore'); api.use('templating'); api.use('underscorestring:underscore.string'); api.use('rocketchat:lib'); - api.addFiles('settings.coffee', 'server'); - api.addFiles('katex.coffee'); + api.addFiles('settings.js', 'server'); + api.addFiles('katex.js'); api.addFiles('client/style.css', 'client'); const katexPath = 'node_modules/katex/dist/'; diff --git a/packages/rocketchat-katex/settings.coffee b/packages/rocketchat-katex/settings.coffee deleted file mode 100644 index 255aa3dc9c23..000000000000 --- a/packages/rocketchat-katex/settings.coffee +++ /dev/null @@ -1,6 +0,0 @@ -Meteor.startup -> - enableQuery = {_id: 'Katex_Enabled', value: true} - RocketChat.settings.add 'Katex_Enabled', true, {type: 'boolean', group: 'Message', section: 'Katex', public: true, i18n: 'Katex_Enabled_Description'} - - RocketChat.settings.add 'Katex_Parenthesis_Syntax', true, {type: 'boolean', group: 'Message', section: 'Katex', public: true, enableQuery, i18nDescription: 'Katex_Parenthesis_Syntax_Description'} - RocketChat.settings.add 'Katex_Dollar_Syntax', false, {type: 'boolean', group: 'Message', section: 'Katex', public: true, enableQuery, i18nDescription: 'Katex_Dollar_Syntax_Description'} diff --git a/packages/rocketchat-katex/settings.js b/packages/rocketchat-katex/settings.js new file mode 100644 index 000000000000..006ef790e64b --- /dev/null +++ b/packages/rocketchat-katex/settings.js @@ -0,0 +1,32 @@ +Meteor.startup(function() { + const enableQuery = { + _id: 'Katex_Enabled', + value: true + }; + RocketChat.settings.add('Katex_Enabled', true, { + type: 'boolean', + group: 'Message', + section: 'Katex', + 'public': true, + i18n: 'Katex_Enabled_Description' + }); + RocketChat.settings.add('Katex_Parenthesis_Syntax', true, { + type: 'boolean', + group: 'Message', + section: 'Katex', + 'public': true, + enableQuery, + i18nDescription: 'Katex_Parenthesis_Syntax_Description' + }); + return RocketChat.settings.add('Katex_Dollar_Syntax', false, { + type: 'boolean', + group: 'Message', + section: 'Katex', + 'public': true, + enableQuery, + i18nDescription: 'Katex_Dollar_Syntax_Description' + }); +}); + +// --- +// generated by coffee-script 1.9.2