diff --git a/css/core.less b/css/core.less index 42dd5d933..e397b4375 100644 --- a/css/core.less +++ b/css/core.less @@ -210,6 +210,10 @@ padding: 0 0.5em; } +.ML__newline { + display: block; +} + .ML__frac-line { width: 100%; min-height: 1px; diff --git a/src/addons/definitions-metadata.ts b/src/addons/definitions-metadata.ts index 5a84422d5..ddbeaaeb9 100644 --- a/src/addons/definitions-metadata.ts +++ b/src/addons/definitions-metadata.ts @@ -1040,6 +1040,7 @@ metadata( RARE, '\\unicode{"203A}$0{1em}\\unicode{"2039}' ); +metadata('Spacing', ['\\\\'], COMMON); // New Line metadata( 'Punctuation', [ diff --git a/src/addons/math-ml.ts b/src/addons/math-ml.ts index 25ced6f0d..ee2963da8 100644 --- a/src/addons/math-ml.ts +++ b/src/addons/math-ml.ts @@ -728,6 +728,10 @@ function atomToMathML(atom, options): string { case 'overlap': break; + case 'newline': + result += ''; + break; + case 'overunder': overscript = atom.above; underscript = atom.below; diff --git a/src/core-atoms/newline.ts b/src/core-atoms/newline.ts new file mode 100644 index 000000000..379f2c535 --- /dev/null +++ b/src/core-atoms/newline.ts @@ -0,0 +1,35 @@ +import { Atom, AtomJson } from '../core/atom-class'; +import { Box } from '../core/box'; +import { Context } from '../core/context'; + +import type { ParseMode, Style } from '../public/core-types'; +import type { GlobalContext } from '../core/types'; + +export class NewLineAtom extends Atom { + constructor(command: string, context: GlobalContext, style: Style) { + super('newline', context, { command, style }); + this.skipBoundary = true; + } + + static fromJson(json: AtomJson, context: GlobalContext): NewLineAtom { + return new NewLineAtom(json.command, context, json as any); + } + + toJson(): AtomJson { + return super.toJson(); + } + + render(context: Context): Box | null { + const box = new Box(null, { + classes: 'ML__newline', + type: 'newline', + }); + box.caret = (this.caret as ParseMode) ?? null; + this.bind(context, box); + return box; + } + + serialize(): string { + return '\\\\'; + } +} diff --git a/src/core-definitions/styling.ts b/src/core-definitions/styling.ts index 79641ea3f..e0bdf4c11 100644 --- a/src/core-definitions/styling.ts +++ b/src/core-definitions/styling.ts @@ -26,6 +26,7 @@ import type { import { GlobalContext, PrivateStyle } from '../core/types'; import { latexCommand } from '../core/tokenizer'; import { atomsBoxType } from '../core/box'; +import { NewLineAtom } from '../core-atoms/newline'; defineFunction('mathtip', '{:math}{:math}', { createAtom: ( @@ -818,6 +819,12 @@ defineFunction('mspace', '{width:glue}', { ), }); +// New line +defineFunction('\\', '', { + createAtom: (command, context, style) => + new NewLineAtom(command, context, style), +}); + defineFunction('mathop', '{:auto}', { createAtom: ( name: string, diff --git a/src/core/atom-class.ts b/src/core/atom-class.ts index 67e5133a4..a35614451 100644 --- a/src/core/atom-class.ts +++ b/src/core/atom-class.ts @@ -107,6 +107,7 @@ export type AtomType = | 'leftright' // Used by the `\left` and `\right` commands | 'line' // Used by `\overline` and `\underline` | 'macro' + | 'newline' // New line command: `\\` | 'subsup' // A carrier for a superscript/subscript | 'overlap' // Display a symbol _over_ another | 'overunder' // Displays an annotation above or below a symbol @@ -565,6 +566,16 @@ export class Atom { return false; } + get isInArrayAtom(): boolean { + // eslint-disable-next-line @typescript-eslint/no-this-alias + let atom: Atom | undefined = this; + while (atom) { + if (atom.type === 'array') return true; + atom = atom.parent; + } + return false; + } + /** Return the parent editable prompt, if it exists */ get parentPrompt(): Atom | null { // eslint-disable-next-line @typescript-eslint/no-this-alias diff --git a/src/core/atom.ts b/src/core/atom.ts index 6445dd138..92ba9b702 100644 --- a/src/core/atom.ts +++ b/src/core/atom.ts @@ -30,6 +30,7 @@ import { SurdAtom } from '../core-atoms/surd'; import { TextAtom } from '../core-atoms/text'; import { TooltipAtom } from '../core-atoms/tooltip'; import { PromptAtom } from '../core-atoms/prompt'; +import { NewLineAtom } from '../core-atoms/newline'; import type { GlobalContext } from '../core/types'; export * from './atom-class'; @@ -69,6 +70,7 @@ export function fromJson( if (type === 'leftright') result = LeftRightAtom.fromJson(json, context); if (type === 'line') result = LineAtom.fromJson(json, context); if (type === 'macro') result = MacroAtom.fromJson(json, context); + if (type === 'newline') result = NewLineAtom.fromJson(json, context); if (type === 'subsup') result = SubsupAtom.fromJson(json, context); if (type === 'overlap') result = OverlapAtom.fromJson(json, context); if (type === 'overunder') result = OverunderAtom.fromJson(json, context); diff --git a/src/core/box.ts b/src/core/box.ts index a0c016f5b..3e01adc01 100644 --- a/src/core/box.ts +++ b/src/core/box.ts @@ -764,6 +764,7 @@ function applyInterAtomSpacing(root: Box | null, context: Context): void { if (!prevBox?.type) return; // console.log(prevBox?.value, prevBox?.type, box.value, box.type); const prevType = prevBox.type; + if (prevType === 'newline') return; const table = box.isTight ? INTER_BOX_TIGHT_SPACING[prevType] ?? null : INTER_BOX_SPACING[prevType] ?? null; diff --git a/src/core/types.ts b/src/core/types.ts index 3d61dca52..40115bb85 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -88,6 +88,7 @@ const BOX_TYPE = [ 'close', // > is a closing atom like `)` 'punct', // > is a punctuation atom like ‘,’ 'inner', // > is an inner atom like `\frac12` + 'newline', // > is a new line box 'spacing', 'first', 'latex', diff --git a/src/editor-mathfield/mathfield-private.ts b/src/editor-mathfield/mathfield-private.ts index efc4e613f..917a1d5ee 100644 --- a/src/editor-mathfield/mathfield-private.ts +++ b/src/editor-mathfield/mathfield-private.ts @@ -1072,11 +1072,12 @@ If you are using Vue, this may be because you are using the runtime-only build o if (options.scrollIntoView) this.scrollIntoView(); - if (s === '\\\\') { - // This string is interpreted as an "insert row after" command - addRowAfter(this.model); - } else if (s === '&') addColumnAfter(this.model); - else { + if (this.model.at(this.model.position).isInArrayAtom) { + if (s === '\\\\') { + // This string is interpreted as an "insert row after" command + addRowAfter(this.model); + } else if (s === '&') addColumnAfter(this.model); + } else { const savedStyle = this.style; ModeEditor.insert(this.mode, this.model, s, { style: this.model.at(this.model.position).computedStyle, diff --git a/src/editor/keybindings-definitions.ts b/src/editor/keybindings-definitions.ts index 5ef45f8e5..682ee982b 100644 --- a/src/editor/keybindings-definitions.ts +++ b/src/editor/keybindings-definitions.ts @@ -87,6 +87,9 @@ export const DEFAULT_KEYBINDINGS: Keybinding[] = [ }, // Complete the suggestion { key: '[Return]', ifMode: 'latex', command: 'complete' }, { key: '[Enter]', ifMode: 'latex', command: 'complete' }, + { key: '[Return]', ifMode: 'math', command: ['insert', '\\\\'] }, + { key: '[Enter]', ifMode: 'math', command: ['insert', '\\\\'] }, + { key: '[NumpadEnter]', ifMode: 'math', command: ['insert', '\\\\'] }, { key: 'shift+[Escape]', ifMode: 'latex', diff --git a/src/virtual-keyboard/data.ts b/src/virtual-keyboard/data.ts index 901ddab6b..c8d23d18a 100644 --- a/src/virtual-keyboard/data.ts +++ b/src/virtual-keyboard/data.ts @@ -135,7 +135,7 @@ export const LAYOUTS: Record = { '[separator-5]', '[left]', '[right]', - { label: '[action]', width: 1.0 }, + { label: '[return]', width: 1.0 }, ], ], }, @@ -372,7 +372,7 @@ export const LAYOUTS: Record = { '[left]', '[right]', - '[action]', + '[return]', ], ], }, @@ -660,7 +660,7 @@ export const LAYOUTS: Record = { width: 1.0, class: 'action hide-shift', }, - { label: '[action]', width: 1.0 }, + { label: '[return]', width: 1.0 }, ], ], }, diff --git a/src/virtual-keyboard/utils.ts b/src/virtual-keyboard/utils.ts index 242663f59..09c4859cb 100644 --- a/src/virtual-keyboard/utils.ts +++ b/src/virtual-keyboard/utils.ts @@ -177,7 +177,7 @@ function alphabeticLayout(): NormalizedVirtualKeyboardLayout { '[.]', '[left]', '[right]', - { label: '[action]', width: 1.5 }, + { label: '[return]', width: 1.5 }, ]); return { @@ -735,7 +735,8 @@ const KEYCAP_SHORTCUTS: Record> = { }, '[return]': { class: 'action', - command: ['performWithFeedback', 'commit'], + insert: '\\\\', + shift: { command: ['performWithFeedback', 'commit'] }, width: 1.5, label: '', },