|
From 471c3edd9f38316a84f2de6d11716d4e3d77a15e Mon Sep 17 00:00:00 2001 |
|
From: Christian Lawson-Perfect <[email protected]> |
|
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 |
|
|