math-preview.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { EditorView, showTooltip, Tooltip, ViewPlugin } from '@codemirror/view'
  2. import {
  3. Compartment,
  4. EditorState,
  5. Extension,
  6. StateEffect,
  7. StateField,
  8. TransactionSpec,
  9. } from '@codemirror/state'
  10. import { loadMathJax } from '../../mathjax/load-mathjax'
  11. import { descendantsOfNodeWithType } from '../utils/tree-query'
  12. import {
  13. MathContainer,
  14. mathAncestorNode,
  15. parseMathContainer,
  16. } from '../utils/tree-operations/math'
  17. import { documentCommands } from '../languages/latex/document-commands'
  18. import { debugConsole } from '@/utils/debugging'
  19. import { nodeHasError } from '../utils/tree-operations/common'
  20. import { documentEnvironments } from '../languages/latex/document-environments'
  21. import { repositionAllTooltips } from './tooltips-reposition'
  22. import { closeAllContextMenusEffect } from '../utils/close-all-context-menus-effect'
  23. const HIDE_TOOLTIP_EVENT = 'editor:hideMathTooltip'
  24. export const mathPreview = (enabled: boolean): Extension => {
  25. return mathPreviewConf.of(
  26. enabled ? [mathPreviewTheme, mathPreviewStateField] : [mathPreviewTheme]
  27. )
  28. }
  29. export const hideTooltipEffect = StateEffect.define<null>()
  30. const mathPreviewConf = new Compartment()
  31. export const setMathPreview = (enabled: boolean): TransactionSpec => ({
  32. effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []),
  33. })
  34. export const mathPreviewStateField = StateField.define<{
  35. tooltip: Tooltip | null
  36. hide: boolean
  37. }>({
  38. create: buildInitialState,
  39. update(state, tr) {
  40. for (const effect of tr.effects) {
  41. if (effect.is(hideTooltipEffect)) {
  42. return { tooltip: null, hide: true }
  43. }
  44. if (effect.is(closeAllContextMenusEffect)) {
  45. return { tooltip: null, hide: state.hide }
  46. }
  47. }
  48. if (tr.docChanged || tr.selection) {
  49. const mathContainer = getMathContainer(tr.state)
  50. if (mathContainer) {
  51. if (state.hide) {
  52. return { tooltip: null, hide: true }
  53. } else {
  54. const mathContent = buildTooltipContent(tr.state, mathContainer)
  55. return {
  56. tooltip: buildTooltip(mathContainer, mathContent),
  57. hide: false,
  58. }
  59. }
  60. }
  61. return { tooltip: null, hide: false }
  62. }
  63. return state
  64. },
  65. provide: field => [
  66. showTooltip.compute([field], state => state.field(field).tooltip),
  67. ViewPlugin.define(view => {
  68. const hideTooltip = () => {
  69. view.dispatch({
  70. effects: hideTooltipEffect.of(null),
  71. })
  72. }
  73. window.addEventListener(HIDE_TOOLTIP_EVENT, hideTooltip)
  74. return {
  75. destroy() {
  76. window.removeEventListener(HIDE_TOOLTIP_EVENT, hideTooltip)
  77. },
  78. }
  79. }),
  80. ],
  81. })
  82. function buildInitialState(state: EditorState) {
  83. const mathContainer = getMathContainer(state)
  84. if (mathContainer) {
  85. const mathContent = buildTooltipContent(state, mathContainer)
  86. return {
  87. tooltip: buildTooltip(mathContainer, mathContent),
  88. mathContent,
  89. hide: false,
  90. }
  91. }
  92. return { tooltip: null, hide: false, mathContent: null }
  93. }
  94. const renderMath = async (
  95. content: string,
  96. displayMode: boolean,
  97. element: HTMLElement,
  98. definitions: string
  99. ) => {
  100. const MathJax = await loadMathJax()
  101. MathJax.texReset([0]) // equation numbering is disabled, but this is still needed
  102. try {
  103. await MathJax.tex2svgPromise(definitions)
  104. } catch {
  105. // ignore errors thrown during parsing command definitions
  106. }
  107. const math = await MathJax.tex2svgPromise(content, {
  108. ...MathJax.getMetricsFor(element),
  109. display: displayMode,
  110. })
  111. element.textContent = ''
  112. element.append(math)
  113. }
  114. function buildTooltip(
  115. mathContainer: MathContainer,
  116. mathContent: HTMLDivElement | null
  117. ): Tooltip | null {
  118. if (!mathContent || !mathContainer) {
  119. return null
  120. }
  121. return {
  122. pos: mathContainer.pos,
  123. above: true,
  124. strictSide: true,
  125. arrow: false,
  126. create() {
  127. const dom = document.createElement('div')
  128. dom.classList.add('ol-cm-math-tooltip-container')
  129. const innerElt = document.createElement('div')
  130. innerElt.classList.add('ol-cm-math-tooltip')
  131. innerElt.id = 'ol-cm-math-tooltip'
  132. innerElt.appendChild(mathContent)
  133. dom.appendChild(innerElt)
  134. return { dom, overlap: true, offset: { x: 0, y: 8 } }
  135. },
  136. }
  137. }
  138. const getMathContainer = (state: EditorState) => {
  139. const range = state.selection.main
  140. if (!range.empty) {
  141. return null
  142. }
  143. // if anywhere inside Math, find the whole Math node
  144. const ancestorNode = mathAncestorNode(state, range.from)
  145. if (!ancestorNode) return null
  146. const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math')
  147. if (!node) return null
  148. if (nodeHasError(ancestorNode)) return null
  149. return parseMathContainer(state, node, ancestorNode)
  150. }
  151. const buildTooltipContent = (
  152. state: EditorState,
  153. math: MathContainer | null
  154. ): HTMLDivElement | null => {
  155. if (!math || !math.content.length) return null
  156. const element = document.createElement('div')
  157. element.style.opacity = '0'
  158. element.textContent = math.content
  159. let definitions = ''
  160. const environmentState = state.field(documentEnvironments, false)
  161. if (environmentState?.items) {
  162. for (const environment of environmentState.items) {
  163. if (environment.type === 'definition') {
  164. definitions += `${environment.raw}\n`
  165. }
  166. }
  167. }
  168. const commandState = state.field(documentCommands, false)
  169. if (commandState?.items) {
  170. for (const command of commandState.items) {
  171. if (command.type === 'definition' && command.raw) {
  172. definitions += `${command.raw}\n`
  173. }
  174. }
  175. }
  176. renderMath(math.content, math.displayMode, element, definitions)
  177. .then(() => {
  178. element.style.opacity = '1'
  179. repositionAllTooltips()
  180. })
  181. .catch(error => {
  182. debugConsole.error(error)
  183. })
  184. return element
  185. }
  186. /**
  187. * Styles for the preview tooltip
  188. */
  189. const mathPreviewTheme = EditorView.baseTheme({
  190. '&light .ol-cm-math-tooltip': {
  191. boxShadow: '0px 2px 4px 0px #1e253029',
  192. border: '1px solid #e7e9ee !important',
  193. backgroundColor: 'white !important',
  194. },
  195. '&dark .ol-cm-math-tooltip': {
  196. boxShadow: '0px 2px 4px 0px #1e253029',
  197. border: '1px solid #2f3a4c !important',
  198. backgroundColor: '#1b222c !important',
  199. },
  200. })