The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/packages/rocketchat-katex/katex.js

251 lines
6.0 KiB

/*
* 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 _ from 'underscore';
import s from 'underscore.string';
import katex from '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: () => this.parenthesis_syntax_enabled(),
}, {
opener: '\\(',
closer: '\\)',
displayMode: false,
enabled: () => this.parenthesis_syntax_enabled(),
}, {
opener: '$$',
closer: '$$',
displayMode: true,
enabled: () => this.dollar_syntax_enabled(),
}, {
opener: '$',
closer: '$',
displayMode: false,
enabled: () => 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;
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 = `<div class="katex-error katex-${ display_mode }-error">`;
rendered += `${ s.escapeHTML(e.message) }`;
rendered += '</div>';
}
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 (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;
}
}
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);
});
}