From 30a88b1b4576712778da37ff52fef451b546f8a9 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 24 Apr 2026 10:46:35 -0500 Subject: [PATCH] Fix pasting and cmd insertion into the SupSub block of a MathFunction. The SubSub block is supposed to only allow a SubSub (i.e., a subscript or superscript) to be entered. Anything else should move to the function parameter block and be inserted there. However, if something is pasted from the clipboard or the API `cmd` method is used to insert a latex command (as is done by the PG mqeditor toolbar), then that is being allowed in the SupSub block. So this fixes those cases to only allow SupSubs as should be the case. Anything else gets moves into the parameter block. --- package-lock.json | 24 ++++++++++++------------ src/abstractFields.ts | 6 ++++-- src/commands/mathBlock.ts | 18 +++++++++++------- src/commands/mathElements.ts | 34 +++++++++++++++++++++++++++++++--- src/controller.ts | 1 - src/tree/node.ts | 3 +++ 6 files changed, 61 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index f875752b..e9ae74c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2834,9 +2834,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -4535,9 +4535,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -9533,9 +9533,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", - "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", + "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", "dev": true, "license": "MIT", "dependencies": { @@ -9782,9 +9782,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, "license": "MIT", "engines": { diff --git a/src/abstractFields.ts b/src/abstractFields.ts index 4528d92a..c39b6a74 100644 --- a/src/abstractFields.ts +++ b/src/abstractFields.ts @@ -162,8 +162,10 @@ export class EditableField extends AbstractMathQuill { if (klass) { const newCmd = new klass(cmd); if (cursor.selection) newCmd.replaces(cursor.replaceSelection()); - newCmd.createLeftOf(cursor.show()); - this.__controller.scrollHoriz(); + if (cursor.parent?.prepareCommandInsertion(cursor, newCmd)) { + newCmd.createLeftOf(cursor.show()); + this.__controller.scrollHoriz(); + } } else { // TODO: API needs better error reporting } diff --git a/src/commands/mathBlock.ts b/src/commands/mathBlock.ts index c9eac8d9..40fd1d77 100644 --- a/src/commands/mathBlock.ts +++ b/src/commands/mathBlock.ts @@ -12,6 +12,7 @@ import { VanillaSymbol, MathElement, MathCommand, Letter, Digit, latexMathParser export const writeMethodMixin = >(Base: TBase) => class extends Base { writeHandler?: (cursor: Cursor, ch: string) => boolean; + writeLatexHandler?: (cursor: Cursor, block: MathBlock) => MathBlock | boolean; chToCmd(ch: string, options?: Options): TNode { const cons = (CharCmds[ch] as Constructor | undefined) || LatexCmds[ch]; @@ -178,16 +179,19 @@ export class MathBlock extends BlockFocusBlur(writeMethodMixin(MathElement)) { const all = Parser.all; const eof = Parser.eof; - const block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); + const block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { - if (cursor.parent) block.children().adopt(cursor.parent, cursor.left, cursor.right); - const elements = block.domify(); + const handlerResult = this.writeLatexHandler?.(cursor, block); + if (handlerResult === true) return; + const blockToInsert = handlerResult || block; + if (cursor.parent) blockToInsert.children().adopt(cursor.parent, cursor.left, cursor.right); + const elements = blockToInsert.domify(); cursor.element.before(...elements.contents); - cursor.left = block.ends.right; - block.finalizeInsert(cursor.options, cursor); - block.ends.right?.right?.siblingCreated?.(cursor.options, 'left'); - block.ends.left?.left?.siblingCreated?.(cursor.options, 'right'); + cursor.left = blockToInsert.ends.right; + blockToInsert.finalizeInsert(cursor.options, cursor); + blockToInsert.ends.right?.right?.siblingCreated?.(cursor.options, 'left'); + blockToInsert.ends.left?.left?.siblingCreated?.(cursor.options, 'right'); cursor.parent?.bubble('reflow'); } } diff --git a/src/commands/mathElements.ts b/src/commands/mathElements.ts index 372b70e7..adc31485 100644 --- a/src/commands/mathElements.ts +++ b/src/commands/mathElements.ts @@ -529,7 +529,7 @@ export class Digit extends VanillaSymbol { } export class Variable extends Symbol { - isItalic = false; + isItalic = true; isPartOfOperator = false; constructor(ch: string, html?: string) { @@ -1431,8 +1431,6 @@ const BracketMixin = >(Base: TBase) => } replaceBracket(brackFrag: HTMLElement, side: Direction) { - if (!(this instanceof Bracket) && !(this instanceof MathFunction)) - throw new Error('can only replace bracket for a Bracket or MathFunction'); const symbol = this.getSymbol(side); brackFrag.innerHTML = symbol.html; @@ -1660,6 +1658,36 @@ export class MathFunction extends BracketMixin(MathCommand) { return false; }; + // Only allow a SubSub to be inserted into the block before the parentheses. If anything else is contained in + // the block, then move to the content block so it will be inserted there. + this.blocks[0].writeLatexHandler = (cursor: Cursor, block: MathBlock) => { + if ( + block.ends.left === block.ends.right && + block.ends.left instanceof Bracket && + block.ends.left.ctrlSeq === '\\left(' + ) { + this.enterContentBlock('left', cursor); + const bracketContents = block.ends.left.blocks[0].children().disown(); + block.ends.left.disown(); + bracketContents.adopt(block); + return block; + } + + block.eachChild((node: TNode) => { + if (node instanceof SupSub) return true; + this.enterContentBlock('left', cursor); + return false; + }); + + return false; + }; + + this.blocks[0].prepareCommandInsertion = (cursor: Cursor, cmd: TNode) => { + if (cmd instanceof SupSub) return true; + this.enterContentBlock('left', cursor); + return true; + }; + return super.html(); } diff --git a/src/controller.ts b/src/controller.ts index 09952c97..2493d797 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -404,7 +404,6 @@ export class Controller extends ExportText( selectAll() { this.notify('move').cursor.insAtRightEnd(this.root); - while (this.cursor.left) this.selectLeft(); this.withIncrementalSelection((selectDir) => { while (this.cursor.left) selectDir('left'); }); diff --git a/src/tree/node.ts b/src/tree/node.ts index 30b5b0a6..641c2b6e 100644 --- a/src/tree/node.ts +++ b/src/tree/node.ts @@ -391,6 +391,9 @@ export class TNode { replaces(_fragment?: string | Fragment) { /* do nothing */ } + prepareCommandInsertion(_cursor: Cursor, _cmd: TNode): boolean { + return true; + } setOptions(_options: { text?: () => string; htmlTemplate?: string; latex?: () => string }) { return this; }