Просмотр исходного кода

Module for loading per language visual editor extensions

GitOrigin-RevId: 5244e754337a1e48b8de63e881648b2397500b25
Domagoj Kriskovic 2 месяцев назад
Родитель
Сommit
7b3692ce6f

+ 1 - 1
services/web/config/settings.defaults.js

@@ -1027,7 +1027,7 @@ module.exports = {
     tprFileViewNotOriginalImporter: [],
     tprFileViewNotOriginalImporter: [],
     contactUsModal: [],
     contactUsModal: [],
     sourceEditorExtensions: [],
     sourceEditorExtensions: [],
-    sourceEditorMarkdownExtensions: [],
+    sourceEditorVisualExtensions: [],
     sourceEditorComponents: [],
     sourceEditorComponents: [],
     pdfLogEntryHeaderActionComponents: [],
     pdfLogEntryHeaderActionComponents: [],
     pdfLogEntryComponents: [],
     pdfLogEntryComponents: [],

+ 1 - 1
services/web/frontend/js/features/source-editor/extensions/index.ts

@@ -157,7 +157,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
     ? historyOT(options.currentDoc.currentDocument)
     ? historyOT(options.currentDoc.currentDocument)
     : ranges(),
     : ranges(),
   trackDetachedComments(options.currentDoc),
   trackDetachedComments(options.currentDoc),
-  visual(options.visual),
+  visual(options.docName, options.visual),
   mathPreview(options.settings.mathPreview),
   mathPreview(options.settings.mathPreview),
   reviewTooltip(options.editorContextMenuEnabled),
   reviewTooltip(options.editorContextMenuEnabled),
   contextMenu(options.editorContextMenuEnabled),
   contextMenu(options.editorContextMenuEnabled),

+ 55 - 17
services/web/frontend/js/features/source-editor/extensions/visual/visual.ts

@@ -22,20 +22,20 @@ import { commandTooltip } from '../command-tooltip'
 import { tableGeneratorTheme } from './table-generator'
 import { tableGeneratorTheme } from './table-generator'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 import { PreviewPath } from '../../../../../../types/preview-path'
 import { PreviewPath } from '../../../../../../types/preview-path'
+import { getFileExtension } from '../../utils/file'
 
 
 type Options = {
 type Options = {
   visual: boolean
   visual: boolean
   previewByPath: (path: string) => PreviewPath | null
   previewByPath: (path: string) => PreviewPath | null
 }
 }
 
 
-// Language-specific visual editor extensions provided by modules (e.g. the
-// markdown visual editor). They live in the visual bundle so they are only
-// active in visual mode and react to switching editor modes.
-const visualEditorExtensions: Array<(options: Options) => Extension> =
-  importOverleafModules('sourceEditorMarkdownExtensions').map(
-    (item: { import: { extension: (options: Options) => Extension } }) =>
-      item.import.extension
-  )
+// Module-provided visual editors registered via the
+// `sourceEditorVisualExtensions` hook. Module exposes `getExtensions(ext)`,
+// returning the visual-mode extensions for that file extension. A module match takes precedence
+// over the LaTeX fallback
+const moduleVisualExtensionProviders: Array<{
+  import: { getExtensions: (ext: string) => Extension }
+}> = importOverleafModules('sourceEditorVisualExtensions')
 
 
 const visualConf = new Compartment()
 const visualConf = new Compartment()
 
 
@@ -56,12 +56,16 @@ const visualState = StateField.define<boolean>({
 })
 })
 
 
 const configureVisualExtensions = (options: Options) =>
 const configureVisualExtensions = (options: Options) =>
-  options.visual ? extension(options) : []
+  options.visual ? sharedVisualExtensions() : []
+
+export const visual = (docName: string, options: Options): Extension => {
+  const extensions =
+    visualModuleExtensions(docName) ?? latexVisualExtensions(options)
 
 
-export const visual = (options: Options): Extension => {
   return [
   return [
     visualState.init(() => options.visual),
     visualState.init(() => options.visual),
     visualConf.of(configureVisualExtensions(options)),
     visualConf.of(configureVisualExtensions(options)),
+    visualOnly(options.visual, extensions),
   ]
   ]
 }
 }
 
 
@@ -78,9 +82,17 @@ export const setVisual = (options: Options): TransactionSpec => {
   }
   }
 }
 }
 
 
