2024-07-24 09:38:42 -04:00
|
|
|
import {
|
2024-08-05 09:22:23 -04:00
|
|
|
EditorView,
|
2024-07-24 09:38:42 -04:00
|
|
|
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 {
|
2024-07-26 06:52:57 -04:00
|
|
|
MathContainer,
|
2024-07-24 09:38:42 -04:00
|
|
|
mathAncestorNode,
|
|
|
|
parseMathContainer,
|
|
|
|
} from '../utils/tree-operations/math'
|
|
|
|
import { documentCommands } from '../languages/latex/document-commands'
|
|
|
|
import { debugConsole } from '@/utils/debugging'
|
|
|
|
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
2024-08-19 09:16:44 -04:00
|
|
|
import ReactDOM from 'react-dom'
|
|
|
|
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
|
|
|
import SplitTestBadge from '@/shared/components/split-test-badge'
|
2024-09-03 11:23:47 -04:00
|
|
|
import { nodeHasError } from '../utils/tree-operations/common'
|
2024-09-16 04:40:44 -04:00
|
|
|
import { documentEnvironments } from '../languages/latex/document-environments'
|
2024-07-24 09:38:42 -04:00
|
|
|
|
|
|
|
const REPOSITION_EVENT = 'editor:repositionMathTooltips'
|
|
|
|
|
|
|
|
export const mathPreview = (enabled: boolean): Extension => {
|
|
|
|
if (!isSplitTestEnabled('math-preview')) {
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
2024-08-05 09:22:23 -04:00
|
|
|
return mathPreviewConf.of(
|
|
|
|
enabled ? [mathPreviewTheme, mathPreviewStateField] : []
|
|
|
|
)
|
2024-07-24 09:38:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const mathPreviewConf = new Compartment()
|
|
|
|
|
|
|
|
export const setMathPreview = (enabled: boolean): TransactionSpec => ({
|
|
|
|
effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []),
|
|
|
|
})
|
|
|
|
|
2024-09-05 05:35:43 -04:00
|
|
|
const mathPreviewStateField = StateField.define<Tooltip | null>({
|
|
|
|
create: buildTooltip,
|
2024-07-24 09:38:42 -04:00
|
|
|
|
|
|
|
update(tooltips, tr) {
|
|
|
|
if (tr.docChanged || tr.selection) {
|
2024-09-05 05:35:43 -04:00
|
|
|
tooltips = buildTooltip(tr.state)
|
2024-07-24 09:38:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return tooltips
|
|
|
|
},
|
|
|
|
|
|
|
|
provide: field => [
|
2024-09-05 05:35:43 -04:00
|
|
|
showTooltip.compute([field], state => state.field(field)),
|
2024-07-24 09:38:42 -04:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-09-05 05:35:43 -04:00
|
|
|
function buildTooltip(state: EditorState): Tooltip | null {
|
|
|
|
const range = state.selection.main
|
|
|
|
|
|
|
|
if (!range.empty) {
|
|
|
|
return null
|
2024-07-24 09:38:42 -04:00
|
|
|
}
|
|
|
|
|
2024-09-05 05:35:43 -04:00
|
|
|
const mathContainer = getMathContainer(state, range.from)
|
|
|
|
const content = buildTooltipContent(state, mathContainer)
|
|
|
|
|
|
|
|
if (!content || !mathContainer) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
pos: mathContainer.pos,
|
|
|
|
above: true,
|
|
|
|
strictSide: true,
|
|
|
|
arrow: false,
|
|
|
|
create() {
|
|
|
|
const dom = document.createElement('div')
|
|
|
|
dom.append(content)
|
|
|
|
const badge = renderSplitTestBadge()
|
|
|
|
dom.append(badge)
|
|
|
|
dom.className = 'ol-cm-math-tooltip'
|
|
|
|
|
|
|
|
return { dom, overlap: true, offset: { x: 0, y: 8 } }
|
|
|
|
},
|
|
|
|
}
|
2024-07-24 09:38:42 -04:00
|
|
|
}
|
|
|
|
|
2024-07-26 06:52:57 -04:00
|
|
|
const getMathContainer = (state: EditorState, pos: number) => {
|
|
|
|
// if anywhere inside Math, find the whole Math node
|
2024-07-24 09:38:42 -04:00
|
|
|
const ancestorNode = mathAncestorNode(state, pos)
|
|
|
|
if (!ancestorNode) return null
|
|
|
|
|
|
|
|
const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math')
|
|
|
|
if (!node) return null
|
|
|
|
|
2024-09-03 11:23:47 -04:00
|
|
|
if (nodeHasError(ancestorNode)) return null
|
|
|
|
|
2024-07-26 06:52:57 -04:00
|
|
|
return parseMathContainer(state, node, ancestorNode)
|
|
|
|
}
|
|
|
|
|
|
|
|
const buildTooltipContent = (
|
|
|
|
state: EditorState,
|
|
|
|
math: MathContainer | null
|
|
|
|
): HTMLDivElement | null => {
|
2024-07-24 09:38:42 -04:00
|
|
|
if (!math || !math.content.length) return null
|
|
|
|
|
|
|
|
const element = document.createElement('div')
|
|
|
|
element.style.opacity = '0'
|
|
|
|
element.textContent = math.content
|
|
|
|
|
|
|
|
let definitions = ''
|
|
|
|
|
2024-09-16 04:40:44 -04:00
|
|
|
const environmentState = state.field(documentEnvironments, false)
|
|
|
|
if (environmentState?.items) {
|
|
|
|
for (const environment of environmentState.items) {
|
|
|
|
if (environment.type === 'definition') {
|
|
|
|
definitions += `${environment.raw}\n`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const commandState = state.field(documentCommands, false)
|
2024-07-24 09:38:42 -04:00
|
|
|
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
|
|
|
|
}
|
2024-08-05 09:22:23 -04:00
|
|
|
|
2024-08-19 09:16:44 -04:00
|
|
|
const renderSplitTestBadge = () => {
|
|
|
|
const element = document.createElement('span')
|
|
|
|
ReactDOM.render(
|
|
|
|
<SplitTestProvider>
|
|
|
|
<SplitTestBadge
|
|
|
|
displayOnVariants={['enabled']}
|
|
|
|
splitTestName="math-preview"
|
|
|
|
/>
|
|
|
|
</SplitTestProvider>,
|
|
|
|
element
|
|
|
|
)
|
|
|
|
return element
|
|
|
|
}
|
|
|
|
|
2024-08-05 09:22:23 -04:00
|
|
|
/**
|
|
|
|
* Styles for the preview tooltip
|
|
|
|
*/
|
|
|
|
const mathPreviewTheme = EditorView.baseTheme({
|
|
|
|
'&light .ol-cm-math-tooltip': {
|
|
|
|
boxShadow: '0px 2px 4px 0px #1e253029',
|
|
|
|
border: '1px solid #e7e9ee !important',
|
|
|
|
backgroundColor: 'white !important',
|
|
|
|
},
|
|
|
|
'&dark .ol-cm-math-tooltip': {
|
|
|
|
boxShadow: '0px 2px 4px 0px #1e253029',
|
|
|
|
border: '1px solid #2f3a4c !important',
|
|
|
|
backgroundColor: '#1b222c !important',
|
|
|
|
},
|
|
|
|
})
|