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: '',
},