import { highlightSpecialChars, drawSelection, lineNumbers, EditorView, highlightActiveLineGutter, highlightActiveLine, keymap } from '@codemirror/view'; export { EditorView, keymap } from '@codemirror/view'; import { Compartment, EditorState } from '@codemirror/state'; export { EditorState } from '@codemirror/state'; import { syntaxHighlighting, defaultHighlightStyle, foldGutter } from '@codemirror/language'; import { history, defaultKeymap, historyKeymap, emacsStyleKeymap } from '@codemirror/commands'; import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'; import { closeBrackets } from '@codemirror/autocomplete'; /** * @copyright (C) 2023 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ const minimalSetup = () => [highlightSpecialChars(), history(), drawSelection(), syntaxHighlighting(defaultHighlightStyle, { fallback: true })]; /** * Configure and return list of extensions for given options * * @param {Object} options * @returns {Promise<[]>} */ const optionsToExtensions = async options => { const extensions = []; const q = []; // Load the language for syntax mode if (options.mode) { const { mode } = options; const modeOptions = options[mode] || {}; // eslint-disable-next-line consistent-return q.push(import(`@codemirror/lang-${options.mode}`).then(modeMod => { // For html and php we need to configure selfClosingTags, to make code folding work correctly with if (mode === 'php') { return import('@codemirror/lang-html').then(({ html }) => { const htmlOptions = options.html || { selfClosingTags: true }; extensions.push(modeMod.php({ baseLanguage: html(htmlOptions).language })); }); } if (mode === 'html') { modeOptions.selfClosingTags = true; } extensions.push(modeMod[options.mode](modeOptions)); }).catch(error => { // eslint-disable-next-line no-console console.error(`Cannot creat an extension for "${options.mode}" syntax mode.`, error); })); } if (options.lineNumbers) { extensions.push(lineNumbers()); } if (options.lineWrapping) { extensions.push(EditorView.lineWrapping); } if (options.activeLine) { extensions.push(highlightActiveLineGutter(), highlightActiveLine()); } if (options.highlightSelection) { extensions.push(highlightSelectionMatches()); } if (options.autoCloseBrackets) { extensions.push(closeBrackets()); } if (options.foldGutter) { extensions.push(foldGutter()); } // Keymaps switch (options.keyMap) { case 'emacs': extensions.push(keymap.of([...emacsStyleKeymap, ...historyKeymap])); break; default: extensions.push(keymap.of([...defaultKeymap, ...searchKeymap, ...historyKeymap])); break; } // Configurable read only const readOnly = new Compartment(); // Set a custom name so later on we can retrieve this Compartment from view.state.config.compartments readOnly.$j_name = 'readOnly'; extensions.push(readOnly.of(EditorState.readOnly.of(!!options.readOnly))); // Check for custom extensions, // in format [['module1 name or URL', ['init method2']], ['module2 name or URL', ['init method2']], () => ] if (options.customExtensions && options.customExtensions.length) { options.customExtensions.forEach(extInfo => { // Check whether we have a callable if (extInfo instanceof Function) { extensions.push(extInfo()); return; } // Import the module const [module, methods] = extInfo; q.push(import(module).then(modObject => { // Call each method methods.forEach(method => { extensions.push(modObject[method]()); }); })); }); } return Promise.all(q).then(() => extensions); }; /** * Create an editor instance for given textarea * * @param {HTMLTextAreaElement} textarea * @param {Object} options * @returns {Promise} */ async function createFromTextarea(textarea, options) { const extensions = [minimalSetup(), await optionsToExtensions(options)]; const view = new EditorView({ doc: textarea.value, root: options.root || null, extensions }); textarea.parentNode.insertBefore(view.dom, textarea); textarea.style.display = 'none'; if (textarea.form) { textarea.form.addEventListener('submit', () => { textarea.value = view.state.doc.toString(); }); } // Set up sizing if (options.width) { view.dom.style.width = options.width; } if (options.height) { view.dom.style.height = options.height; } return view; } export { createFromTextarea, minimalSetup, optionsToExtensions };