diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js
index 13923a46da..0fcdb35945 100644
--- a/services/web/app/src/Features/Project/ProjectController.js
+++ b/services/web/app/src/Features/Project/ProjectController.js
@@ -324,6 +324,7 @@ const _ProjectController = {
const splitTests = [
!anonymous && 'bib-file-tpr-prompt',
'compile-log-events',
+ 'math-preview',
'null-test-share-modal',
'paywall-cta',
'pdf-caching-cached-url-lookup',
@@ -673,6 +674,7 @@ const _ProjectController = {
fontFamily: user.ace.fontFamily || 'lucida',
lineHeight: user.ace.lineHeight || 'normal',
overallTheme: user.ace.overallTheme,
+ mathPreview: user.ace.mathPreview,
},
privilegeLevel,
anonymous,
diff --git a/services/web/app/src/Features/User/UserController.js b/services/web/app/src/Features/User/UserController.js
index 227cef4e74..a0776a1813 100644
--- a/services/web/app/src/Features/User/UserController.js
+++ b/services/web/app/src/Features/User/UserController.js
@@ -372,6 +372,9 @@ async function updateUserSettings(req, res, next) {
if (req.body.lineHeight != null) {
user.ace.lineHeight = req.body.lineHeight
}
+ if (req.body.mathPreview != null) {
+ user.ace.mathPreview = req.body.mathPreview
+ }
await user.save()
const newEmail = req.body.email?.trim().toLowerCase()
diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js
index 12f9e6d675..69e9b900bd 100644
--- a/services/web/app/src/models/User.js
+++ b/services/web/app/src/models/User.js
@@ -86,6 +86,7 @@ const UserSchema = new Schema(
syntaxValidation: { type: Boolean },
fontFamily: { type: String },
lineHeight: { type: String },
+ mathPreview: { type: Boolean, default: true },
},
features: {
collaborators: {
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 72c6c09301..cb55a49dd3 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -396,6 +396,7 @@
"enter_any_size_including_units_or_valid_latex_command": "",
"enter_image_url": "",
"enter_the_confirmation_code": "",
+ "equation_preview": "",
"error": "",
"error_opening_document": "",
"error_opening_document_detail": "",
diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx
index 42c930cb40..90eb4ee0c1 100644
--- a/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx
+++ b/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx
@@ -16,10 +16,13 @@ import SettingsOverallTheme from './settings/settings-overall-theme'
import SettingsPdfViewer from './settings/settings-pdf-viewer'
import SettingsSpellCheckLanguage from './settings/settings-spell-check-language'
import SettingsSyntaxValidation from './settings/settings-syntax-validation'
+import SettingsMathPreview from './settings/settings-math-preview'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
export default function SettingsMenu() {
const { t } = useTranslation()
const anonymous = getMeta('ol-anonymous')
+ const enableMathPreview = useFeatureFlag('math-preview')
if (anonymous) {
return null
@@ -37,6 +40,7 @@ export default function SettingsMenu() {
+ {enableMathPreview && }
diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-math-preview.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-math-preview.tsx
new file mode 100644
index 0000000000..12554ff758
--- /dev/null
+++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-math-preview.tsx
@@ -0,0 +1,27 @@
+import { useTranslation } from 'react-i18next'
+import { useProjectSettingsContext } from '../../context/project-settings-context'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsMathPreview() {
+ const { t } = useTranslation()
+ const { mathPreview, setMathPreview } = useProjectSettingsContext()
+
+ return (
+
+ )
+}
diff --git a/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx b/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx
index 422b9b7d1f..20aa62b2d6 100644
--- a/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx
+++ b/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx
@@ -26,6 +26,7 @@ type ProjectSettingsSetterContextValue = {
setFontFamily: (fontFamily: UserSettings['fontFamily']) => void
setLineHeight: (lineHeight: UserSettings['lineHeight']) => void
setPdfViewer: (pdfViewer: UserSettings['pdfViewer']) => void
+ setMathPreview: (mathPreview: UserSettings['mathPreview']) => void
}
type ProjectSettingsContextValue = Partial &
@@ -69,6 +70,8 @@ export const ProjectSettingsProvider: FC = ({ children }) => {
setLineHeight,
pdfViewer,
setPdfViewer,
+ mathPreview,
+ setMathPreview,
} = useUserWideSettings()
useProjectWideSettingsSocketListener()
@@ -103,6 +106,8 @@ export const ProjectSettingsProvider: FC = ({ children }) => {
setLineHeight,
pdfViewer,
setPdfViewer,
+ mathPreview,
+ setMathPreview,
}),
[
compiler,
@@ -133,6 +138,8 @@ export const ProjectSettingsProvider: FC = ({ children }) => {
setLineHeight,
pdfViewer,
setPdfViewer,
+ mathPreview,
+ setMathPreview,
]
)
diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx
index 058c018b88..70202c9446 100644
--- a/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx
+++ b/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx
@@ -19,6 +19,7 @@ export default function useUserWideSettings() {
fontFamily,
lineHeight,
pdfViewer,
+ mathPreview,
} = userSettings
const setOverallTheme = useSetOverallTheme()
@@ -85,6 +86,13 @@ export default function useUserWideSettings() {
[saveUserSettings]
)
+ const setMathPreview = useCallback(
+ (mathPreview: UserSettings['mathPreview']) => {
+ saveUserSettings('mathPreview', mathPreview)
+ },
+ [saveUserSettings]
+ )
+
return {
autoComplete,
setAutoComplete,
@@ -106,5 +114,7 @@ export default function useUserWideSettings() {
setLineHeight,
pdfViewer,
setPdfViewer,
+ mathPreview,
+ setMathPreview,
}
}
diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts
index 68ddcf9d73..120734af0a 100644
--- a/services/web/frontend/js/features/source-editor/extensions/index.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/index.ts
@@ -48,6 +48,7 @@ import { toolbarPanel } from './toolbar/toolbar-panel'
import { geometryChangeEvent } from './geometry-change-event'
import { docName } from './doc-name'
import { fileTreeItemDrop } from './file-tree-item-drop'
+import { mathPreview } from './math-preview'
const moduleExtensions: Array<() => Extension> = importOverleafModules(
'sourceEditorExtensions'
@@ -125,6 +126,7 @@ export const createExtensions = (options: Record): Extension[] => [
emptyLineFiller(),
trackChanges(options.currentDoc, options.changeManager),
visual(options.visual),
+ mathPreview(options.settings.mathPreview),
toolbarPanel(),
verticalOverflow(),
highlightActiveLine(options.visual.visual),
diff --git a/services/web/frontend/js/features/source-editor/extensions/math-preview.ts b/services/web/frontend/js/features/source-editor/extensions/math-preview.ts
new file mode 100644
index 0000000000..cce06f0fe1
--- /dev/null
+++ b/services/web/frontend/js/features/source-editor/extensions/math-preview.ts
@@ -0,0 +1,159 @@
+import {
+ repositionTooltips,
+ showTooltip,
+ Tooltip,
+ ViewPlugin,
+} from '@codemirror/view'
+import {
+ Compartment,
+ EditorState,
+ Extension,
+ StateField,
+ TransactionSpec,
+} from '@codemirror/state'
+import { loadMathJax } from '../../mathjax/load-mathjax'
+import { descendantsOfNodeWithType } from '../utils/tree-query'
+import {
+ mathAncestorNode,
+ parseMathContainer,
+} from '../utils/tree-operations/math'
+import { documentCommands } from '../languages/latex/document-commands'
+import { debugConsole } from '@/utils/debugging'
+import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+
+const REPOSITION_EVENT = 'editor:repositionMathTooltips'
+
+export const mathPreview = (enabled: boolean): Extension => {
+ if (!isSplitTestEnabled('math-preview')) {
+ return []
+ }
+
+ return mathPreviewConf.of(enabled ? mathPreviewStateField : [])
+}
+
+const mathPreviewConf = new Compartment()
+
+export const setMathPreview = (enabled: boolean): TransactionSpec => ({
+ effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []),
+})
+
+const mathPreviewStateField = StateField.define({
+ create: buildTooltips,
+
+ update(tooltips, tr) {
+ if (tr.docChanged || tr.selection) {
+ tooltips = buildTooltips(tr.state)
+ }
+
+ return tooltips
+ },
+
+ provide: field => [
+ showTooltip.computeN([field], state => state.field(field)),
+
+ ViewPlugin.define(view => {
+ const listener = () => repositionTooltips(view)
+ window.addEventListener(REPOSITION_EVENT, listener)
+ return {
+ destroy() {
+ window.removeEventListener(REPOSITION_EVENT, listener)
+ },
+ }
+ }),
+ ],
+})
+
+const renderMath = async (
+ content: string,
+ displayMode: boolean,
+ element: HTMLElement,
+ definitions: string
+) => {
+ const MathJax = await loadMathJax()
+
+ MathJax.texReset([0]) // equation numbering is disabled, but this is still needed
+
+ try {
+ await MathJax.tex2svgPromise(definitions)
+ } catch {
+ // ignore errors thrown during parsing command definitions
+ }
+
+ const math = await MathJax.tex2svgPromise(content, {
+ ...MathJax.getMetricsFor(element),
+ display: displayMode,
+ })
+ element.textContent = ''
+ element.append(math)
+}
+
+function buildTooltips(state: EditorState): readonly Tooltip[] {
+ const tooltips: Tooltip[] = []
+
+ for (const range of state.selection.ranges) {
+ if (range.empty) {
+ const pos = range.from
+ const content = buildTooltipContent(state, pos)
+ if (content) {
+ const tooltip: Tooltip = {
+ pos,
+ above: true,
+ arrow: false,
+ create() {
+ const dom = document.createElement('div')
+ dom.append(content)
+ dom.className = 'ol-cm-math-tooltip'
+
+ return { dom, overlap: true, offset: { x: 0, y: 8 } }
+ },
+ }
+
+ tooltips.push(tooltip)
+ }
+ }
+ }
+
+ return tooltips
+}
+
+const buildTooltipContent = (
+ state: EditorState,
+ pos: number
+): HTMLDivElement | null => {
+ // if anywhere inside Math, render the whole Math content
+ const ancestorNode = mathAncestorNode(state, pos)
+ if (!ancestorNode) return null
+
+ const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math')
+ if (!node) return null
+
+ const math = parseMathContainer(state, node, ancestorNode)
+ if (!math || !math.content.length) return null
+
+ const element = document.createElement('div')
+ element.style.opacity = '0'
+ element.style.transition = 'opacity .01s ease-in'
+ element.textContent = math.content
+
+ let definitions = ''
+ const commandState = state.field(documentCommands, false)
+
+ if (commandState?.items) {
+ for (const command of commandState.items) {
+ if (command.type === 'definition' && command.raw) {
+ definitions += `${command.raw}\n`
+ }
+ }
+ }
+
+ renderMath(math.content, math.displayMode, element, definitions)
+ .then(() => {
+ element.style.opacity = '1'
+ window.dispatchEvent(new Event(REPOSITION_EVENT))
+ })
+ .catch(error => {
+ debugConsole.error(error)
+ })
+
+ return element
+}
diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
index 51f08865f1..7e33049a52 100644
--- a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
+++ b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts
@@ -82,6 +82,10 @@ import {
createSpaceCommand,
hasSpaceSubstitution,
} from '@/features/source-editor/extensions/visual/visual-widgets/space'
+import {
+ mathAncestorNode,
+ parseMathContainer,
+} from '../../utils/tree-operations/math'
type Options = {
previewByPath: (path: string) => PreviewPath | null
@@ -760,13 +764,7 @@ export const atomicDecorations = (options: Options) => {
return false // no markup in input content
} else if (nodeRef.type.is('Math')) {
// math equations
- let passToMathJax = true
-
- const ancestorNode =
- ancestorNodeOfType(state, nodeRef.from, '$MathContainer') ||
- ancestorNodeOfType(state, nodeRef.from, 'EquationEnvironment') ||
- // NOTE: EquationArrayEnvironment can be nested inside EquationEnvironment
- ancestorNodeOfType(state, nodeRef.from, 'EquationArrayEnvironment')
+ const ancestorNode = mathAncestorNode(state, nodeRef.from)
if (
ancestorNode &&
@@ -774,57 +772,19 @@ export const atomicDecorations = (options: Options) => {
? shouldDecorateFromLineEdges(state, ancestorNode)
: shouldDecorate(state, ancestorNode))
) {
- // the content of the Math element, without braces
- const innerContent = state.doc
- .sliceString(nodeRef.from, nodeRef.to)
- .trim()
+ const math = parseMathContainer(state, nodeRef, ancestorNode)
- // only replace when there's content inside the braces
- if (innerContent.length) {
- let content = innerContent
- let displayMode = false
-
- if (ancestorNode.type.is('$Environment')) {
- const environmentName = getEnvironmentName(ancestorNode, state)
- if (environmentName) {
- // use the outer content of environments that MathJax supports
- // https://docs.mathjax.org/en/latest/input/tex/macros/index.html#environments
- if (environmentName === 'tikzcd') {
- passToMathJax = false
- }
- if (
- environmentName !== 'math' &&
- environmentName !== 'displaymath'
- ) {
- content = state.doc
- .sliceString(ancestorNode.from, ancestorNode.to)
- .trim()
- }
-
- if (environmentName !== 'math') {
- displayMode = true
- }
- }
- } else {
- if (
- ancestorNode.type.is('BracketMath') ||
- Boolean(ancestorNode.getChild('DisplayMath'))
- ) {
- displayMode = true
- }
- }
- if (passToMathJax) {
- decorations.push(
- Decoration.replace({
- widget: new MathWidget(
- content,
- displayMode,
- commandDefinitions
- ),
- block: displayMode,
- }).range(ancestorNode.from, ancestorNode.to)
- )
- }
+ if (math && math.passToMathJax) {
+ decorations.push(
+ Decoration.replace({
+ widget: new MathWidget(
+ math.content,
+ math.displayMode,
+ commandDefinitions
+ ),
+ block: math.displayMode,
+ }).range(ancestorNode.from, ancestorNode.to)
+ )
}
}
diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
index c83fe1c617..5b66a19c81 100644
--- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
+++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
@@ -61,6 +61,7 @@ import { debugConsole } from '@/utils/debugging'
import { useMetadataContext } from '@/features/ide-react/context/metadata-context'
import { useUserContext } from '@/shared/context/user-context'
import { useReferencesContext } from '@/features/ide-react/context/references-context'
+import { setMathPreview } from '@/features/source-editor/extensions/math-preview'
function useCodeMirrorScope(view: EditorView) {
const { fileTreeData } = useFileTreeData()
@@ -96,6 +97,7 @@ function useCodeMirrorScope(view: EditorView) {
autoPairDelimiters,
mode,
syntaxValidation,
+ mathPreview,
} = userSettings
const [cursorHighlights] = useScopeValue>(
@@ -153,6 +155,7 @@ function useCodeMirrorScope(view: EditorView) {
autoPairDelimiters,
mode,
syntaxValidation,
+ mathPreview,
})
const currentDocRef = useRef({
@@ -385,6 +388,11 @@ function useCodeMirrorScope(view: EditorView) {
view.dispatch(setSyntaxValidation(syntaxValidation))
}, [view, syntaxValidation])
+ useEffect(() => {
+ settingsRef.current.mathPreview = mathPreview
+ view.dispatch(setMathPreview(mathPreview))
+ }, [view, mathPreview])
+
const emitSyncToPdf = useScopeEventEmitter('cursor:editor:syncToPdf')
const handleGoToLine = useCallback(
diff --git a/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts b/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts
index f54edebbcd..c37bee1cf0 100644
--- a/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts
+++ b/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts
@@ -46,6 +46,7 @@ const countCommandUsage = (context: CompletionContext) => {
>()
const commandListProjection = context.state.field(documentCommands)
+
if (!commandListProjection.items) {
return result
}
diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts
index f31516df94..5eb8ca2857 100644
--- a/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts
+++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts
@@ -10,6 +10,8 @@ export class Command extends ProjectionItem {
readonly title: string = ''
readonly optionalArgCount: number = 0
readonly requiredArgCount: number = 0
+ readonly type: 'usage' | 'definition' = 'usage'
+ readonly raw: string | undefined = undefined
}
/**
@@ -62,16 +64,16 @@ export const enterNode = (
argCountNumber--
}
- const thisCommand: Readonly = {
+ items.push({
line: state.doc.lineAt(node.from).number,
title: commandNameText,
from: node.from,
to: node.to,
optionalArgCount: commandDefinitionHasOptionalArgument ? 1 : 0,
requiredArgCount: argCountNumber,
- }
-
- items.push(thisCommand)
+ type: 'definition',
+ raw: state.sliceDoc(node.from, node.to),
+ })
} else if (
node.type.is('UnknownCommand') ||
node.type.is('KnownCommand') ||
@@ -112,7 +114,7 @@ export const enterNode = (
commandNode.getChildren('$Argument')
const text = state.doc.sliceString(ctrlSeq.from, ctrlSeq.to)
- const thisCommand = {
+ items.push({
line: state.doc.lineAt(commandNode.from).number,
title: text,
from: commandNode.from,
@@ -120,7 +122,8 @@ export const enterNode = (
optionalArgCount: optionalArguments.length,
requiredArgCount:
commandArgumentsIncludingOptional.length - optionalArguments.length,
- }
- items.push(thisCommand)
+ type: 'usage',
+ raw: undefined,
+ })
}
}
diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/math.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/math.ts
new file mode 100644
index 0000000000..5fa7e4a4ce
--- /dev/null
+++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/math.ts
@@ -0,0 +1,56 @@
+import { getEnvironmentName } from './environments'
+import { EditorState } from '@codemirror/state'
+import { SyntaxNode, SyntaxNodeRef } from '@lezer/common'
+import { ancestorNodeOfType } from './ancestors'
+
+export const mathAncestorNode = (state: EditorState, pos: number) =>
+ ancestorNodeOfType(state, pos, '$MathContainer') ||
+ ancestorNodeOfType(state, pos, 'EquationEnvironment') ||
+ // NOTE: EquationArrayEnvironment can be nested inside EquationEnvironment
+ ancestorNodeOfType(state, pos, 'EquationArrayEnvironment')
+
+export const parseMathContainer = (
+ state: EditorState,
+ nodeRef: SyntaxNodeRef,
+ ancestorNode: SyntaxNode
+) => {
+ // the content of the Math element, without braces
+ const innerContent = state.doc.sliceString(nodeRef.from, nodeRef.to).trim()
+
+ if (!innerContent.length) {
+ return null
+ }
+
+ let content = innerContent
+ let displayMode = false
+ let passToMathJax = true
+
+ if (ancestorNode.type.is('$Environment')) {
+ const environmentName = getEnvironmentName(ancestorNode, state)
+ if (environmentName) {
+ // use the outer content of environments that MathJax supports
+ // https://docs.mathjax.org/en/latest/input/tex/macros/index.html#environments
+ if (environmentName === 'tikzcd') {
+ passToMathJax = false
+ }
+ if (environmentName !== 'math' && environmentName !== 'displaymath') {
+ content = state.doc
+ .sliceString(ancestorNode.from, ancestorNode.to)
+ .trim()
+ }
+
+ if (environmentName !== 'math') {
+ displayMode = true
+ }
+ }
+ } else {
+ if (
+ ancestorNode.type.is('BracketMath') ||
+ Boolean(ancestorNode.getChild('DisplayMath'))
+ ) {
+ displayMode = true
+ }
+ }
+
+ return { content, displayMode, passToMathJax }
+}
diff --git a/services/web/frontend/js/shared/context/user-settings-context.tsx b/services/web/frontend/js/shared/context/user-settings-context.tsx
index 34f49156df..755fc06bbe 100644
--- a/services/web/frontend/js/shared/context/user-settings-context.tsx
+++ b/services/web/frontend/js/shared/context/user-settings-context.tsx
@@ -23,6 +23,7 @@ const defaultSettings: UserSettings = {
fontSize: 12,
fontFamily: 'monaco',
lineHeight: 'normal',
+ mathPreview: true,
}
type UserSettingsContextValue = {
diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less
index d4b449e0c4..33fb81486d 100644
--- a/services/web/frontend/stylesheets/app/editor.less
+++ b/services/web/frontend/stylesheets/app/editor.less
@@ -11,6 +11,7 @@
@import './editor/search.less';
@import './editor/publish-template.less';
@import './editor/online-users.less';
+@import './editor/math-preview.less';
@import './editor/hotkeys.less';
@import './editor/review-panel.less';
@import './editor/publish-modal.less';
diff --git a/services/web/frontend/stylesheets/app/editor/math-preview.less b/services/web/frontend/stylesheets/app/editor/math-preview.less
new file mode 100644
index 0000000000..e4bbbda2b8
--- /dev/null
+++ b/services/web/frontend/stylesheets/app/editor/math-preview.less
@@ -0,0 +1,10 @@
+.ol-cm-math-tooltip {
+ box-shadow: 0px 2px 4px 0px #1e253029;
+ border: 1px solid #e7e9ee !important;
+ border-radius: 4px;
+ background-color: white !important;
+ max-height: 400px;
+ max-width: 800px;
+ overflow: auto;
+ padding: 8px;
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 33f482e8d0..33ff0fe34b 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -565,6 +565,7 @@
"enter_your_email_address": "Enter your email address",
"enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password": "Enter your email address below, and we will send you a link to reset your password",
"enter_your_new_password": "Enter your new password",
+ "equation_preview": "Equation preview",
"error": "Error",
"error_opening_document": "Error opening document",
"error_opening_document_detail": "Sorry, something went wrong opening this document. Please try again.",
diff --git a/services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx b/services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx
new file mode 100644
index 0000000000..d7f3b957cf
--- /dev/null
+++ b/services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx
@@ -0,0 +1,23 @@
+import { screen, within } from '@testing-library/dom'
+import { expect } from 'chai'
+import fetchMock from 'fetch-mock'
+import SettingsMathPreview from '@/features/editor-left-menu/components/settings/settings-math-preview'
+import { renderWithEditorContext } from '../../../../helpers/render-with-context'
+
+describe('', function () {
+ afterEach(function () {
+ fetchMock.reset()
+ })
+
+ it('shows correct menu', async function () {
+ renderWithEditorContext()
+
+ const select = screen.getByLabelText('Equation preview')
+
+ const optionOn = within(select).getByText('On')
+ expect(optionOn.getAttribute('value')).to.equal('true')
+
+ const optionOff = within(select).getByText('Off')
+ expect(optionOff.getAttribute('value')).to.equal('false')
+ })
+})
diff --git a/services/web/test/frontend/helpers/editor-providers.jsx b/services/web/test/frontend/helpers/editor-providers.jsx
index adfca9c2fc..0b86a6746e 100644
--- a/services/web/test/frontend/helpers/editor-providers.jsx
+++ b/services/web/test/frontend/helpers/editor-providers.jsx
@@ -54,6 +54,7 @@ const defaultUserSettings = {
autoPairDelimiters: true,
trackChanges: true,
syntaxValidation: false,
+ mathPreview: true,
}
export function EditorProviders({
diff --git a/services/web/types/user-settings.ts b/services/web/types/user-settings.ts
index 95c71bda24..6dbb05851e 100644
--- a/services/web/types/user-settings.ts
+++ b/services/web/types/user-settings.ts
@@ -16,4 +16,5 @@ export type UserSettings = {
fontSize: number
fontFamily: FontFamily
lineHeight: LineHeight
+ mathPreview: boolean
}