Skip to content

Instantly share code, notes, and snippets.

@christianp

christianp/README.md

Created Aug 25, 2020
Embed
What would you like to do?
Mastodon LaTeX preview

This patch applies on top of the mathstodon branch maintained by christianp. It's only useful if you have something rendering LaTeX inside status content. The mathstodon branch uses MathJax for this.

You need a copy of MathJax 3 in the folder public/MathJax.

From 471c3edd9f38316a84f2de6d11716d4e3d77a15e Mon Sep 17 00:00:00 2001
From: Christian Lawson-Perfect <christianperfect@gmail.com>
Date: Tue, 25 Aug 2020 11:44:25 +0100
Subject: [PATCH] latex preview and completion
This commit adds a latex suggestion type. The main point is to show a
preview rendering of LaTeX as it's being typed, but it can also add
closing braces and the end delimiter.
It might be nice to have suggestions for individual latex commands.
---
app/javascript/mastodon/actions/compose.js | 41 +++++++++++++++++++
.../mastodon/components/autosuggest_input.js | 29 ++++++++++++-
.../mastodon/components/autosuggest_latex.js | 35 ++++++++++++++++
.../components/autosuggest_textarea.js | 23 ++++++++++-
app/javascript/mastodon/reducers/compose.js | 4 +-
5 files changed, 127 insertions(+), 5 deletions(-)
create mode 100644 app/javascript/mastodon/components/autosuggest_latex.js
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 6b73fc90e..277b0e28a 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -400,6 +400,33 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
});
}, 200, { leading: true, trailing: true });
+const fetchComposeSuggestionsLatex = (dispatch, getState, token) => {
+ const start_delimiter = token.slice(0,2);
+ const end_delimiter = {'\\(': '\\)', '\\[': '\\]'}[start_delimiter];
+ let expression = token.slice(2).replace(/\\\)?$/,'');
+ let brace = 0;
+ for(let i=0;i<expression.length;i++) {
+ switch(expression[i]) {
+ case '\\':
+ i += 1;
+ break;
+ case '{':
+ brace += 1;
+ break;
+ case '}':
+ brace -= 1;
+ break;
+ }
+ }
+ for(;brace;brace--) {
+ expression += '}';
+ }
+ const results = [
+ { start_delimiter, end_delimiter, expression }
+ ];
+ dispatch(readyComposeSuggestionsLatex(token, results));
+};
+
export function fetchComposeSuggestions(token) {
return (dispatch, getState) => {
switch (token[0]) {
@@ -409,6 +436,9 @@ export function fetchComposeSuggestions(token) {
case '#':
fetchComposeSuggestionsTags(dispatch, getState, token);
break;
+ case '\\':
+ fetchComposeSuggestionsLatex(dispatch, getState, token);
+ break;
default:
fetchComposeSuggestionsAccounts(dispatch, getState, token);
break;
@@ -416,6 +446,14 @@ export function fetchComposeSuggestions(token) {
};
};
+export function readyComposeSuggestionsLatex(token, latex) {
+ return {
+ type: COMPOSE_SUGGESTIONS_READY,
+ token,
+ latex,
+ };
+};
+
export function readyComposeSuggestionsEmojis(token, emojis) {
return {
type: COMPOSE_SUGGESTIONS_READY,
@@ -453,6 +491,9 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
} else if (suggestion.type === 'account') {
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
startPosition = position;
+ } else if (suggestion.type === 'latex') {
+ completion = `${suggestion.start_delimiter}${suggestion.expression}${suggestion.end_delimiter}`;
+ startPosition = position - 1;
}
dispatch({
diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js
index 6d2035add..97fc6b489 100644
--- a/app/javascript/mastodon/components/autosuggest_input.js
+++ b/app/javascript/mastodon/components/autosuggest_input.js
@@ -1,5 +1,6 @@
import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
+import AutosuggestLatex from './autosuggest_latex';
import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag';
import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -11,9 +12,30 @@ import { List as ImmutableList } from 'immutable';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word;
+ let left;
+ let right;
+
+ left = str.slice(0, caretPosition).search(/\\\((?:(?!\\\)).)*$/);
+ if (left >= 0) {
+ right = str.slice(caretPosition).search(/\\\)/);
+ if (right < 0) {
+ word = str.slice(left);
+ } else {
+ word = str.slice(left, right + caretPosition);
+ }
+ if (word.trim().length >= 3) {
+ return [left + 1, word];
+ }
+ }
- let left = str.slice(0, caretPosition).search(/\S+$/);
- let right = str.slice(caretPosition).search(/\s/);
+ left = str.slice(0, caretPosition).search(/\S+$/);
+ right = str.slice(caretPosition).search(/\s/);
+
+ if (right < 0) {
+ word = str.slice(left);
+ } else {
+ word = str.slice(left, right + caretPosition);
+ }
if (right < 0) {
word = str.slice(left);
@@ -177,6 +199,9 @@ export default class AutosuggestInput extends ImmutablePureComponent {
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
+ } else if (suggestion.type === 'latex') {
+ inner = <AutosuggestLatex latex={suggestion} />;
+ key = 'latex'+suggestion.expression;
}
return (
diff --git a/app/javascript/mastodon/components/autosuggest_latex.js b/app/javascript/mastodon/components/autosuggest_latex.js
new file mode 100644
index 000000000..834f323ce
--- /dev/null
+++ b/app/javascript/mastodon/components/autosuggest_latex.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const assetHost = process.env.CDN_HOST || '';
+
+export default class AutosuggestLatex extends React.PureComponent {
+
+ static propTypes = {
+ latex: PropTypes.object.isRequired,
+ };
+
+ setRef = (c) => {
+ this.node = c;
+ }
+
+ componentDidMount() {
+ try {
+ MathJax.typeset([this.node]);
+ } catch(e) {
+ console.error(e);
+ }
+
+ }
+
+ render () {
+ const { latex } = this.props;
+
+ return (
+ <div className='autosuggest-latex' ref={this.setRef}>
+ \({latex.expression}\)
+ </div>
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index c1050aea8..7efae0804 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -1,6 +1,7 @@
import React from 'react';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji';
+import AutosuggestLatex from './autosuggest_latex';
import AutosuggestHashtag from './autosuggest_hashtag';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
@@ -12,9 +13,24 @@ import try_replace from '../features/compose/util/autolatex.js';
const textAtCursorMatchesToken = (str, caretPosition) => {
let word;
+ let left;
+ let right;
+
+ left = str.slice(0, caretPosition).search(/\\[\(\[](?:(?!\\[\)\]]).)*$/);
+ if (left >= 0) {
+ right = str.slice(caretPosition).search(/\\[\)\]]/);
+ if (right < 0) {
+ word = str.slice(left);
+ } else {
+ word = str.slice(left, right + caretPosition);
+ }
+ if (word.trim().length >= 3) {
+ return [left + 1, word];
+ }
+ }
- let left = str.slice(0, caretPosition).search(/\S+$/);
- let right = str.slice(caretPosition).search(/\s/);
+ left = str.slice(0, caretPosition).search(/\S+$/);
+ right = str.slice(caretPosition).search(/\s/);
if (right < 0) {
word = str.slice(left);
@@ -188,6 +204,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
+ } else if (suggestion.type === 'latex') {
+ inner = <AutosuggestLatex latex={suggestion} />;
+ key = suggestion.expression;
}
return (
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index e6e6d2ae1..99810b16c 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -221,11 +221,13 @@ const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
}
};
-const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
+const normalizeSuggestions = (state, { accounts, emojis, tags, latex, token }) => {
if (accounts) {
return accounts.map(item => ({ id: item.id, type: 'account' }));
} else if (emojis) {
return emojis.map(item => ({ ...item, type: 'emoji' }));
+ } else if (latex) {
+ return latex.map(item => ({ ...item, type: 'latex' }));
} else {
return mergeLocalHashtagResults(sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' }))), token.slice(1), state.get('tagHistory'));
}
--
2.25.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment