Skip to content

Commit

Permalink
Refactor katex package
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan committed Jan 28, 2019
1 parent f66fac9 commit 23c57f2
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 141 deletions.
2 changes: 1 addition & 1 deletion packages/rocketchat-katex/client/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import '../lib/katex';
export { default as katex } from '../lib/katex';
210 changes: 74 additions & 136 deletions packages/rocketchat-katex/lib/katex.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
/*
* KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
* https://github.com/Khan/KaTeX
*/
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { Blaze } from 'meteor/blaze';
import { RocketChat } from 'meteor/rocketchat:lib';
import _ from 'underscore';
import s from 'underscore.string';

import katex from 'katex';

class Boundary {
Expand All @@ -21,125 +14,97 @@ class Boundary {
extract(str) {
return str.substr(this.start, this.length());
}

}

class Katex {
constructor() {
this.delimiters_map = [
this.delimitersMap = [
{
opener: '\\[',
closer: '\\]',
displayMode: true,
enabled: () => this.parenthesis_syntax_enabled(),
enabled: () => this.isParenthesisSyntaxEnabled(),
}, {
opener: '\\(',
closer: '\\)',
displayMode: false,
enabled: () => this.parenthesis_syntax_enabled(),
enabled: () => this.isParenthesisSyntaxEnabled(),
}, {
opener: '$$',
closer: '$$',
displayMode: true,
enabled: () => this.dollar_syntax_enabled(),
enabled: () => this.isDollarSyntaxEnabled(),
}, {
opener: '$',
closer: '$',
displayMode: false,
enabled: () => this.dollar_syntax_enabled(),
enabled: () => this.isDollarSyntaxEnabled(),
},
];
}
// 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;
})();

findOpeningDelimiter(str, start) {
const matches = this.delimitersMap.filter((options) => options.enabled()).map((options) => ({
options,
pos: str.indexOf(options.opener, start),
}));

const positions = matches.filter(({ pos }) => pos >= 0).map(({ pos }) => pos);

// No opening delimiters were found
if (positions.length === 0) {
return null;
}

// Take the first delimiter found
const pos = Math.min.apply(Math, positions);
const minPos = Math.min.apply(Math, positions);

const match_index = (() => {
const results = [];
matches.forEach((m) => {
results.push(m.pos);
});
return results;
})().indexOf(pos);
const matchIndex = matches.findIndex(({ pos }) => pos === minPos);

const match = matches[match_index];
const match = matches[matchIndex];
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) {
getLatexBoundaries(str, { options: { closer }, pos }) {
const closerIndex = str.substr(pos + closer.length).indexOf(closer);
if (closerIndex < 0) {
return null;
}

const inner = new Boundary;
const outer = new Boundary;

// The closing delimiter matching to the opening one
const { closer } = opening_delimiter_match.options;
outer.start = opening_delimiter_match.pos;
inner.start = opening_delimiter_match.pos + closer.length;
inner.start = pos + closer.length;
inner.end = inner.start + closerIndex;

// 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.start = pos;
outer.end = inner.end + closer.length;

return {
outer,
inner,
};
}

// Searches for the first latex block in the given string
find_latex(str) {
findLatex(str) {
let start = 0;
let opening_delimiter_match;
let openingDelimiterMatch;

while ((opening_delimiter_match = this.find_opening_delimiter(str, start++)) != null) {
const match = this.get_latex_boundaries(str, opening_delimiter_match);
while ((openingDelimiterMatch = this.findOpeningDelimiter(str, start++)) != null) {
const match = this.getLatexBoundaries(str, openingDelimiterMatch);
if (match && match.inner.extract(str).trim().length) {
match.options = opening_delimiter_match.options;
match.options = openingDelimiterMatch.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) {
extractLatex(str, match) {
const before = str.substr(0, match.outer.start);
const after = str.substr(match.outer.end);
let latex = match.inner.extract(str);
Expand All @@ -153,103 +118,76 @@ class Katex {

// 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;
renderLatex = (latex, displayMode) => {
try {
rendered = katex.renderToString(latex, {
return katex.renderToString(latex, {
displayMode,
macros: {
'\\href': '\\@secondoftwo',
},
});
} catch (error) {
const e = error;
const display_mode = displayMode ? 'block' : 'inline';
rendered = `<div class="katex-error katex-${ display_mode }-error">`;
rendered += `${ s.escapeHTML(e.message) }`;
rendered += '</div>';
} catch ({ message }) {
return `<div class="katex-error katex-${ displayMode ? 'block' : 'inline' }-error">` +
`${ s.escapeHTML(message) }</div>`;
}
return rendered;
}

// Takes a string and renders all latex blocks inside it
render(str, render_func) {
render(str, renderFunction) {
let result = '';
while (this.find_latex(str) != null) {
while (this.findLatex(str) != null) {
// Find the first latex block in the string
const match = this.find_latex(str);
const parts = this.extract_latex(str, match);
const match = this.findLatex(str);
const parts = this.extractLatex(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);
const rendered = renderFunction(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 (s.trim(message.html)) {
msg = message.html;
} else {
return message;
}
}
if (_.isString(message)) {
render_func = (latex, displayMode) => 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;
}
renderMessage = (message) => {
if (!this.isEnabled()) {
return message;
}
return message;
}

katex_enabled() {
return RocketChat.settings.get('Katex_Enabled');
}
if (_.isString(message)) {
return this.render(message, this.renderLatex);
}

dollar_syntax_enabled() {
return RocketChat.settings.get('Katex_Dollar_Syntax');
}
if (!s.trim(message.html)) {
return message;
}

parenthesis_syntax_enabled() {
return RocketChat.settings.get('Katex_Parenthesis_Syntax');
}
if (message.tokens == null) {
message.tokens = [];
}

}
message.html = this.render(message.html, (latex, displayMode) => {
const token = `=!=${ Random.id() }=!=`;
message.tokens.push({
token,
text: this.renderLatex(latex, displayMode),
});
return token;
});

RocketChat.katex = new Katex;
return message;
}

const cb = RocketChat.katex.render_message.bind(RocketChat.katex);
isEnabled = () => RocketChat.settings.get('Katex_Enabled')

RocketChat.callbacks.add('renderMessage', cb, RocketChat.callbacks.priority.HIGH - 1, 'katex');
isDollarSyntaxEnabled = () => RocketChat.settings.get('Katex_Dollar_Syntax')

if (Meteor.isClient) {
Blaze.registerHelper('RocketChatKatex', function(text) {
return RocketChat.katex.render_message(text);
});
isParenthesisSyntaxEnabled = () => RocketChat.settings.get('Katex_Parenthesis_Syntax')
}

const instance = new Katex;

RocketChat.callbacks.add('renderMessage', instance.renderMessage, RocketChat.callbacks.priority.HIGH - 1, 'katex');

export default instance;
9 changes: 5 additions & 4 deletions packages/rocketchat-ui-message/client/messageBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ import { RocketChat } from 'meteor/rocketchat:lib';
import { fileUploadHandler } from 'meteor/rocketchat:file-upload';
import { ChatSubscription, RoomHistoryManager, RoomManager, KonchatNotification, popover, ChatMessages, fileUpload, AudioRecorder, chatMessages, MsgTyping } from 'meteor/rocketchat:ui';
import { t } from 'meteor/rocketchat:utils';
import { katex } from 'meteor/rocketchat:katex';
import toastr from 'toastr';
import moment from 'moment';
import _ from 'underscore';

let audioMessageIntervalId;

function katexSyntax() {
if (RocketChat.katex.katex_enabled()) {
if (RocketChat.katex.dollar_syntax_enabled()) {
if (katex.isEnabled()) {
if (katex.isDollarSyntaxEnabled()) {
return '$$KaTeX$$';
}
if (RocketChat.katex.parenthesis_syntax_enabled()) {
if (katex.isParenthesisSyntaxEnabled()) {
return '\\[KaTeX\\]';
}
}
Expand Down Expand Up @@ -134,7 +135,7 @@ const markdownButtons = [
{
label: katexSyntax,
link: 'https://khan.github.io/KaTeX/function-support.html',
condition: () => RocketChat.katex.katex_enabled(),
condition: () => katex.isEnabled(),
},
];

Expand Down

0 comments on commit 23c57f2

Please sign in to comment.