diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index c430a40a1f..5e80f0f42a 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1027,7 +1027,7 @@ module.exports = { tprFileViewNotOriginalImporter: [], contactUsModal: [], sourceEditorExtensions: [], - sourceEditorMarkdownExtensions: [], + sourceEditorVisualExtensions: [], sourceEditorComponents: [], pdfLogEntryHeaderActionComponents: [], pdfLogEntryComponents: [], 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 da9546cbfb..e373591f66 100644 --- a/services/web/frontend/js/features/source-editor/extensions/index.ts +++ b/services/web/frontend/js/features/source-editor/extensions/index.ts @@ -157,7 +157,7 @@ export const createExtensions = (options: Record): Extension[] => [ ? historyOT(options.currentDoc.currentDocument) : ranges(), trackDetachedComments(options.currentDoc), - visual(options.visual), + visual(options.docName, options.visual), mathPreview(options.settings.mathPreview), reviewTooltip(options.editorContextMenuEnabled), contextMenu(options.editorContextMenuEnabled), diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index 884733981e..8cac2d7583 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/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 { debugConsole } from '@/utils/debugging' import { PreviewPath } from '../../../../../../types/preview-path' +import { getFileExtension } from '../../utils/file' type Options = { visual: boolean 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() @@ -56,12 +56,16 @@ const visualState = StateField.define({ }) 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 [ visualState.init(() => options.visual), 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 configure = (visual: boolean) => (visual ? [] : extension) + const configure = (visual: boolean) => + visual === activeWhenVisual ? extension : [] return [ 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() /** @@ -175,19 +193,39 @@ const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => { return {} }) -const extension = (options: Options) => [ +const sharedVisualExtensions = () => [ visualTheme, visualHighlightStyle, mousedown, + scrollJumpAdjuster, + showContentWhenParsed, + EditorView.contentAttributes.of({ 'aria-label': 'Visual Editor editing' }), +] + +const latexVisualExtensions = (options: Options): Extension => [ listItemMarker, atomicDecorations(options), - visualEditorExtensions.map(extension => extension(options)), markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets visualKeymap, commandTooltip, - scrollJumpAdjuster, - showContentWhenParsed, pasteHtml, 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 +} diff --git a/services/web/frontend/js/features/source-editor/utils/visual-editor.ts b/services/web/frontend/js/features/source-editor/utils/visual-editor.ts index 256f0c8432..6ca39b6cd7 100644 --- a/services/web/frontend/js/features/source-editor/utils/visual-editor.ts +++ b/services/web/frontend/js/features/source-editor/utils/visual-editor.ts @@ -1,10 +1,11 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import { isValidTeXFile } from '../../../main/is-valid-tex-file' +import { getFileExtension } from './file' 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 @@ -16,8 +17,16 @@ export function isCmVisualEditorAvailable(filename: string): boolean { return true } + const extension = getFileExtension(filename) + if (extension === null) { + return false + } + 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 } }