-export const sourceOnly = (visual: boolean, extension: Extension) => {
+// Loads `extension` only while the editor is in a particular mode, reacting to
+// mode switches via `toggleVisualEffect`. `activeWhenVisual` selects which mode:
+// `true` for visual-only, `false` for source-only.
+const modeOnly = (
+  activeWhenVisual: boolean,
+  visual: boolean,
+  extension: Extension
+) => {
   const conf = new Compartment()
   const conf = new Compartment()
-  const configure = (visual: boolean) => (visual ? [] : extension)
+  const configure = (visual: boolean) =>
+    visual === activeWhenVisual ? extension : []
   return [
   return [
     conf.of(configure(visual)),
     conf.of(configure(visual)),
 
 
@@ -98,6 +110,12 @@ export const sourceOnly = (visual: boolean, extension: Extension) => {
   ]
   ]
 }
 }
 
 
+export const visualOnly = (visual: boolean, extension: Extension) =>
+  modeOnly(true, visual, extension)
+
+export const sourceOnly = (visual: boolean, extension: Extension) =>
+  modeOnly(false, visual, extension)
+
 const parsedAttributesConf = new Compartment()
 const parsedAttributesConf = new Compartment()
 
 
 /**
 /**
@@ -175,19 +193,39 @@ const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
   return {}
   return {}
 })
 })
 
 
-const extension = (options: Options) => [
+const sharedVisualExtensions = () => [
   visualTheme,
   visualTheme,
   visualHighlightStyle,
   visualHighlightStyle,
   mousedown,
   mousedown,
+  scrollJumpAdjuster,
+  showContentWhenParsed,
+  EditorView.contentAttributes.of({ 'aria-label': 'Visual Editor editing' }),
+]
+
+const latexVisualExtensions = (options: Options): Extension => [
   listItemMarker,
   listItemMarker,
   atomicDecorations(options),
   atomicDecorations(options),
-  visualEditorExtensions.map(extension => extension(options)),
   markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
   markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
   visualKeymap,
   visualKeymap,
   commandTooltip,
   commandTooltip,
-  scrollJumpAdjuster,
-  showContentWhenParsed,
   pasteHtml,
   pasteHtml,
   tableGeneratorTheme,
   tableGeneratorTheme,
-  EditorView.contentAttributes.of({ 'aria-label': 'Visual Editor editing' }),
 ]
 ]
+
+// Returns the visual-mode extensions provided by a module for the active document
+const visualModuleExtensions = (docName: string): Extension | null => {
+  const fileExt = getFileExtension(docName)
+  if (!fileExt) {
+    return null
+  }
+
+  for (const provider of moduleVisualExtensionProviders) {
+    const extensions = provider.import.getExtensions(fileExt)
+    if (Array.isArray(extensions) && extensions.length === 0) {
+      continue
+    }
+    return extensions
+  }
+
+  return null
+}

+ 13 - 4
services/web/frontend/js/features/source-editor/utils/visual-editor.ts

@@ -1,10 +1,11 @@
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
 import { isValidTeXFile } from '../../../main/is-valid-tex-file'
 import { isValidTeXFile } from '../../../main/is-valid-tex-file'
+import { getFileExtension } from './file'
 
 
 const visualEditorProviders = importOverleafModules('visualEditorProviders')
 const visualEditorProviders = importOverleafModules('visualEditorProviders')
-const cmVisualEditorProviders = importOverleafModules(
-  'sourceEditorMarkdownExtensions'
-)
+const cmVisualEditorProviders: Array<{
+  import: { getExtensions: (ext: string) => unknown }
+}> = importOverleafModules('sourceEditorVisualExtensions')
 
 
 /**
 /**
  * This currently covers LaTeX and Markdown. Other file
  * This currently covers LaTeX and Markdown. Other file
@@ -16,8 +17,16 @@ export function isCmVisualEditorAvailable(filename: string): boolean {
     return true
     return true
   }
   }
 
 
+  const extension = getFileExtension(filename)
+  if (extension === null) {
+    return false
+  }
+
   for (const provider of cmVisualEditorProviders) {
   for (const provider of cmVisualEditorProviders) {
-    if (provider.import.isCmVisualEditorFile?.(filename)) {
+    const extensions = provider.import.getExtensions(extension)
+    if (
+      Array.isArray(extensions) ? extensions.length > 0 : extensions != null
+    ) {
       return true
       return true
     }
     }
   }
   }