From 1b47d9184c4114621b8f0c118bb5233f432fd32d Mon Sep 17 00:00:00 2001 From: Reffat Date: Fri, 5 Dec 2025 18:10:52 +0300 Subject: [PATCH 1/2] feat(Math): added insertion plugin --- src/extensions/additional/Math/Math.test.ts | 41 +++++++++++++ src/extensions/additional/Math/const.ts | 11 ++++ src/extensions/additional/Math/index.ts | 3 + .../additional/Math/latex-paste-plugin.ts | 58 +++++++++++++++++++ .../additional/Math/test-formulas.tex | 11 ++++ src/extensions/additional/Math/utils.ts | 49 ++++++++++++++++ .../markdown/CodeBlock/handle-paste.ts | 5 ++ 7 files changed, 178 insertions(+) create mode 100644 src/extensions/additional/Math/latex-paste-plugin.ts create mode 100644 src/extensions/additional/Math/test-formulas.tex create mode 100644 src/extensions/additional/Math/utils.ts diff --git a/src/extensions/additional/Math/Math.test.ts b/src/extensions/additional/Math/Math.test.ts index b82fd1b6d..6404b8ed2 100644 --- a/src/extensions/additional/Math/Math.test.ts +++ b/src/extensions/additional/Math/Math.test.ts @@ -5,6 +5,7 @@ import {ExtensionsManager} from '../../../core'; import {BaseNode, BaseSchemaSpecs} from '../../base/specs'; import {MathNode, MathSpecs} from './MathSpecs'; +import {isLatexMode, parseLatexFormulas} from './utils'; const { schema, @@ -47,3 +48,43 @@ describe('Math extension', () => { same(`$$${formula}$$\n\n`, doc(mathB(formula))); }); }); + +describe('latex-paste-plugin utilities', () => { + describe('isLatexMode', () => { + it('should return true for tex/latex modes', () => { + expect(isLatexMode('tex')).toBe(true); + expect(isLatexMode('latex')).toBe(true); + expect(isLatexMode('bibtex')).toBe(true); + }); + + it('should return false for non-latex modes', () => { + expect(isLatexMode('javascript')).toBe(false); + expect(isLatexMode('python')).toBe(false); + expect(isLatexMode(undefined)).toBe(false); + }); + }); + + describe('parseLatexFormulas', () => { + it('should split formulas by double newlines', () => { + const input = 'E = mc^2\n\ne^{i\\pi} + 1 = 0'; + const result = parseLatexFormulas(input); + expect(result).toEqual(['E = mc^2', 'e^{i\\pi} + 1 = 0']); + }); + + it('should filter out comment lines starting with %', () => { + const input = `% Einstein equation +E = mc^2 + +% Euler formula +e^{i\\pi} + 1 = 0`; + const result = parseLatexFormulas(input); + expect(result).toEqual(['E = mc^2', 'e^{i\\pi} + 1 = 0']); + }); + + it('should return empty array for only comments', () => { + const input = '% Comment 1\n% Comment 2'; + const result = parseLatexFormulas(input); + expect(result).toEqual([]); + }); + }); +}); diff --git a/src/extensions/additional/Math/const.ts b/src/extensions/additional/Math/const.ts index 275de207d..b681fd91b 100644 --- a/src/extensions/additional/Math/const.ts +++ b/src/extensions/additional/Math/const.ts @@ -1,2 +1,13 @@ export * from './MathSpecs/const'; export {mathBType, mathIType} from './MathSpecs'; + +export const LATEX_MODES = new Set([ + 'tex', + 'latex', + 'bibtex', + 'doctex', + 'latex-expl3', + 'pweave', + 'jlweave', + 'rsweave', +]); diff --git a/src/extensions/additional/Math/index.ts b/src/extensions/additional/Math/index.ts index ee1c2d3d6..c0b43ca2d 100644 --- a/src/extensions/additional/Math/index.ts +++ b/src/extensions/additional/Math/index.ts @@ -14,12 +14,14 @@ import { removeEmptyMathInlineIfCursorIsAtBeginning, } from './commands'; import {mathBType, mathIType} from './const'; +import {latexPastePlugin} from './latex-paste-plugin'; import {type MathNodeViewOptions, mathViewAndEditPlugin} from './view-and-edit'; import './index.scss'; export {MathNode, mathBType, mathIType} from './MathSpecs'; export {MathBlockNodeView, MathInlineNodeView} from './view-and-edit'; +export {isLatexMode, parseLatexFormulas} from './utils'; const mathIAction = 'addMathInline'; const mathBAction = 'toMathBlock'; @@ -45,6 +47,7 @@ export const Math: ExtensionAuto = (builder, opts) => { })); builder + .addPlugin(latexPastePlugin, builder.Priority.High) .addPlugin(() => mathViewAndEditPlugin({ ...opts, diff --git a/src/extensions/additional/Math/latex-paste-plugin.ts b/src/extensions/additional/Math/latex-paste-plugin.ts new file mode 100644 index 000000000..777c9837d --- /dev/null +++ b/src/extensions/additional/Math/latex-paste-plugin.ts @@ -0,0 +1,58 @@ +import {Plugin} from 'prosemirror-state'; + +import {getLoggerFromState} from '#core'; +import {Fragment} from '#pm/model'; + +import {MathNode} from './const'; +import {getLatexData, parseLatexFormulas} from './utils'; + +export const latexPastePlugin = () => + new Plugin({ + props: { + handleDOMEvents: { + paste(view, e: Event) { + const event = e as ClipboardEvent; + if (!event.clipboardData || view.state.selection.$from.parent.type.spec.code) { + return false; + } + + const latexData = getLatexData(event.clipboardData); + if (!latexData) return false; + + getLoggerFromState(view.state).event({ + domEvent: 'paste', + event: 'paste-latex-from-code-editor', + editor: latexData.editor, + editorMode: latexData.mode, + empty: !latexData.value, + dataTypes: event.clipboardData.types, + }); + + const {tr, schema} = view.state; + const mathBlockType = schema.nodes[MathNode.Block]; + + if (!mathBlockType) return false; + + if (latexData.value) { + const formulas = parseLatexFormulas(latexData.value); + + if (formulas.length > 0) { + const nodes = formulas.map((formula) => + mathBlockType.create(null, schema.text(formula)), + ); + const fragment = Fragment.from(nodes); + tr.replaceWith(tr.selection.from, tr.selection.to, fragment); + } else { + tr.replaceWith(tr.selection.from, tr.selection.to, Fragment.empty); + } + } else { + tr.replaceWith(tr.selection.from, tr.selection.to, Fragment.empty); + } + + view.dispatch(tr.scrollIntoView()); + e.preventDefault(); + return true; + }, + }, + }, + }); diff --git a/src/extensions/additional/Math/test-formulas.tex b/src/extensions/additional/Math/test-formulas.tex new file mode 100644 index 000000000..b32b36d6d --- /dev/null +++ b/src/extensions/additional/Math/test-formulas.tex @@ -0,0 +1,11 @@ +% Формула Эйнштейна (масса-энергия) +E = mc^2 + +% Теорема Пифагора +a^2 + b^2 = c^2 + +% Формула Эйлера +e^{i\pi} + 1 = 0 + +% Квадратное уравнение +x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \ No newline at end of file diff --git a/src/extensions/additional/Math/utils.ts b/src/extensions/additional/Math/utils.ts new file mode 100644 index 000000000..159b430e3 --- /dev/null +++ b/src/extensions/additional/Math/utils.ts @@ -0,0 +1,49 @@ +import dd from 'ts-dedent'; + +import {DataTransferType, isVSCode, tryParseVSCodeData} from 'src/utils/clipboard'; + +import {LATEX_MODES} from './const'; + +export function isLatexMode(mode: string | undefined): boolean { + if (!mode) return false; + return LATEX_MODES.has(mode.toLowerCase()); +} + +export function parseLatexFormulas(content: string): string[] { + const blocks = content.split(/\n\s*\n/); + const formulas: string[] = []; + + for (const block of blocks) { + const lines = block + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.length > 0); + + if (lines.length > 0) { + formulas.push(lines.join('\n')); + } + } + + return formulas; +} + +export function getLatexData( + data: DataTransfer, +): null | {editor: string; mode: string; value: string} { + if (!data.getData(DataTransferType.Text)) return null; + + if (isVSCode(data)) { + const vsCodeData = tryParseVSCodeData(data); + const mode = vsCodeData?.mode; + + if (mode && isLatexMode(mode)) { + return { + editor: 'vscode', + mode, + value: dd(data.getData(DataTransferType.Text)), + }; + } + } + + return null; +} diff --git a/src/extensions/markdown/CodeBlock/handle-paste.ts b/src/extensions/markdown/CodeBlock/handle-paste.ts index 1317926f4..162534473 100644 --- a/src/extensions/markdown/CodeBlock/handle-paste.ts +++ b/src/extensions/markdown/CodeBlock/handle-paste.ts @@ -3,6 +3,7 @@ import dd from 'ts-dedent'; import {getLoggerFromState} from '#core'; import {Fragment} from '#pm/model'; import type {EditorProps} from '#pm/view'; +import {isLatexMode} from 'src/extensions/additional/Math/utils'; import {DataTransferType, isVSCode, tryParseVSCodeData} from 'src/utils/clipboard'; import {CodeBlockNodeAttr} from './CodeBlockSpecs'; @@ -44,6 +45,10 @@ function getCodeData(data: DataTransfer): null | {editor: string; mode?: string; if (isVSCode(data)) { editor = 'vscode'; mode = tryParseVSCodeData(data)?.mode; + + if (isLatexMode(mode)) { + return null; + } } else return null; return {editor, mode, value: dd(data.getData(DataTransferType.Text))}; From 78cb40c56414df02b4ff00e9022efb94270830a4 Mon Sep 17 00:00:00 2001 From: Reffat Date: Fri, 5 Dec 2025 18:16:07 +0300 Subject: [PATCH 2/2] fixed test and remove test file --- src/extensions/additional/Math/Math.test.ts | 11 +++++++---- src/extensions/additional/Math/test-formulas.tex | 11 ----------- 2 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 src/extensions/additional/Math/test-formulas.tex diff --git a/src/extensions/additional/Math/Math.test.ts b/src/extensions/additional/Math/Math.test.ts index 6404b8ed2..ecf0d84d8 100644 --- a/src/extensions/additional/Math/Math.test.ts +++ b/src/extensions/additional/Math/Math.test.ts @@ -71,20 +71,23 @@ describe('latex-paste-plugin utilities', () => { expect(result).toEqual(['E = mc^2', 'e^{i\\pi} + 1 = 0']); }); - it('should filter out comment lines starting with %', () => { + it('should preserve comment lines starting with %', () => { const input = `% Einstein equation E = mc^2 % Euler formula e^{i\\pi} + 1 = 0`; const result = parseLatexFormulas(input); - expect(result).toEqual(['E = mc^2', 'e^{i\\pi} + 1 = 0']); + expect(result).toEqual([ + '% Einstein equation\nE = mc^2', + '% Euler formula\ne^{i\\pi} + 1 = 0', + ]); }); - it('should return empty array for only comments', () => { + it('should return comments as formulas', () => { const input = '% Comment 1\n% Comment 2'; const result = parseLatexFormulas(input); - expect(result).toEqual([]); + expect(result).toEqual(['% Comment 1\n% Comment 2']); }); }); }); diff --git a/src/extensions/additional/Math/test-formulas.tex b/src/extensions/additional/Math/test-formulas.tex deleted file mode 100644 index b32b36d6d..000000000 --- a/src/extensions/additional/Math/test-formulas.tex +++ /dev/null @@ -1,11 +0,0 @@ -% Формула Эйнштейна (масса-энергия) -E = mc^2 - -% Теорема Пифагора -a^2 + b^2 = c^2 - -% Формула Эйлера -e^{i\pi} + 1 = 0 - -% Квадратное уравнение -x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \ No newline at end of file